gnumeric 0.1.0__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.
- gnumeric/.constants.json +8 -0
- gnumeric/__init__.py +41 -0
- gnumeric/cell.py +294 -0
- gnumeric/evaluation_errors.py +24 -0
- gnumeric/exceptions.py +49 -0
- gnumeric/expression.py +151 -0
- gnumeric/expression_evaluation.py +315 -0
- gnumeric/formula_functions/__init__.py +0 -0
- gnumeric/formula_functions/argument_helpers.py +21 -0
- gnumeric/formula_functions/mathematics.py +21 -0
- gnumeric/formula_functions/statistics.py +23 -0
- gnumeric/sheet.py +642 -0
- gnumeric/utils.py +131 -0
- gnumeric/workbook.py +423 -0
- gnumeric-0.1.0.dist-info/METADATA +55 -0
- gnumeric-0.1.0.dist-info/RECORD +17 -0
- gnumeric-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import re
|
|
3
|
+
from typing import Set, Union
|
|
4
|
+
|
|
5
|
+
from lark import Lark, Transformer, v_args
|
|
6
|
+
from lark.exceptions import UnexpectedCharacters, VisitError
|
|
7
|
+
|
|
8
|
+
from gnumeric.evaluation_errors import EvaluationError, ExpressionEvaluationException
|
|
9
|
+
from gnumeric.formula_functions import mathematics, statistics
|
|
10
|
+
from gnumeric.utils import column_from_spreadsheet, row_from_spreadsheet
|
|
11
|
+
|
|
12
|
+
function_map = {
|
|
13
|
+
'abs': abs,
|
|
14
|
+
'len': lambda val: len(str(val)),
|
|
15
|
+
**mathematics.functions,
|
|
16
|
+
**statistics.functions,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CellReference:
|
|
21
|
+
def __init__(self, formula_cell, ss_row, ss_col, sheet, row_is_fixed, col_is_fixed):
|
|
22
|
+
self.formula_cell = formula_cell
|
|
23
|
+
self.ss_row = ss_row
|
|
24
|
+
self.ss_col = ss_col
|
|
25
|
+
self.sheet = sheet
|
|
26
|
+
self.row_is_fixed = row_is_fixed
|
|
27
|
+
self.col_is_fixed = col_is_fixed
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def cell(self):
|
|
31
|
+
try:
|
|
32
|
+
sheet = (
|
|
33
|
+
self.formula_cell.worksheet
|
|
34
|
+
if self.sheet is None
|
|
35
|
+
else self.formula_cell.worksheet.workbook.get_sheet_by_name(self.sheet)
|
|
36
|
+
)
|
|
37
|
+
except KeyError:
|
|
38
|
+
raise ExpressionEvaluationException(EvaluationError.REF)
|
|
39
|
+
|
|
40
|
+
offset_row, offset_col = self.formula_cell.value.reference_coordinate_offset
|
|
41
|
+
|
|
42
|
+
col = column_from_spreadsheet(self.ss_col)
|
|
43
|
+
row = row_from_spreadsheet(self.ss_row)
|
|
44
|
+
|
|
45
|
+
if not self.col_is_fixed:
|
|
46
|
+
col += offset_col
|
|
47
|
+
if not self.row_is_fixed:
|
|
48
|
+
row += offset_row
|
|
49
|
+
|
|
50
|
+
return sheet.cell(row, col, create=True)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def create_from_cell_reference(cls, formula_cell, sheet, ss_col, ss_row):
|
|
54
|
+
col_fixed = ss_col.startswith('$')
|
|
55
|
+
row_fixed = ss_row.startswith('$')
|
|
56
|
+
return cls(
|
|
57
|
+
formula_cell,
|
|
58
|
+
ss_row,
|
|
59
|
+
ss_col,
|
|
60
|
+
sheet,
|
|
61
|
+
row_is_fixed=row_fixed,
|
|
62
|
+
col_is_fixed=col_fixed,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_grammar = f"""
|
|
67
|
+
?start: "=" root
|
|
68
|
+
| "+" root
|
|
69
|
+
?root: logical_stmt
|
|
70
|
+
?logical_stmt: text_stmt
|
|
71
|
+
| logical_stmt logical_op text_stmt -> logical
|
|
72
|
+
?text_stmt: sum
|
|
73
|
+
| text_stmt "&" sum -> concat
|
|
74
|
+
?sum: product
|
|
75
|
+
| sum /[+-]/ product -> arithmetic
|
|
76
|
+
?product: exponentiation
|
|
77
|
+
| product /[*\\/]/ exponentiation -> arithmetic
|
|
78
|
+
?exponentiation: atom
|
|
79
|
+
| atom /\\^/ exponentiation -> arithmetic
|
|
80
|
+
|
|
81
|
+
?function_arguments: function_argument ( "," function_argument )*
|
|
82
|
+
?function_argument: root
|
|
83
|
+
| cell_range
|
|
84
|
+
?cell_range: sheet_cell_reference ":" cell_reference
|
|
85
|
+
|
|
86
|
+
?atom: NUMBER -> number
|
|
87
|
+
| "#REF!" -> error_ref
|
|
88
|
+
| ATOMIC_STR -> atomic_string
|
|
89
|
+
| FUNC_NAME "(" function_arguments ")" -> function
|
|
90
|
+
| FUNC_NAME "(" ")" -> function
|
|
91
|
+
| string
|
|
92
|
+
| "(" root ")"
|
|
93
|
+
// | sheet_cell_reference -> cell_lookup
|
|
94
|
+
|
|
95
|
+
!logical_op: "=" | "<>" | "<" | "<=" | ">" | ">="
|
|
96
|
+
|
|
97
|
+
?sheet_cell_reference: (SHEETNAME "!")? cell_reference -> sheet_cell_ref
|
|
98
|
+
?cell_reference: "$"? COLUMN "$"? ROW -> cell_ref
|
|
99
|
+
COLUMN: LETTER~1..3
|
|
100
|
+
ROW: DIGIT~1..5
|
|
101
|
+
SHEETNAME: LETTER+
|
|
102
|
+
|
|
103
|
+
string : ESCAPED_STRING
|
|
104
|
+
|
|
105
|
+
// FUNC_NAME: {' | '.join(f'"{f}"i' for f in function_map.keys())}
|
|
106
|
+
FUNC_NAME: LETTER ("_"|LETTER|DIGIT|".")*
|
|
107
|
+
ATOMIC_STR: (LETTER | "$" | "'") ("_"|LETTER|DIGIT|"."|"!"|"$"|"'"|" ")*
|
|
108
|
+
|
|
109
|
+
%import common.DIGIT
|
|
110
|
+
%import common.SIGNED_NUMBER -> NUMBER
|
|
111
|
+
%import common.LETTER
|
|
112
|
+
%import common.ESCAPED_STRING
|
|
113
|
+
%import common.WS_INLINE
|
|
114
|
+
%ignore WS_INLINE
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def to_str(value) -> str:
|
|
119
|
+
if isinstance(value, float) and value.is_integer():
|
|
120
|
+
value = int(value)
|
|
121
|
+
elif isinstance(value, bool):
|
|
122
|
+
value = 'TRUE' if value else 'FALSE'
|
|
123
|
+
return str(value)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@v_args(inline=True)
|
|
127
|
+
class ExpressionEvaluator(Transformer):
|
|
128
|
+
def __init__(self, cell):
|
|
129
|
+
self._cell = cell
|
|
130
|
+
self.referenced_cells = set()
|
|
131
|
+
|
|
132
|
+
def number(self, val):
|
|
133
|
+
try:
|
|
134
|
+
return int(val)
|
|
135
|
+
except ValueError:
|
|
136
|
+
return float(val)
|
|
137
|
+
|
|
138
|
+
def string(self, a):
|
|
139
|
+
return a.value[1:-1]
|
|
140
|
+
|
|
141
|
+
def error_ref(self):
|
|
142
|
+
raise ExpressionEvaluationException(EvaluationError.REF)
|
|
143
|
+
|
|
144
|
+
def atomic_string(self, name):
|
|
145
|
+
if name == 'TRUE':
|
|
146
|
+
return True
|
|
147
|
+
elif name == 'FALSE':
|
|
148
|
+
return False
|
|
149
|
+
else:
|
|
150
|
+
res = re.match(
|
|
151
|
+
r"('?[A-Za-z0-9_ ]+'?!)?(\$?[A-Za-z]{1,3})(\$?\d{1,5})", name
|
|
152
|
+
)
|
|
153
|
+
if res:
|
|
154
|
+
sheet, col, row = res.groups()
|
|
155
|
+
sheet = sheet[:-1] if sheet is not None else None
|
|
156
|
+
|
|
157
|
+
if sheet is not None and sheet.startswith("'") and sheet.endswith("'"):
|
|
158
|
+
sheet = sheet[1:-1]
|
|
159
|
+
|
|
160
|
+
cell_value = self.cell_lookup((sheet, col, row))
|
|
161
|
+
if cell_value is not None and not isinstance(
|
|
162
|
+
cell_value, (int, float, str)
|
|
163
|
+
):
|
|
164
|
+
cell_value = cell_value.value
|
|
165
|
+
return cell_value
|
|
166
|
+
|
|
167
|
+
raise ExpressionEvaluationException(EvaluationError.NAME)
|
|
168
|
+
|
|
169
|
+
def logical_op(self, op):
|
|
170
|
+
return op.value
|
|
171
|
+
|
|
172
|
+
def logical(self, a, op, b):
|
|
173
|
+
if isinstance(a, str):
|
|
174
|
+
a = str(a).lower()
|
|
175
|
+
if isinstance(b, str):
|
|
176
|
+
b = str(b).lower()
|
|
177
|
+
|
|
178
|
+
# Numbers are always less than strings and strings always less
|
|
179
|
+
# than bools, so to simplify the logic below, just use different
|
|
180
|
+
# values for a and b if there's a type mismatch
|
|
181
|
+
a_type = float if isinstance(a, int) and not isinstance(a, bool) else type(a)
|
|
182
|
+
b_type = float if isinstance(b, int) and not isinstance(b, bool) else type(b)
|
|
183
|
+
if a_type != b_type:
|
|
184
|
+
type_val_mapper = {float: 0, str: 50, bool: 100}
|
|
185
|
+
a = type_val_mapper[a_type]
|
|
186
|
+
b = type_val_mapper[b_type]
|
|
187
|
+
|
|
188
|
+
if op == '=':
|
|
189
|
+
return a == b
|
|
190
|
+
elif op == '<>':
|
|
191
|
+
return a != b
|
|
192
|
+
elif op == '<':
|
|
193
|
+
return a < b
|
|
194
|
+
elif op == '<=':
|
|
195
|
+
return a <= b
|
|
196
|
+
elif op == '>':
|
|
197
|
+
return a > b
|
|
198
|
+
elif op == '>=':
|
|
199
|
+
return a >= b
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError(f'Unrecognized logical operator: {op}')
|
|
202
|
+
|
|
203
|
+
def arithmetic(self, a, op, b):
|
|
204
|
+
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
|
|
205
|
+
raise ExpressionEvaluationException(EvaluationError.VALUE)
|
|
206
|
+
|
|
207
|
+
if op == '+':
|
|
208
|
+
return a + b
|
|
209
|
+
elif op == '-':
|
|
210
|
+
return a - b
|
|
211
|
+
elif op == '*':
|
|
212
|
+
return a * b
|
|
213
|
+
elif op == '/':
|
|
214
|
+
return a / b
|
|
215
|
+
elif op == '^':
|
|
216
|
+
try:
|
|
217
|
+
return math.pow(a, b)
|
|
218
|
+
except OverflowError:
|
|
219
|
+
raise ExpressionEvaluationException(EvaluationError.NUM)
|
|
220
|
+
|
|
221
|
+
def cell_ref(self, *ref) -> CellReference:
|
|
222
|
+
return CellReference.create_from_cell_reference(
|
|
223
|
+
self._cell, None, ref[0].value, ref[1].value
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def sheet_cell_ref(self, *ref) -> CellReference:
|
|
227
|
+
if len(ref) > 1:
|
|
228
|
+
ref[1].sheet = ref[0].value
|
|
229
|
+
ref = ref[1]
|
|
230
|
+
else:
|
|
231
|
+
ref = ref[0]
|
|
232
|
+
return ref
|
|
233
|
+
|
|
234
|
+
def cell_range(self, start, end):
|
|
235
|
+
start_cell = start.cell
|
|
236
|
+
end_cell = end.cell
|
|
237
|
+
cells = start_cell.worksheet.get_cell_collection(
|
|
238
|
+
start_cell, end_cell, include_empty=True, create_cells=True
|
|
239
|
+
)
|
|
240
|
+
self.referenced_cells.update(cells)
|
|
241
|
+
return cells
|
|
242
|
+
|
|
243
|
+
def cell_lookup(self, ref):
|
|
244
|
+
cell_ref = CellReference.create_from_cell_reference(
|
|
245
|
+
self._cell, ref[0], ref[1], ref[2]
|
|
246
|
+
)
|
|
247
|
+
cell = cell_ref.cell
|
|
248
|
+
self.referenced_cells.add(cell)
|
|
249
|
+
if cell is None:
|
|
250
|
+
return 0
|
|
251
|
+
return cell.result
|
|
252
|
+
|
|
253
|
+
def concat(self, a, b):
|
|
254
|
+
a = to_str(a)
|
|
255
|
+
b = to_str(b)
|
|
256
|
+
return a + b
|
|
257
|
+
|
|
258
|
+
def function(self, name, *args):
|
|
259
|
+
try:
|
|
260
|
+
return function_map[name.lower()](*args)
|
|
261
|
+
except KeyError as ex:
|
|
262
|
+
raise ExpressionEvaluationException(EvaluationError.NAME) from ex
|
|
263
|
+
except TypeError as ex:
|
|
264
|
+
msg = str(ex)
|
|
265
|
+
if msg.startswith('bad operand type'):
|
|
266
|
+
raise ExpressionEvaluationException(EvaluationError.VALUE) from ex
|
|
267
|
+
elif 'takes exactly' in str(ex):
|
|
268
|
+
raise ExpressionEvaluationException(EvaluationError.NA) from ex
|
|
269
|
+
print(type(ex), ex)
|
|
270
|
+
raise ex
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
_parser = Lark(_grammar, start='start', parser='earley')
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def parse(expression: str):
|
|
277
|
+
try:
|
|
278
|
+
tree = _parser.parse(expression)
|
|
279
|
+
except UnexpectedCharacters:
|
|
280
|
+
return EvaluationError.VALUE
|
|
281
|
+
|
|
282
|
+
# print(tree.pretty())
|
|
283
|
+
return tree
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _full_evaluation(expression: str, cell):
|
|
287
|
+
tree = parse(expression)
|
|
288
|
+
|
|
289
|
+
if isinstance(tree, EvaluationError):
|
|
290
|
+
result, referenced_cells = tree, set()
|
|
291
|
+
else:
|
|
292
|
+
evaluator = ExpressionEvaluator(cell)
|
|
293
|
+
try:
|
|
294
|
+
result = evaluator.transform(tree)
|
|
295
|
+
result, referenced_cells = result, evaluator.referenced_cells
|
|
296
|
+
except VisitError as ex:
|
|
297
|
+
if isinstance(ex.orig_exc, ZeroDivisionError):
|
|
298
|
+
result, referenced_cells = EvaluationError.DIV0, set()
|
|
299
|
+
elif isinstance(ex.orig_exc, ExpressionEvaluationException):
|
|
300
|
+
result, referenced_cells = ex.orig_exc.error, set()
|
|
301
|
+
else:
|
|
302
|
+
print(type(ex.orig_exc), ex.orig_exc)
|
|
303
|
+
raise ex.orig_exc
|
|
304
|
+
|
|
305
|
+
return result, referenced_cells
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def evaluate(expression: str, cell) -> Union[str, int, float, EvaluationError]:
|
|
309
|
+
result, _ = _full_evaluation(expression, cell)
|
|
310
|
+
return result
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_referenced_cells(expression: str, cell) -> Set:
|
|
314
|
+
_, references = _full_evaluation(expression, cell)
|
|
315
|
+
return references
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from gnumeric.evaluation_errors import EvaluationError, ExpressionEvaluationException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def flatten_just_type(values, types):
|
|
5
|
+
keep_values = []
|
|
6
|
+
for v in values:
|
|
7
|
+
if isinstance(v, EvaluationError):
|
|
8
|
+
raise ExpressionEvaluationException(v)
|
|
9
|
+
elif isinstance(v, types):
|
|
10
|
+
keep_values.append(v)
|
|
11
|
+
elif isinstance(v, list):
|
|
12
|
+
keep_values.extend(flatten_just_type(v, types))
|
|
13
|
+
elif hasattr(v, 'get_value'):
|
|
14
|
+
cell_value = v.get_value(compute_expression=True)
|
|
15
|
+
keep_values.extend(flatten_just_type([cell_value], types))
|
|
16
|
+
|
|
17
|
+
return keep_values
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_just_numeric(values):
|
|
21
|
+
return flatten_just_type(values, (int, float))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from gnumeric.formula_functions.argument_helpers import get_just_numeric
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def gnm_product(*values):
|
|
7
|
+
values = get_just_numeric(values)
|
|
8
|
+
if len(values) == 0:
|
|
9
|
+
return 0
|
|
10
|
+
else:
|
|
11
|
+
return functools.reduce(lambda x, y: x * y, values)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def gnm_sum(*values):
|
|
15
|
+
return sum(get_just_numeric(values))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
local_objects = locals().copy()
|
|
19
|
+
functions = {
|
|
20
|
+
name[4:]: obj for name, obj in local_objects.items() if name.startswith('gnm_')
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from gnumeric.formula_functions.argument_helpers import get_just_numeric
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def gnm_max(values):
|
|
5
|
+
values = get_just_numeric(values)
|
|
6
|
+
if len(values) == 0:
|
|
7
|
+
return 0
|
|
8
|
+
else:
|
|
9
|
+
return max(values)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def gnm_min(values):
|
|
13
|
+
values = get_just_numeric(values)
|
|
14
|
+
if len(values) == 0:
|
|
15
|
+
return 0
|
|
16
|
+
else:
|
|
17
|
+
return min(values)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
local_objects = locals().copy()
|
|
21
|
+
functions = {
|
|
22
|
+
name[4:]: obj for name, obj in local_objects.items() if name.startswith('gnm_')
|
|
23
|
+
}
|