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