@rowsncolumns/utils 9.5.7 → 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/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/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
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* East Asian number-format tokens `numfmt` does not render: the `[DBNum1]`–`[DBNum4]` numeral
|
|
3
|
+
* systems, the Japanese emperor-era tokens (`g`/`gg`/`ggg`, and `e`/`ee` under a Japanese locale)
|
|
4
|
+
* and the `aaa`/`aaaa` weekday tokens. TypeScript twin of the engine's Rust
|
|
5
|
+
* `rnc-cellmodel/src/cjk_format.rs` — keep the two in step.
|
|
6
|
+
*
|
|
7
|
+
* {@link prepareCjkNumberFormat} rewrites a format code so the base formatter can render it (era
|
|
8
|
+
* names, era years and weekday names become quoted literals computed from the date serial,
|
|
9
|
+
* `[DBNumN]` is removed, and under `[DBNumN]` a date's year becomes a quoted literal in that numeral
|
|
10
|
+
* system); `finish` then turns the remaining ASCII digits into the numeral system:
|
|
11
|
+
*
|
|
12
|
+
* - `General` and date fields read as a number with units (`123` → `百二十三`, `15日` → `十五日`);
|
|
13
|
+
* - digit placeholders (`0`, `#`) and years read digit by digit (`[DBNum1]0` → `一二三`).
|
|
14
|
+
*
|
|
15
|
+
* The language comes from the code's `[$-LCID]` / `[$-tag]` token, then the workbook locale, and
|
|
16
|
+
* defaults to Japanese.
|
|
17
|
+
*/
|
|
18
|
+
import { analyzeFormatSection, pickNumberSection, splitFormatSections, } from "./excel-number-format";
|
|
19
|
+
const DBNUM_TOKEN = /^\[dbnum([1-4])\]$/i;
|
|
20
|
+
/** Cheap pre-filter: a `DBNum` token, a `g` (era name or `General`), `aaa`, or an `e` beside a locale token. */
|
|
21
|
+
const mightContainTokens = (code) => /[gG]|dbnum|aaa/i.test(code) || (/[eE]/.test(code) && code.includes("[$"));
|
|
22
|
+
export const prepareCjkNumberFormat = (code, value, workbookLocale) => {
|
|
23
|
+
if (!mightContainTokens(code))
|
|
24
|
+
return undefined;
|
|
25
|
+
const tag = localeTag(code);
|
|
26
|
+
const tagLang = tag ? cjkLangOfTag(tag) : undefined;
|
|
27
|
+
const lang = tagLang ??
|
|
28
|
+
(workbookLocale ? cjkLangOfTag(workbookLocale) : undefined) ??
|
|
29
|
+
"ja";
|
|
30
|
+
const gannen = !!tag && tag.toLowerCase().includes("-x-gannen");
|
|
31
|
+
const date = serialToDate(value);
|
|
32
|
+
const sections = splitFormatSections(code);
|
|
33
|
+
const rewritten = [];
|
|
34
|
+
const levels = [];
|
|
35
|
+
const dated = [];
|
|
36
|
+
let changed = false;
|
|
37
|
+
for (const section of sections) {
|
|
38
|
+
const level = dbnumLevel(section);
|
|
39
|
+
const result = rewriteSection(section, {
|
|
40
|
+
dbnum: level === undefined ? undefined : { level, lang },
|
|
41
|
+
weekdayLang: lang,
|
|
42
|
+
eraYears: tagLang === "ja" || hasEraNameToken(section),
|
|
43
|
+
gannen,
|
|
44
|
+
date,
|
|
45
|
+
});
|
|
46
|
+
changed ||= result.changed || level !== undefined;
|
|
47
|
+
rewritten.push(result.text);
|
|
48
|
+
levels.push(level);
|
|
49
|
+
dated.push(result.dateToken);
|
|
50
|
+
}
|
|
51
|
+
if (!changed)
|
|
52
|
+
return undefined;
|
|
53
|
+
const anyCondition = sections.some((s) => analyzeFormatSection(s).hasCondition);
|
|
54
|
+
const index = anyCondition
|
|
55
|
+
? 0
|
|
56
|
+
: Math.min(pickNumberSection(sections.length, value).index, sections.length - 1);
|
|
57
|
+
const level = levels[index];
|
|
58
|
+
const dbnum = level !== undefined && (level <= 3 || (level === 4 && lang === "ko"))
|
|
59
|
+
? { level, lang }
|
|
60
|
+
: undefined;
|
|
61
|
+
let mode;
|
|
62
|
+
if (dated[index]) {
|
|
63
|
+
mode = "date";
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
switch (analyzeFormatSection(stripDbNum(sections[index])).kind) {
|
|
67
|
+
case "general":
|
|
68
|
+
mode = "general";
|
|
69
|
+
break;
|
|
70
|
+
case "date":
|
|
71
|
+
mode = "date";
|
|
72
|
+
break;
|
|
73
|
+
case "number":
|
|
74
|
+
case "other":
|
|
75
|
+
mode = "digits";
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
mode = "unchanged";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
code: rewritten.join(";"),
|
|
83
|
+
finish: (out) => dbnum && mode !== "unchanged" ? convertDigits(out, dbnum, mode) : out,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const runLength = (chars, from, letter) => {
|
|
87
|
+
let n = 0;
|
|
88
|
+
while (from + n < chars.length && chars[from + n].toLowerCase() === letter)
|
|
89
|
+
n += 1;
|
|
90
|
+
return n;
|
|
91
|
+
};
|
|
92
|
+
const startsWithCi = (chars, from, word) => chars
|
|
93
|
+
.slice(from, from + word.length)
|
|
94
|
+
.join("")
|
|
95
|
+
.toLowerCase() === word;
|
|
96
|
+
const quote = (text) => `"${text}"`;
|
|
97
|
+
const rewriteSection = (section, ctx) => {
|
|
98
|
+
const chars = Array.from(section);
|
|
99
|
+
let out = "";
|
|
100
|
+
let changed = false;
|
|
101
|
+
let dateToken = false;
|
|
102
|
+
let i = 0;
|
|
103
|
+
while (i < chars.length) {
|
|
104
|
+
const c = chars[i];
|
|
105
|
+
const lower = c.toLowerCase();
|
|
106
|
+
if (c === '"') {
|
|
107
|
+
const close = chars.indexOf('"', i + 1);
|
|
108
|
+
const end = close === -1 ? chars.length : close + 1;
|
|
109
|
+
out += chars.slice(i, end).join("");
|
|
110
|
+
i = end;
|
|
111
|
+
}
|
|
112
|
+
else if (c === "\\" || c === "_" || c === "*") {
|
|
113
|
+
out += chars.slice(i, i + 2).join("");
|
|
114
|
+
i += 2;
|
|
115
|
+
}
|
|
116
|
+
else if (c === "[") {
|
|
117
|
+
const close = chars.indexOf("]", i);
|
|
118
|
+
const end = close === -1 ? chars.length : close + 1;
|
|
119
|
+
const token = chars.slice(i, end).join("");
|
|
120
|
+
if (DBNUM_TOKEN.test(token))
|
|
121
|
+
changed = true;
|
|
122
|
+
else
|
|
123
|
+
out += token;
|
|
124
|
+
i = end;
|
|
125
|
+
}
|
|
126
|
+
else if (lower === "g" && startsWithCi(chars, i, "general")) {
|
|
127
|
+
out += chars.slice(i, i + 7).join("");
|
|
128
|
+
i += 7;
|
|
129
|
+
}
|
|
130
|
+
else if (lower === "g") {
|
|
131
|
+
const run = runLength(chars, i, "g");
|
|
132
|
+
if (ctx.date) {
|
|
133
|
+
const era = eraOf(ctx.date);
|
|
134
|
+
out += quote(run === 1 ? era.letter : run === 2 ? era.short : era.name);
|
|
135
|
+
}
|
|
136
|
+
changed = true;
|
|
137
|
+
dateToken = true;
|
|
138
|
+
i += run;
|
|
139
|
+
}
|
|
140
|
+
else if (lower === "e" && chars[i + 1] !== "+" && chars[i + 1] !== "-") {
|
|
141
|
+
const run = runLength(chars, i, "e");
|
|
142
|
+
if (ctx.date && ctx.eraYears) {
|
|
143
|
+
const eraYear = ctx.date.year - eraOf(ctx.date).start[0] + 1;
|
|
144
|
+
const text = eraYear === 1 && ctx.gannen
|
|
145
|
+
? "元"
|
|
146
|
+
: run >= 2
|
|
147
|
+
? String(eraYear).padStart(2, "0")
|
|
148
|
+
: String(eraYear);
|
|
149
|
+
out += quote(text);
|
|
150
|
+
changed = true;
|
|
151
|
+
dateToken = true;
|
|
152
|
+
}
|
|
153
|
+
else if (ctx.date && ctx.dbnum) {
|
|
154
|
+
out += quote(perDigit(String(ctx.date.year), ctx.dbnum));
|
|
155
|
+
changed = true;
|
|
156
|
+
dateToken = true;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
out += chars.slice(i, i + run).join("");
|
|
160
|
+
}
|
|
161
|
+
i += run;
|
|
162
|
+
}
|
|
163
|
+
else if (lower === "a" && startsWithCi(chars, i, "am/pm")) {
|
|
164
|
+
out += chars.slice(i, i + 5).join("");
|
|
165
|
+
i += 5;
|
|
166
|
+
}
|
|
167
|
+
else if (lower === "a" && startsWithCi(chars, i, "a/p")) {
|
|
168
|
+
out += chars.slice(i, i + 3).join("");
|
|
169
|
+
i += 3;
|
|
170
|
+
}
|
|
171
|
+
else if (lower === "a" && runLength(chars, i, "a") >= 3) {
|
|
172
|
+
const run = runLength(chars, i, "a");
|
|
173
|
+
if (ctx.date)
|
|
174
|
+
out += quote(weekdayName(ctx.weekdayLang, ctx.date.weekday, run >= 4));
|
|
175
|
+
changed = true;
|
|
176
|
+
dateToken = true;
|
|
177
|
+
i += run;
|
|
178
|
+
}
|
|
179
|
+
else if (lower === "y" && ctx.dbnum && ctx.date) {
|
|
180
|
+
const run = runLength(chars, i, "y");
|
|
181
|
+
const digits = run <= 2
|
|
182
|
+
? String(ctx.date.year % 100).padStart(2, "0")
|
|
183
|
+
: String(ctx.date.year);
|
|
184
|
+
out += quote(perDigit(digits, ctx.dbnum));
|
|
185
|
+
changed = true;
|
|
186
|
+
dateToken = true;
|
|
187
|
+
i += run;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
out += c;
|
|
191
|
+
i += 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { text: out, changed, dateToken };
|
|
195
|
+
};
|
|
196
|
+
/** `[...]` tokens outside quoted literals and escapes. */
|
|
197
|
+
const bracketTokens = (section) => {
|
|
198
|
+
const tokens = [];
|
|
199
|
+
let inQuotes = false;
|
|
200
|
+
for (let i = 0; i < section.length; i += 1) {
|
|
201
|
+
const c = section[i];
|
|
202
|
+
if (c === '"')
|
|
203
|
+
inQuotes = !inQuotes;
|
|
204
|
+
else if (!inQuotes && (c === "\\" || c === "_" || c === "*"))
|
|
205
|
+
i += 1;
|
|
206
|
+
else if (!inQuotes && c === "[") {
|
|
207
|
+
const end = section.indexOf("]", i);
|
|
208
|
+
if (end !== -1) {
|
|
209
|
+
tokens.push(section.slice(i, end + 1));
|
|
210
|
+
i = end;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return tokens;
|
|
215
|
+
};
|
|
216
|
+
const dbnumLevel = (section) => {
|
|
217
|
+
for (const token of bracketTokens(section)) {
|
|
218
|
+
const m = DBNUM_TOKEN.exec(token);
|
|
219
|
+
if (m)
|
|
220
|
+
return Number(m[1]);
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
};
|
|
224
|
+
const stripDbNum = (section) => bracketTokens(section)
|
|
225
|
+
.filter((t) => DBNUM_TOKEN.test(t))
|
|
226
|
+
.reduce((s, t) => s.replace(t, ""), section);
|
|
227
|
+
/** Whether a section names an era (`g`) outside quotes, brackets and `General`. */
|
|
228
|
+
const hasEraNameToken = (section) => {
|
|
229
|
+
let inQuotes = false;
|
|
230
|
+
let inBrackets = false;
|
|
231
|
+
for (let i = 0; i < section.length; i += 1) {
|
|
232
|
+
const c = section[i];
|
|
233
|
+
if (inQuotes)
|
|
234
|
+
inQuotes = c !== '"';
|
|
235
|
+
else if (inBrackets)
|
|
236
|
+
inBrackets = c !== "]";
|
|
237
|
+
else if (c === '"')
|
|
238
|
+
inQuotes = true;
|
|
239
|
+
else if (c === "[")
|
|
240
|
+
inBrackets = true;
|
|
241
|
+
else if (c === "\\" || c === "_" || c === "*")
|
|
242
|
+
i += 1;
|
|
243
|
+
else if (c.toLowerCase() === "g") {
|
|
244
|
+
if (section.slice(i, i + 7).toLowerCase() === "general")
|
|
245
|
+
i += 6;
|
|
246
|
+
else
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
};
|
|
252
|
+
/** The locale spec of the code's first `[$…-spec]` token (`411`, `ja-JP`, `ja-JP-x-gannen`). */
|
|
253
|
+
const localeTag = (code) => {
|
|
254
|
+
const start = code.indexOf("[$");
|
|
255
|
+
if (start === -1)
|
|
256
|
+
return undefined;
|
|
257
|
+
const end = code.indexOf("]", start);
|
|
258
|
+
if (end === -1)
|
|
259
|
+
return undefined;
|
|
260
|
+
const inner = code.slice(start + 2, end);
|
|
261
|
+
const dash = inner.indexOf("-");
|
|
262
|
+
if (dash === -1)
|
|
263
|
+
return undefined;
|
|
264
|
+
const spec = inner.slice(dash + 1);
|
|
265
|
+
return spec || undefined;
|
|
266
|
+
};
|
|
267
|
+
/** Language of an LCID (hex, low 16 bits) or a BCP-47 tag, for the CJK locales. */
|
|
268
|
+
export const cjkLangOfTag = (input) => {
|
|
269
|
+
const spec = input.split(",")[0].trim();
|
|
270
|
+
if (/^[0-9a-f]+$/i.test(spec)) {
|
|
271
|
+
switch (parseInt(spec, 16) & 0xffff) {
|
|
272
|
+
case 0x0411:
|
|
273
|
+
return "ja";
|
|
274
|
+
case 0x0804:
|
|
275
|
+
case 0x1004:
|
|
276
|
+
return "zh-Hans";
|
|
277
|
+
case 0x0404:
|
|
278
|
+
case 0x0c04:
|
|
279
|
+
case 0x1404:
|
|
280
|
+
return "zh-Hant";
|
|
281
|
+
case 0x0412:
|
|
282
|
+
return "ko";
|
|
283
|
+
default:
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const [primary, ...rest] = spec.toLowerCase().split(/[-_]/);
|
|
288
|
+
if (primary === "ja")
|
|
289
|
+
return "ja";
|
|
290
|
+
if (primary === "ko")
|
|
291
|
+
return "ko";
|
|
292
|
+
if (primary === "zh") {
|
|
293
|
+
return rest.some((p) => p === "hant" || p === "tw" || p === "hk" || p === "mo")
|
|
294
|
+
? "zh-Hant"
|
|
295
|
+
: "zh-Hans";
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
};
|
|
299
|
+
// ---- dates ------------------------------------------------------------------------------------
|
|
300
|
+
/**
|
|
301
|
+
* Excel 1900-system serial → calendar date, keeping Excel's fictitious 1900-02-29 (serial 60)
|
|
302
|
+
* and its weekday numbering (serial 1 is a Sunday). The time of day rounds to the nearest second.
|
|
303
|
+
*/
|
|
304
|
+
const serialToDate = (value) => {
|
|
305
|
+
if (!Number.isFinite(value) || value < 0 || value >= 2958466)
|
|
306
|
+
return undefined;
|
|
307
|
+
const days = Math.floor(Math.round(value * 86400) / 86400);
|
|
308
|
+
const weekday = (days + 6) % 7;
|
|
309
|
+
if (days === 60)
|
|
310
|
+
return { year: 1900, month: 2, day: 29, weekday };
|
|
311
|
+
const offset = days < 60 ? days : days - 1;
|
|
312
|
+
const d = new Date(Date.UTC(1899, 11, 31) + offset * 86400000);
|
|
313
|
+
return {
|
|
314
|
+
year: d.getUTCFullYear(),
|
|
315
|
+
month: d.getUTCMonth() + 1,
|
|
316
|
+
day: d.getUTCDate(),
|
|
317
|
+
weekday,
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
/** Japanese eras with the start dates Windows' Japanese calendar (and so Excel) uses. */
|
|
321
|
+
const ERAS = [
|
|
322
|
+
{ start: [1868, 1, 1], letter: "M", short: "明", name: "明治" },
|
|
323
|
+
{ start: [1912, 7, 30], letter: "T", short: "大", name: "大正" },
|
|
324
|
+
{ start: [1926, 12, 25], letter: "S", short: "昭", name: "昭和" },
|
|
325
|
+
{ start: [1989, 1, 8], letter: "H", short: "平", name: "平成" },
|
|
326
|
+
{ start: [2019, 5, 1], letter: "R", short: "令", name: "令和" },
|
|
327
|
+
];
|
|
328
|
+
const eraOf = (date) => {
|
|
329
|
+
const key = date.year * 10000 + date.month * 100 + date.day;
|
|
330
|
+
for (let i = ERAS.length - 1; i >= 0; i -= 1) {
|
|
331
|
+
const [y, m, d] = ERAS[i].start;
|
|
332
|
+
if (key >= y * 10000 + m * 100 + d)
|
|
333
|
+
return ERAS[i];
|
|
334
|
+
}
|
|
335
|
+
return ERAS[0];
|
|
336
|
+
};
|
|
337
|
+
const weekdayName = (lang, weekday, long) => {
|
|
338
|
+
const w = weekday % 7;
|
|
339
|
+
switch (lang) {
|
|
340
|
+
case "ja": {
|
|
341
|
+
const short = "日月火水木金土"[w];
|
|
342
|
+
return long ? `${short}曜日` : short;
|
|
343
|
+
}
|
|
344
|
+
case "ko": {
|
|
345
|
+
const short = "일월화수목금토"[w];
|
|
346
|
+
return long ? `${short}요일` : short;
|
|
347
|
+
}
|
|
348
|
+
default: {
|
|
349
|
+
const short = "日一二三四五六"[w];
|
|
350
|
+
return long ? `星期${short}` : short;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const KANJI = Array.from("〇一二三四五六七八九");
|
|
355
|
+
const FULL_WIDTH = Array.from("0123456789");
|
|
356
|
+
const BIG_HANS = ["万", "亿", "兆"];
|
|
357
|
+
const BIG_TRAD = ["萬", "億", "兆"];
|
|
358
|
+
const BIG_JA = ["万", "億", "兆"];
|
|
359
|
+
const numerals = ({ level, lang }) => {
|
|
360
|
+
const zh = lang === "zh-Hans" || lang === "zh-Hant";
|
|
361
|
+
const level1Big = lang === "ja" ? BIG_JA : lang === "zh-Hans" ? BIG_HANS : BIG_TRAD;
|
|
362
|
+
if (level === 2) {
|
|
363
|
+
switch (lang) {
|
|
364
|
+
case "ja":
|
|
365
|
+
return {
|
|
366
|
+
digits: Array.from("〇壱弐参四伍六七八九"),
|
|
367
|
+
small: ["拾", "百", "阡"],
|
|
368
|
+
big: BIG_TRAD,
|
|
369
|
+
omitOne: false,
|
|
370
|
+
fillZero: false,
|
|
371
|
+
};
|
|
372
|
+
case "zh-Hans":
|
|
373
|
+
return {
|
|
374
|
+
digits: Array.from("零壹贰叁肆伍陆柒捌玖"),
|
|
375
|
+
small: ["拾", "佰", "仟"],
|
|
376
|
+
big: BIG_HANS,
|
|
377
|
+
omitOne: false,
|
|
378
|
+
fillZero: true,
|
|
379
|
+
};
|
|
380
|
+
case "zh-Hant":
|
|
381
|
+
return {
|
|
382
|
+
digits: Array.from("零壹貳參肆伍陸柒捌玖"),
|
|
383
|
+
small: ["拾", "佰", "仟"],
|
|
384
|
+
big: BIG_TRAD,
|
|
385
|
+
omitOne: false,
|
|
386
|
+
fillZero: true,
|
|
387
|
+
};
|
|
388
|
+
case "ko":
|
|
389
|
+
return {
|
|
390
|
+
digits: Array.from("零壹貳參四伍六七八九"),
|
|
391
|
+
small: ["拾", "百", "阡"],
|
|
392
|
+
big: BIG_TRAD,
|
|
393
|
+
omitOne: false,
|
|
394
|
+
fillZero: false,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (level === 3) {
|
|
399
|
+
return {
|
|
400
|
+
digits: FULL_WIDTH,
|
|
401
|
+
small: ["十", "百", "千"],
|
|
402
|
+
big: level1Big,
|
|
403
|
+
omitOne: false,
|
|
404
|
+
fillZero: zh,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (level === 4 && lang === "ko") {
|
|
408
|
+
return {
|
|
409
|
+
digits: Array.from("영일이삼사오육칠팔구"),
|
|
410
|
+
small: ["십", "백", "천"],
|
|
411
|
+
big: ["만", "억", "조"],
|
|
412
|
+
omitOne: true,
|
|
413
|
+
fillZero: false,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
digits: KANJI,
|
|
418
|
+
small: ["十", "百", "千"],
|
|
419
|
+
big: level1Big,
|
|
420
|
+
omitOne: !zh,
|
|
421
|
+
fillZero: zh,
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
const perDigit = (digits, dbnum) => {
|
|
425
|
+
const table = numerals(dbnum).digits;
|
|
426
|
+
return Array.from(digits)
|
|
427
|
+
.map((c) => (c >= "0" && c <= "9" ? table[Number(c)] : c))
|
|
428
|
+
.join("");
|
|
429
|
+
};
|
|
430
|
+
/** `digits` read as a number with units; past 兆 (≥ 10^16) digit by digit. */
|
|
431
|
+
const withUnits = (digits, dbnum) => {
|
|
432
|
+
const n = numerals(dbnum);
|
|
433
|
+
const trimmed = digits.replace(/^0+/, "");
|
|
434
|
+
if (!trimmed)
|
|
435
|
+
return n.digits[0];
|
|
436
|
+
if (trimmed.length > 16)
|
|
437
|
+
return perDigit(digits, dbnum);
|
|
438
|
+
const values = Array.from(trimmed, Number);
|
|
439
|
+
const groups = Math.ceil(values.length / 4);
|
|
440
|
+
let out = "";
|
|
441
|
+
let pendingZero = false;
|
|
442
|
+
for (let g = 0; g < groups; g += 1) {
|
|
443
|
+
const groupIndex = groups - 1 - g;
|
|
444
|
+
const end = values.length - groupIndex * 4;
|
|
445
|
+
const group = values.slice(Math.max(0, end - 4), end);
|
|
446
|
+
let wrote = false;
|
|
447
|
+
group.forEach((d, k) => {
|
|
448
|
+
const pos = group.length - 1 - k;
|
|
449
|
+
if (d === 0) {
|
|
450
|
+
pendingZero ||= out.length > 0 || wrote;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (pendingZero && n.fillZero)
|
|
454
|
+
out += "零";
|
|
455
|
+
pendingZero = false;
|
|
456
|
+
if (!(d === 1 && pos > 0 && n.omitOne))
|
|
457
|
+
out += n.digits[d];
|
|
458
|
+
if (pos > 0)
|
|
459
|
+
out += n.small[pos - 1];
|
|
460
|
+
wrote = true;
|
|
461
|
+
});
|
|
462
|
+
if (wrote && groupIndex > 0)
|
|
463
|
+
out += n.big[groupIndex - 1];
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
};
|
|
467
|
+
const isAsciiDigit = (c) => !!c && c >= "0" && c <= "9";
|
|
468
|
+
const convertDigits = (text, dbnum, mode) => {
|
|
469
|
+
const chars = Array.from(text);
|
|
470
|
+
let out = "";
|
|
471
|
+
let i = 0;
|
|
472
|
+
while (i < chars.length) {
|
|
473
|
+
if (!isAsciiDigit(chars[i])) {
|
|
474
|
+
out += chars[i];
|
|
475
|
+
i += 1;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
let end = i;
|
|
479
|
+
while (end < chars.length && isAsciiDigit(chars[end]))
|
|
480
|
+
end += 1;
|
|
481
|
+
const run = chars.slice(i, end).join("");
|
|
482
|
+
const fraction = i >= 2 &&
|
|
483
|
+
(chars[i - 1] === "." || chars[i - 1] === ",") &&
|
|
484
|
+
isAsciiDigit(chars[i - 2]);
|
|
485
|
+
const units = mode === "date"
|
|
486
|
+
? dbnum.level !== 3
|
|
487
|
+
: mode === "general"
|
|
488
|
+
? !fraction
|
|
489
|
+
: false;
|
|
490
|
+
out += units ? withUnits(run, dbnum) : perDigit(run, dbnum);
|
|
491
|
+
i = end;
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
};
|
package/dist/esm/color.d.ts
CHANGED
|
@@ -10,6 +10,15 @@ export declare const rgbaToArgbHex: ({ r, g, b, a }: RGBA) => string;
|
|
|
10
10
|
export declare const isSameColor: (colorA: string | undefined, colorB: string | undefined) => boolean | undefined;
|
|
11
11
|
export declare const colorStringToHex: (color: string | undefined) => string | undefined;
|
|
12
12
|
export declare const lightenColor: (unit: number | undefined, color: string) => string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Excel's tint (ECMA-376 Part 1, §18.8.19 `CT_Color@tint`) — what a `{ theme, tint }` colour paints
|
|
15
|
+
* as. Unlike `lightenColor`, which blends each channel toward black or white, the colour moves to
|
|
16
|
+
* HLS and only its luminance changes: `L·(1+tint)` for a negative tint, `L·(1−tint) + tint` for a
|
|
17
|
+
* positive one. Hue and saturation stay, so accent1 `#4472C4` at −25 % is Excel's `#2F5597` (not
|
|
18
|
+
* `#335693`) and accent2 `#ED7D31` at −25 % is `#C55A11`. The engine's `rnc_model::apply_tint`
|
|
19
|
+
* is the same rule. Returns `#rrggbb` (as `rgbToHex`); `undefined` for a colour it cannot read.
|
|
20
|
+
*/
|
|
21
|
+
export declare const applyTint: (tint: number, color: string) => string | undefined;
|
|
13
22
|
export declare const hexToRgb: (hex: string) => {
|
|
14
23
|
r: number;
|
|
15
24
|
g: number;
|
package/dist/esm/color.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../color.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,WAoCnD,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAYnE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAkBzD;AAED,eAAO,MAAM,aAAa,GAAI,gBAAoB,IAAI,WAUrD,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,MAAM,GAAG,SAAS,wBAiB3B,CAAC;AAQF,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,GAAG,SAAS,uBAczD,CAAC;AAGF,eAAO,MAAM,YAAY,GAAI,wBAAU,EAAE,OAAO,MAAM,uBAGrD,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM;;;;CAOnC,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,WAGvD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAAC;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAUA,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,QAAQ,MAAM,EACd,QAAQ,MAAM,KACb,MAUF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,SAAS,MAAM,EAAE,EAC1B,OAAO,MAAM,KACZ,MAMF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,SAAS,MAAM,EAAE,KAAG,MAAM,EAInE,CAAC;AAEJ,eAAO,MAAM,gBAAgB,GAC3B,iBAAiB,MAAM,EACvB,YAAY,MAAM,EAClB,WAAW,MAAM,uBAUlB,CAAC"}
|
|
1
|
+
{"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../color.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,WAoCnD,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAYnE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAkBzD;AAED,eAAO,MAAM,aAAa,GAAI,gBAAoB,IAAI,WAUrD,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,MAAM,GAAG,SAAS,wBAiB3B,CAAC;AAQF,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,GAAG,SAAS,uBAczD,CAAC;AAGF,eAAO,MAAM,YAAY,GAAI,wBAAU,EAAE,OAAO,MAAM,uBAGrD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,MAAM,GAAG,SAYhE,CAAC;AA8BF,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM;;;;CAOnC,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,WAGvD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAAC;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAUA,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,QAAQ,MAAM,EACd,QAAQ,MAAM,KACb,MAUF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,SAAS,MAAM,EAAE,EAC1B,OAAO,MAAM,KACZ,MAMF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,SAAS,MAAM,EAAE,KAAG,MAAM,EAInE,CAAC;AAEJ,eAAO,MAAM,gBAAgB,GAC3B,iBAAiB,MAAM,EACvB,YAAY,MAAM,EAClB,WAAW,MAAM,uBAUlB,CAAC"}
|
package/dist/esm/color.js
CHANGED
|
@@ -114,6 +114,59 @@ export const lightenColor = (unit = 0.2, color) => {
|
|
|
114
114
|
const rgbColor = normalizeColor(color);
|
|
115
115
|
return rgbColor ? shade(unit, rgbColor) : undefined;
|
|
116
116
|
};
|
|
117
|
+
/**
|
|
118
|
+
* Excel's tint (ECMA-376 Part 1, §18.8.19 `CT_Color@tint`) — what a `{ theme, tint }` colour paints
|
|
119
|
+
* as. Unlike `lightenColor`, which blends each channel toward black or white, the colour moves to
|
|
120
|
+
* HLS and only its luminance changes: `L·(1+tint)` for a negative tint, `L·(1−tint) + tint` for a
|
|
121
|
+
* positive one. Hue and saturation stay, so accent1 `#4472C4` at −25 % is Excel's `#2F5597` (not
|
|
122
|
+
* `#335693`) and accent2 `#ED7D31` at −25 % is `#C55A11`. The engine's `rnc_model::apply_tint`
|
|
123
|
+
* is the same rule. Returns `#rrggbb` (as `rgbToHex`); `undefined` for a colour it cannot read.
|
|
124
|
+
*/
|
|
125
|
+
export const applyTint = (tint, color) => {
|
|
126
|
+
const rgba = normalizeColor(color);
|
|
127
|
+
if (!rgba || rgba.a === 0)
|
|
128
|
+
return undefined;
|
|
129
|
+
if (!Number.isFinite(tint) || tint === 0) {
|
|
130
|
+
return rgbToHex(Math.round(rgba.r), Math.round(rgba.g), Math.round(rgba.b));
|
|
131
|
+
}
|
|
132
|
+
const t = Math.max(-1, Math.min(1, tint));
|
|
133
|
+
const [h, l, s] = rgbToHls(rgba.r / 255, rgba.g / 255, rgba.b / 255);
|
|
134
|
+
const lum = Math.max(0, Math.min(1, t < 0 ? l * (1 + t) : l * (1 - t) + t));
|
|
135
|
+
const [r, g, b] = hlsToRgb(h, lum, s);
|
|
136
|
+
const byte = (c) => Math.max(0, Math.min(255, Math.round(c * 255)));
|
|
137
|
+
return rgbToHex(byte(r), byte(g), byte(b));
|
|
138
|
+
};
|
|
139
|
+
const rgbToHls = (r, g, b) => {
|
|
140
|
+
const max = Math.max(r, g, b);
|
|
141
|
+
const min = Math.min(r, g, b);
|
|
142
|
+
const l = (max + min) / 2;
|
|
143
|
+
if (max === min)
|
|
144
|
+
return [0, l, 0];
|
|
145
|
+
const range = max - min;
|
|
146
|
+
const s = l <= 0.5 ? range / (max + min) : range / (2 - max - min);
|
|
147
|
+
const rc = (max - r) / range;
|
|
148
|
+
const gc = (max - g) / range;
|
|
149
|
+
const bc = (max - b) / range;
|
|
150
|
+
const h = r === max ? bc - gc : g === max ? 2 + rc - bc : 4 + gc - rc;
|
|
151
|
+
return [(((h / 6) % 1) + 1) % 1, l, s];
|
|
152
|
+
};
|
|
153
|
+
const hlsToRgb = (h, l, s) => {
|
|
154
|
+
if (s === 0)
|
|
155
|
+
return [l, l, l];
|
|
156
|
+
const m2 = l <= 0.5 ? l * (1 + s) : l + s - l * s;
|
|
157
|
+
const m1 = 2 * l - m2;
|
|
158
|
+
const v = (hue) => {
|
|
159
|
+
const x = ((hue % 1) + 1) % 1;
|
|
160
|
+
if (x < 1 / 6)
|
|
161
|
+
return m1 + (m2 - m1) * x * 6;
|
|
162
|
+
if (x < 0.5)
|
|
163
|
+
return m2;
|
|
164
|
+
if (x < 2 / 3)
|
|
165
|
+
return m1 + (m2 - m1) * (2 / 3 - x) * 6;
|
|
166
|
+
return m1;
|
|
167
|
+
};
|
|
168
|
+
return [v(h + 1 / 3), v(h), v(h - 1 / 3)];
|
|
169
|
+
};
|
|
117
170
|
export const hexToRgb = (hex) => {
|
|
118
171
|
const bigint = parseInt(hex.slice(1), 16);
|
|
119
172
|
const r = (bigint >> 16) & 255;
|
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"}
|