aspose-cells-foss 25.12.1__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.
- aspose/__init__.py +14 -0
- aspose/cells/__init__.py +31 -0
- aspose/cells/cell.py +350 -0
- aspose/cells/constants.py +44 -0
- aspose/cells/converters/__init__.py +13 -0
- aspose/cells/converters/csv_converter.py +55 -0
- aspose/cells/converters/json_converter.py +46 -0
- aspose/cells/converters/markdown_converter.py +453 -0
- aspose/cells/drawing/__init__.py +17 -0
- aspose/cells/drawing/anchor.py +172 -0
- aspose/cells/drawing/collection.py +233 -0
- aspose/cells/drawing/image.py +338 -0
- aspose/cells/formats.py +80 -0
- aspose/cells/formula/__init__.py +10 -0
- aspose/cells/formula/evaluator.py +360 -0
- aspose/cells/formula/functions.py +433 -0
- aspose/cells/formula/tokenizer.py +340 -0
- aspose/cells/io/__init__.py +27 -0
- aspose/cells/io/csv/__init__.py +8 -0
- aspose/cells/io/csv/reader.py +88 -0
- aspose/cells/io/csv/writer.py +98 -0
- aspose/cells/io/factory.py +138 -0
- aspose/cells/io/interfaces.py +48 -0
- aspose/cells/io/json/__init__.py +8 -0
- aspose/cells/io/json/reader.py +126 -0
- aspose/cells/io/json/writer.py +119 -0
- aspose/cells/io/md/__init__.py +8 -0
- aspose/cells/io/md/reader.py +161 -0
- aspose/cells/io/md/writer.py +334 -0
- aspose/cells/io/models.py +64 -0
- aspose/cells/io/xlsx/__init__.py +9 -0
- aspose/cells/io/xlsx/constants.py +312 -0
- aspose/cells/io/xlsx/image_writer.py +311 -0
- aspose/cells/io/xlsx/reader.py +284 -0
- aspose/cells/io/xlsx/writer.py +931 -0
- aspose/cells/plugins/__init__.py +6 -0
- aspose/cells/plugins/docling_backend/__init__.py +7 -0
- aspose/cells/plugins/docling_backend/backend.py +535 -0
- aspose/cells/plugins/markitdown_plugin/__init__.py +15 -0
- aspose/cells/plugins/markitdown_plugin/plugin.py +128 -0
- aspose/cells/range.py +210 -0
- aspose/cells/style.py +287 -0
- aspose/cells/utils/__init__.py +54 -0
- aspose/cells/utils/coordinates.py +68 -0
- aspose/cells/utils/exceptions.py +43 -0
- aspose/cells/utils/validation.py +102 -0
- aspose/cells/workbook.py +352 -0
- aspose/cells/worksheet.py +670 -0
- aspose_cells_foss-25.12.1.dist-info/METADATA +189 -0
- aspose_cells_foss-25.12.1.dist-info/RECORD +53 -0
- aspose_cells_foss-25.12.1.dist-info/WHEEL +5 -0
- aspose_cells_foss-25.12.1.dist-info/entry_points.txt +2 -0
- aspose_cells_foss-25.12.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Formula Evaluator - Evaluates Excel formulas using tokens and functions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, List, Union, Optional, TYPE_CHECKING
|
|
7
|
+
from .tokenizer import Tokenizer, Token
|
|
8
|
+
from .functions import BUILTIN_FUNCTIONS, ExcelError, ValueErrorExcel, DivisionByZeroError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..worksheet import Worksheet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CircularReferenceError(ExcelError):
|
|
15
|
+
"""Circular reference detected."""
|
|
16
|
+
def __str__(self):
|
|
17
|
+
return "#CIRCULAR!"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FormulaEvaluator:
|
|
21
|
+
"""Evaluates Excel formulas."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, worksheet: Optional['Worksheet'] = None):
|
|
24
|
+
self.worksheet = worksheet
|
|
25
|
+
self._evaluation_stack = set() # Track cells being evaluated to detect circular references
|
|
26
|
+
|
|
27
|
+
def evaluate(self, formula: str, cell_address: Optional[str] = None) -> Any:
|
|
28
|
+
"""
|
|
29
|
+
Evaluate a formula and return the result.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
formula: The formula to evaluate (with or without = prefix)
|
|
33
|
+
cell_address: Address of the cell containing this formula (for circular ref detection)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The evaluated result
|
|
37
|
+
"""
|
|
38
|
+
if not formula:
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
# Remove leading = if present
|
|
42
|
+
if formula.startswith('='):
|
|
43
|
+
formula = formula[1:]
|
|
44
|
+
|
|
45
|
+
if not formula.strip():
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
# Check for circular references
|
|
49
|
+
if cell_address and cell_address in self._evaluation_stack:
|
|
50
|
+
raise CircularReferenceError()
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
if cell_address:
|
|
54
|
+
self._evaluation_stack.add(cell_address)
|
|
55
|
+
|
|
56
|
+
# Tokenize the formula
|
|
57
|
+
tokenizer = Tokenizer('=' + formula)
|
|
58
|
+
tokens = list(tokenizer)
|
|
59
|
+
|
|
60
|
+
if not tokens:
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
# Evaluate the token stream
|
|
64
|
+
result = self._evaluate_tokens(tokens)
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
except ExcelError:
|
|
68
|
+
raise
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise ValueErrorExcel() from e
|
|
71
|
+
finally:
|
|
72
|
+
if cell_address:
|
|
73
|
+
self._evaluation_stack.discard(cell_address)
|
|
74
|
+
|
|
75
|
+
def _evaluate_tokens(self, tokens: List[Token]) -> Any:
|
|
76
|
+
"""Evaluate a list of tokens."""
|
|
77
|
+
if not tokens:
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
# Simple expression evaluation using shunting yard algorithm
|
|
81
|
+
output_queue = []
|
|
82
|
+
operator_stack = []
|
|
83
|
+
|
|
84
|
+
i = 0
|
|
85
|
+
while i < len(tokens):
|
|
86
|
+
token = tokens[i]
|
|
87
|
+
|
|
88
|
+
if token.type == Token.OPERAND:
|
|
89
|
+
output_queue.append(self._evaluate_operand(token))
|
|
90
|
+
|
|
91
|
+
elif token.type == Token.FUNCTION:
|
|
92
|
+
# Find matching closing parenthesis
|
|
93
|
+
func_name = token.value
|
|
94
|
+
if i + 1 < len(tokens) and tokens[i + 1].type == Token.SUBEXPR and tokens[i + 1].subtype == "OPEN":
|
|
95
|
+
args_start = i + 2
|
|
96
|
+
args_end = self._find_matching_paren(tokens, i + 1)
|
|
97
|
+
|
|
98
|
+
# Extract and evaluate arguments
|
|
99
|
+
args_tokens = tokens[args_start:args_end]
|
|
100
|
+
args = self._evaluate_function_args(args_tokens)
|
|
101
|
+
|
|
102
|
+
# Call the function
|
|
103
|
+
result = self._call_function(func_name, args)
|
|
104
|
+
output_queue.append(result)
|
|
105
|
+
|
|
106
|
+
i = args_end # Skip to after closing paren
|
|
107
|
+
else:
|
|
108
|
+
# Function without parentheses (like PI)
|
|
109
|
+
result = self._call_function(func_name, [])
|
|
110
|
+
output_queue.append(result)
|
|
111
|
+
|
|
112
|
+
elif token.type == Token.OPERATOR:
|
|
113
|
+
# Handle operators
|
|
114
|
+
while (operator_stack and
|
|
115
|
+
operator_stack[-1].type == Token.OPERATOR and
|
|
116
|
+
self._precedence(operator_stack[-1]) >= self._precedence(token)):
|
|
117
|
+
op = operator_stack.pop()
|
|
118
|
+
right = output_queue.pop() if output_queue else 0
|
|
119
|
+
left = output_queue.pop() if output_queue else 0
|
|
120
|
+
result = self._apply_operator(op, left, right)
|
|
121
|
+
output_queue.append(result)
|
|
122
|
+
operator_stack.append(token)
|
|
123
|
+
|
|
124
|
+
elif token.type == Token.SUBEXPR:
|
|
125
|
+
if token.subtype == "OPEN":
|
|
126
|
+
operator_stack.append(token)
|
|
127
|
+
elif token.subtype == "CLOSE":
|
|
128
|
+
while (operator_stack and
|
|
129
|
+
operator_stack[-1].type != Token.SUBEXPR):
|
|
130
|
+
op = operator_stack.pop()
|
|
131
|
+
right = output_queue.pop() if output_queue else 0
|
|
132
|
+
left = output_queue.pop() if output_queue else 0
|
|
133
|
+
result = self._apply_operator(op, left, right)
|
|
134
|
+
output_queue.append(result)
|
|
135
|
+
if operator_stack:
|
|
136
|
+
operator_stack.pop() # Remove opening paren
|
|
137
|
+
|
|
138
|
+
i += 1
|
|
139
|
+
|
|
140
|
+
# Process remaining operators
|
|
141
|
+
while operator_stack:
|
|
142
|
+
op = operator_stack.pop()
|
|
143
|
+
if op.type == Token.OPERATOR:
|
|
144
|
+
right = output_queue.pop() if output_queue else 0
|
|
145
|
+
left = output_queue.pop() if output_queue else 0
|
|
146
|
+
result = self._apply_operator(op, left, right)
|
|
147
|
+
output_queue.append(result)
|
|
148
|
+
|
|
149
|
+
return output_queue[0] if output_queue else ""
|
|
150
|
+
|
|
151
|
+
def _evaluate_operand(self, token: Token) -> Any:
|
|
152
|
+
"""Evaluate a single operand token."""
|
|
153
|
+
if token.subtype == Token.NUMBER:
|
|
154
|
+
try:
|
|
155
|
+
return int(token.value) if '.' not in token.value else float(token.value)
|
|
156
|
+
except ValueError:
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
elif token.subtype == Token.TEXT:
|
|
160
|
+
return token.value
|
|
161
|
+
|
|
162
|
+
elif token.subtype == Token.REFERENCE:
|
|
163
|
+
# Cell reference like A1, B2
|
|
164
|
+
return self._get_cell_value(token.value)
|
|
165
|
+
|
|
166
|
+
elif token.subtype == Token.RANGE:
|
|
167
|
+
# Range like A1:B2
|
|
168
|
+
return self._get_range_values(token.value)
|
|
169
|
+
|
|
170
|
+
elif token.subtype == Token.LOGICAL:
|
|
171
|
+
return token.value.upper() == "TRUE"
|
|
172
|
+
|
|
173
|
+
elif token.subtype == Token.ERROR:
|
|
174
|
+
raise ValueErrorExcel()
|
|
175
|
+
|
|
176
|
+
else:
|
|
177
|
+
return token.value
|
|
178
|
+
|
|
179
|
+
def _get_cell_value(self, cell_ref: str) -> Any:
|
|
180
|
+
"""Get value from a cell reference."""
|
|
181
|
+
if not self.worksheet:
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
# Parse cell reference (e.g., A1, $B$2)
|
|
185
|
+
match = re.match(r'(\$?)([A-Z]+)(\$?)(\d+)', cell_ref)
|
|
186
|
+
if not match:
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
col_letters = match.group(2)
|
|
190
|
+
row_num = int(match.group(4))
|
|
191
|
+
|
|
192
|
+
# Convert column letters to number
|
|
193
|
+
col_num = 0
|
|
194
|
+
for i, letter in enumerate(reversed(col_letters)):
|
|
195
|
+
col_num += (ord(letter) - ord('A') + 1) * (26 ** i)
|
|
196
|
+
|
|
197
|
+
# Get cell from worksheet
|
|
198
|
+
cell = self.worksheet._cells.get((row_num, col_num))
|
|
199
|
+
if not cell:
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
# If it's a formula, evaluate it recursively
|
|
203
|
+
if cell.is_formula():
|
|
204
|
+
try:
|
|
205
|
+
return self.evaluate(cell.formula, cell_ref)
|
|
206
|
+
except CircularReferenceError:
|
|
207
|
+
return "#CIRCULAR!"
|
|
208
|
+
|
|
209
|
+
return cell.value if cell.value is not None else 0
|
|
210
|
+
|
|
211
|
+
def _get_range_values(self, range_ref: str) -> List[Any]:
|
|
212
|
+
"""Get values from a range reference."""
|
|
213
|
+
if ':' not in range_ref:
|
|
214
|
+
return [self._get_cell_value(range_ref)]
|
|
215
|
+
|
|
216
|
+
start_ref, end_ref = range_ref.split(':')
|
|
217
|
+
|
|
218
|
+
# Parse start and end references
|
|
219
|
+
start_match = re.match(r'(\$?)([A-Z]+)(\$?)(\d+)', start_ref)
|
|
220
|
+
end_match = re.match(r'(\$?)([A-Z]+)(\$?)(\d+)', end_ref)
|
|
221
|
+
|
|
222
|
+
if not start_match or not end_match:
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
# Convert to column/row numbers
|
|
226
|
+
start_col = sum((ord(c) - ord('A') + 1) * (26 ** i)
|
|
227
|
+
for i, c in enumerate(reversed(start_match.group(2))))
|
|
228
|
+
start_row = int(start_match.group(4))
|
|
229
|
+
|
|
230
|
+
end_col = sum((ord(c) - ord('A') + 1) * (26 ** i)
|
|
231
|
+
for i, c in enumerate(reversed(end_match.group(2))))
|
|
232
|
+
end_row = int(end_match.group(4))
|
|
233
|
+
|
|
234
|
+
# Collect values from range
|
|
235
|
+
values = []
|
|
236
|
+
for row in range(min(start_row, end_row), max(start_row, end_row) + 1):
|
|
237
|
+
for col in range(min(start_col, end_col), max(start_col, end_col) + 1):
|
|
238
|
+
cell_ref = f"{self._col_num_to_letter(col)}{row}"
|
|
239
|
+
values.append(self._get_cell_value(cell_ref))
|
|
240
|
+
|
|
241
|
+
return values
|
|
242
|
+
|
|
243
|
+
def _col_num_to_letter(self, col_num: int) -> str:
|
|
244
|
+
"""Convert column number to letter."""
|
|
245
|
+
result = ""
|
|
246
|
+
while col_num > 0:
|
|
247
|
+
col_num -= 1
|
|
248
|
+
result = chr(col_num % 26 + ord('A')) + result
|
|
249
|
+
col_num //= 26
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
def _find_matching_paren(self, tokens: List[Token], start_pos: int) -> int:
|
|
253
|
+
"""Find the matching closing parenthesis."""
|
|
254
|
+
paren_count = 1
|
|
255
|
+
pos = start_pos + 1
|
|
256
|
+
|
|
257
|
+
while pos < len(tokens) and paren_count > 0:
|
|
258
|
+
token = tokens[pos]
|
|
259
|
+
if token.type == Token.SUBEXPR:
|
|
260
|
+
if token.subtype == "OPEN":
|
|
261
|
+
paren_count += 1
|
|
262
|
+
elif token.subtype == "CLOSE":
|
|
263
|
+
paren_count -= 1
|
|
264
|
+
pos += 1
|
|
265
|
+
|
|
266
|
+
return pos - 1 # Position of closing paren
|
|
267
|
+
|
|
268
|
+
def _evaluate_function_args(self, tokens: List[Token]) -> List[Any]:
|
|
269
|
+
"""Evaluate function arguments."""
|
|
270
|
+
if not tokens:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
args = []
|
|
274
|
+
current_arg = []
|
|
275
|
+
paren_depth = 0
|
|
276
|
+
|
|
277
|
+
for token in tokens:
|
|
278
|
+
if token.type == Token.ARGUMENT and paren_depth == 0:
|
|
279
|
+
# End of current argument
|
|
280
|
+
if current_arg:
|
|
281
|
+
arg_result = self._evaluate_tokens(current_arg)
|
|
282
|
+
args.append(arg_result)
|
|
283
|
+
current_arg = []
|
|
284
|
+
else:
|
|
285
|
+
if token.type == Token.SUBEXPR:
|
|
286
|
+
if token.subtype == "OPEN":
|
|
287
|
+
paren_depth += 1
|
|
288
|
+
elif token.subtype == "CLOSE":
|
|
289
|
+
paren_depth -= 1
|
|
290
|
+
current_arg.append(token)
|
|
291
|
+
|
|
292
|
+
# Add final argument
|
|
293
|
+
if current_arg:
|
|
294
|
+
arg_result = self._evaluate_tokens(current_arg)
|
|
295
|
+
args.append(arg_result)
|
|
296
|
+
|
|
297
|
+
return args
|
|
298
|
+
|
|
299
|
+
def _call_function(self, func_name: str, args: List[Any]) -> Any:
|
|
300
|
+
"""Call a built-in function."""
|
|
301
|
+
if func_name in BUILTIN_FUNCTIONS:
|
|
302
|
+
func = BUILTIN_FUNCTIONS[func_name]
|
|
303
|
+
try:
|
|
304
|
+
return func(*args)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
if isinstance(e, ExcelError):
|
|
307
|
+
return str(e)
|
|
308
|
+
else:
|
|
309
|
+
return "#VALUE!"
|
|
310
|
+
else:
|
|
311
|
+
return f"#NAME?"
|
|
312
|
+
|
|
313
|
+
def _precedence(self, token: Token) -> int:
|
|
314
|
+
"""Get operator precedence."""
|
|
315
|
+
precedences = {
|
|
316
|
+
'^': 4,
|
|
317
|
+
'*': 3, '/': 3,
|
|
318
|
+
'+': 2, '-': 2,
|
|
319
|
+
'&': 2,
|
|
320
|
+
'=': 1, '<': 1, '>': 1, '<=': 1, '>=': 1, '<>': 1,
|
|
321
|
+
}
|
|
322
|
+
return precedences.get(token.value, 0)
|
|
323
|
+
|
|
324
|
+
def _apply_operator(self, op_token: Token, left: Any, right: Any) -> Any:
|
|
325
|
+
"""Apply an operator to two operands."""
|
|
326
|
+
op = op_token.value
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
if op == '+':
|
|
330
|
+
return float(left) + float(right)
|
|
331
|
+
elif op == '-':
|
|
332
|
+
return float(left) - float(right)
|
|
333
|
+
elif op == '*':
|
|
334
|
+
return float(left) * float(right)
|
|
335
|
+
elif op == '/':
|
|
336
|
+
if float(right) == 0:
|
|
337
|
+
raise DivisionByZeroError()
|
|
338
|
+
return float(left) / float(right)
|
|
339
|
+
elif op == '^':
|
|
340
|
+
return float(left) ** float(right)
|
|
341
|
+
elif op == '&':
|
|
342
|
+
return str(left) + str(right)
|
|
343
|
+
elif op == '=':
|
|
344
|
+
return left == right
|
|
345
|
+
elif op == '<':
|
|
346
|
+
return float(left) < float(right)
|
|
347
|
+
elif op == '>':
|
|
348
|
+
return float(left) > float(right)
|
|
349
|
+
elif op == '<=':
|
|
350
|
+
return float(left) <= float(right)
|
|
351
|
+
elif op == '>=':
|
|
352
|
+
return float(left) >= float(right)
|
|
353
|
+
elif op == '<>':
|
|
354
|
+
return left != right
|
|
355
|
+
else:
|
|
356
|
+
return 0
|
|
357
|
+
except (ValueError, TypeError):
|
|
358
|
+
raise ValueErrorExcel()
|
|
359
|
+
except ZeroDivisionError:
|
|
360
|
+
raise DivisionByZeroError()
|