numbers-parser 4.10.1__py3-none-any.whl → 4.10.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,927 +0,0 @@
1
- import logging
2
- import math
3
- import re
4
- from fractions import Fraction
5
- from struct import unpack
6
- from typing import Tuple, Union
7
- from warnings import warn
8
-
9
- import sigfig
10
- from pendulum import datetime, duration
11
-
12
- from numbers_parser import __name__ as numbers_parser_name
13
- from numbers_parser.constants import (
14
- CHECKBOX_FALSE_VALUE,
15
- CHECKBOX_TRUE_VALUE,
16
- CURRENCY_CELL_TYPE,
17
- CUSTOM_TEXT_PLACEHOLDER,
18
- DATETIME_FIELD_MAP,
19
- DECIMAL_PLACES_AUTO,
20
- EPOCH,
21
- MAX_SIGNIFICANT_DIGITS,
22
- PACKAGE_ID,
23
- SECONDS_IN_DAY,
24
- SECONDS_IN_HOUR,
25
- SECONDS_IN_WEEK,
26
- STAR_RATING_VALUE,
27
- CellPadding,
28
- CellType,
29
- CustomFormattingType,
30
- DurationStyle,
31
- DurationUnits,
32
- FormattingType,
33
- FormatType,
34
- )
35
- from numbers_parser.currencies import CURRENCY_SYMBOLS
36
- from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
37
- from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
38
- from numbers_parser.numbers_cache import Cacheable, cache
39
- from numbers_parser.numbers_uuid import NumbersUUID
40
-
41
- logger = logging.getLogger(numbers_parser_name)
42
- debug = logger.debug
43
-
44
-
45
- class CellStorage(Cacheable):
46
- # 15% performance uplift for using slots
47
- __slots__ = (
48
- "buffer",
49
- "datetime",
50
- "model",
51
- "table_id",
52
- "row",
53
- "col",
54
- "value",
55
- "type",
56
- "d128",
57
- "double",
58
- "seconds",
59
- "string_id",
60
- "rich_id",
61
- "cell_style_id",
62
- "text_style_id",
63
- # "cond_style_id",
64
- # "cond_rule_style_id",
65
- "formula_id",
66
- "control_id",
67
- "formula_error_id",
68
- "suggest_id",
69
- "num_format_id",
70
- "currency_format_id",
71
- "date_format_id",
72
- "duration_format_id",
73
- "text_format_id",
74
- "bool_format_id",
75
- # "comment_id",
76
- # "import_warning_id",
77
- "is_currency",
78
- "_cache",
79
- )
80
-
81
- # @profile
82
- def __init__( # noqa: PLR0912, PLR0913, PLR0915
83
- self, model: object, table_id: int, buffer, row, col
84
- ):
85
- self.buffer = buffer
86
- self.model = model
87
- self.table_id = table_id
88
- self.row = row
89
- self.col = col
90
-
91
- self.d128 = None
92
- self.double = None
93
- self.seconds = None
94
- self.string_id = None
95
- self.rich_id = None
96
- self.cell_style_id = None
97
- self.text_style_id = None
98
- # self.cond_style_id = None
99
- # self.cond_rule_style_id = None
100
- self.formula_id = None
101
- self.control_id = None
102
- self.formula_error_id = None
103
- self.suggest_id = None
104
- self.num_format_id = None
105
- self.currency_format_id = None
106
- self.date_format_id = None
107
- self.duration_format_id = None
108
- self.text_format_id = None
109
- self.bool_format_id = None
110
- # self.comment_id = None
111
- # self.import_warning_id = None
112
- self.is_currency = False
113
-
114
- if buffer is None:
115
- return
116
-
117
- version = buffer[0]
118
- if version != 5:
119
- raise UnsupportedError(f"Cell storage version {version} is unsupported")
120
-
121
- offset = 12
122
- flags = unpack("<i", buffer[8:12])[0]
123
-
124
- if flags & 0x1:
125
- self.d128 = unpack_decimal128(buffer[offset : offset + 16])
126
- offset += 16
127
- if flags & 0x2:
128
- self.double = unpack("<d", buffer[offset : offset + 8])[0]
129
- offset += 8
130
- if flags & 0x4:
131
- self.seconds = unpack("<d", buffer[offset : offset + 8])[0]
132
- offset += 8
133
- if flags & 0x8:
134
- self.string_id = unpack("<i", buffer[offset : offset + 4])[0]
135
- offset += 4
136
- if flags & 0x10:
137
- self.rich_id = unpack("<i", buffer[offset : offset + 4])[0]
138
- offset += 4
139
- if flags & 0x20:
140
- self.cell_style_id = unpack("<i", buffer[offset : offset + 4])[0]
141
- offset += 4
142
- if flags & 0x40:
143
- self.text_style_id = unpack("<i", buffer[offset : offset + 4])[0]
144
- offset += 4
145
- if flags & 0x80:
146
- # self.cond_style_id = unpack("<i", buffer[offset : offset + 4])[0]
147
- offset += 4
148
- # if flags & 0x100:
149
- # self.cond_rule_style_id = unpack("<i", buffer[offset : offset + 4])[0]
150
- # offset += 4
151
- if flags & 0x200:
152
- self.formula_id = unpack("<i", buffer[offset : offset + 4])[0]
153
- offset += 4
154
- if flags & 0x400:
155
- self.control_id = unpack("<i", buffer[offset : offset + 4])[0]
156
- offset += 4
157
- # if flags & 0x800:
158
- # self.formula_error_id = unpack("<i", buffer[offset : offset + 4])[0]
159
- # offset += 4
160
- if flags & 0x1000:
161
- self.suggest_id = unpack("<i", buffer[offset : offset + 4])[0]
162
- offset += 4
163
- # Skip unused flags
164
- offset += 4 * bin(flags & 0x900).count("1")
165
- #
166
- if flags & 0x2000:
167
- self.num_format_id = unpack("<i", buffer[offset : offset + 4])[0]
168
- offset += 4
169
- if flags & 0x4000:
170
- self.currency_format_id = unpack("<i", buffer[offset : offset + 4])[0]
171
- offset += 4
172
- if flags & 0x8000:
173
- self.date_format_id = unpack("<i", buffer[offset : offset + 4])[0]
174
- offset += 4
175
- if flags & 0x10000:
176
- self.duration_format_id = unpack("<i", buffer[offset : offset + 4])[0]
177
- offset += 4
178
- if flags & 0x20000:
179
- self.text_format_id = unpack("<i", buffer[offset : offset + 4])[0]
180
- offset += 4
181
- if flags & 0x40000:
182
- self.bool_format_id = unpack("<i", buffer[offset : offset + 4])[0]
183
- offset += 4
184
- # if flags & 0x80000:
185
- # self.comment_id = unpack("<i", buffer[offset : offset + 4])[0]
186
- # offset += 4
187
- # if flags & 0x100000:
188
- # self.import_warning_id = unpack("<i", buffer[offset : offset + 4])[0]
189
- # offset += 4
190
-
191
- cell_type = buffer[1]
192
- if cell_type == TSTArchives.genericCellType:
193
- self.type = CellType.EMPTY
194
- self.value = None
195
- elif cell_type == TSTArchives.numberCellType:
196
- self.value = self.d128
197
- self.type = CellType.NUMBER
198
- elif cell_type == TSTArchives.textCellType:
199
- self.value = self.model.table_string(table_id, self.string_id)
200
- self.type = CellType.TEXT
201
- elif cell_type == TSTArchives.dateCellType:
202
- self.value = EPOCH + duration(seconds=self.seconds)
203
- self.datetime = self.value
204
- self.type = CellType.DATE
205
- elif cell_type == TSTArchives.boolCellType:
206
- self.value = self.double > 0.0
207
- self.type = CellType.BOOL
208
- elif cell_type == TSTArchives.durationCellType:
209
- self.value = self.double
210
- self.type = CellType.DURATION
211
- elif cell_type == TSTArchives.formulaErrorCellType:
212
- self.value = None
213
- self.type = CellType.ERROR
214
- elif cell_type == TSTArchives.automaticCellType:
215
- self.value = self.model.table_rich_text(self.table_id, self.rich_id)
216
- self.type = CellType.RICH_TEXT
217
- elif cell_type == CURRENCY_CELL_TYPE:
218
- self.value = self.d128
219
- self.is_currency = True
220
- self.type = CellType.NUMBER
221
- else:
222
- raise UnsupportedError(f"Cell type ID {cell_type} is not recognised")
223
-
224
- if logging.getLogger(__package__).level == logging.DEBUG:
225
- # Guard to reduce expense of computing fields
226
- extras = unpack("<H", buffer[6:8])[0]
227
- table_name = model.table_name(table_id)
228
- sheet_name = model.sheet_name(model.table_id_to_sheet_id(table_id))
229
- fields = [
230
- f"{x}=" + str(getattr(self, x)) if getattr(self, x) is not None else None
231
- for x in self.__slots__
232
- if x.endswith("_id")
233
- ]
234
- fields = ", ".join([x for x in fields if x if not None])
235
- debug(
236
- "%s@%s@[%d,%d]: table_id=%d, type=%s, value=%s, flags=%08x, extras=%04x, %s",
237
- sheet_name,
238
- table_name,
239
- row,
240
- col,
241
- table_id,
242
- self.type.name,
243
- self.value,
244
- flags,
245
- extras,
246
- fields,
247
- )
248
-
249
- def update_value(self, value, cell: object) -> None:
250
- if cell._type == TSTArchives.numberCellType:
251
- self.d128 = value
252
- self.type = CellType.NUMBER
253
- elif cell._type == TSTArchives.dateCellType:
254
- self.datetime = value
255
- self.type = CellType.DATE
256
- elif cell._type == TSTArchives.durationCellType:
257
- self.double = value
258
- self.value = value
259
-
260
- @property
261
- def formatted(self):
262
- if self.duration_format_id is not None and self.double is not None:
263
- return self.duration_format()
264
- elif self.date_format_id is not None and self.seconds is not None:
265
- return self.date_format()
266
- elif (
267
- self.text_format_id is not None
268
- or self.num_format_id is not None
269
- or self.currency_format_id is not None
270
- or self.bool_format_id is not None
271
- ):
272
- return self.custom_format()
273
- else:
274
- return str(self.value)
275
-
276
- @property
277
- @cache(num_args=0)
278
- def image_data(self) -> Tuple[bytes, str]:
279
- """Return the background image data for a cell or None if no image."""
280
- if self.cell_style_id is None:
281
- return None
282
- style = self.model.table_style(self.table_id, self.cell_style_id)
283
- if not style.cell_properties.cell_fill.HasField("image"):
284
- return None
285
-
286
- image_id = style.cell_properties.cell_fill.image.imagedata.identifier
287
- datas = self.model.objects[PACKAGE_ID].datas
288
- stored_filename = [x.file_name for x in datas if x.identifier == image_id][0]
289
- preferred_filename = [x.preferred_file_name for x in datas if x.identifier == image_id][0]
290
- all_paths = self.model.objects.file_store.keys()
291
- image_pathnames = [x for x in all_paths if x == f"Data/{stored_filename}"]
292
- if len(image_pathnames) == 0:
293
- warn(
294
- f"Cannot find file '{preferred_filename}' in Numbers archive",
295
- RuntimeWarning,
296
- stacklevel=3,
297
- )
298
- else:
299
- return (self.model.objects.file_store[image_pathnames[0]], preferred_filename)
300
-
301
- def custom_format(self) -> str: # noqa: PLR0911
302
- if self.text_format_id is not None and self.type == CellType.TEXT:
303
- format = self.model.table_format(self.table_id, self.text_format_id)
304
- elif self.currency_format_id is not None:
305
- format = self.model.table_format(self.table_id, self.currency_format_id)
306
- elif self.bool_format_id is not None and self.type == CellType.BOOL:
307
- format = self.model.table_format(self.table_id, self.bool_format_id)
308
- elif self.num_format_id is not None:
309
- format = self.model.table_format(self.table_id, self.num_format_id)
310
- else:
311
- return str(self.value)
312
-
313
- debug("custom_format: @[%d,%d]: format_type=%s, ", self.row, self.col, format.format_type)
314
-
315
- if format.HasField("custom_uid"):
316
- format_uuid = NumbersUUID(format.custom_uid).hex
317
- format_map = self.model.custom_format_map()
318
- custom_format = format_map[format_uuid].default_format
319
- if custom_format.requires_fraction_replacement:
320
- formatted_value = format_fraction(self.d128, custom_format)
321
- elif custom_format.format_type == FormatType.CUSTOM_TEXT:
322
- formatted_value = decode_text_format(
323
- custom_format,
324
- self.model.table_string(self.table_id, self.string_id),
325
- )
326
- else:
327
- formatted_value = decode_number_format(
328
- custom_format, self.d128, format_map[format_uuid].name
329
- )
330
- elif format.format_type == FormatType.DECIMAL:
331
- return format_decimal(self.d128, format)
332
- elif format.format_type == FormatType.CURRENCY:
333
- return format_currency(self.d128, format)
334
- elif format.format_type == FormatType.BOOLEAN:
335
- return "TRUE" if self.value else "FALSE"
336
- elif format.format_type == FormatType.PERCENT:
337
- return format_decimal(self.d128 * 100, format, percent=True)
338
- elif format.format_type == FormatType.BASE:
339
- return format_base(self.d128, format)
340
- elif format.format_type == FormatType.FRACTION:
341
- return format_fraction(self.d128, format)
342
- elif format.format_type == FormatType.SCIENTIFIC:
343
- return format_scientific(self.d128, format)
344
- elif format.format_type == FormatType.CHECKBOX:
345
- return CHECKBOX_TRUE_VALUE if self.value else CHECKBOX_FALSE_VALUE
346
- elif format.format_type == FormatType.RATING:
347
- return STAR_RATING_VALUE * int(self.d128)
348
- else:
349
- formatted_value = str(self.value)
350
- return formatted_value
351
-
352
- def date_format(self) -> str:
353
- format = self.model.table_format(self.table_id, self.date_format_id)
354
- if format.HasField("custom_uid"):
355
- format_uuid = NumbersUUID(format.custom_uid).hex
356
- format_map = self.model.custom_format_map()
357
- custom_format = format_map[format_uuid].default_format
358
- custom_format_string = custom_format.custom_format_string
359
- if custom_format.format_type == FormatType.CUSTOM_DATE:
360
- formatted_value = decode_date_format(custom_format_string, self.datetime)
361
- else:
362
- warn(
363
- f"Unexpected custom format type {custom_format.format_type}",
364
- UnsupportedWarning,
365
- stacklevel=3,
366
- )
367
- return ""
368
- else:
369
- formatted_value = decode_date_format(format.date_time_format, self.datetime)
370
- return formatted_value
371
-
372
- def duration_format(self) -> str:
373
- format = self.model.table_format(self.table_id, self.duration_format_id)
374
- debug(
375
- "duration_format: @[%d,%d]: table_id=%d, duration_format_id=%d, duration_style=%s",
376
- self.row,
377
- self.col,
378
- self.table_id,
379
- self.duration_format_id,
380
- format.duration_style,
381
- )
382
-
383
- duration_style = format.duration_style
384
- unit_largest = format.duration_unit_largest
385
- unit_smallest = format.duration_unit_smallest
386
- if format.use_automatic_duration_units:
387
- unit_smallest, unit_largest = auto_units(self.double, format)
388
-
389
- d = self.double
390
- dd = int(self.double)
391
- dstr = []
392
-
393
- def unit_in_range(largest, smallest, unit_type):
394
- return largest <= unit_type and smallest >= unit_type
395
-
396
- def pad_digits(d, largest, smallest, unit_type):
397
- return (largest == unit_type and smallest == unit_type) or d >= 10
398
-
399
- if unit_largest == DurationUnits.WEEK:
400
- dd = int(d / SECONDS_IN_WEEK)
401
- if unit_smallest != DurationUnits.WEEK:
402
- d -= SECONDS_IN_WEEK * dd
403
- dstr.append(str(dd) + unit_format("week", dd, duration_style))
404
-
405
- if unit_in_range(unit_largest, unit_smallest, DurationUnits.DAY):
406
- dd = int(d / SECONDS_IN_DAY)
407
- if unit_smallest > DurationUnits.DAY:
408
- d -= SECONDS_IN_DAY * dd
409
- dstr.append(str(dd) + unit_format("day", dd, duration_style))
410
-
411
- if unit_in_range(unit_largest, unit_smallest, DurationUnits.HOUR):
412
- dd = int(d / SECONDS_IN_HOUR)
413
- if unit_smallest > DurationUnits.HOUR:
414
- d -= SECONDS_IN_HOUR * dd
415
- dstr.append(str(dd) + unit_format("hour", dd, duration_style))
416
-
417
- if unit_in_range(unit_largest, unit_smallest, DurationUnits.MINUTE):
418
- dd = int(d / 60)
419
- if unit_smallest > DurationUnits.MINUTE:
420
- d -= 60 * dd
421
- if duration_style == DurationStyle.COMPACT:
422
- pad = pad_digits(dd, unit_smallest, unit_largest, DurationUnits.MINUTE)
423
- dstr.append(("" if pad else "0") + str(dd))
424
- else:
425
- dstr.append(str(dd) + unit_format("minute", dd, duration_style))
426
-
427
- if unit_in_range(unit_largest, unit_smallest, DurationUnits.SECOND):
428
- dd = int(d)
429
- if unit_smallest > DurationUnits.SECOND:
430
- d -= dd
431
- if duration_style == DurationStyle.COMPACT:
432
- pad = pad_digits(dd, unit_smallest, unit_largest, DurationUnits.SECOND)
433
- dstr.append(("" if pad else "0") + str(dd))
434
- else:
435
- dstr.append(str(dd) + unit_format("second", dd, duration_style))
436
-
437
- if unit_smallest >= DurationUnits.MILLISECOND:
438
- dd = int(round(1000 * d))
439
- if duration_style == DurationStyle.COMPACT:
440
- padding = "0" if dd >= 10 else "00"
441
- padding = "" if dd >= 100 else padding
442
- dstr.append(f"{padding}{dd}")
443
- else:
444
- dstr.append(str(dd) + unit_format("millisecond", dd, duration_style, "ms"))
445
- duration_str = (":" if duration_style == 0 else " ").join(dstr)
446
- if duration_style == DurationStyle.COMPACT:
447
- duration_str = re.sub(r":(\d\d\d)$", r".\1", duration_str)
448
-
449
- return duration_str
450
-
451
- def set_formatting(
452
- self,
453
- format_id: int,
454
- format_type: Union[FormattingType, CustomFormattingType],
455
- control_id: int = None,
456
- is_currency: bool = False,
457
- ) -> None:
458
- self.is_currency = is_currency
459
- if format_type == FormattingType.CURRENCY:
460
- self.currency_format_id = format_id
461
- elif format_type == FormattingType.TICKBOX:
462
- self.bool_format_id = format_id
463
- self.control_id = control_id
464
- elif format_type == FormattingType.RATING:
465
- self.num_format_id = format_id
466
- self.control_id = control_id
467
- elif format_type in [FormattingType.SLIDER, FormattingType.STEPPER]:
468
- if is_currency:
469
- self.currency_format_id = format_id
470
- else:
471
- self.num_format_id = format_id
472
- self.control_id = control_id
473
- elif format_type == FormattingType.POPUP:
474
- self.text_format_id = format_id
475
- self.control_id = control_id
476
- elif format_type in [FormattingType.DATETIME, CustomFormattingType.DATETIME]:
477
- self.date_format_id = format_id
478
- elif format_type in [FormattingType.TEXT, CustomFormattingType.TEXT]:
479
- self.text_format_id = format_id
480
- else:
481
- self.num_format_id = format_id
482
-
483
-
484
- def unpack_decimal128(buffer: bytearray) -> float:
485
- exp = (((buffer[15] & 0x7F) << 7) | (buffer[14] >> 1)) - 0x1820
486
- mantissa = buffer[14] & 1
487
- for i in range(13, -1, -1):
488
- mantissa = mantissa * 256 + buffer[i]
489
- sign = 1 if buffer[15] & 0x80 else 0
490
- if sign == 1:
491
- mantissa = -mantissa
492
- value = mantissa * 10**exp
493
- return float(value)
494
-
495
-
496
- def decode_date_format_field(field: str, value: datetime) -> str:
497
- if field in DATETIME_FIELD_MAP:
498
- s = DATETIME_FIELD_MAP[field]
499
- if callable(s):
500
- return s(value)
501
- else:
502
- return value.strftime(s)
503
- else:
504
- warn(f"Unsupported field code '{field}'", UnsupportedWarning, stacklevel=4)
505
- return ""
506
-
507
-
508
- def decode_date_format(format, value):
509
- """Parse a custom date format string and return a formatted datetime value."""
510
- chars = [*format]
511
- index = 0
512
- in_string = False
513
- in_field = False
514
- result = ""
515
- field = ""
516
- while index < len(chars):
517
- current_char = chars[index]
518
- next_char = chars[index + 1] if index < len(chars) - 1 else None
519
- if current_char == "'":
520
- if next_char is None:
521
- break
522
- elif chars[index + 1] == "'":
523
- result += "'"
524
- index += 2
525
- elif in_string:
526
- in_string = False
527
- index += 1
528
- else:
529
- in_string = True
530
- if in_field:
531
- result += decode_date_format_field(field, value)
532
- in_field = False
533
- index += 1
534
- elif in_string:
535
- result += current_char
536
- index += 1
537
- elif not current_char.isalpha():
538
- if in_field:
539
- result += decode_date_format_field(field, value)
540
- in_field = False
541
- result += current_char
542
- index += 1
543
- elif in_field:
544
- field += current_char
545
- index += 1
546
- else:
547
- in_field = True
548
- field = current_char
549
- index += 1
550
- if in_field:
551
- result += decode_date_format_field(field, value)
552
-
553
- return result
554
-
555
-
556
- def decode_text_format(format, value: str):
557
- """Parse a custom date format string and return a formatted number value."""
558
- custom_format_string = format.custom_format_string
559
- return custom_format_string.replace(CUSTOM_TEXT_PLACEHOLDER, value)
560
-
561
-
562
- def expand_quotes(value: str) -> str:
563
- chars = [*value]
564
- index = 0
565
- in_string = False
566
- formatted_value = ""
567
- while index < len(chars):
568
- current_char = chars[index]
569
- next_char = chars[index + 1] if index < len(chars) - 1 else None
570
- if current_char == "'":
571
- if next_char is None:
572
- break
573
- elif chars[index + 1] == "'":
574
- formatted_value += "'"
575
- index += 2
576
- elif in_string:
577
- in_string = False
578
- index += 1
579
- else:
580
- in_string = True
581
- index += 1
582
- else:
583
- formatted_value += current_char
584
- index += 1
585
- return formatted_value
586
-
587
-
588
- def decode_number_format(format, value, name): # noqa: PLR0912
589
- """Parse a custom date format string and return a formatted number value."""
590
- custom_format_string = format.custom_format_string
591
- value *= format.scale_factor
592
- if "%" in custom_format_string and format.scale_factor == 1.0:
593
- # Per cent scale has 100x but % does not
594
- value *= 100.0
595
-
596
- if format.currency_code != "":
597
- # Replace currency code with symbol and no-break space
598
- custom_format_string = custom_format_string.replace(
599
- "\u00a4", format.currency_code + "\u00a0"
600
- )
601
-
602
- if (match := re.search(r"([#0.,]+(E[+]\d+)?)", custom_format_string)) is None:
603
- warn(
604
- f"Can't parse format string '{custom_format_string}'; skipping",
605
- UnsupportedWarning,
606
- stacklevel=1,
607
- )
608
- return custom_format_string
609
- format_spec = match.group(1)
610
- scientific_spec = match.group(2)
611
-
612
- if format_spec[0] == ".":
613
- (int_part, dec_part) = ("", format_spec[1:])
614
- elif "." in custom_format_string:
615
- (int_part, dec_part) = format_spec.split(".")
616
- else:
617
- (int_part, dec_part) = (format_spec, "")
618
-
619
- if scientific_spec is not None:
620
- # Scientific notation
621
- formatted_value = f"{value:.{len(dec_part) - 4}E}"
622
- formatted_value = custom_format_string.replace(format_spec, formatted_value)
623
- return expand_quotes(formatted_value)
624
-
625
- num_decimals = len(dec_part)
626
- if num_decimals > 0:
627
- if dec_part[0] == "#":
628
- dec_pad = None
629
- elif format.num_nonspace_decimal_digits > 0:
630
- dec_pad = CellPadding.ZERO
631
- else:
632
- dec_pad = CellPadding.SPACE
633
- else:
634
- dec_pad = None
635
- dec_width = num_decimals
636
-
637
- (integer, decimal) = str(float(value)).split(".")
638
- if num_decimals > 0:
639
- integer = int(integer)
640
- decimal = round(float(f"0.{decimal}"), num_decimals)
641
- else:
642
- integer = round(value)
643
- decimal = float(f"0.{decimal}")
644
-
645
- num_integers = len(int_part.replace(",", ""))
646
- if not format.show_thousands_separator:
647
- int_part = int_part.replace(",", "")
648
- if num_integers > 0:
649
- if int_part[0] == "#":
650
- int_pad = None
651
- int_width = len(int_part)
652
- elif format.num_nonspace_integer_digits > 0:
653
- int_pad = CellPadding.ZERO
654
- if format.show_thousands_separator:
655
- num_commas = int(math.floor(math.log10(integer)) / 3) if integer != 0 else 0
656
- num_commas = max([num_commas, int((num_integers - 1) / 3)])
657
- int_width = num_integers + num_commas
658
- else:
659
- int_width = num_integers
660
- else:
661
- int_pad = CellPadding.SPACE
662
- int_width = len(int_part)
663
- else:
664
- int_pad = None
665
- int_width = num_integers
666
-
667
- # value_1 = str(value).split(".")[0]
668
- # value_2 = sigfig.round(str(value).split(".")[1], sigfig=MAX_SIGNIFICANT_DIGITS, warn=False)
669
- # int_pad_space_as_zero = (
670
- # num_integers > 0
671
- # and num_decimals > 0
672
- # and int_pad == CellPadding.SPACE
673
- # and dec_pad is None
674
- # and num_integers > len(value_1)
675
- # and num_decimals > len(value_2)
676
- # )
677
- int_pad_space_as_zero = False
678
-
679
- # Formatting integer zero:
680
- # Blank (padded if needed) if int_pad is SPACE and no decimals
681
- # No leading zero if:
682
- # int_pad is NONE, dec_pad is SPACE
683
- # int_pad is SPACE, dec_pad is SPACE
684
- # int_pad is SPACE, dec_pad is ZERO
685
- # int_pad is SPACE, dec_pad is NONE if num decimals < decimals length
686
- if integer == 0 and int_pad == CellPadding.SPACE and num_decimals == 0:
687
- formatted_value = "".rjust(int_width)
688
- elif integer == 0 and int_pad is None and dec_pad == CellPadding.SPACE:
689
- formatted_value = ""
690
- elif integer == 0 and int_pad == CellPadding.SPACE and dec_pad is not None:
691
- formatted_value = "".rjust(int_width)
692
- elif (
693
- integer == 0
694
- and int_pad == CellPadding.SPACE
695
- and dec_pad is None
696
- and len(str(decimal)) > num_decimals
697
- ):
698
- formatted_value = "".rjust(int_width)
699
- elif int_pad_space_as_zero or int_pad == CellPadding.ZERO:
700
- if format.show_thousands_separator:
701
- formatted_value = f"{integer:0{int_width},}"
702
- else:
703
- formatted_value = f"{integer:0{int_width}}"
704
- elif int_pad == CellPadding.SPACE:
705
- if format.show_thousands_separator:
706
- formatted_value = f"{integer:,}".rjust(int_width)
707
- else:
708
- formatted_value = str(integer).rjust(int_width)
709
- elif format.show_thousands_separator:
710
- formatted_value = f"{integer:,}"
711
- else:
712
- formatted_value = str(integer)
713
-
714
- if num_decimals:
715
- if dec_pad == CellPadding.ZERO or (dec_pad == CellPadding.SPACE and num_integers == 0):
716
- formatted_value += "." + f"{decimal:,.{dec_width}f}"[2:]
717
- elif dec_pad == CellPadding.SPACE and decimal == 0 and num_integers > 0:
718
- formatted_value += ".".ljust(dec_width + 1)
719
- elif dec_pad == CellPadding.SPACE:
720
- decimal_str = str(decimal)[2:]
721
- formatted_value += "." + decimal_str.ljust(dec_width)
722
- elif decimal or num_integers == 0:
723
- formatted_value += "." + str(decimal)[2:]
724
-
725
- formatted_value = custom_format_string.replace(format_spec, formatted_value)
726
- return expand_quotes(formatted_value)
727
-
728
-
729
- def format_decimal(value: float, format, percent: bool = False) -> str:
730
- if value is None:
731
- return ""
732
- if value < 0 and format.negative_style == 1:
733
- accounting_style = False
734
- value = -value
735
- elif value < 0 and format.negative_style >= 2:
736
- accounting_style = True
737
- value = -value
738
- else:
739
- accounting_style = False
740
- thousands = "," if format.show_thousands_separator else ""
741
-
742
- if value.is_integer() and format.decimal_places >= DECIMAL_PLACES_AUTO:
743
- formatted_value = f"{int(value):{thousands}}"
744
- else:
745
- if format.decimal_places >= DECIMAL_PLACES_AUTO:
746
- formatted_value = str(sigfig.round(value, MAX_SIGNIFICANT_DIGITS, warn=False))
747
- else:
748
- formatted_value = sigfig.round(value, MAX_SIGNIFICANT_DIGITS, type=str, warn=False)
749
- formatted_value = sigfig.round(
750
- formatted_value, decimals=format.decimal_places, type=str
751
- )
752
- if format.show_thousands_separator:
753
- formatted_value = sigfig.round(formatted_value, spacer=",", spacing=3, type=str)
754
- try:
755
- (integer, decimal) = formatted_value.split(".")
756
- formatted_value = integer + "." + decimal.replace(",", "")
757
- except ValueError:
758
- pass
759
-
760
- if percent:
761
- formatted_value += "%"
762
-
763
- if accounting_style:
764
- return f"({formatted_value})"
765
- else:
766
- return formatted_value
767
-
768
-
769
- def format_currency(value: float, format) -> str:
770
- formatted_value = format_decimal(value, format)
771
- if format.currency_code in CURRENCY_SYMBOLS:
772
- symbol = CURRENCY_SYMBOLS[format.currency_code]
773
- else:
774
- symbol = format.currency_code + " "
775
- if format.use_accounting_style and value < 0:
776
- return f"{symbol}\t({formatted_value[1:]})"
777
- elif format.use_accounting_style:
778
- return f"{symbol}\t{formatted_value}"
779
- else:
780
- return symbol + formatted_value
781
-
782
-
783
- INT_TO_BASE_CHAR = [str(x) for x in range(0, 10)] + [chr(x) for x in range(ord("A"), ord("Z") + 1)]
784
-
785
-
786
- def invert_bit_str(value: str) -> str:
787
- """Invert a binary value"""
788
- return "".join(["0" if b == "1" else "1" for b in value])
789
-
790
-
791
- def twos_complement(value: int, base: int) -> str:
792
- """Calculate the twos complement of a negative integer with minimum 32-bit precision"""
793
- num_bits = max([32, math.ceil(math.log2(abs(value))) + 1])
794
- bin_value = bin(abs(value))[2:]
795
- inverted_bin_value = invert_bit_str(bin_value).rjust(num_bits, "1")
796
- twos_complement_dec = int(inverted_bin_value, 2) + 1
797
-
798
- if base == 2:
799
- return bin(twos_complement_dec)[2:].rjust(num_bits, "1")
800
- elif base == 8:
801
- return oct(twos_complement_dec)[2:]
802
- else:
803
- return hex(twos_complement_dec)[2:].upper()
804
-
805
-
806
- def format_base(value: float, format) -> str:
807
- if value == 0:
808
- return "0".zfill(format.base_places)
809
-
810
- value = round(value)
811
-
812
- is_negative = False
813
- if not format.base_use_minus_sign and format.base in [2, 8, 16]:
814
- if value < 0:
815
- return twos_complement(value, format.base)
816
- else:
817
- value = abs(value)
818
- elif value < 0:
819
- is_negative = True
820
- value = abs(value)
821
-
822
- formatted_value = []
823
- while value:
824
- formatted_value.append(int(value % format.base))
825
- value //= format.base
826
- formatted_value = "".join([INT_TO_BASE_CHAR[x] for x in formatted_value[::-1]])
827
-
828
- if is_negative:
829
- return "-" + formatted_value.zfill(format.base_places)
830
- else:
831
- return formatted_value.zfill(format.base_places)
832
-
833
-
834
- def format_fraction_parts_to(whole: int, numerator: int, denominator: int):
835
- if whole > 0:
836
- if numerator == 0:
837
- return str(whole)
838
- else:
839
- return f"{whole} {numerator}/{denominator}"
840
- elif numerator == 0:
841
- return "0"
842
- elif numerator == denominator:
843
- return "1"
844
- return f"{numerator}/{denominator}"
845
-
846
-
847
- def float_to_fraction(value: float, denominator: int) -> str:
848
- """Convert a float to the nearest fraction and return as a string."""
849
- whole = int(value)
850
- numerator = round(denominator * (value - whole))
851
- return format_fraction_parts_to(whole, numerator, denominator)
852
-
853
-
854
- def float_to_n_digit_fraction(value: float, max_digits: int) -> str:
855
- """Convert a float to a fraction of a maxinum number of digits
856
- and return as a string.
857
- """
858
- max_denominator = 10**max_digits - 1
859
- (numerator, denominator) = (
860
- Fraction.from_float(value).limit_denominator(max_denominator).as_integer_ratio()
861
- )
862
- whole = int(value)
863
- numerator -= whole * denominator
864
- return format_fraction_parts_to(whole, numerator, denominator)
865
-
866
-
867
- def format_fraction(value: float, format) -> str:
868
- accuracy = format.fraction_accuracy
869
- if accuracy & 0xFF000000:
870
- num_digits = 0x100000000 - accuracy
871
- return float_to_n_digit_fraction(value, num_digits)
872
- else:
873
- return float_to_fraction(value, accuracy)
874
-
875
-
876
- def format_scientific(value: float, format) -> str:
877
- formatted_value = sigfig.round(value, sigfigs=MAX_SIGNIFICANT_DIGITS, warn=False)
878
- return f"{formatted_value:.{format.decimal_places}E}"
879
-
880
-
881
- def unit_format(unit: str, value: int, style: int, abbrev: str = None):
882
- plural = "" if value == 1 else "s"
883
- if abbrev is None:
884
- abbrev = unit[0]
885
- if style == DurationStyle.COMPACT:
886
- return ""
887
- elif style == DurationStyle.SHORT:
888
- return f"{abbrev}"
889
- else:
890
- return f" {unit}" + plural
891
-
892
-
893
- def auto_units(cell_value, format):
894
- unit_largest = format.duration_unit_largest
895
- unit_smallest = format.duration_unit_smallest
896
-
897
- if cell_value == 0:
898
- unit_largest = DurationUnits.DAY
899
- unit_smallest = DurationUnits.DAY
900
- else:
901
- if cell_value >= SECONDS_IN_WEEK:
902
- unit_largest = DurationUnits.WEEK
903
- elif cell_value >= SECONDS_IN_DAY:
904
- unit_largest = DurationUnits.DAY
905
- elif cell_value >= SECONDS_IN_HOUR:
906
- unit_largest = DurationUnits.HOUR
907
- elif cell_value >= 60:
908
- unit_largest = DurationUnits.MINUTE
909
- elif cell_value >= 1:
910
- unit_largest = DurationUnits.SECOND
911
- else:
912
- unit_largest = DurationUnits.MILLISECOND
913
-
914
- if math.floor(cell_value) != cell_value:
915
- unit_smallest = DurationUnits.MILLISECOND
916
- elif cell_value % 60:
917
- unit_smallest = DurationUnits.SECOND
918
- elif cell_value % SECONDS_IN_HOUR:
919
- unit_smallest = DurationUnits.MINUTE
920
- elif cell_value % SECONDS_IN_DAY:
921
- unit_smallest = DurationUnits.HOUR
922
- elif cell_value % SECONDS_IN_WEEK:
923
- unit_smallest = DurationUnits.DAY
924
- if unit_smallest < unit_largest:
925
- unit_smallest = unit_largest
926
-
927
- return unit_smallest, unit_largest