@rowsncolumns/utils 9.2.35

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.
Files changed (154) hide show
  1. package/LICENSE.md +100 -0
  2. package/dist/cjs/array.d.ts +3 -0
  3. package/dist/cjs/array.d.ts.map +1 -0
  4. package/dist/cjs/array.js +19 -0
  5. package/dist/cjs/cell-data-helpers.d.ts +19 -0
  6. package/dist/cjs/cell-data-helpers.d.ts.map +1 -0
  7. package/dist/cjs/cell-data-helpers.js +64 -0
  8. package/dist/cjs/color.d.ts +20 -0
  9. package/dist/cjs/color.d.ts.map +1 -0
  10. package/dist/cjs/color.js +168 -0
  11. package/dist/cjs/colors.d.ts +2 -0
  12. package/dist/cjs/colors.d.ts.map +1 -0
  13. package/dist/cjs/colors.js +78 -0
  14. package/dist/cjs/conditional-format.d.ts +49 -0
  15. package/dist/cjs/conditional-format.d.ts.map +1 -0
  16. package/dist/cjs/conditional-format.js +227 -0
  17. package/dist/cjs/content-hash.d.ts +16 -0
  18. package/dist/cjs/content-hash.d.ts.map +1 -0
  19. package/dist/cjs/content-hash.js +72 -0
  20. package/dist/cjs/data-validation.d.ts +54 -0
  21. package/dist/cjs/data-validation.d.ts.map +1 -0
  22. package/dist/cjs/data-validation.js +249 -0
  23. package/dist/cjs/date.d.ts +15 -0
  24. package/dist/cjs/date.d.ts.map +1 -0
  25. package/dist/cjs/date.js +56 -0
  26. package/dist/cjs/dom.d.ts +10 -0
  27. package/dist/cjs/dom.d.ts.map +1 -0
  28. package/dist/cjs/dom.js +97 -0
  29. package/dist/cjs/formatter.d.ts +22 -0
  30. package/dist/cjs/formatter.d.ts.map +1 -0
  31. package/dist/cjs/formatter.js +264 -0
  32. package/dist/cjs/formula-refs.d.ts +7 -0
  33. package/dist/cjs/formula-refs.d.ts.map +1 -0
  34. package/dist/cjs/formula-refs.js +54 -0
  35. package/dist/cjs/formula.d.ts +80 -0
  36. package/dist/cjs/formula.d.ts.map +1 -0
  37. package/dist/cjs/formula.js +401 -0
  38. package/dist/cjs/index.d.ts +24 -0
  39. package/dist/cjs/index.d.ts.map +1 -0
  40. package/dist/cjs/index.js +26 -0
  41. package/dist/cjs/locale.d.ts +34 -0
  42. package/dist/cjs/locale.d.ts.map +1 -0
  43. package/dist/cjs/locale.js +260 -0
  44. package/dist/cjs/log.d.ts +4 -0
  45. package/dist/cjs/log.d.ts.map +1 -0
  46. package/dist/cjs/log.js +28 -0
  47. package/dist/cjs/memoize.d.ts +18 -0
  48. package/dist/cjs/memoize.d.ts.map +1 -0
  49. package/dist/cjs/memoize.js +128 -0
  50. package/dist/cjs/numbers.d.ts +19 -0
  51. package/dist/cjs/numbers.d.ts.map +1 -0
  52. package/dist/cjs/numbers.js +47 -0
  53. package/dist/cjs/selection.d.ts +88 -0
  54. package/dist/cjs/selection.d.ts.map +1 -0
  55. package/dist/cjs/selection.js +368 -0
  56. package/dist/cjs/shared-string-canonical.d.ts +18 -0
  57. package/dist/cjs/shared-string-canonical.d.ts.map +1 -0
  58. package/dist/cjs/shared-string-canonical.js +74 -0
  59. package/dist/cjs/sort.d.ts +15 -0
  60. package/dist/cjs/sort.d.ts.map +1 -0
  61. package/dist/cjs/sort.js +44 -0
  62. package/dist/cjs/string.d.ts +74 -0
  63. package/dist/cjs/string.d.ts.map +1 -0
  64. package/dist/cjs/string.js +237 -0
  65. package/dist/cjs/table-styles.d.ts +56 -0
  66. package/dist/cjs/table-styles.d.ts.map +1 -0
  67. package/dist/cjs/table-styles.js +362 -0
  68. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  69. package/dist/cjs/typeguards.d.ts +4 -0
  70. package/dist/cjs/typeguards.d.ts.map +1 -0
  71. package/dist/cjs/typeguards.js +15 -0
  72. package/dist/cjs/uuid.d.ts +8 -0
  73. package/dist/cjs/uuid.d.ts.map +1 -0
  74. package/dist/cjs/uuid.js +30 -0
  75. package/dist/cjs/vitest.config.d.ts +3 -0
  76. package/dist/cjs/vitest.config.d.ts.map +1 -0
  77. package/dist/cjs/vitest.config.js +9 -0
  78. package/dist/esm/array.d.ts +3 -0
  79. package/dist/esm/array.d.ts.map +1 -0
  80. package/dist/esm/array.js +14 -0
  81. package/dist/esm/cell-data-helpers.d.ts +19 -0
  82. package/dist/esm/cell-data-helpers.d.ts.map +1 -0
  83. package/dist/esm/cell-data-helpers.js +39 -0
  84. package/dist/esm/color.d.ts +20 -0
  85. package/dist/esm/color.d.ts.map +1 -0
  86. package/dist/esm/color.js +152 -0
  87. package/dist/esm/colors.d.ts +2 -0
  88. package/dist/esm/colors.d.ts.map +1 -0
  89. package/dist/esm/colors.js +75 -0
  90. package/dist/esm/conditional-format.d.ts +49 -0
  91. package/dist/esm/conditional-format.d.ts.map +1 -0
  92. package/dist/esm/conditional-format.js +219 -0
  93. package/dist/esm/content-hash.d.ts +16 -0
  94. package/dist/esm/content-hash.d.ts.map +1 -0
  95. package/dist/esm/content-hash.js +68 -0
  96. package/dist/esm/data-validation.d.ts +54 -0
  97. package/dist/esm/data-validation.d.ts.map +1 -0
  98. package/dist/esm/data-validation.js +237 -0
  99. package/dist/esm/date.d.ts +15 -0
  100. package/dist/esm/date.d.ts.map +1 -0
  101. package/dist/esm/date.js +46 -0
  102. package/dist/esm/dom.d.ts +10 -0
  103. package/dist/esm/dom.d.ts.map +1 -0
  104. package/dist/esm/dom.js +90 -0
  105. package/dist/esm/formatter.d.ts +22 -0
  106. package/dist/esm/formatter.d.ts.map +1 -0
  107. package/dist/esm/formatter.js +253 -0
  108. package/dist/esm/formula-refs.d.ts +7 -0
  109. package/dist/esm/formula-refs.d.ts.map +1 -0
  110. package/dist/esm/formula-refs.js +49 -0
  111. package/dist/esm/formula.d.ts +80 -0
  112. package/dist/esm/formula.d.ts.map +1 -0
  113. package/dist/esm/formula.js +298 -0
  114. package/dist/esm/index.d.ts +24 -0
  115. package/dist/esm/index.d.ts.map +1 -0
  116. package/dist/esm/index.js +23 -0
  117. package/dist/esm/locale.d.ts +34 -0
  118. package/dist/esm/locale.d.ts.map +1 -0
  119. package/dist/esm/locale.js +223 -0
  120. package/dist/esm/log.d.ts +4 -0
  121. package/dist/esm/log.d.ts.map +1 -0
  122. package/dist/esm/log.js +3 -0
  123. package/dist/esm/memoize.d.ts +18 -0
  124. package/dist/esm/memoize.d.ts.map +1 -0
  125. package/dist/esm/memoize.js +95 -0
  126. package/dist/esm/numbers.d.ts +19 -0
  127. package/dist/esm/numbers.d.ts.map +1 -0
  128. package/dist/esm/numbers.js +40 -0
  129. package/dist/esm/selection.d.ts +88 -0
  130. package/dist/esm/selection.d.ts.map +1 -0
  131. package/dist/esm/selection.js +311 -0
  132. package/dist/esm/shared-string-canonical.d.ts +18 -0
  133. package/dist/esm/shared-string-canonical.d.ts.map +1 -0
  134. package/dist/esm/shared-string-canonical.js +68 -0
  135. package/dist/esm/sort.d.ts +15 -0
  136. package/dist/esm/sort.d.ts.map +1 -0
  137. package/dist/esm/sort.js +39 -0
  138. package/dist/esm/string.d.ts +74 -0
  139. package/dist/esm/string.d.ts.map +1 -0
  140. package/dist/esm/string.js +213 -0
  141. package/dist/esm/table-styles.d.ts +56 -0
  142. package/dist/esm/table-styles.d.ts.map +1 -0
  143. package/dist/esm/table-styles.js +367 -0
  144. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  145. package/dist/esm/typeguards.d.ts +4 -0
  146. package/dist/esm/typeguards.d.ts.map +1 -0
  147. package/dist/esm/typeguards.js +7 -0
  148. package/dist/esm/uuid.d.ts +8 -0
  149. package/dist/esm/uuid.d.ts.map +1 -0
  150. package/dist/esm/uuid.js +25 -0
  151. package/dist/esm/vitest.config.d.ts +3 -0
  152. package/dist/esm/vitest.config.d.ts.map +1 -0
  153. package/dist/esm/vitest.config.js +7 -0
  154. package/package.json +49 -0
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildConditionalFormatExpression = exports.isConditionalFormatFormulaRule = void 0;
4
+ var data_validation_1 = require("./data-validation");
5
+ var selection_1 = require("./selection");
6
+ var FORMULA_PREDICATE_BASE_CELL_REF = "A1";
7
+ var NUMBER_LITERAL_PATTERN = /^-?\d+(?:\.\d+)?$/;
8
+ var BOOLEAN_LITERAL_PATTERN = /^(TRUE|FALSE)$/i;
9
+ var escapeFormulaText = function (value) { return value.replace(/"/g, '""'); };
10
+ /**
11
+ * Parse a locale-formatted numeric string (e.g., "6,5" in fr-FR) into a JS
12
+ * number. Uses `Intl.NumberFormat` to detect decimal/group separators.
13
+ * Returns NaN on failure.
14
+ */
15
+ var parseLocaleNumberLoose = function (value, locale) {
16
+ var _a, _b, _c, _d;
17
+ try {
18
+ var parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
19
+ var decimal = (_b = (_a = parts.find(function (part) { return part.type === "decimal"; })) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : ".";
20
+ var group = (_d = (_c = parts.find(function (part) { return part.type === "group"; })) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : ",";
21
+ var cleaned = value.split(group).join("").replace(decimal, ".");
22
+ return Number(cleaned);
23
+ }
24
+ catch (_e) {
25
+ return NaN;
26
+ }
27
+ };
28
+ /**
29
+ * Normalize a condition value for use in a formula expression.
30
+ * Handles locale-specific number formats (e.g., "6,5" -> "6.5").
31
+ */
32
+ var normalizeConditionValue = function (raw, allowStringLiteral, locale) {
33
+ if (allowStringLiteral === void 0) { allowStringLiteral = false; }
34
+ if (!raw)
35
+ return "";
36
+ var trimmed = raw.trim();
37
+ if (!trimmed)
38
+ return "";
39
+ if (trimmed.startsWith("=")) {
40
+ return trimmed.slice(1);
41
+ }
42
+ if ((0, selection_1.checkIfStringIsReference)(trimmed)) {
43
+ return trimmed;
44
+ }
45
+ if (NUMBER_LITERAL_PATTERN.test(trimmed) ||
46
+ BOOLEAN_LITERAL_PATTERN.test(trimmed)) {
47
+ return trimmed;
48
+ }
49
+ // Try parsing as locale-aware number
50
+ if (locale) {
51
+ var parsed = parseLocaleNumberLoose(trimmed, locale);
52
+ if (!isNaN(parsed)) {
53
+ return String(parsed);
54
+ }
55
+ }
56
+ if (allowStringLiteral) {
57
+ return "\"".concat(escapeFormulaText(trimmed), "\"");
58
+ }
59
+ return trimmed;
60
+ };
61
+ /**
62
+ * Whether a CF rule is evaluated via the calculator (formula path) instead of
63
+ * the synchronous `checkBooleanRule` path used in
64
+ * `apps/spreadsheet/hooks/use-conditional-formatting.ts`.
65
+ *
66
+ * Returns `true` for every condition type that `buildConditionalFormatExpression`
67
+ * can express as a single-cell formula: CUSTOM_FORMULA, all NUMBER_/DATE_/TEXT_
68
+ * comparisons, BETWEEN/NOT_BETWEEN, CONTAINS/NOT_CONTAINS, STARTS_WITH/ENDS_WITH,
69
+ * ONE_OF_LIST/ONE_OF_RANGE, BLANK/NOT_BLANK, BOOLEAN, TEXT_IS_EMAIL/TEXT_IS_URL.
70
+ * The calculator stores the per-cell boolean on
71
+ * `cellData.conditionalFormattingResultById[ruleId]`.
72
+ *
73
+ * Returns `false` — leaving the rule on the sync path — for rules that aren't
74
+ * expressible as a single-cell formula:
75
+ * - Aggregate-over-range: gradient/heatmap, TOP/BOTTOM N, DUPLICATE/UNIQUE,
76
+ * NUMBER_ABOVE_AVERAGE / NUMBER_BELOW_AVERAGE — the result depends on
77
+ * other cells in the range, not just the current cell.
78
+ * - Loop-state: NUMBER_GREATER_THAN_PREVIOUS and the other `*_PREVIOUS`
79
+ * variants — compare against the prior row's value held in iteration state.
80
+ * - Calendar-relative dates: DATE_TODAY / DATE_THIS_WEEK / DATE_THIS_MONTH
81
+ * and the surrounding family, plus DATE_IS_VALID. Could be expressed via
82
+ * TODAY()-based formulas if we ever want to migrate them.
83
+ * - Cell error state: ERROR / NOT_ERROR — read directly from the extended
84
+ * value in `getValidationValue`.
85
+ *
86
+ * The consumers (rule create/update/delete enqueueing in spreadsheet-state,
87
+ * `getConditionalFormatFormula` building the expression, `getValidationValue`
88
+ * choosing where to read the per-cell value, and the `isFormulaRule` branch
89
+ * in `use-conditional-formatting.ts`) MUST stay in lockstep with this
90
+ * predicate — if it returns `true`, the calculator owns the result and the
91
+ * sync `checkBooleanRule` path must not run for that rule. Delegating to
92
+ * `buildConditionalFormatExpression` keeps the predicate and the builder from
93
+ * drifting.
94
+ */
95
+ var isConditionalFormatFormulaRule = function (rule) {
96
+ return Boolean((0, exports.buildConditionalFormatExpression)(rule, FORMULA_PREDICATE_BASE_CELL_REF));
97
+ };
98
+ exports.isConditionalFormatFormulaRule = isConditionalFormatFormulaRule;
99
+ var buildConditionalFormatExpression = function (rule, baseCellRef, locale) {
100
+ var _a, _b, _c, _d;
101
+ var condition = (_a = rule === null || rule === void 0 ? void 0 : rule.booleanRule) === null || _a === void 0 ? void 0 : _a.condition;
102
+ if (!(condition === null || condition === void 0 ? void 0 : condition.type))
103
+ return undefined;
104
+ var values = (_b = condition.values) !== null && _b !== void 0 ? _b : [];
105
+ var fromRaw = (_c = values[0]) === null || _c === void 0 ? void 0 : _c.userEnteredValue;
106
+ var toRaw = (_d = values[1]) === null || _d === void 0 ? void 0 : _d.userEnteredValue;
107
+ if (condition.type === "CUSTOM_FORMULA") {
108
+ var raw = fromRaw !== null && fromRaw !== void 0 ? fromRaw : "";
109
+ var trimmed = raw.trim();
110
+ if (!trimmed)
111
+ return undefined;
112
+ return trimmed.startsWith("=") ? trimmed : "=".concat(trimmed);
113
+ }
114
+ var fromExpr = normalizeConditionValue(fromRaw, true, locale);
115
+ var toExpr = normalizeConditionValue(toRaw, true, locale);
116
+ if (!fromExpr && !toExpr)
117
+ return undefined;
118
+ var comparisonOperatorMap = {
119
+ NUMBER_EQ: "=",
120
+ NUMBER_NOT_EQ: "<>",
121
+ NUMBER_GREATER: ">",
122
+ NUMBER_GREATER_THAN_EQ: ">=",
123
+ NUMBER_LESS: "<",
124
+ NUMBER_LESS_THAN_EQ: "<=",
125
+ DATE_EQ: "=",
126
+ DATE_NOT_EQ: "<>",
127
+ DATE_BEFORE: "<",
128
+ DATE_AFTER: ">",
129
+ DATE_ON_OR_BEFORE: "<=",
130
+ DATE_ON_OR_AFTER: ">=",
131
+ TEXT_EQ: "=",
132
+ TEXT_NOT_EQ: "<>",
133
+ };
134
+ if (condition.type === "NUMBER_BETWEEN" ||
135
+ condition.type === "DATE_BETWEEN") {
136
+ if (!fromExpr || !toExpr)
137
+ return undefined;
138
+ return "=AND(".concat(baseCellRef, ">=").concat(fromExpr, ",").concat(baseCellRef, "<=").concat(toExpr, ")");
139
+ }
140
+ if (condition.type === "NUMBER_NOT_BETWEEN" ||
141
+ condition.type === "DATE_NOT_BETWEEN") {
142
+ if (!fromExpr || !toExpr)
143
+ return undefined;
144
+ return "=OR(".concat(baseCellRef, "<").concat(fromExpr, ",").concat(baseCellRef, ">").concat(toExpr, ")");
145
+ }
146
+ var comparisonOperator = comparisonOperatorMap[condition.type];
147
+ if (comparisonOperator) {
148
+ if (!fromExpr)
149
+ return undefined;
150
+ return "=".concat(baseCellRef).concat(comparisonOperator).concat(fromExpr);
151
+ }
152
+ if (condition.type === "TEXT_CONTAINS") {
153
+ if (!fromExpr)
154
+ return undefined;
155
+ return "=ISNUMBER(SEARCH(".concat(fromExpr, ",").concat(baseCellRef, "))");
156
+ }
157
+ if (condition.type === "TEXT_NOT_CONTAINS") {
158
+ if (!fromExpr)
159
+ return undefined;
160
+ return "=NOT(ISNUMBER(SEARCH(".concat(fromExpr, ",").concat(baseCellRef, ")))");
161
+ }
162
+ if (condition.type === "TEXT_STARTS_WITH") {
163
+ if (!fromExpr)
164
+ return undefined;
165
+ return "=LEFT(".concat(baseCellRef, ",LEN(").concat(fromExpr, "))=").concat(fromExpr);
166
+ }
167
+ if (condition.type === "TEXT_ENDS_WITH") {
168
+ if (!fromExpr)
169
+ return undefined;
170
+ return "=RIGHT(".concat(baseCellRef, ",LEN(").concat(fromExpr, "))=").concat(fromExpr);
171
+ }
172
+ if (condition.type === "TEXT_IS_EMAIL") {
173
+ return "=ISNUMBER(SEARCH(\"@\",".concat(baseCellRef, "))");
174
+ }
175
+ if (condition.type === "TEXT_IS_URL") {
176
+ return "=OR(LEFT(".concat(baseCellRef, ",7)=\"http://\",LEFT(").concat(baseCellRef, ",8)=\"https://\")");
177
+ }
178
+ if (condition.type === "ONE_OF_RANGE") {
179
+ if (!fromExpr)
180
+ return undefined;
181
+ return "=COUNTIF(".concat(fromExpr, ",").concat(baseCellRef, ")>0");
182
+ }
183
+ if (condition.type === "ONE_OF_LIST") {
184
+ var entries = fromRaw
185
+ ? fromRaw.split(",").map(function (entry) { return entry.trim(); })
186
+ : [];
187
+ // A leading apostrophe is Excel's text prefix — `'000s` is the text
188
+ // "000s" and a bare `'` is empty text — never part of the value. A cell
189
+ // storing the lone text-prefix apostrophe is equivalent to a blank cell,
190
+ // so the bare `'` entry accepts it too.
191
+ var conditions = entries
192
+ .flatMap(function (entry) {
193
+ if (entry.startsWith("'")) {
194
+ var stripped = (0, data_validation_1.stripDataValidationTextPrefix)(entry);
195
+ var textCondition = "".concat(baseCellRef, "=\"").concat(escapeFormulaText(stripped), "\"");
196
+ return stripped === ""
197
+ ? [textCondition, "".concat(baseCellRef, "=\"'\"")]
198
+ : [textCondition];
199
+ }
200
+ return entry
201
+ ? ["".concat(baseCellRef, "=").concat(normalizeConditionValue(entry, true, locale))]
202
+ : [];
203
+ })
204
+ .filter(function (entry) { return Boolean(entry); });
205
+ if (!conditions.length)
206
+ return undefined;
207
+ return conditions.length === 1
208
+ ? "=".concat(conditions[0])
209
+ : "=OR(".concat(conditions.join(","), ")");
210
+ }
211
+ if (condition.type === "BLANK") {
212
+ return "=".concat(baseCellRef, "=\"\"");
213
+ }
214
+ if (condition.type === "NOT_BLANK") {
215
+ return "=".concat(baseCellRef, "<>\"\"");
216
+ }
217
+ if (condition.type === "BOOLEAN") {
218
+ return "=OR(".concat(baseCellRef, "=TRUE,").concat(baseCellRef, "=FALSE)");
219
+ }
220
+ if (condition.type === "TEXT_EQ" || condition.type === "TEXT_NOT_EQ") {
221
+ if (!fromExpr)
222
+ return undefined;
223
+ return "=".concat(baseCellRef).concat(comparisonOperatorMap[condition.type]).concat(fromExpr);
224
+ }
225
+ return undefined;
226
+ };
227
+ exports.buildConditionalFormatExpression = buildConditionalFormatExpression;
@@ -0,0 +1,16 @@
1
+ export declare const CONTENT_HASH_LENGTH = 8;
2
+ /**
3
+ * Stable, deterministic 8-character base62 ID derived from the input string.
4
+ * The same value always produces the same id across sessions, clients, and
5
+ * languages (FNV-1a 64-bit over UTF-8 bytes → low-48 → base62).
6
+ *
7
+ * 62^8 ≈ 2.18e14 ids; birthday-collision risk is negligible for spreadsheet
8
+ * shared-string tables. Callers that need defense against the astronomically
9
+ * rare same-id-different-value case should keep a random-id fallback.
10
+ *
11
+ * IMPORTANT: This algorithm is mirrored in
12
+ * `python/rowsncolumns_spreadsheet/utils.py::content_hash`. Any change to the
13
+ * constants, byte encoding, or base62 alphabet must be applied to both ports.
14
+ */
15
+ export declare const contentHash: (value: string) => string;
16
+ //# sourceMappingURL=content-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-hash.d.ts","sourceRoot":"","sources":["../../content-hash.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAsCrC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAa3C,CAAC"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.contentHash = exports.CONTENT_HASH_LENGTH = void 0;
4
+ // BigInt literals (e.g. `0x…n`) need ES2020+; the utils package targets older
5
+ // runtimes, so build them via the BigInt() constructor instead.
6
+ var FNV_OFFSET_BASIS_64 = BigInt("0xcbf29ce484222325");
7
+ var FNV_PRIME_64 = BigInt("0x100000001b3");
8
+ var ONE = BigInt(1);
9
+ var SIXTY_FOUR = BigInt(64);
10
+ var MASK_64 = (ONE << SIXTY_FOUR) - ONE;
11
+ var BASE62_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
12
+ var BASE62 = BigInt(BASE62_ALPHABET.length);
13
+ var TEXT_ENCODER = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
14
+ exports.CONTENT_HASH_LENGTH = 8;
15
+ var toUtf8Bytes = function (value) {
16
+ if (TEXT_ENCODER)
17
+ return TEXT_ENCODER.encode(value);
18
+ // Minimal UTF-8 fallback for environments without TextEncoder. Mirrors the
19
+ // standard encoding so the hash stays identical across runtimes.
20
+ var bytes = [];
21
+ for (var i = 0; i < value.length; i += 1) {
22
+ var codePoint = value.charCodeAt(i);
23
+ if (codePoint >= 0xd800 && codePoint <= 0xdbff && i + 1 < value.length) {
24
+ var next = value.charCodeAt(i + 1);
25
+ if (next >= 0xdc00 && next <= 0xdfff) {
26
+ codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00);
27
+ i += 1;
28
+ }
29
+ }
30
+ if (codePoint < 0x80) {
31
+ bytes.push(codePoint);
32
+ }
33
+ else if (codePoint < 0x800) {
34
+ bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
35
+ }
36
+ else if (codePoint < 0x10000) {
37
+ bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
38
+ }
39
+ else {
40
+ bytes.push(0xf0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 0x3f), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
41
+ }
42
+ }
43
+ return bytes;
44
+ };
45
+ /**
46
+ * Stable, deterministic 8-character base62 ID derived from the input string.
47
+ * The same value always produces the same id across sessions, clients, and
48
+ * languages (FNV-1a 64-bit over UTF-8 bytes → low-48 → base62).
49
+ *
50
+ * 62^8 ≈ 2.18e14 ids; birthday-collision risk is negligible for spreadsheet
51
+ * shared-string tables. Callers that need defense against the astronomically
52
+ * rare same-id-different-value case should keep a random-id fallback.
53
+ *
54
+ * IMPORTANT: This algorithm is mirrored in
55
+ * `python/rowsncolumns_spreadsheet/utils.py::content_hash`. Any change to the
56
+ * constants, byte encoding, or base62 alphabet must be applied to both ports.
57
+ */
58
+ var contentHash = function (value) {
59
+ var hash = FNV_OFFSET_BASIS_64;
60
+ var bytes = toUtf8Bytes(value);
61
+ for (var i = 0; i < bytes.length; i += 1) {
62
+ hash ^= BigInt(bytes[i]);
63
+ hash = (hash * FNV_PRIME_64) & MASK_64;
64
+ }
65
+ var result = "";
66
+ for (var i = 0; i < exports.CONTENT_HASH_LENGTH; i += 1) {
67
+ result = BASE62_ALPHABET[Number(hash % BASE62)] + result;
68
+ hash = hash / BASE62;
69
+ }
70
+ return result;
71
+ };
72
+ exports.contentHash = contentHash;
@@ -0,0 +1,54 @@
1
+ import type { DataValidationRule } from "@rowsncolumns/common-types";
2
+ /**
3
+ * A leading apostrophe in a data-validation / conditional-format list entry is
4
+ * Excel's text prefix — `'000s` is the text "000s" and a bare `'` is empty
5
+ * text — never part of the value itself.
6
+ */
7
+ export declare const stripDataValidationTextPrefix: (entry: string) => string;
8
+ /**
9
+ * Whether a cell's current text matches a ONE_OF_LIST option, with Excel's
10
+ * text-prefix semantics on BOTH sides: the option strips its leading `'`
11
+ * (`'000s` ≡ "000s", a bare `'` ≡ empty text) and a cell storing the lone
12
+ * text-prefix apostrophe is equivalent to a blank cell. Case-insensitive,
13
+ * mirroring `checkBooleanRule`'s ONE_OF_LIST matching.
14
+ */
15
+ export declare const dataValidationOptionMatchesCellValue: (option: string | undefined | null, cellValue: string | undefined | null) => boolean;
16
+ /**
17
+ * Whether a data validation rule is evaluated via the calculator (formula path)
18
+ * instead of the synchronous `checkBooleanRule` path used in
19
+ * `SheetCell.validate()` (`libs/spreadsheet-state/sheet-cell.ts`).
20
+ *
21
+ * Returns `true` for every condition type that `buildDataValidationExpression`
22
+ * can express as a single-cell formula: CUSTOM_FORMULA, all NUMBER_/DATE_/TEXT_
23
+ * comparisons, BETWEEN/NOT_BETWEEN, CONTAINS/NOT_CONTAINS, STARTS_WITH/ENDS_WITH,
24
+ * ONE_OF_LIST/ONE_OF_RANGE, BLANK/NOT_BLANK, BOOLEAN, TEXT_IS_EMAIL/TEXT_IS_URL.
25
+ * The calculator stores the per-cell boolean on
26
+ * `cellData.dataValidationResultById` and `SheetCell.validate()` reads
27
+ * `this.dataValidationResult` to decide validity.
28
+ *
29
+ * Returns `false` — leaving the rule on the sync path — for rules that aren't
30
+ * expressible as a single-cell formula:
31
+ * - Loop-state: NUMBER_*_PREVIOUS comparisons (compare to prior row).
32
+ * - Calendar-relative dates: DATE_TODAY / DATE_THIS_WEEK / DATE_THIS_MONTH
33
+ * and the surrounding family, plus DATE_IS_VALID.
34
+ * - Cell error state: ERROR / NOT_ERROR.
35
+ *
36
+ * Note (input-gating tradeoff): unlike conditional formatting, data validation
37
+ * decides whether a cell is flagged invalid. The formula path runs through the
38
+ * calculator, so a freshly-typed value spends one calc tick at
39
+ * `dataValidationResult === undefined`, during which `SheetCell.validate()`
40
+ * treats the cell as valid. Anything reading validity synchronously right
41
+ * after a write may see stale-valid for one tick.
42
+ *
43
+ * Consumers that must stay in lockstep with this predicate:
44
+ * - `createDataValidationRule` / update / delete (enqueueing into the calc).
45
+ * - `getDataValidationFormula` (builds the expression for the calculator).
46
+ * - `SheetCell.validate()` (chooses formula-result vs sync `checkBooleanRule`).
47
+ * - `use-on-filter-table` (uses the same predicate to decide its formula path).
48
+ *
49
+ * Delegating to `buildDataValidationExpression` keeps the predicate and the
50
+ * builder from drifting.
51
+ */
52
+ export declare const isDataValidationFormulaRule: (rule: DataValidationRule | undefined) => boolean;
53
+ export declare const buildDataValidationExpression: (rule: DataValidationRule | undefined, baseCellRef: string, locale?: string) => string | undefined;
54
+ //# sourceMappingURL=data-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-validation.d.ts","sourceRoot":"","sources":["../../data-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAWpC;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAI,OAAO,MAAM,KAAG,MACd,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAC/C,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,EACjC,WAAW,MAAM,GAAG,SAAS,GAAG,IAAI,KACnC,OAKF,CAAC;AAwDF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,2BAA2B,GACtC,MAAM,kBAAkB,GAAG,SAAS,YAKrC,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,MAAM,kBAAkB,GAAG,SAAS,EACpC,aAAa,MAAM,EACnB,SAAS,MAAM,uBA4HhB,CAAC"}
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildDataValidationExpression = exports.isDataValidationFormulaRule = exports.dataValidationOptionMatchesCellValue = exports.stripDataValidationTextPrefix = void 0;
4
+ var selection_1 = require("./selection");
5
+ var FORMULA_PREDICATE_BASE_CELL_REF = "A1";
6
+ var NUMBER_LITERAL_PATTERN = /^-?\d+(?:\.\d+)?$/;
7
+ var BOOLEAN_LITERAL_PATTERN = /^(TRUE|FALSE)$/i;
8
+ var escapeFormulaText = function (value) { return value.replace(/"/g, '""'); };
9
+ /**
10
+ * A leading apostrophe in a data-validation / conditional-format list entry is
11
+ * Excel's text prefix — `'000s` is the text "000s" and a bare `'` is empty
12
+ * text — never part of the value itself.
13
+ */
14
+ var stripDataValidationTextPrefix = function (entry) {
15
+ return entry.startsWith("'") ? entry.slice(1) : entry;
16
+ };
17
+ exports.stripDataValidationTextPrefix = stripDataValidationTextPrefix;
18
+ /**
19
+ * Whether a cell's current text matches a ONE_OF_LIST option, with Excel's
20
+ * text-prefix semantics on BOTH sides: the option strips its leading `'`
21
+ * (`'000s` ≡ "000s", a bare `'` ≡ empty text) and a cell storing the lone
22
+ * text-prefix apostrophe is equivalent to a blank cell. Case-insensitive,
23
+ * mirroring `checkBooleanRule`'s ONE_OF_LIST matching.
24
+ */
25
+ var dataValidationOptionMatchesCellValue = function (option, cellValue) {
26
+ var normalizedOption = (0, exports.stripDataValidationTextPrefix)(option !== null && option !== void 0 ? option : "");
27
+ var rawValue = cellValue !== null && cellValue !== void 0 ? cellValue : "";
28
+ var normalizedValue = rawValue === "'" ? "" : rawValue;
29
+ return normalizedOption.toLowerCase() === normalizedValue.toLowerCase();
30
+ };
31
+ exports.dataValidationOptionMatchesCellValue = dataValidationOptionMatchesCellValue;
32
+ /**
33
+ * Parse a locale-formatted numeric string (e.g., "6,5" in fr-FR) into a JS
34
+ * number. Uses `Intl.NumberFormat` to detect decimal/group separators.
35
+ * Returns NaN on failure.
36
+ */
37
+ var parseLocaleNumberLoose = function (value, locale) {
38
+ var _a, _b, _c, _d;
39
+ try {
40
+ var parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
41
+ var decimal = (_b = (_a = parts.find(function (part) { return part.type === "decimal"; })) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : ".";
42
+ var group = (_d = (_c = parts.find(function (part) { return part.type === "group"; })) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : ",";
43
+ var cleaned = value.split(group).join("").replace(decimal, ".");
44
+ return Number(cleaned);
45
+ }
46
+ catch (_e) {
47
+ return NaN;
48
+ }
49
+ };
50
+ /**
51
+ * Normalize a condition value for use in a formula expression.
52
+ * Handles locale-specific number formats (e.g., "6,5" -> "6.5").
53
+ */
54
+ var normalizeConditionValue = function (raw, allowStringLiteral, locale) {
55
+ if (allowStringLiteral === void 0) { allowStringLiteral = false; }
56
+ if (!raw)
57
+ return "";
58
+ var trimmed = raw.trim();
59
+ if (!trimmed)
60
+ return "";
61
+ if (trimmed.startsWith("=")) {
62
+ return trimmed.slice(1);
63
+ }
64
+ if ((0, selection_1.checkIfStringIsReference)(trimmed)) {
65
+ return trimmed;
66
+ }
67
+ if (NUMBER_LITERAL_PATTERN.test(trimmed) ||
68
+ BOOLEAN_LITERAL_PATTERN.test(trimmed)) {
69
+ return trimmed;
70
+ }
71
+ // Try parsing as locale-aware number
72
+ if (locale) {
73
+ var parsed = parseLocaleNumberLoose(trimmed, locale);
74
+ if (!isNaN(parsed)) {
75
+ return String(parsed);
76
+ }
77
+ }
78
+ if (allowStringLiteral) {
79
+ return "\"".concat(escapeFormulaText(trimmed), "\"");
80
+ }
81
+ return trimmed;
82
+ };
83
+ /**
84
+ * Whether a data validation rule is evaluated via the calculator (formula path)
85
+ * instead of the synchronous `checkBooleanRule` path used in
86
+ * `SheetCell.validate()` (`libs/spreadsheet-state/sheet-cell.ts`).
87
+ *
88
+ * Returns `true` for every condition type that `buildDataValidationExpression`
89
+ * can express as a single-cell formula: CUSTOM_FORMULA, all NUMBER_/DATE_/TEXT_
90
+ * comparisons, BETWEEN/NOT_BETWEEN, CONTAINS/NOT_CONTAINS, STARTS_WITH/ENDS_WITH,
91
+ * ONE_OF_LIST/ONE_OF_RANGE, BLANK/NOT_BLANK, BOOLEAN, TEXT_IS_EMAIL/TEXT_IS_URL.
92
+ * The calculator stores the per-cell boolean on
93
+ * `cellData.dataValidationResultById` and `SheetCell.validate()` reads
94
+ * `this.dataValidationResult` to decide validity.
95
+ *
96
+ * Returns `false` — leaving the rule on the sync path — for rules that aren't
97
+ * expressible as a single-cell formula:
98
+ * - Loop-state: NUMBER_*_PREVIOUS comparisons (compare to prior row).
99
+ * - Calendar-relative dates: DATE_TODAY / DATE_THIS_WEEK / DATE_THIS_MONTH
100
+ * and the surrounding family, plus DATE_IS_VALID.
101
+ * - Cell error state: ERROR / NOT_ERROR.
102
+ *
103
+ * Note (input-gating tradeoff): unlike conditional formatting, data validation
104
+ * decides whether a cell is flagged invalid. The formula path runs through the
105
+ * calculator, so a freshly-typed value spends one calc tick at
106
+ * `dataValidationResult === undefined`, during which `SheetCell.validate()`
107
+ * treats the cell as valid. Anything reading validity synchronously right
108
+ * after a write may see stale-valid for one tick.
109
+ *
110
+ * Consumers that must stay in lockstep with this predicate:
111
+ * - `createDataValidationRule` / update / delete (enqueueing into the calc).
112
+ * - `getDataValidationFormula` (builds the expression for the calculator).
113
+ * - `SheetCell.validate()` (chooses formula-result vs sync `checkBooleanRule`).
114
+ * - `use-on-filter-table` (uses the same predicate to decide its formula path).
115
+ *
116
+ * Delegating to `buildDataValidationExpression` keeps the predicate and the
117
+ * builder from drifting.
118
+ */
119
+ var isDataValidationFormulaRule = function (rule) {
120
+ return Boolean((0, exports.buildDataValidationExpression)(rule, FORMULA_PREDICATE_BASE_CELL_REF));
121
+ };
122
+ exports.isDataValidationFormulaRule = isDataValidationFormulaRule;
123
+ var buildDataValidationExpression = function (rule, baseCellRef, locale) {
124
+ var _a, _b, _c;
125
+ var condition = rule === null || rule === void 0 ? void 0 : rule.condition;
126
+ if (!(condition === null || condition === void 0 ? void 0 : condition.type))
127
+ return undefined;
128
+ var values = (_a = condition.values) !== null && _a !== void 0 ? _a : [];
129
+ var fromRaw = (_b = values[0]) === null || _b === void 0 ? void 0 : _b.userEnteredValue;
130
+ var toRaw = (_c = values[1]) === null || _c === void 0 ? void 0 : _c.userEnteredValue;
131
+ if (condition.type === "CUSTOM_FORMULA") {
132
+ var raw = fromRaw !== null && fromRaw !== void 0 ? fromRaw : "";
133
+ var trimmed = raw.trim();
134
+ if (!trimmed)
135
+ return undefined;
136
+ return trimmed.startsWith("=") ? trimmed : "=".concat(trimmed);
137
+ }
138
+ var fromExpr = normalizeConditionValue(fromRaw, true, locale);
139
+ var toExpr = normalizeConditionValue(toRaw, true, locale);
140
+ if (!fromExpr && !toExpr)
141
+ return undefined;
142
+ var comparisonOperatorMap = {
143
+ NUMBER_EQ: "=",
144
+ NUMBER_NOT_EQ: "<>",
145
+ NUMBER_GREATER: ">",
146
+ NUMBER_GREATER_THAN_EQ: ">=",
147
+ NUMBER_LESS: "<",
148
+ NUMBER_LESS_THAN_EQ: "<=",
149
+ DATE_EQ: "=",
150
+ DATE_NOT_EQ: "<>",
151
+ DATE_BEFORE: "<",
152
+ DATE_AFTER: ">",
153
+ DATE_ON_OR_BEFORE: "<=",
154
+ DATE_ON_OR_AFTER: ">=",
155
+ TEXT_EQ: "=",
156
+ TEXT_NOT_EQ: "<>",
157
+ };
158
+ if (condition.type === "NUMBER_BETWEEN" ||
159
+ condition.type === "DATE_BETWEEN") {
160
+ if (!fromExpr || !toExpr)
161
+ return undefined;
162
+ return "=AND(".concat(baseCellRef, ">=").concat(fromExpr, ",").concat(baseCellRef, "<=").concat(toExpr, ")");
163
+ }
164
+ if (condition.type === "NUMBER_NOT_BETWEEN" ||
165
+ condition.type === "DATE_NOT_BETWEEN") {
166
+ if (!fromExpr || !toExpr)
167
+ return undefined;
168
+ return "=OR(".concat(baseCellRef, "<").concat(fromExpr, ",").concat(baseCellRef, ">").concat(toExpr, ")");
169
+ }
170
+ var comparisonOperator = comparisonOperatorMap[condition.type];
171
+ if (comparisonOperator) {
172
+ if (!fromExpr)
173
+ return undefined;
174
+ return "=".concat(baseCellRef).concat(comparisonOperator).concat(fromExpr);
175
+ }
176
+ if (condition.type === "TEXT_CONTAINS") {
177
+ if (!fromExpr)
178
+ return undefined;
179
+ return "=ISNUMBER(SEARCH(".concat(fromExpr, ",").concat(baseCellRef, "))");
180
+ }
181
+ if (condition.type === "TEXT_NOT_CONTAINS") {
182
+ if (!fromExpr)
183
+ return undefined;
184
+ return "=NOT(ISNUMBER(SEARCH(".concat(fromExpr, ",").concat(baseCellRef, ")))");
185
+ }
186
+ if (condition.type === "TEXT_STARTS_WITH") {
187
+ if (!fromExpr)
188
+ return undefined;
189
+ return "=LEFT(".concat(baseCellRef, ",LEN(").concat(fromExpr, "))=").concat(fromExpr);
190
+ }
191
+ if (condition.type === "TEXT_ENDS_WITH") {
192
+ if (!fromExpr)
193
+ return undefined;
194
+ return "=RIGHT(".concat(baseCellRef, ",LEN(").concat(fromExpr, "))=").concat(fromExpr);
195
+ }
196
+ if (condition.type === "TEXT_IS_EMAIL") {
197
+ return "=ISNUMBER(SEARCH(\"@\",".concat(baseCellRef, "))");
198
+ }
199
+ if (condition.type === "TEXT_IS_URL") {
200
+ return "=OR(LEFT(".concat(baseCellRef, ",7)=\"http://\",LEFT(").concat(baseCellRef, ",8)=\"https://\")");
201
+ }
202
+ if (condition.type === "ONE_OF_RANGE") {
203
+ if (!fromExpr)
204
+ return undefined;
205
+ if (rule === null || rule === void 0 ? void 0 : rule.allowBlank) {
206
+ return "=IF(".concat(baseCellRef, "=\"\",TRUE,COUNTIF(").concat(fromExpr, ",").concat(baseCellRef, ")>0)");
207
+ }
208
+ return "=COUNTIF(".concat(fromExpr, ",").concat(baseCellRef, ")>0");
209
+ }
210
+ if (condition.type === "ONE_OF_LIST") {
211
+ var entries = fromRaw
212
+ ? fromRaw.split(",").map(function (entry) { return entry.trim(); })
213
+ : [];
214
+ // A leading apostrophe is Excel's text prefix — `'000s` is the text
215
+ // "000s" and a bare `'` is empty text — never part of the value.
216
+ var conditions = entries
217
+ .flatMap(function (entry) {
218
+ if (entry.startsWith("'")) {
219
+ var stripped = (0, exports.stripDataValidationTextPrefix)(entry);
220
+ var textCondition = "".concat(baseCellRef, "=\"").concat(escapeFormulaText(stripped), "\"");
221
+ // The bare `'` entry means empty text, and a cell storing the lone
222
+ // text-prefix apostrophe is equivalent to a blank cell — accept it too.
223
+ return stripped === ""
224
+ ? [textCondition, "".concat(baseCellRef, "=\"'\"")]
225
+ : [textCondition];
226
+ }
227
+ return entry
228
+ ? ["".concat(baseCellRef, "=").concat(normalizeConditionValue(entry, true, locale))]
229
+ : [];
230
+ })
231
+ .filter(function (entry) { return Boolean(entry); });
232
+ if (!conditions.length)
233
+ return undefined;
234
+ return conditions.length === 1
235
+ ? "=".concat(conditions[0])
236
+ : "=OR(".concat(conditions.join(","), ")");
237
+ }
238
+ if (condition.type === "BLANK") {
239
+ return "=".concat(baseCellRef, "=\"\"");
240
+ }
241
+ if (condition.type === "NOT_BLANK") {
242
+ return "=".concat(baseCellRef, "<>\"\"");
243
+ }
244
+ if (condition.type === "BOOLEAN") {
245
+ return "=OR(".concat(baseCellRef, "=TRUE,").concat(baseCellRef, "=FALSE)");
246
+ }
247
+ return undefined;
248
+ };
249
+ exports.buildDataValidationExpression = buildDataValidationExpression;
@@ -0,0 +1,15 @@
1
+ export type DateSerialMode = "local" | "excel-utc";
2
+ /**
3
+ * Convert date to serial with explicit time zone behavior.
4
+ * - local: interpret date components in local time (UI entry behavior).
5
+ * - excel-utc: treat Date as UTC (formula engine behavior).
6
+ */
7
+ export declare const toSerialWithMode: (date: Date, use1904?: boolean, mode?: DateSerialMode) => number;
8
+ export declare const toSerial: (date: Date, use1904?: boolean) => number;
9
+ /**
10
+ * Convert serial to date
11
+ * @param serial
12
+ * @returns
13
+ */
14
+ export declare const toDate: (serial: number, use1904?: boolean) => Date;
15
+ //# sourceMappingURL=date.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../date.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,WAAW,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAM,IAAI,EACV,iBAAe,EACf,OAAM,cAAwB,WAwB/B,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,MAAM,IAAI,EAAE,iBAAe,WAEnD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,MAAM,GAAI,QAAQ,MAAM,EAAE,iBAAe,SA+BrD,CAAC"}