@rowsncolumns/utils 9.5.6 → 9.6.1
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/dist/cjs/cell-data-helpers.d.ts +8 -2
- package/dist/cjs/cell-data-helpers.d.ts.map +1 -1
- package/dist/cjs/cell-data-helpers.js +11 -3
- package/dist/cjs/cjk-number-format.d.ts +11 -0
- package/dist/cjs/cjk-number-format.d.ts.map +1 -0
- package/dist/cjs/cjk-number-format.js +533 -0
- package/dist/cjs/color.d.ts +9 -0
- package/dist/cjs/color.d.ts.map +1 -1
- package/dist/cjs/color.js +55 -1
- package/dist/cjs/data-validation.d.ts +29 -1
- package/dist/cjs/data-validation.d.ts.map +1 -1
- package/dist/cjs/data-validation.js +83 -24
- package/dist/cjs/formatter.d.ts +42 -0
- package/dist/cjs/formatter.d.ts.map +1 -1
- package/dist/cjs/formatter.js +179 -6
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/cell-data-helpers.d.ts +8 -2
- package/dist/esm/cell-data-helpers.d.ts.map +1 -1
- package/dist/esm/cell-data-helpers.js +9 -2
- package/dist/esm/cjk-number-format.d.ts +11 -0
- package/dist/esm/cjk-number-format.d.ts.map +1 -0
- package/dist/esm/cjk-number-format.js +494 -0
- package/dist/esm/color.d.ts +9 -0
- package/dist/esm/color.d.ts.map +1 -1
- package/dist/esm/color.js +53 -0
- package/dist/esm/data-validation.d.ts +29 -1
- package/dist/esm/data-validation.d.ts.map +1 -1
- package/dist/esm/data-validation.js +75 -23
- package/dist/esm/formatter.d.ts +42 -0
- package/dist/esm/formatter.d.ts.map +1 -1
- package/dist/esm/formatter.js +162 -4
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeConditionFormulaText, normalizeConditionOperand, } from "./condition-operand";
|
|
2
|
+
import { checkIfStringIsReference } from "./selection";
|
|
2
3
|
const FORMULA_PREDICATE_BASE_CELL_REF = "A1";
|
|
3
4
|
/**
|
|
4
5
|
* A leading apostrophe in a data-validation / conditional-format list entry is
|
|
@@ -6,6 +7,52 @@ const FORMULA_PREDICATE_BASE_CELL_REF = "A1";
|
|
|
6
7
|
* text — never part of the value itself.
|
|
7
8
|
*/
|
|
8
9
|
export const stripDataValidationTextPrefix = (entry) => entry.startsWith("'") ? entry.slice(1) : entry;
|
|
10
|
+
/**
|
|
11
|
+
* A `ONE_OF_LIST` value that is a SOURCE FORMULA rather than literal items,
|
|
12
|
+
* returned trimmed with its leading `=` (`=RegionList`, `=$A$2:$A$9`,
|
|
13
|
+
* `=Sheet2!A1:A5`, `=INDIRECT("…")`). Excel's List type has one Source box
|
|
14
|
+
* that takes either literal items (`Yes,No`) or a formula starting with `=`;
|
|
15
|
+
* the "Dropdown" (`ONE_OF_LIST`) / "Dropdown (from a range)" (`ONE_OF_RANGE`)
|
|
16
|
+
* split means a `=…` typed into the literal dropdown is read the way Excel
|
|
17
|
+
* reads it — as a `ONE_OF_RANGE` over that source. A quoted literal
|
|
18
|
+
* (`"=a,b"`, Excel's quoted list), a `=` inside a later item (`a,=b`) and a
|
|
19
|
+
* bare `=` stay literal. Rust twin: `rnc_model::validation::one_of_list_formula_source`.
|
|
20
|
+
*/
|
|
21
|
+
export const getDataValidationListFormulaSource = (value) => {
|
|
22
|
+
const trimmed = (value ?? "").trim();
|
|
23
|
+
return trimmed.startsWith("=") && trimmed.slice(1).trim()
|
|
24
|
+
? trimmed
|
|
25
|
+
: undefined;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The condition type a rule behaves as: a `ONE_OF_LIST` whose first value is
|
|
29
|
+
* a source formula (`getDataValidationListFormulaSource`) behaves as
|
|
30
|
+
* `ONE_OF_RANGE`; every other condition is its own type. Rust twin:
|
|
31
|
+
* `rnc_model::validation::effective_condition_kind`.
|
|
32
|
+
*/
|
|
33
|
+
export const getEffectiveDataValidationConditionType = (condition) => condition?.type === "ONE_OF_LIST" &&
|
|
34
|
+
getDataValidationListFormulaSource(condition.values?.[0]?.userEnteredValue)
|
|
35
|
+
? "ONE_OF_RANGE"
|
|
36
|
+
: condition?.type ?? undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The reference a list rule draws its options from, or `undefined` when the
|
|
39
|
+
* rule is a literal item list (or not a list rule at all). `ONE_OF_RANGE` is
|
|
40
|
+
* always a reference (its value, `""` when missing); a `ONE_OF_LIST` is one
|
|
41
|
+
* when its value is a source formula (`=RegionList`) or a bare A1 reference
|
|
42
|
+
* (`$A$1:$A$5`). Shared by the dropdown options, the commit gate and
|
|
43
|
+
* `checkBooleanRule`, so the three always agree.
|
|
44
|
+
*/
|
|
45
|
+
export const getDataValidationListReference = (condition) => {
|
|
46
|
+
const firstValue = condition?.values?.[0]?.userEnteredValue;
|
|
47
|
+
if (getEffectiveDataValidationConditionType(condition) === "ONE_OF_RANGE") {
|
|
48
|
+
return getDataValidationListFormulaSource(firstValue) ?? firstValue ?? "";
|
|
49
|
+
}
|
|
50
|
+
if (condition?.type === "ONE_OF_LIST" &&
|
|
51
|
+
checkIfStringIsReference(firstValue)) {
|
|
52
|
+
return firstValue;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
9
56
|
/**
|
|
10
57
|
* Whether a cell's current text matches a ONE_OF_LIST option, with Excel's
|
|
11
58
|
* text-prefix semantics on BOTH sides: the option strips its leading `'`
|
|
@@ -80,20 +127,23 @@ export const buildDataValidationExpression = (rule, baseCellRef, locale) => {
|
|
|
80
127
|
};
|
|
81
128
|
const buildDataValidationConditionExpression = (rule, baseCellRef, locale) => {
|
|
82
129
|
const condition = rule?.condition;
|
|
83
|
-
|
|
130
|
+
// A ONE_OF_LIST whose value is a source formula (`=RegionList`) compiles as
|
|
131
|
+
// the ONE_OF_RANGE over that source, as Excel's List type reads it.
|
|
132
|
+
const type = getEffectiveDataValidationConditionType(condition);
|
|
133
|
+
if (!condition || !type)
|
|
84
134
|
return undefined;
|
|
85
135
|
const values = condition.values ?? [];
|
|
86
136
|
const fromRaw = values[0]?.userEnteredValue;
|
|
87
137
|
const toRaw = values[1]?.userEnteredValue;
|
|
88
|
-
if (
|
|
138
|
+
if (type === "CUSTOM_FORMULA") {
|
|
89
139
|
const raw = fromRaw ?? "";
|
|
90
140
|
const trimmed = raw.trim();
|
|
91
141
|
if (!trimmed)
|
|
92
142
|
return undefined;
|
|
93
143
|
return trimmed.startsWith("=") ? trimmed : `=${trimmed}`;
|
|
94
144
|
}
|
|
95
|
-
const fromExpr = normalizeConditionOperand(fromRaw,
|
|
96
|
-
const toExpr = normalizeConditionOperand(toRaw,
|
|
145
|
+
const fromExpr = normalizeConditionOperand(fromRaw, type, locale);
|
|
146
|
+
const toExpr = normalizeConditionOperand(toRaw, type, locale);
|
|
97
147
|
if (!fromExpr && !toExpr)
|
|
98
148
|
return undefined;
|
|
99
149
|
const comparisonOperatorMap = {
|
|
@@ -112,51 +162,49 @@ const buildDataValidationConditionExpression = (rule, baseCellRef, locale) => {
|
|
|
112
162
|
TEXT_EQ: "=",
|
|
113
163
|
TEXT_NOT_EQ: "<>",
|
|
114
164
|
};
|
|
115
|
-
if (
|
|
116
|
-
condition.type === "DATE_BETWEEN") {
|
|
165
|
+
if (type === "NUMBER_BETWEEN" || type === "DATE_BETWEEN") {
|
|
117
166
|
if (!fromExpr || !toExpr)
|
|
118
167
|
return undefined;
|
|
119
168
|
return `=AND(${baseCellRef}>=${fromExpr},${baseCellRef}<=${toExpr})`;
|
|
120
169
|
}
|
|
121
|
-
if (
|
|
122
|
-
condition.type === "DATE_NOT_BETWEEN") {
|
|
170
|
+
if (type === "NUMBER_NOT_BETWEEN" || type === "DATE_NOT_BETWEEN") {
|
|
123
171
|
if (!fromExpr || !toExpr)
|
|
124
172
|
return undefined;
|
|
125
173
|
return `=OR(${baseCellRef}<${fromExpr},${baseCellRef}>${toExpr})`;
|
|
126
174
|
}
|
|
127
|
-
const comparisonOperator = comparisonOperatorMap[
|
|
175
|
+
const comparisonOperator = comparisonOperatorMap[type];
|
|
128
176
|
if (comparisonOperator) {
|
|
129
177
|
if (!fromExpr)
|
|
130
178
|
return undefined;
|
|
131
179
|
return `=${baseCellRef}${comparisonOperator}${fromExpr}`;
|
|
132
180
|
}
|
|
133
|
-
if (
|
|
181
|
+
if (type === "TEXT_CONTAINS") {
|
|
134
182
|
if (!fromExpr)
|
|
135
183
|
return undefined;
|
|
136
184
|
return `=ISNUMBER(SEARCH(${fromExpr},${baseCellRef}))`;
|
|
137
185
|
}
|
|
138
|
-
if (
|
|
186
|
+
if (type === "TEXT_NOT_CONTAINS") {
|
|
139
187
|
if (!fromExpr)
|
|
140
188
|
return undefined;
|
|
141
189
|
return `=NOT(ISNUMBER(SEARCH(${fromExpr},${baseCellRef})))`;
|
|
142
190
|
}
|
|
143
|
-
if (
|
|
191
|
+
if (type === "TEXT_STARTS_WITH") {
|
|
144
192
|
if (!fromExpr)
|
|
145
193
|
return undefined;
|
|
146
194
|
return `=LEFT(${baseCellRef},LEN(${fromExpr}))=${fromExpr}`;
|
|
147
195
|
}
|
|
148
|
-
if (
|
|
196
|
+
if (type === "TEXT_ENDS_WITH") {
|
|
149
197
|
if (!fromExpr)
|
|
150
198
|
return undefined;
|
|
151
199
|
return `=RIGHT(${baseCellRef},LEN(${fromExpr}))=${fromExpr}`;
|
|
152
200
|
}
|
|
153
|
-
if (
|
|
201
|
+
if (type === "TEXT_IS_EMAIL") {
|
|
154
202
|
return `=ISNUMBER(SEARCH("@",${baseCellRef}))`;
|
|
155
203
|
}
|
|
156
|
-
if (
|
|
204
|
+
if (type === "TEXT_IS_URL") {
|
|
157
205
|
return `=OR(LEFT(${baseCellRef},7)="http://",LEFT(${baseCellRef},8)="https://")`;
|
|
158
206
|
}
|
|
159
|
-
if (
|
|
207
|
+
if (type === "ONE_OF_RANGE") {
|
|
160
208
|
if (!fromExpr)
|
|
161
209
|
return undefined;
|
|
162
210
|
if (rule?.allowBlank) {
|
|
@@ -164,7 +212,7 @@ const buildDataValidationConditionExpression = (rule, baseCellRef, locale) => {
|
|
|
164
212
|
}
|
|
165
213
|
return `=COUNTIF(${fromExpr},${baseCellRef})>0`;
|
|
166
214
|
}
|
|
167
|
-
if (
|
|
215
|
+
if (type === "ONE_OF_LIST") {
|
|
168
216
|
const entries = fromRaw
|
|
169
217
|
? fromRaw.split(",").map((entry) => entry.trim())
|
|
170
218
|
: [];
|
|
@@ -181,10 +229,14 @@ const buildDataValidationConditionExpression = (rule, baseCellRef, locale) => {
|
|
|
181
229
|
? [textCondition, `${baseCellRef}="'"`]
|
|
182
230
|
: [textCondition];
|
|
183
231
|
}
|
|
232
|
+
// A list ITEM is never a formula (a source formula is the whole value,
|
|
233
|
+
// see `getEffectiveDataValidationConditionType`): `"=a,b"` and `a,=b`
|
|
234
|
+
// list the text `=a` / `=b`.
|
|
235
|
+
if (entry.startsWith("=")) {
|
|
236
|
+
return [`${baseCellRef}="${escapeConditionFormulaText(entry)}"`];
|
|
237
|
+
}
|
|
184
238
|
return entry
|
|
185
|
-
? [
|
|
186
|
-
`${baseCellRef}=${normalizeConditionOperand(entry, condition.type, locale)}`,
|
|
187
|
-
]
|
|
239
|
+
? [`${baseCellRef}=${normalizeConditionOperand(entry, type, locale)}`]
|
|
188
240
|
: [];
|
|
189
241
|
})
|
|
190
242
|
.filter((entry) => Boolean(entry));
|
|
@@ -194,13 +246,13 @@ const buildDataValidationConditionExpression = (rule, baseCellRef, locale) => {
|
|
|
194
246
|
? `=${conditions[0]}`
|
|
195
247
|
: `=OR(${conditions.join(",")})`;
|
|
196
248
|
}
|
|
197
|
-
if (
|
|
249
|
+
if (type === "BLANK") {
|
|
198
250
|
return `=${baseCellRef}=""`;
|
|
199
251
|
}
|
|
200
|
-
if (
|
|
252
|
+
if (type === "NOT_BLANK") {
|
|
201
253
|
return `=${baseCellRef}<>""`;
|
|
202
254
|
}
|
|
203
|
-
if (
|
|
255
|
+
if (type === "BOOLEAN") {
|
|
204
256
|
return `=OR(${baseCellRef}=TRUE,${baseCellRef}=FALSE)`;
|
|
205
257
|
}
|
|
206
258
|
return undefined;
|
package/dist/esm/formatter.d.ts
CHANGED
|
@@ -44,5 +44,47 @@ export declare const withShortAmPmMarker: (pattern: string) => string | undefine
|
|
|
44
44
|
*/
|
|
45
45
|
export declare const withWorkbookSeparators: (pattern: string, locale?: string) => string | undefined;
|
|
46
46
|
export declare const ssfFormat: (pattern: string, value: any, locale?: string) => string;
|
|
47
|
+
/**
|
|
48
|
+
* One piece of a number format's width-aware output (see
|
|
49
|
+
* {@link ssfFormatLayout}):
|
|
50
|
+
* - `text` — literal characters, painted as they are;
|
|
51
|
+
* - `fill` — Excel's `*x`: `x` repeated to fill the cell's free width;
|
|
52
|
+
* - `skip` — Excel's `_x`: blank space as wide as the glyph `x`;
|
|
53
|
+
* - `digit` — a `?` placeholder with no digit: blank space as wide as a
|
|
54
|
+
* digit.
|
|
55
|
+
*/
|
|
56
|
+
export type NumberFormatLayoutPart = {
|
|
57
|
+
type: "text";
|
|
58
|
+
text: string;
|
|
59
|
+
} | {
|
|
60
|
+
type: "fill";
|
|
61
|
+
char: string;
|
|
62
|
+
} | {
|
|
63
|
+
type: "skip";
|
|
64
|
+
char: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: "digit";
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Excel's width-aware number-format output: `*x` repeats `x` to the cell's
|
|
70
|
+
* width, `_x` reserves the width of `x` and `?` reserves a digit's width
|
|
71
|
+
* ("Number format codes", support.microsoft.com). A plain formatted string
|
|
72
|
+
* cannot carry that — `ssfFormat` drops the fill and renders the reserves as
|
|
73
|
+
* single spaces — so the painter asks for the parts and expands them with
|
|
74
|
+
* measured glyph widths.
|
|
75
|
+
*
|
|
76
|
+
* Returns `undefined` when the pattern has none of those tokens (the plain
|
|
77
|
+
* string is already exact), and for East Asian numeral codes. The parts'
|
|
78
|
+
* plain text ({@link numberFormatLayoutText}) equals `ssfFormat`'s output;
|
|
79
|
+
* callers compare it against the string they paint and ignore the layout
|
|
80
|
+
* when the two disagree.
|
|
81
|
+
*/
|
|
82
|
+
export declare const ssfFormatLayout: (pattern: string, value: any, locale?: string) => NumberFormatLayoutPart[] | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* The plain string a layout stands for — what `ssfFormat` returns for the
|
|
85
|
+
* same pattern and value: fills drop out, skips and digit pads are one
|
|
86
|
+
* space each.
|
|
87
|
+
*/
|
|
88
|
+
export declare const numberFormatLayoutText: (parts: readonly NumberFormatLayoutPart[]) => string;
|
|
47
89
|
export declare const ssfFormatColor: (pattern: string, value: any) => string | undefined;
|
|
48
90
|
//# sourceMappingURL=formatter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../../formatter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../../formatter.ts"],"names":[],"mappings":"AAkBA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,GAAG,SAAS,GAAG,IAAI,KAAG,OASlE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,SAAS,MAAM,GAAG,SAAS,GAAG,IAAI,KACjC,OAWF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,GAAG,SAAS,GAAG,IAAI,KAAG,OAWlE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,GAAG,SAAS,GAAG,IAAI,KAAG,OAOlE,CAAC;AAMF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,MAAM,KAAG,OA4B1D,CAAC;AA+LF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,SA4C9D,CAAC;AAiIF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAS,MAAM,EACf,SAAS,MAAM,KACd,MAAM,GAAG,SAiBX,CAAC;AAiBF,eAAO,MAAM,SAAS,GACpB,SAAS,MAAM,EACf,OAAO,GAAG,EACV,SAAS,MAAM,KACd,MASF,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAoDtB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,EACf,OAAO,GAAG,EACV,SAAS,MAAM,KACd,sBAAsB,EAAE,GAAG,SA8C7B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO,SAAS,sBAAsB,EAAE,KACvC,MAOF,CAAC;AAkQF,eAAO,MAAM,cAAc,GACzB,SAAS,MAAM,EACf,OAAO,GAAG,KACT,MAAM,GAAG,SAoBX,CAAC"}
|
package/dist/esm/formatter.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { addLocale, format, formatColor, getFormatDateInfo, getLocale, isDateFormat, isTextFormat, } from "numfmt";
|
|
2
|
+
import { prepareCjkNumberFormat } from "./cjk-number-format";
|
|
2
3
|
import { IndexedColors } from "./colors";
|
|
3
4
|
import { excelNumberPrePass, splitFormatSections, } from "./excel-number-format";
|
|
4
5
|
/**
|
|
@@ -498,6 +499,153 @@ const withWorkbookSeparatorsCached = (code, locale) => {
|
|
|
498
499
|
return rewritten;
|
|
499
500
|
};
|
|
500
501
|
export const ssfFormat = (pattern, value, locale) => {
|
|
502
|
+
// East Asian tokens numfmt does not render (`[DBNumN]`, Japanese era `g`/`e`,
|
|
503
|
+
// `aaa` weekdays): rewrite the code to plain literals, then convert the
|
|
504
|
+
// rendered digits to the `[DBNumN]` numeral system (see cjk-number-format).
|
|
505
|
+
if (typeof value === "number") {
|
|
506
|
+
const cjk = prepareCjkNumberFormat(pattern, value, locale);
|
|
507
|
+
if (cjk)
|
|
508
|
+
return cjk.finish(ssfFormatBase(cjk.code, value, locale));
|
|
509
|
+
}
|
|
510
|
+
return ssfFormatBase(pattern, value, locale);
|
|
511
|
+
};
|
|
512
|
+
const LAYOUT_FILL_MARK = "\uE000";
|
|
513
|
+
const LAYOUT_SKIP_MARK = "\uE001";
|
|
514
|
+
const LAYOUT_LITERAL_SPACE = "\uE002";
|
|
515
|
+
const LAYOUT_DIGIT_PAD = "\u00a0";
|
|
516
|
+
/**
|
|
517
|
+
* Quote every literal space of `code` as a private-use sentinel so that,
|
|
518
|
+
* rendered with numfmt's `nbsp` option, the only non-breaking spaces left
|
|
519
|
+
* in the output are the `?` digit pads.
|
|
520
|
+
*/
|
|
521
|
+
const markLiteralSpaces = (code) => {
|
|
522
|
+
let out = "";
|
|
523
|
+
let inQuotes = false;
|
|
524
|
+
let inBrackets = false;
|
|
525
|
+
for (let i = 0; i < code.length; i++) {
|
|
526
|
+
const char = code[i];
|
|
527
|
+
if (inQuotes) {
|
|
528
|
+
if (char === '"')
|
|
529
|
+
inQuotes = false;
|
|
530
|
+
out += char === " " ? LAYOUT_LITERAL_SPACE : char;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (inBrackets) {
|
|
534
|
+
if (char === "]")
|
|
535
|
+
inBrackets = false;
|
|
536
|
+
out += char;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (char === '"') {
|
|
540
|
+
inQuotes = true;
|
|
541
|
+
}
|
|
542
|
+
else if (char === "[") {
|
|
543
|
+
inBrackets = true;
|
|
544
|
+
}
|
|
545
|
+
else if (char === "\\" && code[i + 1] === " ") {
|
|
546
|
+
out += `"${LAYOUT_LITERAL_SPACE}"`;
|
|
547
|
+
i++;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
else if (char === "\\" || char === "_" || char === "*") {
|
|
551
|
+
out += char + (code[i + 1] ?? "");
|
|
552
|
+
i++;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
else if (char === " ") {
|
|
556
|
+
out += `"${LAYOUT_LITERAL_SPACE}"`;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
out += char;
|
|
560
|
+
}
|
|
561
|
+
return out;
|
|
562
|
+
};
|
|
563
|
+
/** Does the pattern carry a `*` fill, `_` skip or `?` placeholder at all? */
|
|
564
|
+
const LAYOUT_TOKEN_REGEX = /[*_?]/;
|
|
565
|
+
/**
|
|
566
|
+
* Excel's width-aware number-format output: `*x` repeats `x` to the cell's
|
|
567
|
+
* width, `_x` reserves the width of `x` and `?` reserves a digit's width
|
|
568
|
+
* ("Number format codes", support.microsoft.com). A plain formatted string
|
|
569
|
+
* cannot carry that — `ssfFormat` drops the fill and renders the reserves as
|
|
570
|
+
* single spaces — so the painter asks for the parts and expands them with
|
|
571
|
+
* measured glyph widths.
|
|
572
|
+
*
|
|
573
|
+
* Returns `undefined` when the pattern has none of those tokens (the plain
|
|
574
|
+
* string is already exact), and for East Asian numeral codes. The parts'
|
|
575
|
+
* plain text ({@link numberFormatLayoutText}) equals `ssfFormat`'s output;
|
|
576
|
+
* callers compare it against the string they paint and ignore the layout
|
|
577
|
+
* when the two disagree.
|
|
578
|
+
*/
|
|
579
|
+
export const ssfFormatLayout = (pattern, value, locale) => {
|
|
580
|
+
if (!pattern || !LAYOUT_TOKEN_REGEX.test(pattern))
|
|
581
|
+
return undefined;
|
|
582
|
+
if (typeof value === "number" && prepareCjkNumberFormat(pattern, value, locale)) {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
const raw = ssfFormatBase(pattern, value, locale, {
|
|
586
|
+
nbsp: true,
|
|
587
|
+
fillChar: LAYOUT_FILL_MARK,
|
|
588
|
+
skipChar: LAYOUT_SKIP_MARK,
|
|
589
|
+
transform: markLiteralSpaces,
|
|
590
|
+
});
|
|
591
|
+
if (raw === undefined)
|
|
592
|
+
return undefined;
|
|
593
|
+
// A locale whose group separator is itself a non-breaking space makes the
|
|
594
|
+
// digit pads ambiguous: keep those as plain spaces.
|
|
595
|
+
const digitPads = separatorsFor(locale).group !== LAYOUT_DIGIT_PAD;
|
|
596
|
+
const parts = [];
|
|
597
|
+
let text = "";
|
|
598
|
+
const flush = () => {
|
|
599
|
+
if (text)
|
|
600
|
+
parts.push({ type: "text", text });
|
|
601
|
+
text = "";
|
|
602
|
+
};
|
|
603
|
+
for (let i = 0; i < raw.length; i++) {
|
|
604
|
+
const char = raw[i];
|
|
605
|
+
if (char === LAYOUT_FILL_MARK || char === LAYOUT_SKIP_MARK) {
|
|
606
|
+
const next = raw[i + 1] ?? " ";
|
|
607
|
+
flush();
|
|
608
|
+
parts.push({
|
|
609
|
+
type: char === LAYOUT_FILL_MARK ? "fill" : "skip",
|
|
610
|
+
char: next,
|
|
611
|
+
});
|
|
612
|
+
i++;
|
|
613
|
+
}
|
|
614
|
+
else if (char === LAYOUT_DIGIT_PAD) {
|
|
615
|
+
if (digitPads) {
|
|
616
|
+
flush();
|
|
617
|
+
parts.push({ type: "digit" });
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
text += char;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else if (char === LAYOUT_LITERAL_SPACE) {
|
|
624
|
+
text += " ";
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
text += char;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
flush();
|
|
631
|
+
return parts.some((part) => part.type !== "text") ? parts : undefined;
|
|
632
|
+
};
|
|
633
|
+
/**
|
|
634
|
+
* The plain string a layout stands for — what `ssfFormat` returns for the
|
|
635
|
+
* same pattern and value: fills drop out, skips and digit pads are one
|
|
636
|
+
* space each.
|
|
637
|
+
*/
|
|
638
|
+
export const numberFormatLayoutText = (parts) => {
|
|
639
|
+
let out = "";
|
|
640
|
+
for (const part of parts) {
|
|
641
|
+
if (part.type === "text")
|
|
642
|
+
out += part.text;
|
|
643
|
+
else if (part.type !== "fill")
|
|
644
|
+
out += " ";
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
};
|
|
648
|
+
function ssfFormatBase(pattern, value, locale, layout) {
|
|
501
649
|
try {
|
|
502
650
|
if (locale)
|
|
503
651
|
ensureNumfmtLocale(locale);
|
|
@@ -506,12 +654,22 @@ export const ssfFormat = (pattern, value, locale) => {
|
|
|
506
654
|
shortAmPm = withShortAmPmMarker(pattern);
|
|
507
655
|
shortAmPmPatternCache.set(pattern, shortAmPm);
|
|
508
656
|
}
|
|
509
|
-
const
|
|
657
|
+
const baseCode = withWorkbookSeparatorsCached(shortAmPm ?? pattern, locale);
|
|
658
|
+
const code = layout ? layout.transform(baseCode) : baseCode;
|
|
510
659
|
let patternLocale = localeForPattern(pattern, locale);
|
|
511
660
|
if (shortAmPm !== undefined && patternLocale) {
|
|
512
661
|
patternLocale = getShortAmPmLocale(patternLocale);
|
|
513
662
|
}
|
|
514
|
-
|
|
663
|
+
// numfmt types `fillChar` / `skipChar` as booleans; at runtime they are
|
|
664
|
+
// the marker strings it emits ahead of the fill / skip character.
|
|
665
|
+
const options = layout
|
|
666
|
+
? {
|
|
667
|
+
locale: patternLocale,
|
|
668
|
+
nbsp: layout.nbsp,
|
|
669
|
+
fillChar: layout.fillChar,
|
|
670
|
+
skipChar: layout.skipChar,
|
|
671
|
+
}
|
|
672
|
+
: { locale: patternLocale };
|
|
515
673
|
if (typeof value === "number") {
|
|
516
674
|
// Excel decides rounding, `General`, huge magnitudes and conditional
|
|
517
675
|
// sections from the 15-significant-digit decimal, not the binary
|
|
@@ -529,9 +687,9 @@ export const ssfFormat = (pattern, value, locale) => {
|
|
|
529
687
|
return isNegativeElapsedTime ? `-${formatted}` : formatted;
|
|
530
688
|
}
|
|
531
689
|
catch (err) {
|
|
532
|
-
return String(value);
|
|
690
|
+
return layout ? undefined : String(value);
|
|
533
691
|
}
|
|
534
|
-
}
|
|
692
|
+
}
|
|
535
693
|
/**
|
|
536
694
|
* Excel's legacy fixed 56-color palette used by `[ColorN]` directives
|
|
537
695
|
* inside number format patterns (1-indexed). This palette is INDEPENDENT
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../../array.ts","../../cell-data-helpers.ts","../../clipboard-html-sanitize.ts","../../color.ts","../../colors.ts","../../condition-operand.ts","../../conditional-format.ts","../../content-hash.ts","../../data-validation.ts","../../date.ts","../../dom.ts","../../excel-number-format.ts","../../formatter.ts","../../formula-locale.ts","../../formula-refs.ts","../../formula.ts","../../index.ts","../../keyboard.ts","../../locale.ts","../../log.ts","../../memoize.ts","../../numbers.ts","../../rich-text-html.ts","../../rich-text-links.ts","../../selection.ts","../../shared-string-canonical.ts","../../sheet-names.ts","../../sheet-protection.ts","../../sheet-span.ts","../../sort.ts","../../sparkline-groups.ts","../../string.ts","../../table-names.ts","../../table-styles.ts","../../table-totals.ts","../../typeguards.ts","../../uuid.ts","../../vitest.config.ts","../../formula-locale/formula-locale-data.generated.ts"],"version":"5.8.3"}
|
|
1
|
+
{"root":["../../array.ts","../../cell-data-helpers.ts","../../cjk-number-format.ts","../../clipboard-html-sanitize.ts","../../color.ts","../../colors.ts","../../condition-operand.ts","../../conditional-format.ts","../../content-hash.ts","../../data-validation.ts","../../date.ts","../../dom.ts","../../excel-number-format.ts","../../formatter.ts","../../formula-locale.ts","../../formula-refs.ts","../../formula.ts","../../index.ts","../../keyboard.ts","../../locale.ts","../../log.ts","../../memoize.ts","../../numbers.ts","../../rich-text-html.ts","../../rich-text-links.ts","../../selection.ts","../../shared-string-canonical.ts","../../sheet-names.ts","../../sheet-protection.ts","../../sheet-span.ts","../../sort.ts","../../sparkline-groups.ts","../../string.ts","../../table-names.ts","../../table-styles.ts","../../table-totals.ts","../../typeguards.ts","../../uuid.ts","../../vitest.config.ts","../../formula-locale/formula-locale-data.generated.ts"],"version":"5.8.3"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rowsncolumns/utils",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.6.1",
|
|
4
4
|
"author": "Vinay M",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"numfmt": "^3.2.2",
|
|
47
|
-
"@rowsncolumns/common-types": ">=5.3.
|
|
47
|
+
"@rowsncolumns/common-types": ">=5.3.5"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {},
|
|
50
50
|
"peerDependencies": {}
|