csvpath 0.0.2__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.
- csvpath/__init__.py +1 -0
- csvpath/csvpath.py +368 -0
- csvpath/matching/__init__.py +1 -0
- csvpath/matching/expression_encoder.py +108 -0
- csvpath/matching/expression_math.py +123 -0
- csvpath/matching/expression_utility.py +29 -0
- csvpath/matching/functions/above.py +36 -0
- csvpath/matching/functions/add.py +24 -0
- csvpath/matching/functions/below.py +36 -0
- csvpath/matching/functions/concat.py +25 -0
- csvpath/matching/functions/count.py +44 -0
- csvpath/matching/functions/count_lines.py +12 -0
- csvpath/matching/functions/count_scans.py +13 -0
- csvpath/matching/functions/divide.py +30 -0
- csvpath/matching/functions/end.py +18 -0
- csvpath/matching/functions/every.py +33 -0
- csvpath/matching/functions/first.py +46 -0
- csvpath/matching/functions/function.py +31 -0
- csvpath/matching/functions/function_factory.py +114 -0
- csvpath/matching/functions/inf.py +38 -0
- csvpath/matching/functions/is_instance.py +95 -0
- csvpath/matching/functions/length.py +33 -0
- csvpath/matching/functions/lower.py +21 -0
- csvpath/matching/functions/minf.py +167 -0
- csvpath/matching/functions/multiply.py +27 -0
- csvpath/matching/functions/no.py +10 -0
- csvpath/matching/functions/notf.py +26 -0
- csvpath/matching/functions/now.py +33 -0
- csvpath/matching/functions/orf.py +28 -0
- csvpath/matching/functions/percent.py +29 -0
- csvpath/matching/functions/random.py +33 -0
- csvpath/matching/functions/regex.py +38 -0
- csvpath/matching/functions/subtract.py +28 -0
- csvpath/matching/functions/tally.py +36 -0
- csvpath/matching/functions/upper.py +21 -0
- csvpath/matching/matcher.py +215 -0
- csvpath/matching/matching_lexer.py +66 -0
- csvpath/matching/parser.out +1287 -0
- csvpath/matching/parsetab.py +1427 -0
- csvpath/matching/productions/equality.py +158 -0
- csvpath/matching/productions/expression.py +16 -0
- csvpath/matching/productions/header.py +30 -0
- csvpath/matching/productions/matchable.py +41 -0
- csvpath/matching/productions/term.py +11 -0
- csvpath/matching/productions/variable.py +15 -0
- csvpath/parser_utility.py +39 -0
- csvpath/scanning/__init__.py +1 -0
- csvpath/scanning/parser.out +1 -0
- csvpath/scanning/parsetab.py +231 -0
- csvpath/scanning/scanner.py +165 -0
- csvpath/scanning/scanning_lexer.py +47 -0
- csvpath-0.0.2.dist-info/METADATA +184 -0
- csvpath-0.0.2.dist-info/RECORD +54 -0
- csvpath-0.0.2.dist-info/WHEEL +4 -0
csvpath/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
csvpath/csvpath.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from typing import List, Dict, Any
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from csvpath.matching.matcher import Matcher
|
|
5
|
+
from csvpath.matching.expression_encoder import ExpressionEncoder
|
|
6
|
+
from csvpath.matching.expression_math import ExpressionMath
|
|
7
|
+
from csvpath.scanning.scanner import Scanner
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NoFileException(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CsvPath:
|
|
15
|
+
def __init__(
|
|
16
|
+
self, *, filename=None, delimiter=",", quotechar='"', block_print=True
|
|
17
|
+
):
|
|
18
|
+
self.filename = filename
|
|
19
|
+
self.scanner = None
|
|
20
|
+
self.value = None
|
|
21
|
+
self.scan = None
|
|
22
|
+
self.match = None
|
|
23
|
+
self.modify = None
|
|
24
|
+
self.headers = None
|
|
25
|
+
self.line_number = 0
|
|
26
|
+
self.scan_count = 0
|
|
27
|
+
self.match_count = 0
|
|
28
|
+
self.variables: Dict[str, Any] = {}
|
|
29
|
+
self.delimiter = delimiter
|
|
30
|
+
self.quotechar = quotechar
|
|
31
|
+
self.block_print = block_print
|
|
32
|
+
self.total_lines = -1
|
|
33
|
+
self._verbose = False
|
|
34
|
+
self._dump_json = False
|
|
35
|
+
self._do_math = False # off by default, still experimental
|
|
36
|
+
self._collect_matchers = False
|
|
37
|
+
self.matchers = []
|
|
38
|
+
self.jsons = []
|
|
39
|
+
|
|
40
|
+
def dump_json(self):
|
|
41
|
+
self._dump_json = not self._dump_json
|
|
42
|
+
|
|
43
|
+
def parse(self, data):
|
|
44
|
+
self.scanner = Scanner()
|
|
45
|
+
s, mat, mod = self._find_scan_match_modify(data)
|
|
46
|
+
self.scan = s
|
|
47
|
+
self.match = mat
|
|
48
|
+
self.modify = mod
|
|
49
|
+
self.scanner.parse(s)
|
|
50
|
+
self._load_headers()
|
|
51
|
+
self.get_total_lines()
|
|
52
|
+
return self.scanner
|
|
53
|
+
|
|
54
|
+
def verbose(self, set_verbose: bool = True) -> None:
|
|
55
|
+
self._verbose = set_verbose
|
|
56
|
+
|
|
57
|
+
# prints what the user needs to see
|
|
58
|
+
def verbosity(self, msg: Any) -> None:
|
|
59
|
+
if self._verbose:
|
|
60
|
+
print(f"{msg}")
|
|
61
|
+
|
|
62
|
+
# prints what the developer needs to see
|
|
63
|
+
def print(self, msg: str) -> None:
|
|
64
|
+
if not self.block_print:
|
|
65
|
+
print(msg)
|
|
66
|
+
|
|
67
|
+
def _load_headers(self) -> None:
|
|
68
|
+
with open(self.scanner.filename, "r") as file:
|
|
69
|
+
reader = csv.reader(
|
|
70
|
+
file, delimiter=self.delimiter, quotechar=self.quotechar
|
|
71
|
+
)
|
|
72
|
+
for row in reader:
|
|
73
|
+
self.headers = row
|
|
74
|
+
break
|
|
75
|
+
hs = self.headers[:]
|
|
76
|
+
self.headers = []
|
|
77
|
+
for header in hs:
|
|
78
|
+
header = header.strip()
|
|
79
|
+
header = header.replace(";", "")
|
|
80
|
+
header = header.replace(",", "")
|
|
81
|
+
header = header.replace("|", "")
|
|
82
|
+
header = header.replace("\t", "")
|
|
83
|
+
header = header.replace("`", "")
|
|
84
|
+
self.headers.append(header)
|
|
85
|
+
self.verbosity(f"header: {header}")
|
|
86
|
+
|
|
87
|
+
def _find_scan_match_modify(self, data):
|
|
88
|
+
scan = ""
|
|
89
|
+
matches = ""
|
|
90
|
+
modify = ""
|
|
91
|
+
p = 0
|
|
92
|
+
for i, c in enumerate(data):
|
|
93
|
+
if p == 0:
|
|
94
|
+
scan = scan + c
|
|
95
|
+
elif p == 1:
|
|
96
|
+
matches = matches + c
|
|
97
|
+
else:
|
|
98
|
+
modify = modify + c
|
|
99
|
+
if c == "]":
|
|
100
|
+
p = p + 1
|
|
101
|
+
scan = scan.strip()
|
|
102
|
+
scan = scan if len(scan) > 0 else None
|
|
103
|
+
matches = matches.strip()
|
|
104
|
+
matches = matches if len(matches) > 0 else None
|
|
105
|
+
modify = modify.strip()
|
|
106
|
+
modify = modify if len(modify) > 0 else None
|
|
107
|
+
self.verbosity(f"scan: {scan}")
|
|
108
|
+
self.verbosity(f"matches: {matches}")
|
|
109
|
+
self.verbosity(f"modify: {modify}")
|
|
110
|
+
return scan, matches, modify
|
|
111
|
+
|
|
112
|
+
def __str__(self):
|
|
113
|
+
return f"""
|
|
114
|
+
path: {self.scanner.path}
|
|
115
|
+
filename: {self.filename}
|
|
116
|
+
parser: {self.scanner}
|
|
117
|
+
from_line: {self.scanner.from_line}
|
|
118
|
+
to_line: {self.scanner.to_line}
|
|
119
|
+
all_lines: {self.scanner.all_lines}
|
|
120
|
+
these: {self.scanner.these}
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def from_line(self):
|
|
125
|
+
return self.scanner.from_line
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def to_line(self):
|
|
129
|
+
return self.scanner.to_line
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def all_lines(self):
|
|
133
|
+
return self.scanner.all_lines
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def path(self):
|
|
137
|
+
return self.scanner.path
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def these(self):
|
|
141
|
+
return self.scanner.these
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def filename(self):
|
|
145
|
+
return self.file_name
|
|
146
|
+
|
|
147
|
+
@filename.setter
|
|
148
|
+
def filename(self, f):
|
|
149
|
+
self.file_name = f
|
|
150
|
+
|
|
151
|
+
def collect(self) -> List[List[Any]]:
|
|
152
|
+
lines = []
|
|
153
|
+
for _ in self.next():
|
|
154
|
+
_ = _[:]
|
|
155
|
+
lines.append(_)
|
|
156
|
+
return lines
|
|
157
|
+
|
|
158
|
+
def next(self):
|
|
159
|
+
if self.scanner.filename is None:
|
|
160
|
+
raise NoFileException("there is no filename")
|
|
161
|
+
self.verbosity(f"filename: {self.scanner.filename}")
|
|
162
|
+
total_lines = -1
|
|
163
|
+
if self._verbose:
|
|
164
|
+
total_lines = self.get_total_lines()
|
|
165
|
+
self.verbosity(f"total lines: {total_lines}")
|
|
166
|
+
|
|
167
|
+
with open(self.scanner.filename, "r") as file:
|
|
168
|
+
reader = csv.reader(
|
|
169
|
+
file, delimiter=self.delimiter, quotechar=self.quotechar
|
|
170
|
+
)
|
|
171
|
+
for line in reader:
|
|
172
|
+
self.verbosity(f"line number: {self.line_number} of {total_lines}")
|
|
173
|
+
if self.includes(self.line_number):
|
|
174
|
+
self.scan_count = self.scan_count + 1
|
|
175
|
+
self.print(f"CsvPath.next: line:{line}")
|
|
176
|
+
self.verbosity(f"scan count: {self.scan_count}")
|
|
177
|
+
if self.matches(line):
|
|
178
|
+
self.match_count = self.match_count + 1
|
|
179
|
+
self.verbosity(f"match count: {self.match_count}")
|
|
180
|
+
yield line
|
|
181
|
+
self.line_number = self.line_number + 1
|
|
182
|
+
|
|
183
|
+
def get_total_lines(self) -> int:
|
|
184
|
+
if self.total_lines == -1:
|
|
185
|
+
with open(self.scanner.filename, "r") as file:
|
|
186
|
+
reader = csv.reader(
|
|
187
|
+
file, delimiter=self.delimiter, quotechar=self.quotechar
|
|
188
|
+
)
|
|
189
|
+
for line in reader:
|
|
190
|
+
self.total_lines += 1
|
|
191
|
+
return self.total_lines
|
|
192
|
+
|
|
193
|
+
def current_line_number(self) -> int:
|
|
194
|
+
return self.line_number
|
|
195
|
+
|
|
196
|
+
def current_scan_count(self) -> int:
|
|
197
|
+
return self.scan_count
|
|
198
|
+
|
|
199
|
+
def current_match_count(self) -> int:
|
|
200
|
+
return self.match_count
|
|
201
|
+
|
|
202
|
+
def do_math(self):
|
|
203
|
+
self._do_math = not self._do_math
|
|
204
|
+
|
|
205
|
+
def collect_matchers(self):
|
|
206
|
+
self._collect_matchers = not self._collect_matchers
|
|
207
|
+
|
|
208
|
+
def matches(self, line) -> bool:
|
|
209
|
+
if not self.match:
|
|
210
|
+
return True
|
|
211
|
+
self.print(f"CsvPath.matches: the match path: {self.match}")
|
|
212
|
+
|
|
213
|
+
matcher = Matcher(
|
|
214
|
+
csvpath=self, data=self.match, line=line, headers=self.headers
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if self._do_math:
|
|
218
|
+
em = ExpressionMath()
|
|
219
|
+
for e in matcher.expressions:
|
|
220
|
+
em.do_math(e[0])
|
|
221
|
+
|
|
222
|
+
if self._dump_json:
|
|
223
|
+
jsonstr = ExpressionEncoder().valued_list_to_json(matcher.expressions)
|
|
224
|
+
self.jsons.append(jsonstr)
|
|
225
|
+
|
|
226
|
+
matched = matcher.matches()
|
|
227
|
+
if self._collect_matchers: # and matched
|
|
228
|
+
self.matchers.append(matcher)
|
|
229
|
+
|
|
230
|
+
return matched
|
|
231
|
+
|
|
232
|
+
def set_variable(self, name: str, *, value: Any, tracking: Any = None) -> None:
|
|
233
|
+
if not name:
|
|
234
|
+
raise Exception("name cannot be None")
|
|
235
|
+
if name in self.variables:
|
|
236
|
+
self.print(f"CsvPath.set_variable: existing value: {self.variables[name]}")
|
|
237
|
+
else:
|
|
238
|
+
self.print("CsvPath.set_variable: no existing value")
|
|
239
|
+
if tracking is not None:
|
|
240
|
+
if name not in self.variables:
|
|
241
|
+
self.variables[name] = {}
|
|
242
|
+
instances = self.variables[name]
|
|
243
|
+
instances[tracking] = value
|
|
244
|
+
else:
|
|
245
|
+
self.variables[name] = value
|
|
246
|
+
|
|
247
|
+
def get_variable(
|
|
248
|
+
self, name: str, *, tracking: Any = None, set_if_none: Any = None
|
|
249
|
+
) -> Any:
|
|
250
|
+
if not name:
|
|
251
|
+
raise Exception("name cannot be None")
|
|
252
|
+
thevalue = None
|
|
253
|
+
if tracking is not None:
|
|
254
|
+
thedict = None
|
|
255
|
+
if name in self.variables:
|
|
256
|
+
thedict = self.variables[name]
|
|
257
|
+
if not thedict:
|
|
258
|
+
thedict = {}
|
|
259
|
+
self.variables[name] = thedict
|
|
260
|
+
thedict[tracking] = set_if_none
|
|
261
|
+
else:
|
|
262
|
+
thedict = {}
|
|
263
|
+
thedict[tracking] = set_if_none
|
|
264
|
+
self.variables[name] = thedict
|
|
265
|
+
thevalue = thedict.get(tracking)
|
|
266
|
+
if not thevalue and set_if_none is not None:
|
|
267
|
+
thedict[tracking] = set_if_none
|
|
268
|
+
thevalue = set_if_none
|
|
269
|
+
else:
|
|
270
|
+
if name not in self.variables:
|
|
271
|
+
self.variables[name] = set_if_none
|
|
272
|
+
thevalue = self.variables[name]
|
|
273
|
+
return thevalue
|
|
274
|
+
|
|
275
|
+
def includes(self, line: int) -> bool:
|
|
276
|
+
from_line = self.scanner.from_line
|
|
277
|
+
to_line = self.scanner.to_line
|
|
278
|
+
all_lines = self.scanner.all_lines
|
|
279
|
+
these = self.scanner.these
|
|
280
|
+
return self._includes(
|
|
281
|
+
line, from_line=from_line, to_line=to_line, all_lines=all_lines, these=these
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _includes(
|
|
285
|
+
self,
|
|
286
|
+
line: int,
|
|
287
|
+
*,
|
|
288
|
+
from_line: int = None,
|
|
289
|
+
to_line: int = None,
|
|
290
|
+
all_lines: bool = None,
|
|
291
|
+
these: List[int] = [],
|
|
292
|
+
) -> bool:
|
|
293
|
+
if line is None:
|
|
294
|
+
return False
|
|
295
|
+
if from_line is None and all_lines:
|
|
296
|
+
return True
|
|
297
|
+
if from_line is not None and all_lines:
|
|
298
|
+
return line >= from_line
|
|
299
|
+
if from_line == line:
|
|
300
|
+
return True
|
|
301
|
+
if from_line is not None and to_line is not None and from_line > to_line:
|
|
302
|
+
return line >= to_line and line <= from_line
|
|
303
|
+
if from_line is not None and to_line is not None:
|
|
304
|
+
return line >= from_line and line <= to_line
|
|
305
|
+
if line in these:
|
|
306
|
+
return True
|
|
307
|
+
if to_line is not None:
|
|
308
|
+
return line < to_line
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
def line_numbers(self) -> Iterator[int | str]:
|
|
312
|
+
these = self.scanner.these
|
|
313
|
+
from_line = self.scanner.from_line
|
|
314
|
+
to_line = self.scanner.to_line
|
|
315
|
+
all_lines = self.scanner.all_lines
|
|
316
|
+
return self._line_numbers(
|
|
317
|
+
these=these, from_line=from_line, to_line=to_line, all_lines=all_lines
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def _line_numbers(
|
|
321
|
+
self,
|
|
322
|
+
*,
|
|
323
|
+
these: List[int] = [],
|
|
324
|
+
from_line: int = None,
|
|
325
|
+
to_line: int = None,
|
|
326
|
+
all_lines: bool = None,
|
|
327
|
+
) -> Iterator[int | str]:
|
|
328
|
+
if len(these) > 0:
|
|
329
|
+
for i in these:
|
|
330
|
+
yield i
|
|
331
|
+
else:
|
|
332
|
+
if from_line is not None and to_line is not None and from_line > to_line:
|
|
333
|
+
for i in range(to_line, from_line + 1):
|
|
334
|
+
yield i
|
|
335
|
+
elif from_line is not None and to_line is not None:
|
|
336
|
+
for i in range(from_line, to_line + 1):
|
|
337
|
+
yield i
|
|
338
|
+
elif from_line is not None:
|
|
339
|
+
if all_lines:
|
|
340
|
+
yield f"{from_line}..."
|
|
341
|
+
else:
|
|
342
|
+
yield from_line
|
|
343
|
+
elif to_line is not None:
|
|
344
|
+
yield f"0..{to_line}"
|
|
345
|
+
|
|
346
|
+
def collect_line_numbers(self) -> List[int | str]:
|
|
347
|
+
these = self.scanner.these
|
|
348
|
+
from_line = self.scanner.from_line
|
|
349
|
+
to_line = self.scanner.to_line
|
|
350
|
+
all_lines = self.scanner.all_lines
|
|
351
|
+
return self._collect_line_numbers(
|
|
352
|
+
these=these, from_line=from_line, to_line=to_line, all_lines=all_lines
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def _collect_line_numbers(
|
|
356
|
+
self,
|
|
357
|
+
*,
|
|
358
|
+
these: List[int] = [],
|
|
359
|
+
from_line: int = None,
|
|
360
|
+
to_line: int = None,
|
|
361
|
+
all_lines: bool = None,
|
|
362
|
+
) -> List[int | str]:
|
|
363
|
+
collect = []
|
|
364
|
+
for i in self._line_numbers(
|
|
365
|
+
these=these, from_line=from_line, to_line=to_line, all_lines=all_lines
|
|
366
|
+
):
|
|
367
|
+
collect.append(i)
|
|
368
|
+
return collect
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from csvpath.matching.productions.expression import Expression
|
|
2
|
+
from csvpath.matching.productions.equality import Equality
|
|
3
|
+
from csvpath.matching.productions.variable import Variable
|
|
4
|
+
from csvpath.matching.productions.header import Header
|
|
5
|
+
from csvpath.matching.productions.term import Term
|
|
6
|
+
from csvpath.matching.functions.function import Function
|
|
7
|
+
from typing import Any, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExpressionEncoder:
|
|
11
|
+
def list_to_json(self, alist: List[Any]) -> str:
|
|
12
|
+
json = "[ "
|
|
13
|
+
for _ in alist:
|
|
14
|
+
json = f"{json} {self.to_json(_)} "
|
|
15
|
+
json = f"{json} ] "
|
|
16
|
+
return json
|
|
17
|
+
|
|
18
|
+
def simple_list_to_json(self, alist: List[Any]) -> str:
|
|
19
|
+
json = "[ "
|
|
20
|
+
for i, _ in enumerate(alist):
|
|
21
|
+
json = f"{json} {self.to_json(_)} "
|
|
22
|
+
if i < len(alist) - 1:
|
|
23
|
+
json = f"{json}, "
|
|
24
|
+
json = f"{json} ] "
|
|
25
|
+
return json
|
|
26
|
+
|
|
27
|
+
def valued_list_to_json(self, alist: List[List[Any]]) -> str:
|
|
28
|
+
json = "[ "
|
|
29
|
+
for i, _ in enumerate(alist):
|
|
30
|
+
json = f"{json} {self.to_json(_[0])} "
|
|
31
|
+
if i < len(alist) - 1:
|
|
32
|
+
json = f"{json}, "
|
|
33
|
+
json = f"{json} ] "
|
|
34
|
+
return json
|
|
35
|
+
|
|
36
|
+
def to_json(self, o):
|
|
37
|
+
if o is None:
|
|
38
|
+
return "None"
|
|
39
|
+
json = ""
|
|
40
|
+
return self._encode(json, o)
|
|
41
|
+
|
|
42
|
+
def _encode(self, json: str, o) -> str:
|
|
43
|
+
if isinstance(o, Expression):
|
|
44
|
+
return self.expression(json, o)
|
|
45
|
+
elif isinstance(o, Equality):
|
|
46
|
+
return self.equality(json, o)
|
|
47
|
+
elif isinstance(o, Function):
|
|
48
|
+
return self.function(json, o)
|
|
49
|
+
elif isinstance(o, Header):
|
|
50
|
+
return self.header(json, o)
|
|
51
|
+
elif isinstance(o, Variable):
|
|
52
|
+
return self.variable(json, o)
|
|
53
|
+
elif isinstance(o, Term):
|
|
54
|
+
return self.term(json, o)
|
|
55
|
+
elif o is None:
|
|
56
|
+
return f'{json} "None" '
|
|
57
|
+
else:
|
|
58
|
+
raise Exception(f"what am I {o}")
|
|
59
|
+
|
|
60
|
+
def matchable(self, json: str, m) -> str:
|
|
61
|
+
json = f'{json} "base_class":"matchable", '
|
|
62
|
+
json = f'{json} "parent_class":"{m.parent.__class__}", '
|
|
63
|
+
json = f'{json} "value":"{m.value}", '
|
|
64
|
+
json = f'{json} "name":"{m.name}", '
|
|
65
|
+
json = f'{json} "children": [ '
|
|
66
|
+
for i, _ in enumerate(m.children):
|
|
67
|
+
json = self._encode(json, _)
|
|
68
|
+
if i < len(m.children) - 1:
|
|
69
|
+
json = f"{json}, "
|
|
70
|
+
json = f"{json} ] "
|
|
71
|
+
return json
|
|
72
|
+
|
|
73
|
+
def expression(self, json: str, e) -> str:
|
|
74
|
+
json = f"{json} " + '{ "type":"expression", '
|
|
75
|
+
json = self.matchable(json, e)
|
|
76
|
+
json = f"{json} " + "} "
|
|
77
|
+
return json
|
|
78
|
+
|
|
79
|
+
def equality(self, json: str, e) -> str:
|
|
80
|
+
json = f"{json} " + '{ "type":"equality", '
|
|
81
|
+
json = self.matchable(json, e)
|
|
82
|
+
json = f'{json}, "op":"{e.op}" '
|
|
83
|
+
json = f"{json} " + "} "
|
|
84
|
+
return json
|
|
85
|
+
|
|
86
|
+
def function(self, json: str, f) -> str:
|
|
87
|
+
json = f"{json} " + '{ "type":"function", '
|
|
88
|
+
json = self.matchable(json, f)
|
|
89
|
+
json = f"{json} " + "} "
|
|
90
|
+
return json
|
|
91
|
+
|
|
92
|
+
def header(self, json: str, h) -> str:
|
|
93
|
+
json = f"{json} " + '{ "type":"header", '
|
|
94
|
+
json = self.matchable(json, h)
|
|
95
|
+
json = f"{json} " + "} "
|
|
96
|
+
return json
|
|
97
|
+
|
|
98
|
+
def variable(self, json: str, v) -> str:
|
|
99
|
+
json = f"{json} " + '{ "type":"variable", '
|
|
100
|
+
json = self.matchable(json, v)
|
|
101
|
+
json = f"{json} " + "} "
|
|
102
|
+
return json
|
|
103
|
+
|
|
104
|
+
def term(self, json: str, t) -> str:
|
|
105
|
+
json = f"{json} " + '{ "type":"term", '
|
|
106
|
+
json = self.matchable(json, t)
|
|
107
|
+
json = f"{json} " + "} "
|
|
108
|
+
return json
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from csvpath.matching.productions.equality import Equality
|
|
2
|
+
from csvpath.matching.productions.variable import Variable
|
|
3
|
+
from csvpath.matching.productions.term import Term
|
|
4
|
+
from csvpath.matching.productions.header import Header
|
|
5
|
+
from csvpath.matching.functions.function import Function
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from csvpath.matching.expression_encoder import ExpressionEncoder
|
|
9
|
+
from csvpath.matching.expression_utility import ExpressionUtility
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExpressionMath:
|
|
14
|
+
"""this code works up to a point. there are limitations in
|
|
15
|
+
the number of operations and there is no precedence or
|
|
16
|
+
grouping. you can enable math with the CsvPath.do_math()
|
|
17
|
+
toggle but unless you know it will solve a specific problem
|
|
18
|
+
you shouldn't. the grammar needs to be reworked to make
|
|
19
|
+
arithmetic possible without functions, but it isn't a
|
|
20
|
+
priority."""
|
|
21
|
+
|
|
22
|
+
def is_terminal(self, o):
|
|
23
|
+
return (
|
|
24
|
+
isinstance(o, Variable)
|
|
25
|
+
or isinstance(o, Term)
|
|
26
|
+
or isinstance(o, Header)
|
|
27
|
+
or isinstance(o, Function)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def do_math(self, expression):
|
|
31
|
+
for i, _ in enumerate(expression.children):
|
|
32
|
+
self.drop_down_pull_up(expression, i, _)
|
|
33
|
+
|
|
34
|
+
def math(self, op, left, right):
|
|
35
|
+
if left is None or right is None:
|
|
36
|
+
raise Exception(
|
|
37
|
+
f"ExpresionMath.math: operands cannot be None: {left}, {right}"
|
|
38
|
+
)
|
|
39
|
+
if op == "+":
|
|
40
|
+
return left + right
|
|
41
|
+
elif op == "-":
|
|
42
|
+
return left - right
|
|
43
|
+
elif op == "*":
|
|
44
|
+
return left * right
|
|
45
|
+
elif op == "/":
|
|
46
|
+
return left / right
|
|
47
|
+
else:
|
|
48
|
+
raise Exception(f"op cannot be {op}")
|
|
49
|
+
|
|
50
|
+
#
|
|
51
|
+
# why is this not combining the last two terms?
|
|
52
|
+
#
|
|
53
|
+
def combine_terms(self, parent, i, child):
|
|
54
|
+
if isinstance(child, Equality) and child.op in ["-", "+", "*", "/"]:
|
|
55
|
+
lv = child.left.to_value()
|
|
56
|
+
if child.right is not None:
|
|
57
|
+
rv = child.right.to_value()
|
|
58
|
+
term = Term(parent.matcher)
|
|
59
|
+
term.value = self.math(child.op, lv, rv)
|
|
60
|
+
parent.children[i] = term
|
|
61
|
+
term.parent = parent
|
|
62
|
+
return term, i
|
|
63
|
+
else:
|
|
64
|
+
print("not combining terms")
|
|
65
|
+
return child, i
|
|
66
|
+
else:
|
|
67
|
+
print("not an equality with math")
|
|
68
|
+
return child, i
|
|
69
|
+
|
|
70
|
+
def push_down_right_terminal(self, parent, i, child):
|
|
71
|
+
eq = isinstance(parent, Equality)
|
|
72
|
+
op = parent.op in ["-", "+", "*", "/"] if eq else None
|
|
73
|
+
"""
|
|
74
|
+
print(f"@ push_down_right_terminal: child {ExpressionUtility._dotted('', child)}")
|
|
75
|
+
json2 = ExpressionEncoder().simple_list_to_json([child])
|
|
76
|
+
print(f"@ push_down_right_terminal: child: {json2}")
|
|
77
|
+
"""
|
|
78
|
+
if eq and op:
|
|
79
|
+
second = self.is_terminal(child)
|
|
80
|
+
if second:
|
|
81
|
+
third = isinstance(child.parent, Equality) and isinstance(
|
|
82
|
+
child.parent.left, Equality
|
|
83
|
+
)
|
|
84
|
+
if third:
|
|
85
|
+
# move child down to left
|
|
86
|
+
term = Term(child.matcher)
|
|
87
|
+
try:
|
|
88
|
+
term.value = self.math(
|
|
89
|
+
parent.op, child.value, child.parent.left.right.value
|
|
90
|
+
)
|
|
91
|
+
child.parent.left.right = term
|
|
92
|
+
term.parent = child.parent.left
|
|
93
|
+
# remove child from it's original place now that term includes its value
|
|
94
|
+
child.parent.right = None
|
|
95
|
+
replace_me = child.parent.parent.index_of_child(child.parent)
|
|
96
|
+
child.parent.parent.children[replace_me] = child.parent.left
|
|
97
|
+
except Exception as ex:
|
|
98
|
+
print(f"problems? or maybe just mathed out. ex: {ex}")
|
|
99
|
+
|
|
100
|
+
def drop_down_pull_up(self, parent, i, child):
|
|
101
|
+
# work right to left
|
|
102
|
+
cs = child.children[:]
|
|
103
|
+
cs.reverse()
|
|
104
|
+
for j, _ in enumerate(cs):
|
|
105
|
+
self.drop_down_pull_up(child, j, _)
|
|
106
|
+
|
|
107
|
+
# if we're a terminal within an equality with math and another terminal
|
|
108
|
+
# we want to reduce ourselves before anything else. to do that we need to
|
|
109
|
+
# shortcut to the parent equality
|
|
110
|
+
choose = isinstance(parent, Equality) and parent.op in ["-", "+", "*", "/"]
|
|
111
|
+
if choose and child.parent.both_terminal():
|
|
112
|
+
try:
|
|
113
|
+
x = child.parent.parent.children.index(parent)
|
|
114
|
+
child, i = self.combine_terms(parent.parent, x, parent)
|
|
115
|
+
except Exception:
|
|
116
|
+
print("no such child anymore. presumably mathed out")
|
|
117
|
+
pass
|
|
118
|
+
else:
|
|
119
|
+
# this method won't combine terms in this equality, but we might
|
|
120
|
+
# do a pull-up-push-down below. do we need to make this call?
|
|
121
|
+
child, i = self.combine_terms(parent, i, child)
|
|
122
|
+
|
|
123
|
+
self.push_down_right_terminal(parent, i, child)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExpressionUtility:
|
|
5
|
+
@classmethod
|
|
6
|
+
def get_id(self, thing):
|
|
7
|
+
# gets a durable ID so funcs like count() can persist throughout the scan
|
|
8
|
+
id = str(thing)
|
|
9
|
+
p = thing.parent
|
|
10
|
+
while p:
|
|
11
|
+
id = id + str(p)
|
|
12
|
+
if p.parent:
|
|
13
|
+
p = p.parent
|
|
14
|
+
else:
|
|
15
|
+
break
|
|
16
|
+
return hashlib.sha256(id.encode("utf-8")).hexdigest()
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def _dotted(self, s, o):
|
|
20
|
+
if o is None:
|
|
21
|
+
return s
|
|
22
|
+
cs = str(o.__class__)
|
|
23
|
+
cs = cs[cs.rfind(".") :]
|
|
24
|
+
c = cs[0 : cs.find("'")]
|
|
25
|
+
s = f"{c}{s}"
|
|
26
|
+
try:
|
|
27
|
+
return self._dotted(s, o.parent)
|
|
28
|
+
except Exception:
|
|
29
|
+
return s
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from csvpath.matching.functions.function import Function, ChildrenException
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Above(Function):
|
|
6
|
+
def to_value(self, *, skip=[]) -> Any:
|
|
7
|
+
if self in skip:
|
|
8
|
+
return True
|
|
9
|
+
if len(self.children) != 1:
|
|
10
|
+
self.matcher.print(
|
|
11
|
+
f"Above.to_value: must have 1 equality child: {self.children}"
|
|
12
|
+
)
|
|
13
|
+
raise ChildrenException("Above function must have 1 child")
|
|
14
|
+
if self.children[0].op != ",":
|
|
15
|
+
raise ChildrenException(
|
|
16
|
+
f"Above function must have an equality with the ',' operation, not {self.children[0].op}"
|
|
17
|
+
)
|
|
18
|
+
thischild = self.children[0].children[0]
|
|
19
|
+
abovethatchild = self.children[0].children[1]
|
|
20
|
+
|
|
21
|
+
this_is = thischild.to_value(skip=skip)
|
|
22
|
+
above_that = abovethatchild.to_value(skip=skip)
|
|
23
|
+
this = -1
|
|
24
|
+
that = -1
|
|
25
|
+
try:
|
|
26
|
+
this = float(this_is)
|
|
27
|
+
that = float(above_that)
|
|
28
|
+
except Exception:
|
|
29
|
+
raise Exception(
|
|
30
|
+
f"Above.to_value: this: {this}, a {this.__class__}, and {that}, a {that.__class__}"
|
|
31
|
+
)
|
|
32
|
+
b = this > that
|
|
33
|
+
return b
|
|
34
|
+
|
|
35
|
+
def matches(self, *, skip=[]) -> bool:
|
|
36
|
+
return self.to_value(skip=skip)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from csvpath.matching.functions.function import Function, ChildrenException
|
|
3
|
+
from csvpath.matching.productions.equality import Equality
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Add(Function):
|
|
7
|
+
def to_value(self, *, skip=[]) -> Any:
|
|
8
|
+
if not self.value:
|
|
9
|
+
if len(self.children) != 1:
|
|
10
|
+
raise ChildrenException("no children. there must be 1 equality child")
|
|
11
|
+
child = self.children[0]
|
|
12
|
+
if not isinstance(child, Equality):
|
|
13
|
+
raise ChildrenException("must be 1 equality child")
|
|
14
|
+
|
|
15
|
+
siblings = child.commas_to_list()
|
|
16
|
+
ret = 0
|
|
17
|
+
for i, sib in enumerate(siblings):
|
|
18
|
+
v = sib.to_value(skip=skip)
|
|
19
|
+
ret = v + ret
|
|
20
|
+
self.value = ret
|
|
21
|
+
return self.value
|
|
22
|
+
|
|
23
|
+
def matches(self, *, skip=[]) -> bool:
|
|
24
|
+
return True
|