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.
- numbers_parser/__init__.py +8 -23
- numbers_parser/cell.py +1152 -165
- numbers_parser/constants.py +8 -0
- numbers_parser/containers.py +1 -1
- numbers_parser/document.py +17 -15
- numbers_parser/model.py +89 -263
- {numbers_parser-4.10.1.dist-info → numbers_parser-4.10.3.dist-info}/METADATA +15 -12
- {numbers_parser-4.10.1.dist-info → numbers_parser-4.10.3.dist-info}/RECORD +11 -12
- numbers_parser/cell_storage.py +0 -927
- {numbers_parser-4.10.1.dist-info → numbers_parser-4.10.3.dist-info}/LICENSE.rst +0 -0
- {numbers_parser-4.10.1.dist-info → numbers_parser-4.10.3.dist-info}/WHEEL +0 -0
- {numbers_parser-4.10.1.dist-info → numbers_parser-4.10.3.dist-info}/entry_points.txt +0 -0
numbers_parser/cell_storage.py
DELETED
|
@@ -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
|