xtl-py 0.1.0a0__tar.gz

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.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .mypy_cache/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ dist/
9
+ build/
10
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtl-py
3
+ Version: 0.1.0a0
4
+ Summary: XTL (Excel Template Language) 0.1 — Python reference implementation
5
+ Project-URL: Spec, https://github.com/jinyoung4478/xl3
6
+ Author-email: jinyoung4478 <skswls0@daum.net>
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: openpyxl<4,>=3.1
10
+ Requires-Dist: pyyaml<7,>=6
@@ -0,0 +1,160 @@
1
+ # Porting Notes — XTL TS → Python
2
+
3
+ A running log of points where the spec is genuinely underspecified,
4
+ internally inconsistent, or where the Python port had to make a non-obvious
5
+ call. Format per entry:
6
+
7
+ - **Where**: spec section / ADR / fixture number
8
+ - **Question**: the ambiguity in one sentence
9
+ - **TS impl behavior**: what the JS reference implementation does today
10
+ - **Other reasonable interpretations**: alternatives we considered
11
+ - **Our choice**: what the Python port does, and why
12
+ - **Severity**: spec/impl/test (does it block conformance? does it block
13
+ cross-impl portability?)
14
+
15
+ When this file accumulates 5+ entries, batch them into an issue against
16
+ `xl3` and propose ADR amendments.
17
+
18
+ ---
19
+
20
+ ## #1. ECMA-262 scientific-notation cutoff is `1e-6`, not `1e-4`
21
+
22
+ **Where**: `spec/language.md` "Canonical String Form"; `spec/decisions/0009-comparison-and-string-coercion.md` §"Canonical string form".
23
+
24
+ **Question**: ADR-0009 says the canonical-string form of a finite number uses
25
+ "no scientific notation for magnitudes between `1e-4` and `1e21`," and claims
26
+ this matches `Number.prototype.toString`. But ECMA-262 §6.1.6.1.13 actually
27
+ uses a **`-6`** cutoff, not `-4`. So `(0.00005).toString()` is `"0.00005"` in
28
+ JS — decimal, not scientific. The spec text and the cited authority disagree.
29
+
30
+ **TS impl behavior**: uses the host `Number.prototype.toString`, so follows
31
+ ECMA-262 (`-6` cutoff). The fixture corpus was authored against this
32
+ behavior.
33
+
34
+ **Other reasonable interpretations**:
35
+ - Take the spec text literally: scientific for `0 < |x| < 1e-4`. Diverges
36
+ from JS impl + corpus.
37
+ - Take ECMA-262 literally: `-6` cutoff. Matches impl + corpus, contradicts
38
+ spec text.
39
+
40
+ **Our choice**: ECMA-262 cutoff (`-6`). Implementing the literal spec
41
+ text would fail any fixture exercising values in `[1e-6, 1e-4)`. Our
42
+ `canonical_number` re-implements ECMA-262 §6.1.6.1.13 directly so the
43
+ behavior is JS-compatible regardless of host language quirks.
44
+
45
+ **Severity**: spec — propose an ADR amendment that replaces "1e-4" with
46
+ "1e-6" and either drops the redundant `Number.prototype.toString` cite or
47
+ keeps it now that the text matches.
48
+
49
+ ---
50
+
51
+ ## #2. ECMA `String.prototype.trim` historically includes U+FEFF; ADR-0007 excludes it
52
+
53
+ **Where**: `spec/decisions/0007-empty-value-definition.md`.
54
+
55
+ **Question**: ADR-0007 says whitespace "matches the set recognized by
56
+ ECMAScript `String.prototype.trim` — equivalent to the Unicode-mode `\s`
57
+ character class," but then explicitly excludes U+FEFF (zero-width no-break
58
+ space / BOM) and other zero-width characters. ECMAScript's `WhiteSpace`
59
+ production has historically *included* U+FEFF, and most engines implement
60
+ `trim()` accordingly. So a string of bare U+FEFF is empty per native JS
61
+ trim but **non-empty** per ADR-0007.
62
+
63
+ **TS impl behavior**: uses native `String.prototype.trim`, which strips
64
+ U+FEFF on V8/JSC/SM. So a bare-U+FEFF source cell is empty per the impl,
65
+ non-empty per the spec.
66
+
67
+ **Other reasonable interpretations**:
68
+ - Follow Unicode `White_Space` property strictly (what the ADR text says).
69
+ - Follow ECMA-262 `WhiteSpace` production (what the impl actually does).
70
+
71
+ **Our choice**: Unicode `White_Space` (Python `str.isspace()`), per the
72
+ ADR's normative bullet. No fixture currently asserts this edge case, so the
73
+ divergence is silent for the bootstrap corpus, but we follow the spec.
74
+
75
+ **Severity**: impl — TS impl deviates from spec on a technicality. Worth a
76
+ fixture (`empty-zwnbsp-not-whitespace`) to pin behavior either way.
77
+
78
+ ---
79
+
80
+ ## #3. Python `repr(float)` exponent padding differs from ECMA `Number.prototype.toString`
81
+
82
+ **Where**: implementation detail; affects every numeric `&` concat and every
83
+ single-expression cell coercion fallback.
84
+
85
+ **Question**: Python `repr(1e-7)` is `"1e-07"` (two-digit padded exponent);
86
+ JS `(1e-7).toString()` is `"1e-7"`. Python `repr(-0.0)` is `"-0.0"`; JS
87
+ `(-0).toString()` is `"0"`.
88
+
89
+ **TS impl behavior**: matches ECMA-262 by virtue of being JS.
90
+
91
+ **Our choice**: re-implement ECMA-262 §6.1.6.1.13 directly in
92
+ `canonical_number`. Don't trust `repr(float)` to format identically — we
93
+ extract digits + decimal exponent from `repr` and re-render using the
94
+ ECMA format selection rules (decimal in [1e-6, 1e21), scientific outside).
95
+ Negative zero returns `"0"`.
96
+
97
+ **Severity**: impl — just a thing the port has to handle.
98
+
99
+ ---
100
+
101
+ ## #4. Excel serial dates are timezone-naive; ADR-0017 mandates UTC
102
+
103
+ **Where**: ADR-0017 §"Timezone (normative)".
104
+
105
+ **Question**: ADR-0017 says Date components MUST be read in UTC.
106
+ ExcelJS exposes Excel's timezone-naive serial dates as `Date` objects
107
+ *anchored at UTC midnight*, and the impl uses `getUTCFullYear` etc.
108
+ openpyxl returns Python `datetime.datetime` objects with `tzinfo=None`
109
+ (naive); calling `.year`/`.month`/`.date()` on them yields the serial's
110
+ naked components, which is what ADR-0017 wants — but only if you don't
111
+ "localize" them first.
112
+
113
+ **TS impl behavior**: explicit `getUTC*` calls.
114
+
115
+ **Our choice**: never call `.astimezone()` on a naive datetime; treat
116
+ naive openpyxl datetimes as already-UTC. `canonical_date` strips
117
+ `tzinfo` defensively if present.
118
+
119
+ **Severity**: impl — easy to get wrong subtly. Worth a Stage 1 timezone
120
+ matrix fixture run (CI matrix per `STABILITY.md`).
121
+
122
+ ---
123
+
124
+ ## #5. TS IF-condition normalizer recognizes `==` but NOT `=` — spec uses `=`
125
+
126
+ **Where**: `xl3/src/normalizer.ts` lines ~215-225; fixture
127
+ `048-if-and-comparison-boundaries`.
128
+
129
+ **Question**: `language.md` §"Comparison Operators" lists `=` (single equal)
130
+ as the equality operator. The TS normalizer's IF-condition op list is
131
+ `[['!=', 'ne'], ['>=', 'ge'], ['<=', 'le'], ['==', 'eq'], ['>', 'gt'],
132
+ ['<', 'lt']]` — it has `==` but NOT `=`. So
133
+ `IF([Amount] = 0, "zero", "non-zero")` is normalized as a function call
134
+ where the condition is the *raw string* `"[Amount] = 0"`. At eval time
135
+ it falls through to "bare string literal" which is non-empty → truthy →
136
+ IF always takes the THEN branch regardless of Amount.
137
+
138
+ Fixture 048 was authored against this buggy behavior. Its
139
+ `expected.xlsx` has `D3='zero'` for an Amount=1 row, even though
140
+ spec-correct evaluation of `IF(1 = 0, "zero", "non-zero")` is
141
+ `"non-zero"`.
142
+
143
+ **TS impl behavior**: bug — `=` not recognized in IF condition; treated
144
+ as literal string (always truthy).
145
+
146
+ **Other reasonable interpretations**: this is unambiguously a TS impl
147
+ bug under the spec.
148
+
149
+ **Our choice**: follow the spec — `=` is the equality operator
150
+ everywhere, including IF conditions. We FAIL fixture 048.
151
+
152
+ **Severity**: impl + test — `xl3/src/normalizer.ts` should add `=` to
153
+ the ops list before `==`; and fixture 048's `expected.xlsx` should be
154
+ re-authored to reflect spec-correct evaluation. Note that `@filter [col]
155
+ = value` works correctly in TS (different parser). Other fixtures using
156
+ `=` (063, 064, 079, 080, 081, 088, 092) are inside @filter / @join
157
+ clauses that TS parses with a separate code path that does handle `=`.
158
+ Fixture 048 is the only one currently affected.
159
+
160
+ ## #6. (placeholder — append future ambiguities below)
File without changes
@@ -0,0 +1,485 @@
1
+ {
2
+ "implementation": "xl3-py",
3
+ "version": "0.1.0a0",
4
+ "spec_version": "0.1",
5
+ "comparison_stage": 1,
6
+ "results": [
7
+ {
8
+ "fixture": "001-bracket-substitution",
9
+ "status": "pass",
10
+ "duration_ms": 12
11
+ },
12
+ {
13
+ "fixture": "002-if-function",
14
+ "status": "pass",
15
+ "duration_ms": 5
16
+ },
17
+ {
18
+ "fixture": "003-list-sheet-filter",
19
+ "status": "pass",
20
+ "duration_ms": 5
21
+ },
22
+ {
23
+ "fixture": "004-repeat-right-default",
24
+ "status": "pass",
25
+ "duration_ms": 5
26
+ },
27
+ {
28
+ "fixture": "005-round-half-away-from-zero",
29
+ "status": "pass",
30
+ "duration_ms": 5
31
+ },
32
+ {
33
+ "fixture": "006-filename-forbidden-chars",
34
+ "status": "pass",
35
+ "duration_ms": 4
36
+ },
37
+ {
38
+ "fixture": "007-filename-reserved-name",
39
+ "status": "pass",
40
+ "duration_ms": 4
41
+ },
42
+ {
43
+ "fixture": "008-numfmt-numeric-string-coercion",
44
+ "status": "pass",
45
+ "duration_ms": 4
46
+ },
47
+ {
48
+ "fixture": "009-numfmt-date-string-coercion",
49
+ "status": "pass",
50
+ "duration_ms": 4
51
+ },
52
+ {
53
+ "fixture": "010-numfmt-text-format-coercion",
54
+ "status": "pass",
55
+ "duration_ms": 4
56
+ },
57
+ {
58
+ "fixture": "011-text-date-format",
59
+ "status": "pass",
60
+ "duration_ms": 4
61
+ },
62
+ {
63
+ "fixture": "012-text-number-format",
64
+ "status": "pass",
65
+ "duration_ms": 5
66
+ },
67
+ {
68
+ "fixture": "013-rich-text-template-expression",
69
+ "status": "pass",
70
+ "duration_ms": 5
71
+ },
72
+ {
73
+ "fixture": "014-source-formula-cached-result",
74
+ "status": "pass",
75
+ "duration_ms": 5
76
+ },
77
+ {
78
+ "fixture": "015-source-sheet-prefix-first-match",
79
+ "status": "pass",
80
+ "duration_ms": 5
81
+ },
82
+ {
83
+ "fixture": "016-text-number-negative-rounding",
84
+ "status": "pass",
85
+ "duration_ms": 5
86
+ },
87
+ {
88
+ "fixture": "017-source-sheet-prefix-no-match-error",
89
+ "status": "pass",
90
+ "duration_ms": 2
91
+ },
92
+ {
93
+ "fixture": "018-source-formula-missing-cached-result-error",
94
+ "status": "pass",
95
+ "duration_ms": 2
96
+ },
97
+ {
98
+ "fixture": "019-filename-empty-basename-error",
99
+ "status": "pass",
100
+ "duration_ms": 5
101
+ },
102
+ {
103
+ "fixture": "020-filename-length-overflow-error",
104
+ "status": "pass",
105
+ "duration_ms": 8
106
+ },
107
+ {
108
+ "fixture": "021-numfmt-number-coercion-error",
109
+ "status": "pass",
110
+ "duration_ms": 3
111
+ },
112
+ {
113
+ "fixture": "022-numfmt-date-coercion-error",
114
+ "status": "pass",
115
+ "duration_ms": 3
116
+ },
117
+ {
118
+ "fixture": "023-today-utc-dynamic",
119
+ "status": "pass",
120
+ "duration_ms": 4
121
+ },
122
+ {
123
+ "fixture": "024-stage2-merge-preservation",
124
+ "status": "skip",
125
+ "reason": "requires comparison_stage 2"
126
+ },
127
+ {
128
+ "fixture": "025-stage2-style-numfmt-preservation",
129
+ "status": "skip",
130
+ "reason": "requires comparison_stage 2"
131
+ },
132
+ {
133
+ "fixture": "026-stage2-splice-merge-style-preservation",
134
+ "status": "skip",
135
+ "reason": "requires comparison_stage 2"
136
+ },
137
+ {
138
+ "fixture": "027-stage2-cross-writer-canonicalization",
139
+ "status": "skip",
140
+ "reason": "requires comparison_stage 2"
141
+ },
142
+ {
143
+ "fixture": "028-source-table-row-shorthand",
144
+ "status": "pass",
145
+ "duration_ms": 5
146
+ },
147
+ {
148
+ "fixture": "029-source-table-open-range",
149
+ "status": "pass",
150
+ "duration_ms": 5
151
+ },
152
+ {
153
+ "fixture": "030-source-table-finite-range",
154
+ "status": "pass",
155
+ "duration_ms": 5
156
+ },
157
+ {
158
+ "fixture": "031-source-table-zero-data-range",
159
+ "status": "pass",
160
+ "duration_ms": 3
161
+ },
162
+ {
163
+ "fixture": "032-source-table-empty-column-name-error",
164
+ "status": "pass",
165
+ "duration_ms": 2
166
+ },
167
+ {
168
+ "fixture": "033-source-table-duplicate-column-name-error",
169
+ "status": "pass",
170
+ "duration_ms": 2
171
+ },
172
+ {
173
+ "fixture": "034-source-table-invalid-selector-error",
174
+ "status": "pass",
175
+ "duration_ms": 2
176
+ },
177
+ {
178
+ "fixture": "035-source-table-rich-text-header",
179
+ "status": "pass",
180
+ "duration_ms": 5
181
+ },
182
+ {
183
+ "fixture": "036-source-table-formula-header",
184
+ "status": "pass",
185
+ "duration_ms": 5
186
+ },
187
+ {
188
+ "fixture": "037-source-table-formula-header-missing-cache-error",
189
+ "status": "pass",
190
+ "duration_ms": 2
191
+ },
192
+ {
193
+ "fixture": "038-source-sheet-exact-match-beats-prefix",
194
+ "status": "pass",
195
+ "duration_ms": 5
196
+ },
197
+ {
198
+ "fixture": "039-source-sheet-default-first-worksheet",
199
+ "status": "pass",
200
+ "duration_ms": 5
201
+ },
202
+ {
203
+ "fixture": "040-list-sheet-hidden-states-removed",
204
+ "status": "pass",
205
+ "duration_ms": 6
206
+ },
207
+ {
208
+ "fixture": "041-row-function-inside-repeat-block",
209
+ "status": "pass",
210
+ "duration_ms": 5
211
+ },
212
+ {
213
+ "fixture": "042-row-function-outside-repeat-block-error",
214
+ "status": "pass",
215
+ "duration_ms": 3
216
+ },
217
+ {
218
+ "fixture": "043-ifempty-function",
219
+ "status": "pass",
220
+ "duration_ms": 5
221
+ },
222
+ {
223
+ "fixture": "044-sort-and-top-order",
224
+ "status": "pass",
225
+ "duration_ms": 5
226
+ },
227
+ {
228
+ "fixture": "045-list-sheet-not-in-filter",
229
+ "status": "pass",
230
+ "duration_ms": 5
231
+ },
232
+ {
233
+ "fixture": "046-count-field-non-empty",
234
+ "status": "pass",
235
+ "duration_ms": 5
236
+ },
237
+ {
238
+ "fixture": "047-aggregate-functions",
239
+ "status": "pass",
240
+ "duration_ms": 6
241
+ },
242
+ {
243
+ "fixture": "048-if-and-comparison-boundaries",
244
+ "status": "fail",
245
+ "duration_ms": 6,
246
+ "diff": "Report!D3: expected 'zero', got 'non-zero'"
247
+ },
248
+ {
249
+ "fixture": "049-filename-sanitization-warning",
250
+ "status": "pass",
251
+ "duration_ms": 5
252
+ },
253
+ {
254
+ "fixture": "050-empty-ifempty-whitespace-only",
255
+ "status": "pass",
256
+ "duration_ms": 5
257
+ },
258
+ {
259
+ "fixture": "051-empty-ifempty-zero-not-empty",
260
+ "status": "pass",
261
+ "duration_ms": 5
262
+ },
263
+ {
264
+ "fixture": "052-empty-count-field-whitespace-zero-false",
265
+ "status": "pass",
266
+ "duration_ms": 6
267
+ },
268
+ {
269
+ "fixture": "053-empty-row-skip-whitespace-only",
270
+ "status": "pass",
271
+ "duration_ms": 5
272
+ },
273
+ {
274
+ "fixture": "054-empty-list-membership",
275
+ "status": "pass",
276
+ "duration_ms": 5
277
+ },
278
+ {
279
+ "fixture": "055-if-truthy-zero-and-empty",
280
+ "status": "pass",
281
+ "duration_ms": 5
282
+ },
283
+ {
284
+ "fixture": "056-if-truthy-string-zero-not-special",
285
+ "status": "pass",
286
+ "duration_ms": 5
287
+ },
288
+ {
289
+ "fixture": "057-if-truthy-boolean",
290
+ "status": "pass",
291
+ "duration_ms": 5
292
+ },
293
+ {
294
+ "fixture": "058-if-comparison-result",
295
+ "status": "pass",
296
+ "duration_ms": 5
297
+ },
298
+ {
299
+ "fixture": "059-compare-numeric-string-vs-number",
300
+ "status": "pass",
301
+ "duration_ms": 5
302
+ },
303
+ {
304
+ "fixture": "060-compare-string-codepoint-order",
305
+ "status": "pass",
306
+ "duration_ms": 5
307
+ },
308
+ {
309
+ "fixture": "061-concat-canonical-form",
310
+ "status": "pass",
311
+ "duration_ms": 5
312
+ },
313
+ {
314
+ "fixture": "062-concat-empty-stringifies-to-empty",
315
+ "status": "pass",
316
+ "duration_ms": 5
317
+ },
318
+ {
319
+ "fixture": "063-compare-empty-vs-value",
320
+ "status": "pass",
321
+ "duration_ms": 5
322
+ },
323
+ {
324
+ "fixture": "064-compare-unicode-minus-not-numeric",
325
+ "status": "pass",
326
+ "duration_ms": 5
327
+ },
328
+ {
329
+ "fixture": "065-input-text-default-applied",
330
+ "status": "pass",
331
+ "duration_ms": 5
332
+ },
333
+ {
334
+ "fixture": "066-input-text-host-supplied",
335
+ "status": "pass",
336
+ "duration_ms": 5
337
+ },
338
+ {
339
+ "fixture": "067-input-missing-required-error",
340
+ "status": "pass",
341
+ "duration_ms": 1
342
+ },
343
+ {
344
+ "fixture": "068-input-select-host-supplied",
345
+ "status": "pass",
346
+ "duration_ms": 5
347
+ },
348
+ {
349
+ "fixture": "069-source-multi-declaration",
350
+ "status": "pass",
351
+ "duration_ms": 5
352
+ },
353
+ {
354
+ "fixture": "070-source-aggregate-cross-source",
355
+ "status": "pass",
356
+ "duration_ms": 6
357
+ },
358
+ {
359
+ "fixture": "071-source-directive-active",
360
+ "status": "pass",
361
+ "duration_ms": 5
362
+ },
363
+ {
364
+ "fixture": "072-source-undeclared-error",
365
+ "status": "pass",
366
+ "duration_ms": 3
367
+ },
368
+ {
369
+ "fixture": "073-source-row-cross-error",
370
+ "status": "pass",
371
+ "duration_ms": 4
372
+ },
373
+ {
374
+ "fixture": "074-xlookup-basic",
375
+ "status": "pass",
376
+ "duration_ms": 5
377
+ },
378
+ {
379
+ "fixture": "075-xlookup-fallback",
380
+ "status": "pass",
381
+ "duration_ms": 5
382
+ },
383
+ {
384
+ "fixture": "076-xlookup-no-match-error",
385
+ "status": "pass",
386
+ "duration_ms": 8
387
+ },
388
+ {
389
+ "fixture": "077-xlookup-source-mismatch-error",
390
+ "status": "pass",
391
+ "duration_ms": 4
392
+ },
393
+ {
394
+ "fixture": "078-xlookup-bare-bracket-error",
395
+ "status": "pass",
396
+ "duration_ms": 4
397
+ },
398
+ {
399
+ "fixture": "079-join-basic-inner",
400
+ "status": "pass",
401
+ "duration_ms": 6
402
+ },
403
+ {
404
+ "fixture": "080-join-no-match-dropped",
405
+ "status": "pass",
406
+ "duration_ms": 5
407
+ },
408
+ {
409
+ "fixture": "081-join-undeclared-source-error",
410
+ "status": "pass",
411
+ "duration_ms": 3
412
+ },
413
+ {
414
+ "fixture": "082-join-bad-on-clause-error",
415
+ "status": "pass",
416
+ "duration_ms": 4
417
+ },
418
+ {
419
+ "fixture": "083-sort-stable-equal-keys",
420
+ "status": "pass",
421
+ "duration_ms": 5
422
+ },
423
+ {
424
+ "fixture": "084-sort-multi-stable-priority",
425
+ "status": "pass",
426
+ "duration_ms": 5
427
+ },
428
+ {
429
+ "fixture": "085-file-group-first-seen-order",
430
+ "status": "pass",
431
+ "duration_ms": 10
432
+ },
433
+ {
434
+ "fixture": "086-sheet-group-first-seen-order",
435
+ "status": "pass",
436
+ "duration_ms": 5
437
+ },
438
+ {
439
+ "fixture": "087-date-canonical-string-concat",
440
+ "status": "pass",
441
+ "duration_ms": 5
442
+ },
443
+ {
444
+ "fixture": "088-date-comparison-equality",
445
+ "status": "pass",
446
+ "duration_ms": 4
447
+ },
448
+ {
449
+ "fixture": "089-error-sentinel-empty",
450
+ "status": "pass",
451
+ "duration_ms": 5
452
+ },
453
+ {
454
+ "fixture": "090-percentage-numeric-flow",
455
+ "status": "pass",
456
+ "duration_ms": 5
457
+ },
458
+ {
459
+ "fixture": "091-source-unknown-column-error",
460
+ "status": "pass",
461
+ "duration_ms": 4
462
+ },
463
+ {
464
+ "fixture": "092-composed-multi-source-join-filter-sort",
465
+ "status": "pass",
466
+ "duration_ms": 7
467
+ },
468
+ {
469
+ "fixture": "093-stage2-excel-authored-expected",
470
+ "status": "skip",
471
+ "reason": "requires comparison_stage 2"
472
+ },
473
+ {
474
+ "fixture": "094-reserved-sheet-name-error",
475
+ "status": "pass",
476
+ "duration_ms": 1
477
+ }
478
+ ],
479
+ "summary": {
480
+ "total": 89,
481
+ "passed": 88,
482
+ "failed": 1,
483
+ "skipped": 5
484
+ }
485
+ }