@particle-academy/holy-sheet 2.11.0 → 2.11.2

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.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [2.11.2] - 2026-10-28
14
+
15
+ ### Fixed
16
+
17
+ `Agent.lint()` now follows Excel's formula semantics in the places below,
18
+ ported from PHP holy-sheet 2.11.1 (plus lazy `IF`, which all three linters
19
+ gain together). What you will notice: **fewer false `#NAME?` and `#DIV/0!`
20
+ reports** on formulas Excel accepts, and **some new true positives** on
21
+ formulas Excel rejects or errors on that the linter used to pass. `lint()`'s
22
+ signature and the issue shape `{sheet, address, formula, error, hint}` are
23
+ unchanged. Nothing to do unless you assert on the exact set of issues a formula
24
+ produces. Error labels and hint texts are byte-identical to PHP and Python.
25
+
26
+ - **`IF` evaluates only the branch it takes.** `=IF(A2=0,0,1/A2)` with A2 = 0
27
+ used to lint as `#DIV/0!`; it now lints clean, as does any error in the
28
+ untaken branch. That branch is still parsed, so a syntax error or an unknown
29
+ function there is still `#NAME?`. An error in the condition still propagates,
30
+ after the whole `IF` has been parsed. `IFERROR` is not in the linter's
31
+ vocabulary and is still `#NAME?`.
32
+ - **`%` is postfix percent.** `=50%` is 0.5 and `=A1%` is A1/100; these used to
33
+ lint as `#NAME?`. A binary `%` such as `=10%2`, which the linter used to
34
+ evaluate as `10/100*2`, is now reported, because Excel rejects it.
35
+ - **`^` binds tighter than `*` and `/`.** `=2*3^2` is 18, not 36. It stays
36
+ left-associative (`=2^3^2` is 64) and below negation (`=-2^2` is 4), as in
37
+ Excel.
38
+ - **A doubled quote inside text is one quote.** `="a""b"` is the three
39
+ characters `a"b`; it used to lint as `#NAME?`. A text literal that never
40
+ closes (`="abc`) is now reported instead of passing.
41
+ - **IF reads its condition as Excel does.** Text is TRUE or FALSE only when it
42
+ spells one of them, in any case: `"false"` used to count as TRUE. Any other
43
+ text, `""` included, is now `#VALUE!` (a new true positive; it used to pass).
44
+ An error in the condition now propagates instead of picking a branch.
45
+ - **Comparisons follow Excel's type order.** Numeric text is text, and Excel
46
+ sorts text above every number: `="5">9` is TRUE and `="10"=10` is FALSE (both
47
+ used to compare as numbers). Text compares without regard to case
48
+ (`="a"="A"` is TRUE), logicals sort above text, and a blank cell equals 0,
49
+ `""` and FALSE. An error on either side now propagates instead of being
50
+ compared as text. Text is ordered by code point, as PHP and Python order it.
51
+ - **A `#NAME?` hint says which kind it is.** The label is still `#NAME?`, but
52
+ every cause used to get one hint, "references an unknown function or has a
53
+ syntax error". A malformed formula now gets `Syntax error: <what and
54
+ where>...`; an unknown function gets `Unknown function VLOOKUP. ...`; a
55
+ `#NAME?` inherited from another cell names that cell; and an exception inside
56
+ the linter itself says it is a linter defect. Match on the hint's prefix if
57
+ you need to tell them apart.
58
+ - **An unknown function is `#NAME?` wherever it sits.** `=IF(FALSE, FOO(1), 1)`
59
+ used to pass, because the error was a value that IF discarded. Excel resolves
60
+ names first, so it is `#NAME?` there and now here (a new true positive).
61
+ - **`ROUND` rounds the decimal value, half away from zero.** `=ROUND(1.005,2)`
62
+ was 1, `=ROUND(0.285,2)` 0.28 and `=ROUND(8.325,2)` 8.32, because the digits
63
+ were shifted in binary, where 1.005 is 1.00499999.... They are now 1.01, 0.29
64
+ and 8.33, as in Excel, PHP and Python.
65
+ - **The tokenizer uses PHP's ASCII character classes.** A no-break space, or a
66
+ letter such as `é` outside a text literal, is now a syntax error, as it is in
67
+ PHP; the no-break space used to be skipped as whitespace.
68
+
69
+ ## [2.11.1] - 2026-10-27
70
+
71
+ ### Fixed
72
+
73
+ - **An explicit `{value: <string>}` cell is now written as a string, exactly
74
+ as given**, matching PHP holy-sheet and holy-sheet-py. Numeric-string
75
+ coercion used to run on the object form too, so `{value: "+1"}` was written
76
+ as the number 1, `{value: "007"}` as 7 and `{value: "1e5"}` as 100000: a
77
+ phone prefix, zip code or part number lost its characters with no error.
78
+ This applies in `rows` and in the sparse `cells` map, under any column type
79
+ or number format, and to a `set_cell` op's `value`. Unchanged: a BARE string
80
+ in a row or a `cells` entry is still coerced (`"+1"` is still 1) and a bare
81
+ `"=..."` is still a formula; an object value that is already a number is
82
+ still a number; and a string under a `date` / `datetime` display format is
83
+ still converted to a date serial. `read()` returns such a cell as the same
84
+ string.
85
+ **What a consumer must do:** if you relied on `{value: "100"}` becoming a
86
+ number, pass the number (`{value: 100}`) or the bare string (`"100"`).
87
+ Otherwise nothing.
88
+
13
89
  ## [2.11.0] - 2026-10-27
14
90
 
15
91
  ### Added