@bilig/formula 0.1.2 → 0.1.4
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/builtins/placeholder.d.ts +2 -2
- package/dist/builtins/text-format-builtins.d.ts +14 -0
- package/dist/builtins/text-format-builtins.js +537 -0
- package/dist/builtins/text-format-builtins.js.map +1 -0
- package/dist/builtins/text-search-builtins.d.ts +21 -0
- package/dist/builtins/text-search-builtins.js +469 -0
- package/dist/builtins/text-search-builtins.js.map +1 -0
- package/dist/builtins/text.js +31 -968
- package/dist/builtins/text.js.map +1 -1
- package/dist/js-evaluator-array-special-calls.d.ts +28 -0
- package/dist/js-evaluator-array-special-calls.js +340 -0
- package/dist/js-evaluator-array-special-calls.js.map +1 -0
- package/dist/js-evaluator-context-special-calls.d.ts +24 -0
- package/dist/js-evaluator-context-special-calls.js +156 -0
- package/dist/js-evaluator-context-special-calls.js.map +1 -0
- package/dist/js-evaluator-types.d.ts +117 -0
- package/dist/js-evaluator-types.js +2 -0
- package/dist/js-evaluator-types.js.map +1 -0
- package/dist/js-evaluator-workbook-special-calls.d.ts +24 -0
- package/dist/js-evaluator-workbook-special-calls.js +185 -0
- package/dist/js-evaluator-workbook-special-calls.js.map +1 -0
- package/dist/js-evaluator.d.ts +3 -100
- package/dist/js-evaluator.js +58 -672
- package/dist/js-evaluator.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type CellValue } from "@bilig/protocol";
|
|
2
|
-
export declare const scalarPlaceholderBuiltinNames: readonly ("
|
|
2
|
+
export declare const scalarPlaceholderBuiltinNames: readonly ("CELL" | "COLUMN" | "FORMULATEXT" | "INDIRECT" | "ROW" | "SHEET" | "SHEETS" | "DATEDIF" | "BIN2DEC" | "BIN2HEX" | "BIN2OCT" | "DEC2BIN" | "DEC2HEX" | "DEC2OCT" | "HEX2BIN" | "HEX2DEC" | "HEX2OCT" | "OCT2BIN" | "OCT2DEC" | "OCT2HEX" | "CONVERT" | "EUROCONVERT" | "BESSELI" | "BESSELJ" | "BESSELK" | "BESSELY" | "ASC" | "JIS" | "DBCS" | "BAHTTEXT" | "TEXTSPLIT" | "FORECAST" | "FORECAST.LINEAR" | "GROWTH" | "INTERCEPT" | "LINEST" | "LOGEST" | "MODE.MULT" | "FREQUENCY" | "PERCENTILE" | "PERCENTILE.INC" | "PERCENTILE.EXC" | "PERCENTRANK" | "PERCENTRANK.INC" | "PERCENTRANK.EXC" | "QUARTILE" | "QUARTILE.INC" | "QUARTILE.EXC" | "RANK.AVG" | "RSQ" | "SLOPE" | "STEYX" | "TREND" | "EXPAND" | "OFFSET" | "TAKE" | "DROP" | "SORT" | "SORTBY" | "TOCOL" | "TOROW" | "WRAPROWS" | "WRAPCOLS" | "TRIMRANGE" | "ERF" | "ERF.PRECISE" | "ERFC" | "ERFC.PRECISE" | "FISHER" | "FISHERINV" | "GAMMALN" | "GAMMALN.PRECISE" | "GAMMA" | "GAMMA.INV" | "GAMMAINV" | "CONFIDENCE" | "CONFIDENCE.T" | "EXPONDIST" | "EXPON.DIST" | "POISSON" | "POISSON.DIST" | "WEIBULL" | "WEIBULL.DIST" | "GAMMADIST" | "GAMMA.DIST" | "CHIDIST" | "CHIINV" | "CHISQ.DIST.RT" | "CHISQ.DIST" | "CHISQ.INV.RT" | "CHISQ.INV" | "F.TEST" | "FTEST" | "Z.TEST" | "ZTEST" | "BETA.DIST" | "BETADIST" | "BETAINV" | "F.DIST" | "F.DIST.RT" | "F.INV" | "F.INV.RT" | "FDIST" | "FINV" | "T.DIST" | "T.DIST.RT" | "T.DIST.2T" | "T.INV" | "T.INV.2T" | "TDIST" | "TINV" | "T.TEST" | "TTEST" | "BINOMDIST" | "BINOM.DIST" | "BINOM.DIST.RANGE" | "CRITBINOM" | "BINOM.INV" | "HYPGEOMDIST" | "HYPGEOM.DIST" | "NEGBINOMDIST" | "NEGBINOM.DIST" | "FVSCHEDULE" | "RATE" | "CUMIPMT" | "CUMPRINC" | "IRR" | "MIRR" | "XNPV" | "XIRR" | "DB" | "DDB" | "VDB" | "SLN" | "SYD" | "COUPDAYBS" | "COUPDAYS" | "COUPDAYSNC" | "COUPNCD" | "COUPNUM" | "COUPPCD" | "PRICEDISC" | "YIELDDISC" | "PRICEMAT" | "YIELDMAT" | "PRICE" | "YIELD" | "DURATION" | "MDURATION" | "TBILLPRICE" | "TBILLYIELD" | "TBILLEQ" | "COMPLEX" | "IMREAL" | "IMAGINARY" | "IMABS" | "IMARGUMENT" | "IMCONJUGATE" | "IMSUM" | "IMSUB" | "IMPRODUCT" | "IMDIV" | "IMEXP" | "IMLN" | "IMLOG10" | "IMLOG2" | "IMPOWER" | "IMSQRT" | "IMSIN" | "IMCOS" | "IMTAN" | "IMSINH" | "IMCOSH" | "IMSEC" | "IMCSC" | "IMCOT" | "IMSECH" | "IMCSCH" | "CALL" | "CUBEKPIMEMBER" | "CUBEMEMBER" | "CUBEMEMBERPROPERTY" | "CUBERANKEDMEMBER" | "CUBESET" | "CUBESETCOUNT" | "CUBEVALUE" | "DDE" | "DETECTLANGUAGE" | "FILTERXML" | "HYPERLINK" | "IMAGE" | "INFO" | "ISOMITTED" | "REGEXEXTRACT" | "REGEXREPLACE" | "REGEXTEST" | "REGISTER.ID" | "RTD" | "TRANSLATE" | "STOCKHISTORY" | "WEBSERVICE" | "COPILOT" | "FORECAST.ETS" | "FORECAST.ETS.CONFINT" | "FORECAST.ETS.SEASONALITY" | "FORECAST.ETS.STAT")[];
|
|
3
3
|
export declare const logicalPlaceholderBuiltinNames: readonly [];
|
|
4
4
|
export declare const datetimePlaceholderBuiltinNames: readonly [];
|
|
5
5
|
export declare const textPlaceholderBuiltinNames: readonly [];
|
|
6
|
-
export declare const placeholderBuiltinNames: readonly ("
|
|
6
|
+
export declare const placeholderBuiltinNames: readonly ("CELL" | "COLUMN" | "FORMULATEXT" | "INDIRECT" | "ROW" | "SHEET" | "SHEETS" | "DATEDIF" | "BIN2DEC" | "BIN2HEX" | "BIN2OCT" | "DEC2BIN" | "DEC2HEX" | "DEC2OCT" | "HEX2BIN" | "HEX2DEC" | "HEX2OCT" | "OCT2BIN" | "OCT2DEC" | "OCT2HEX" | "CONVERT" | "EUROCONVERT" | "BESSELI" | "BESSELJ" | "BESSELK" | "BESSELY" | "ASC" | "JIS" | "DBCS" | "BAHTTEXT" | "TEXTSPLIT" | "FORECAST" | "FORECAST.LINEAR" | "GROWTH" | "INTERCEPT" | "LINEST" | "LOGEST" | "MODE.MULT" | "FREQUENCY" | "PERCENTILE" | "PERCENTILE.INC" | "PERCENTILE.EXC" | "PERCENTRANK" | "PERCENTRANK.INC" | "PERCENTRANK.EXC" | "QUARTILE" | "QUARTILE.INC" | "QUARTILE.EXC" | "RANK.AVG" | "RSQ" | "SLOPE" | "STEYX" | "TREND" | "EXPAND" | "OFFSET" | "TAKE" | "DROP" | "SORT" | "SORTBY" | "TOCOL" | "TOROW" | "WRAPROWS" | "WRAPCOLS" | "TRIMRANGE" | "ERF" | "ERF.PRECISE" | "ERFC" | "ERFC.PRECISE" | "FISHER" | "FISHERINV" | "GAMMALN" | "GAMMALN.PRECISE" | "GAMMA" | "GAMMA.INV" | "GAMMAINV" | "CONFIDENCE" | "CONFIDENCE.T" | "EXPONDIST" | "EXPON.DIST" | "POISSON" | "POISSON.DIST" | "WEIBULL" | "WEIBULL.DIST" | "GAMMADIST" | "GAMMA.DIST" | "CHIDIST" | "CHIINV" | "CHISQ.DIST.RT" | "CHISQ.DIST" | "CHISQ.INV.RT" | "CHISQ.INV" | "F.TEST" | "FTEST" | "Z.TEST" | "ZTEST" | "BETA.DIST" | "BETADIST" | "BETAINV" | "F.DIST" | "F.DIST.RT" | "F.INV" | "F.INV.RT" | "FDIST" | "FINV" | "T.DIST" | "T.DIST.RT" | "T.DIST.2T" | "T.INV" | "T.INV.2T" | "TDIST" | "TINV" | "T.TEST" | "TTEST" | "BINOMDIST" | "BINOM.DIST" | "BINOM.DIST.RANGE" | "CRITBINOM" | "BINOM.INV" | "HYPGEOMDIST" | "HYPGEOM.DIST" | "NEGBINOMDIST" | "NEGBINOM.DIST" | "FVSCHEDULE" | "RATE" | "CUMIPMT" | "CUMPRINC" | "IRR" | "MIRR" | "XNPV" | "XIRR" | "DB" | "DDB" | "VDB" | "SLN" | "SYD" | "COUPDAYBS" | "COUPDAYS" | "COUPDAYSNC" | "COUPNCD" | "COUPNUM" | "COUPPCD" | "PRICEDISC" | "YIELDDISC" | "PRICEMAT" | "YIELDMAT" | "PRICE" | "YIELD" | "DURATION" | "MDURATION" | "TBILLPRICE" | "TBILLYIELD" | "TBILLEQ" | "COMPLEX" | "IMREAL" | "IMAGINARY" | "IMABS" | "IMARGUMENT" | "IMCONJUGATE" | "IMSUM" | "IMSUB" | "IMPRODUCT" | "IMDIV" | "IMEXP" | "IMLN" | "IMLOG10" | "IMLOG2" | "IMPOWER" | "IMSQRT" | "IMSIN" | "IMCOS" | "IMTAN" | "IMSINH" | "IMCOSH" | "IMSEC" | "IMCSC" | "IMCOT" | "IMSECH" | "IMCSCH" | "CALL" | "CUBEKPIMEMBER" | "CUBEMEMBER" | "CUBEMEMBERPROPERTY" | "CUBERANKEDMEMBER" | "CUBESET" | "CUBESETCOUNT" | "CUBEVALUE" | "DDE" | "DETECTLANGUAGE" | "FILTERXML" | "HYPERLINK" | "IMAGE" | "INFO" | "ISOMITTED" | "REGEXEXTRACT" | "REGEXREPLACE" | "REGEXTEST" | "REGISTER.ID" | "RTD" | "TRANSLATE" | "STOCKHISTORY" | "WEBSERVICE" | "COPILOT" | "FORECAST.ETS" | "FORECAST.ETS.CONFINT" | "FORECAST.ETS.SEASONALITY" | "FORECAST.ETS.STAT")[];
|
|
7
7
|
export declare const protocolPlaceholderBuiltinNames: readonly [];
|
|
8
8
|
export type PlaceholderBuiltin = (...args: CellValue[]) => CellValue;
|
|
9
9
|
export declare function createBlockedBuiltinMap(names: readonly string[]): Record<string, PlaceholderBuiltin>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ErrorCode, type CellValue } from "@bilig/protocol";
|
|
2
|
+
import type { TextBuiltin } from "./text.js";
|
|
3
|
+
interface TextFormatBuiltinDeps {
|
|
4
|
+
error: (code: ErrorCode) => CellValue;
|
|
5
|
+
stringResult: (value: string) => CellValue;
|
|
6
|
+
numberResult: (value: number) => CellValue;
|
|
7
|
+
firstError: (args: readonly (CellValue | undefined)[]) => CellValue | undefined;
|
|
8
|
+
coerceText: (value: CellValue) => string;
|
|
9
|
+
coerceNumber: (value: CellValue) => number | undefined;
|
|
10
|
+
coerceInteger: (value: CellValue | undefined, defaultValue: number) => number | CellValue;
|
|
11
|
+
isErrorValue: (value: number | CellValue) => value is CellValue;
|
|
12
|
+
}
|
|
13
|
+
export declare function createTextFormatBuiltins(deps: TextFormatBuiltinDeps): Record<string, TextBuiltin>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { ErrorCode, ValueTag } from "@bilig/protocol";
|
|
2
|
+
import { excelSerialToDateParts } from "./datetime.js";
|
|
3
|
+
function valueToTextResult(deps, value, format) {
|
|
4
|
+
if (format !== 0 && format !== 1) {
|
|
5
|
+
return deps.error(ErrorCode.Value);
|
|
6
|
+
}
|
|
7
|
+
switch (value.tag) {
|
|
8
|
+
case ValueTag.Empty:
|
|
9
|
+
return deps.stringResult("");
|
|
10
|
+
case ValueTag.Number:
|
|
11
|
+
return deps.stringResult(String(value.value));
|
|
12
|
+
case ValueTag.Boolean:
|
|
13
|
+
return deps.stringResult(value.value ? "TRUE" : "FALSE");
|
|
14
|
+
case ValueTag.String:
|
|
15
|
+
return deps.stringResult(format === 1 ? JSON.stringify(value.value) : value.value);
|
|
16
|
+
case ValueTag.Error: {
|
|
17
|
+
const label = value.code === ErrorCode.Div0
|
|
18
|
+
? "#DIV/0!"
|
|
19
|
+
: value.code === ErrorCode.Ref
|
|
20
|
+
? "#REF!"
|
|
21
|
+
: value.code === ErrorCode.Value
|
|
22
|
+
? "#VALUE!"
|
|
23
|
+
: value.code === ErrorCode.Name
|
|
24
|
+
? "#NAME?"
|
|
25
|
+
: value.code === ErrorCode.NA
|
|
26
|
+
? "#N/A"
|
|
27
|
+
: value.code === ErrorCode.Cycle
|
|
28
|
+
? "#CYCLE!"
|
|
29
|
+
: value.code === ErrorCode.Spill
|
|
30
|
+
? "#SPILL!"
|
|
31
|
+
: value.code === ErrorCode.Blocked
|
|
32
|
+
? "#BLOCKED!"
|
|
33
|
+
: "#ERROR!";
|
|
34
|
+
return deps.stringResult(label);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const shortMonthNames = [
|
|
39
|
+
"Jan",
|
|
40
|
+
"Feb",
|
|
41
|
+
"Mar",
|
|
42
|
+
"Apr",
|
|
43
|
+
"May",
|
|
44
|
+
"Jun",
|
|
45
|
+
"Jul",
|
|
46
|
+
"Aug",
|
|
47
|
+
"Sep",
|
|
48
|
+
"Oct",
|
|
49
|
+
"Nov",
|
|
50
|
+
"Dec",
|
|
51
|
+
];
|
|
52
|
+
const fullMonthNames = [
|
|
53
|
+
"January",
|
|
54
|
+
"February",
|
|
55
|
+
"March",
|
|
56
|
+
"April",
|
|
57
|
+
"May",
|
|
58
|
+
"June",
|
|
59
|
+
"July",
|
|
60
|
+
"August",
|
|
61
|
+
"September",
|
|
62
|
+
"October",
|
|
63
|
+
"November",
|
|
64
|
+
"December",
|
|
65
|
+
];
|
|
66
|
+
const shortWeekdayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
67
|
+
const fullWeekdayNames = [
|
|
68
|
+
"Sunday",
|
|
69
|
+
"Monday",
|
|
70
|
+
"Tuesday",
|
|
71
|
+
"Wednesday",
|
|
72
|
+
"Thursday",
|
|
73
|
+
"Friday",
|
|
74
|
+
"Saturday",
|
|
75
|
+
];
|
|
76
|
+
function splitFormatSections(format) {
|
|
77
|
+
const sections = [];
|
|
78
|
+
let current = "";
|
|
79
|
+
let inQuotes = false;
|
|
80
|
+
let bracketDepth = 0;
|
|
81
|
+
let escaped = false;
|
|
82
|
+
for (let index = 0; index < format.length; index += 1) {
|
|
83
|
+
const char = format[index];
|
|
84
|
+
if (escaped) {
|
|
85
|
+
current += char;
|
|
86
|
+
escaped = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (char === "\\") {
|
|
90
|
+
current += char;
|
|
91
|
+
escaped = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (char === '"') {
|
|
95
|
+
current += char;
|
|
96
|
+
inQuotes = !inQuotes;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!inQuotes && char === "[") {
|
|
100
|
+
bracketDepth += 1;
|
|
101
|
+
current += char;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!inQuotes && char === "]" && bracketDepth > 0) {
|
|
105
|
+
bracketDepth -= 1;
|
|
106
|
+
current += char;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!inQuotes && bracketDepth === 0 && char === ";") {
|
|
110
|
+
sections.push(current);
|
|
111
|
+
current = "";
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
current += char;
|
|
115
|
+
}
|
|
116
|
+
sections.push(current);
|
|
117
|
+
return sections;
|
|
118
|
+
}
|
|
119
|
+
function stripFormatDecorations(section) {
|
|
120
|
+
let output = "";
|
|
121
|
+
let inQuotes = false;
|
|
122
|
+
for (let index = 0; index < section.length; index += 1) {
|
|
123
|
+
const char = section[index];
|
|
124
|
+
if (inQuotes) {
|
|
125
|
+
if (char === '"') {
|
|
126
|
+
inQuotes = false;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
output += char;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (char === '"') {
|
|
134
|
+
inQuotes = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (char === "\\") {
|
|
138
|
+
output += section[index + 1] ?? "";
|
|
139
|
+
index += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (char === "_") {
|
|
143
|
+
output += " ";
|
|
144
|
+
index += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (char === "*") {
|
|
148
|
+
index += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (char === "[") {
|
|
152
|
+
const end = section.indexOf("]", index + 1);
|
|
153
|
+
if (end === -1) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
index = end;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
output += char;
|
|
160
|
+
}
|
|
161
|
+
return output;
|
|
162
|
+
}
|
|
163
|
+
function formatThousandsText(integerPart) {
|
|
164
|
+
return integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
165
|
+
}
|
|
166
|
+
function zeroPadText(value, width) {
|
|
167
|
+
return String(Math.trunc(Math.abs(value))).padStart(width, "0");
|
|
168
|
+
}
|
|
169
|
+
function roundToDigits(value, digits) {
|
|
170
|
+
if (!Number.isFinite(value)) {
|
|
171
|
+
return Number.NaN;
|
|
172
|
+
}
|
|
173
|
+
const factor = 10 ** Math.max(0, digits);
|
|
174
|
+
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
175
|
+
}
|
|
176
|
+
function excelSecondOfDay(serial) {
|
|
177
|
+
if (!Number.isFinite(serial)) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const whole = Math.floor(serial);
|
|
181
|
+
let fraction = serial - whole;
|
|
182
|
+
if (fraction < 0) {
|
|
183
|
+
fraction += 1;
|
|
184
|
+
}
|
|
185
|
+
let seconds = Math.floor(fraction * 86_400 + 1e-9);
|
|
186
|
+
if (seconds >= 86_400) {
|
|
187
|
+
seconds = 0;
|
|
188
|
+
}
|
|
189
|
+
return seconds;
|
|
190
|
+
}
|
|
191
|
+
function excelWeekdayIndex(serial) {
|
|
192
|
+
if (!Number.isFinite(serial)) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
const whole = Math.floor(serial);
|
|
196
|
+
if (whole < 0) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
const adjustedWhole = whole < 60 ? whole : whole - 1;
|
|
200
|
+
return ((adjustedWhole % 7) + 7) % 7;
|
|
201
|
+
}
|
|
202
|
+
function isDateTimeFormat(section) {
|
|
203
|
+
const cleaned = stripFormatDecorations(section).toUpperCase();
|
|
204
|
+
return (cleaned.includes("AM/PM") ||
|
|
205
|
+
cleaned.includes("A/P") ||
|
|
206
|
+
/[YDSH]/.test(cleaned) ||
|
|
207
|
+
/(^|[^0#?])M+([^0#?]|$)/.test(cleaned));
|
|
208
|
+
}
|
|
209
|
+
function isTextFormat(section) {
|
|
210
|
+
return stripFormatDecorations(section).includes("@");
|
|
211
|
+
}
|
|
212
|
+
function chooseFormatSection(deps, value, formatText) {
|
|
213
|
+
const sections = splitFormatSections(formatText);
|
|
214
|
+
if (value.tag === ValueTag.String) {
|
|
215
|
+
return { section: sections[3] ?? sections[0] ?? "", autoNegative: false };
|
|
216
|
+
}
|
|
217
|
+
const numeric = deps.coerceNumber(value);
|
|
218
|
+
if (numeric === undefined) {
|
|
219
|
+
return deps.error(ErrorCode.Value);
|
|
220
|
+
}
|
|
221
|
+
if (numeric < 0) {
|
|
222
|
+
if (sections[1] !== undefined) {
|
|
223
|
+
return { section: sections[1], numeric: -numeric, autoNegative: false };
|
|
224
|
+
}
|
|
225
|
+
return { section: sections[0] ?? "", numeric: -numeric, autoNegative: true };
|
|
226
|
+
}
|
|
227
|
+
if (numeric === 0 && sections[2] !== undefined) {
|
|
228
|
+
return { section: sections[2], numeric, autoNegative: false };
|
|
229
|
+
}
|
|
230
|
+
return { section: sections[0] ?? "", numeric, autoNegative: false };
|
|
231
|
+
}
|
|
232
|
+
function formatTextSectionValue(value, section) {
|
|
233
|
+
const cleaned = stripFormatDecorations(section);
|
|
234
|
+
return cleaned.includes("@") ? cleaned.replace(/@/g, value) : cleaned;
|
|
235
|
+
}
|
|
236
|
+
function tokenizeDateTimeFormat(section) {
|
|
237
|
+
const cleaned = stripFormatDecorations(section);
|
|
238
|
+
const tokens = [];
|
|
239
|
+
let index = 0;
|
|
240
|
+
while (index < cleaned.length) {
|
|
241
|
+
const remainder = cleaned.slice(index);
|
|
242
|
+
const upperRemainder = remainder.toUpperCase();
|
|
243
|
+
if (upperRemainder.startsWith("AM/PM")) {
|
|
244
|
+
tokens.push({ kind: "ampm", text: cleaned.slice(index, index + 5) });
|
|
245
|
+
index += 5;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (upperRemainder.startsWith("A/P")) {
|
|
249
|
+
tokens.push({ kind: "ampm", text: cleaned.slice(index, index + 3) });
|
|
250
|
+
index += 3;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const char = cleaned[index];
|
|
254
|
+
const lower = char.toLowerCase();
|
|
255
|
+
if ("ymdhms".includes(lower)) {
|
|
256
|
+
let end = index + 1;
|
|
257
|
+
while (end < cleaned.length && cleaned[end].toLowerCase() === lower) {
|
|
258
|
+
end += 1;
|
|
259
|
+
}
|
|
260
|
+
const tokenText = cleaned.slice(index, end);
|
|
261
|
+
const baseKind = lower === "y"
|
|
262
|
+
? "year"
|
|
263
|
+
: lower === "d"
|
|
264
|
+
? "day"
|
|
265
|
+
: lower === "h"
|
|
266
|
+
? "hour"
|
|
267
|
+
: lower === "s"
|
|
268
|
+
? "second"
|
|
269
|
+
: "month";
|
|
270
|
+
tokens.push({ kind: baseKind, text: tokenText });
|
|
271
|
+
index = end;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
tokens.push({ kind: "literal", text: char });
|
|
275
|
+
index += 1;
|
|
276
|
+
}
|
|
277
|
+
return tokens.map((token, tokenIndex, allTokens) => {
|
|
278
|
+
if (token.kind !== "month") {
|
|
279
|
+
return token;
|
|
280
|
+
}
|
|
281
|
+
const previous = allTokens.slice(0, tokenIndex).findLast((entry) => entry.kind !== "literal");
|
|
282
|
+
const next = allTokens.slice(tokenIndex + 1).find((entry) => entry.kind !== "literal");
|
|
283
|
+
if (previous?.kind === "hour" || previous?.kind === "minute" || next?.kind === "second") {
|
|
284
|
+
return { kind: "minute", text: token.text };
|
|
285
|
+
}
|
|
286
|
+
return token;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function formatAmPmToken(token, hour) {
|
|
290
|
+
const isPm = hour >= 12;
|
|
291
|
+
const upper = token.toUpperCase();
|
|
292
|
+
if (upper === "A/P") {
|
|
293
|
+
const letter = isPm ? "P" : "A";
|
|
294
|
+
return token === token.toLowerCase() ? letter.toLowerCase() : letter;
|
|
295
|
+
}
|
|
296
|
+
if (token === token.toLowerCase()) {
|
|
297
|
+
return isPm ? "pm" : "am";
|
|
298
|
+
}
|
|
299
|
+
return isPm ? "PM" : "AM";
|
|
300
|
+
}
|
|
301
|
+
function formatDateTimeSectionValue(serial, section) {
|
|
302
|
+
const dateParts = excelSerialToDateParts(serial);
|
|
303
|
+
const weekdayIndex = excelWeekdayIndex(serial);
|
|
304
|
+
const secondOfDay = excelSecondOfDay(serial);
|
|
305
|
+
if (!dateParts || weekdayIndex === undefined || secondOfDay === undefined) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
const hour24 = Math.floor(secondOfDay / 3600);
|
|
309
|
+
const minute = Math.floor((secondOfDay % 3600) / 60);
|
|
310
|
+
const second = secondOfDay % 60;
|
|
311
|
+
const tokens = tokenizeDateTimeFormat(section);
|
|
312
|
+
const hasAmPm = tokens.some((token) => token.kind === "ampm");
|
|
313
|
+
return tokens
|
|
314
|
+
.map((token) => {
|
|
315
|
+
switch (token.kind) {
|
|
316
|
+
case "literal":
|
|
317
|
+
return token.text;
|
|
318
|
+
case "year":
|
|
319
|
+
return token.text.length === 2
|
|
320
|
+
? zeroPadText(dateParts.year % 100, 2)
|
|
321
|
+
: String(dateParts.year).padStart(Math.max(4, token.text.length), "0");
|
|
322
|
+
case "month":
|
|
323
|
+
return token.text.length === 1
|
|
324
|
+
? String(dateParts.month)
|
|
325
|
+
: token.text.length === 2
|
|
326
|
+
? zeroPadText(dateParts.month, 2)
|
|
327
|
+
: token.text.length === 3
|
|
328
|
+
? shortMonthNames[dateParts.month - 1]
|
|
329
|
+
: fullMonthNames[dateParts.month - 1];
|
|
330
|
+
case "minute":
|
|
331
|
+
return token.text.length >= 2 ? zeroPadText(minute, 2) : String(minute);
|
|
332
|
+
case "day":
|
|
333
|
+
return token.text.length === 1
|
|
334
|
+
? String(dateParts.day)
|
|
335
|
+
: token.text.length === 2
|
|
336
|
+
? zeroPadText(dateParts.day, 2)
|
|
337
|
+
: token.text.length === 3
|
|
338
|
+
? shortWeekdayNames[weekdayIndex]
|
|
339
|
+
: fullWeekdayNames[weekdayIndex];
|
|
340
|
+
case "hour": {
|
|
341
|
+
const normalizedHour = hasAmPm ? ((hour24 + 11) % 12) + 1 : hour24;
|
|
342
|
+
return token.text.length >= 2 ? zeroPadText(normalizedHour, 2) : String(normalizedHour);
|
|
343
|
+
}
|
|
344
|
+
case "second":
|
|
345
|
+
return token.text.length >= 2 ? zeroPadText(second, 2) : String(second);
|
|
346
|
+
case "ampm":
|
|
347
|
+
return formatAmPmToken(token.text, hour24);
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
.join("");
|
|
351
|
+
}
|
|
352
|
+
function trimOptionalFractionDigits(fraction, minDigits) {
|
|
353
|
+
let trimmed = fraction;
|
|
354
|
+
while (trimmed.length > minDigits && trimmed.endsWith("0")) {
|
|
355
|
+
trimmed = trimmed.slice(0, -1);
|
|
356
|
+
}
|
|
357
|
+
return trimmed;
|
|
358
|
+
}
|
|
359
|
+
function formatScientificSection(value, core) {
|
|
360
|
+
const exponentIndex = core.search(/[Ee][+-]/);
|
|
361
|
+
const mantissaPattern = core.slice(0, exponentIndex);
|
|
362
|
+
const exponentPattern = core.slice(exponentIndex + 2);
|
|
363
|
+
const mantissaParts = mantissaPattern.split(".");
|
|
364
|
+
const fractionPattern = mantissaParts[1] ?? "";
|
|
365
|
+
const maxFractionDigits = (fractionPattern.match(/[0#?]/g) ?? []).length;
|
|
366
|
+
const minFractionDigits = (fractionPattern.match(/0/g) ?? []).length;
|
|
367
|
+
const [mantissaRaw = "0", exponentRaw] = value.toExponential(maxFractionDigits).split("e");
|
|
368
|
+
let [integerPart = "0", fractionPart = ""] = mantissaRaw.split(".");
|
|
369
|
+
fractionPart = trimOptionalFractionDigits(fractionPart, minFractionDigits);
|
|
370
|
+
const exponentValue = Number(exponentRaw ?? 0);
|
|
371
|
+
const exponentText = String(Math.abs(exponentValue)).padStart(exponentPattern.length, "0");
|
|
372
|
+
return `${integerPart}${fractionPart === "" ? "" : `.${fractionPart}`}E${exponentValue < 0 ? "-" : "+"}${exponentText}`;
|
|
373
|
+
}
|
|
374
|
+
function formatNumericSectionValue(value, section, autoNegative) {
|
|
375
|
+
const cleaned = stripFormatDecorations(section);
|
|
376
|
+
if (!/[0#?]/.test(cleaned)) {
|
|
377
|
+
return autoNegative && !cleaned.startsWith("-") ? `-${cleaned}` : cleaned;
|
|
378
|
+
}
|
|
379
|
+
const firstPlaceholder = cleaned.search(/[0#?]/);
|
|
380
|
+
let lastPlaceholder = -1;
|
|
381
|
+
for (let index = cleaned.length - 1; index >= 0; index -= 1) {
|
|
382
|
+
if (/[0#?]/.test(cleaned[index])) {
|
|
383
|
+
lastPlaceholder = index;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const prefix = cleaned.slice(0, firstPlaceholder);
|
|
388
|
+
const core = cleaned.slice(firstPlaceholder, lastPlaceholder + 1);
|
|
389
|
+
const suffix = cleaned.slice(lastPlaceholder + 1);
|
|
390
|
+
const percentCount = (cleaned.match(/%/g) ?? []).length;
|
|
391
|
+
const scaledValue = Math.abs(value) * 100 ** percentCount;
|
|
392
|
+
let numericText = "";
|
|
393
|
+
if (/[Ee][+-]/.test(core)) {
|
|
394
|
+
numericText = formatScientificSection(scaledValue, core);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
const decimalIndex = core.indexOf(".");
|
|
398
|
+
const integerPattern = (decimalIndex === -1 ? core : core.slice(0, decimalIndex)).replaceAll(",", "");
|
|
399
|
+
const fractionPattern = decimalIndex === -1 ? "" : core.slice(decimalIndex + 1);
|
|
400
|
+
const maxFractionDigits = (fractionPattern.match(/[0#?]/g) ?? []).length;
|
|
401
|
+
const minFractionDigits = (fractionPattern.match(/0/g) ?? []).length;
|
|
402
|
+
const minIntegerDigits = (integerPattern.match(/0/g) ?? []).length;
|
|
403
|
+
const roundedValue = roundToDigits(scaledValue, maxFractionDigits);
|
|
404
|
+
const fixed = roundedValue.toFixed(maxFractionDigits);
|
|
405
|
+
let [integerPart = "0", fractionPart = ""] = fixed.split(".");
|
|
406
|
+
if (integerPart.length < minIntegerDigits) {
|
|
407
|
+
integerPart = integerPart.padStart(minIntegerDigits, "0");
|
|
408
|
+
}
|
|
409
|
+
if (core.includes(",")) {
|
|
410
|
+
integerPart = formatThousandsText(integerPart);
|
|
411
|
+
}
|
|
412
|
+
fractionPart = trimOptionalFractionDigits(fractionPart, minFractionDigits);
|
|
413
|
+
numericText = `${integerPart}${fractionPart === "" ? "" : `.${fractionPart}`}`;
|
|
414
|
+
}
|
|
415
|
+
const combined = `${prefix}${numericText}${suffix}`;
|
|
416
|
+
return autoNegative && !combined.startsWith("-") ? `-${combined}` : combined;
|
|
417
|
+
}
|
|
418
|
+
function formatTextBuiltinValue(deps, value, formatText) {
|
|
419
|
+
const chosen = chooseFormatSection(deps, value, formatText);
|
|
420
|
+
if ("tag" in chosen) {
|
|
421
|
+
return chosen;
|
|
422
|
+
}
|
|
423
|
+
const { section, numeric, autoNegative } = chosen;
|
|
424
|
+
if (value.tag === ValueTag.String) {
|
|
425
|
+
const cleaned = stripFormatDecorations(section);
|
|
426
|
+
if (isTextFormat(section) || !/[0#?YMDHS]/i.test(cleaned)) {
|
|
427
|
+
return deps.stringResult(formatTextSectionValue(value.value, section));
|
|
428
|
+
}
|
|
429
|
+
return deps.error(ErrorCode.Value);
|
|
430
|
+
}
|
|
431
|
+
if (numeric === undefined) {
|
|
432
|
+
return deps.error(ErrorCode.Value);
|
|
433
|
+
}
|
|
434
|
+
if (isDateTimeFormat(section)) {
|
|
435
|
+
const formatted = formatDateTimeSectionValue(numeric, section);
|
|
436
|
+
return formatted === undefined ? deps.error(ErrorCode.Value) : deps.stringResult(formatted);
|
|
437
|
+
}
|
|
438
|
+
return deps.stringResult(formatNumericSectionValue(numeric, section, autoNegative));
|
|
439
|
+
}
|
|
440
|
+
function parseNumberValueText(input, decimalSeparator, groupSeparator) {
|
|
441
|
+
const compact = input.replaceAll(/\s+/g, "");
|
|
442
|
+
if (compact === "") {
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
const percentMatch = compact.match(/%+$/);
|
|
446
|
+
const percentCount = percentMatch?.[0].length ?? 0;
|
|
447
|
+
const core = percentCount === 0 ? compact : compact.slice(0, -percentCount);
|
|
448
|
+
if (core.includes("%")) {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
if (decimalSeparator !== "" && groupSeparator !== "" && decimalSeparator === groupSeparator) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
const decimal = decimalSeparator === "" ? "." : decimalSeparator[0];
|
|
455
|
+
const group = groupSeparator === "" ? "" : groupSeparator[0];
|
|
456
|
+
const decimalIndex = decimal === "" ? -1 : core.indexOf(decimal);
|
|
457
|
+
if (decimalIndex !== -1 && core.indexOf(decimal, decimalIndex + 1) !== -1) {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
let normalized = core;
|
|
461
|
+
if (group !== "") {
|
|
462
|
+
const groupAfterDecimal = decimalIndex === -1 ? -1 : normalized.indexOf(group, decimalIndex + decimal.length);
|
|
463
|
+
if (groupAfterDecimal !== -1) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
normalized = normalized.replaceAll(group, "");
|
|
467
|
+
}
|
|
468
|
+
if (decimal !== "." && decimal !== "") {
|
|
469
|
+
normalized = normalized.replace(decimal, ".");
|
|
470
|
+
}
|
|
471
|
+
if (normalized === "" || normalized === "." || normalized === "+" || normalized === "-") {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
const parsed = Number(normalized);
|
|
475
|
+
if (!Number.isFinite(parsed)) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
return parsed / 100 ** percentCount;
|
|
479
|
+
}
|
|
480
|
+
export function createTextFormatBuiltins(deps) {
|
|
481
|
+
return {
|
|
482
|
+
TEXT: (...args) => {
|
|
483
|
+
const existingError = deps.firstError(args);
|
|
484
|
+
if (existingError) {
|
|
485
|
+
return existingError;
|
|
486
|
+
}
|
|
487
|
+
const [value, formatValue] = args;
|
|
488
|
+
if (value === undefined || formatValue === undefined) {
|
|
489
|
+
return deps.error(ErrorCode.Value);
|
|
490
|
+
}
|
|
491
|
+
return formatTextBuiltinValue(deps, value, deps.coerceText(formatValue));
|
|
492
|
+
},
|
|
493
|
+
VALUE: (...args) => {
|
|
494
|
+
const existingError = deps.firstError(args);
|
|
495
|
+
if (existingError) {
|
|
496
|
+
return existingError;
|
|
497
|
+
}
|
|
498
|
+
const [value] = args;
|
|
499
|
+
if (value === undefined) {
|
|
500
|
+
return deps.error(ErrorCode.Value);
|
|
501
|
+
}
|
|
502
|
+
const coerced = deps.coerceNumber(value);
|
|
503
|
+
return coerced === undefined ? deps.error(ErrorCode.Value) : deps.numberResult(coerced);
|
|
504
|
+
},
|
|
505
|
+
NUMBERVALUE: (...args) => {
|
|
506
|
+
const existingError = deps.firstError(args);
|
|
507
|
+
if (existingError) {
|
|
508
|
+
return existingError;
|
|
509
|
+
}
|
|
510
|
+
const [textValue, decimalSeparatorValue, groupSeparatorValue] = args;
|
|
511
|
+
if (textValue === undefined) {
|
|
512
|
+
return deps.error(ErrorCode.Value);
|
|
513
|
+
}
|
|
514
|
+
const text = deps.coerceText(textValue);
|
|
515
|
+
const decimalSeparator = decimalSeparatorValue === undefined ? "." : deps.coerceText(decimalSeparatorValue);
|
|
516
|
+
const groupSeparator = groupSeparatorValue === undefined ? "," : deps.coerceText(groupSeparatorValue);
|
|
517
|
+
const parsed = parseNumberValueText(text, decimalSeparator, groupSeparator);
|
|
518
|
+
return parsed === undefined ? deps.error(ErrorCode.Value) : deps.numberResult(parsed);
|
|
519
|
+
},
|
|
520
|
+
VALUETOTEXT: (...args) => {
|
|
521
|
+
const existingError = deps.firstError(args);
|
|
522
|
+
if (existingError) {
|
|
523
|
+
return valueToTextResult(deps, existingError, 0);
|
|
524
|
+
}
|
|
525
|
+
const [value, formatValue] = args;
|
|
526
|
+
if (value === undefined) {
|
|
527
|
+
return deps.error(ErrorCode.Value);
|
|
528
|
+
}
|
|
529
|
+
const format = deps.coerceInteger(formatValue, 0);
|
|
530
|
+
if (deps.isErrorValue(format)) {
|
|
531
|
+
return format;
|
|
532
|
+
}
|
|
533
|
+
return valueToTextResult(deps, value, format);
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
//# sourceMappingURL=text-format-builtins.js.map
|