@particle-academy/holy-sheet 2.11.0 → 2.11.2
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/CHANGELOG.md +76 -0
- package/dist/{chunk-OL6E7DE5.js → chunk-Y32UXRQ6.js} +322 -75
- package/dist/chunk-Y32UXRQ6.js.map +1 -0
- package/dist/cli.cjs +320 -73
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +320 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -3
- package/dist/index.d.ts +70 -3
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-OL6E7DE5.js.map +0 -1
|
@@ -780,7 +780,7 @@ var Normalizer = class {
|
|
|
780
780
|
address,
|
|
781
781
|
new Cell(
|
|
782
782
|
address,
|
|
783
|
-
this.coerceValue(rawValue, format, address, sheetName2),
|
|
783
|
+
this.coerceValue(rawValue, format, address, sheetName2, true),
|
|
784
784
|
cellData?.formula ?? null,
|
|
785
785
|
format,
|
|
786
786
|
comment,
|
|
@@ -873,7 +873,7 @@ var Normalizer = class {
|
|
|
873
873
|
const rawValue = value.value ?? null;
|
|
874
874
|
return new Cell(
|
|
875
875
|
address,
|
|
876
|
-
this.coerceValue(rawValue, merged, address, sheetName2),
|
|
876
|
+
this.coerceValue(rawValue, merged, address, sheetName2, true),
|
|
877
877
|
value.formula ?? null,
|
|
878
878
|
merged,
|
|
879
879
|
comment,
|
|
@@ -888,7 +888,15 @@ var Normalizer = class {
|
|
|
888
888
|
columnFormat
|
|
889
889
|
);
|
|
890
890
|
}
|
|
891
|
-
|
|
891
|
+
/**
|
|
892
|
+
* `explicit`: the value came from an object cell's `value` key. An explicit
|
|
893
|
+
* string is the caller saying "this text, exactly", so it is never coerced
|
|
894
|
+
* to a number: `{value: "+1"}` stays "+1" and `{value: "007"}` stays "007".
|
|
895
|
+
* Only a date/datetime display format still turns a string into a serial,
|
|
896
|
+
* because that conversion is what the format asks for. Bare values keep
|
|
897
|
+
* numeric-string coercion. Mirrors PHP `Normalizer::coerceValue`.
|
|
898
|
+
*/
|
|
899
|
+
coerceValue(value, format, address = "?", sheetName2, explicit = false) {
|
|
892
900
|
if (value === null || value === void 0) return null;
|
|
893
901
|
const df = format?.displayFormat;
|
|
894
902
|
if (df === "date" || df === "datetime") {
|
|
@@ -896,6 +904,9 @@ var Normalizer = class {
|
|
|
896
904
|
return DateConverter.toSerial(value, df === "datetime");
|
|
897
905
|
}
|
|
898
906
|
}
|
|
907
|
+
if (explicit && typeof value === "string") {
|
|
908
|
+
return value;
|
|
909
|
+
}
|
|
899
910
|
if (typeof value === "string" && isNumericString(value)) {
|
|
900
911
|
return numericStringToNumber(value);
|
|
901
912
|
}
|
|
@@ -1044,13 +1055,46 @@ var ERR_REF = "#REF!";
|
|
|
1044
1055
|
var ERR_NAME = "#NAME?";
|
|
1045
1056
|
var ERR_DIV0 = "#DIV/0!";
|
|
1046
1057
|
var ERR_CIRC = "#CIRC!";
|
|
1058
|
+
var FUNCTIONS = /* @__PURE__ */ new Set([
|
|
1059
|
+
"SUM",
|
|
1060
|
+
"AVERAGE",
|
|
1061
|
+
"AVG",
|
|
1062
|
+
"COUNT",
|
|
1063
|
+
"COUNTA",
|
|
1064
|
+
"MIN",
|
|
1065
|
+
"MAX",
|
|
1066
|
+
"IF",
|
|
1067
|
+
"ROUND",
|
|
1068
|
+
"ABS",
|
|
1069
|
+
"LEN",
|
|
1070
|
+
"UPPER",
|
|
1071
|
+
"LOWER",
|
|
1072
|
+
"CONCAT",
|
|
1073
|
+
"CONCATENATE",
|
|
1074
|
+
"TRUE",
|
|
1075
|
+
"FALSE"
|
|
1076
|
+
]);
|
|
1047
1077
|
var SHEET_QUALIFIER = "('(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!";
|
|
1048
|
-
var LinterError = class extends Error {
|
|
1049
|
-
|
|
1078
|
+
var LinterError = class _LinterError extends Error {
|
|
1079
|
+
/**
|
|
1080
|
+
* @param cause Why a #NAME? arose in THIS formula: "syntax" or "function".
|
|
1081
|
+
* Null for an error that was computed, or inherited from another cell.
|
|
1082
|
+
* @param detail The syntax fault, or the unknown function's name.
|
|
1083
|
+
*/
|
|
1084
|
+
constructor(errorCode, cause = null, detail = "") {
|
|
1050
1085
|
super(errorCode);
|
|
1051
1086
|
this.errorCode = errorCode;
|
|
1087
|
+
this.cause = cause;
|
|
1088
|
+
this.detail = detail;
|
|
1089
|
+
}
|
|
1090
|
+
static syntax(detail) {
|
|
1091
|
+
return new _LinterError(ERR_NAME, "syntax", detail);
|
|
1052
1092
|
}
|
|
1053
1093
|
};
|
|
1094
|
+
var isSpace = (ch) => ch === " " || ch >= " " && ch <= "\r";
|
|
1095
|
+
var isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
1096
|
+
var isAlpha = (ch) => ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z";
|
|
1097
|
+
var isAlnum = (ch) => isAlpha(ch) || isDigit(ch);
|
|
1054
1098
|
var FormulaLinter = class {
|
|
1055
1099
|
constructor() {
|
|
1056
1100
|
this.index = /* @__PURE__ */ new Map();
|
|
@@ -1061,10 +1105,25 @@ var FormulaLinter = class {
|
|
|
1061
1105
|
* `#REF!` rather than a range of blanks.
|
|
1062
1106
|
*/
|
|
1063
1107
|
this.sheetNames = /* @__PURE__ */ new Map();
|
|
1108
|
+
/**
|
|
1109
|
+
* Why a cell's own formula produced #NAME?, keyed `Sheet!A1`: a syntax
|
|
1110
|
+
* error, an unknown function, or a linter fault. A cell with #NAME? and no
|
|
1111
|
+
* entry here inherited it from a cell it references.
|
|
1112
|
+
*/
|
|
1113
|
+
this.nameCauses = /* @__PURE__ */ new Map();
|
|
1114
|
+
/**
|
|
1115
|
+
* Greater than 0 while parsing an IF branch that is not taken. The parser
|
|
1116
|
+
* evaluates as it goes, so a skipped branch is still PARSED (its syntax
|
|
1117
|
+
* errors and unknown functions are reported) but nothing in it is
|
|
1118
|
+
* EVALUATED: no operator runs, no cell is read, and every value is null.
|
|
1119
|
+
*/
|
|
1120
|
+
this.skipping = 0;
|
|
1064
1121
|
}
|
|
1065
1122
|
lint(schema) {
|
|
1066
1123
|
const workbook = new Normalizer().normalize(schema);
|
|
1067
1124
|
this.sheetNames = new Map(workbook.sheets.map((sheet) => [sheet.name.toLowerCase(), sheet.name]));
|
|
1125
|
+
this.nameCauses = /* @__PURE__ */ new Map();
|
|
1126
|
+
this.skipping = 0;
|
|
1068
1127
|
this.index = this.buildIndex(workbook);
|
|
1069
1128
|
this.cache = /* @__PURE__ */ new Map();
|
|
1070
1129
|
const issues = [];
|
|
@@ -1080,7 +1139,7 @@ var FormulaLinter = class {
|
|
|
1080
1139
|
address,
|
|
1081
1140
|
formula: cell.formula,
|
|
1082
1141
|
error: result,
|
|
1083
|
-
hint: this.hint(result, cell.formula, sheet.name)
|
|
1142
|
+
hint: this.hint(result, key, cell.formula, sheet.name)
|
|
1084
1143
|
});
|
|
1085
1144
|
}
|
|
1086
1145
|
}
|
|
@@ -1100,25 +1159,39 @@ var FormulaLinter = class {
|
|
|
1100
1159
|
return typeof v === "string" && v.length > 0 && v[0] === "#";
|
|
1101
1160
|
}
|
|
1102
1161
|
evaluate(formula2, defaultSheet, stack) {
|
|
1162
|
+
const key = stack[stack.length - 1];
|
|
1163
|
+
this.nameCauses.delete(key);
|
|
1103
1164
|
try {
|
|
1104
1165
|
const c = { tokens: this.tokenize(formula2), pos: 0 };
|
|
1105
1166
|
const result = this.parseExpr(c, defaultSheet, stack);
|
|
1106
|
-
if (c.pos < c.tokens.length)
|
|
1167
|
+
if (c.pos < c.tokens.length) {
|
|
1168
|
+
throw LinterError.syntax(`unexpected ${this.describeToken(c.tokens[c.pos])} after a complete expression`);
|
|
1169
|
+
}
|
|
1107
1170
|
return result;
|
|
1108
1171
|
} catch (e) {
|
|
1109
|
-
if (e instanceof LinterError)
|
|
1172
|
+
if (e instanceof LinterError) {
|
|
1173
|
+
if (e.cause !== null) this.nameCauses.set(key, { cause: e.cause, detail: e.detail });
|
|
1174
|
+
return e.errorCode;
|
|
1175
|
+
}
|
|
1176
|
+
this.nameCauses.set(key, { cause: "internal", detail: e instanceof Error ? e.constructor.name : typeof e });
|
|
1110
1177
|
return ERR_NAME;
|
|
1111
1178
|
}
|
|
1112
1179
|
}
|
|
1180
|
+
describeToken(tok) {
|
|
1181
|
+
switch (tok.type) {
|
|
1182
|
+
case "STRING":
|
|
1183
|
+
return 'text "' + tok.value + '"';
|
|
1184
|
+
case "NUMBER":
|
|
1185
|
+
return "number " + tok.value;
|
|
1186
|
+
default:
|
|
1187
|
+
return "'" + tok.value + "'";
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1113
1190
|
// ---- Tokenizer ----
|
|
1114
1191
|
tokenize(src) {
|
|
1115
1192
|
const tokens = [];
|
|
1116
1193
|
let i = 0;
|
|
1117
1194
|
const len = src.length;
|
|
1118
|
-
const isSpace = (ch) => /\s/.test(ch);
|
|
1119
|
-
const isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
1120
|
-
const isAlpha = (ch) => /[A-Za-z]/.test(ch);
|
|
1121
|
-
const isAlnum = (ch) => /[A-Za-z0-9]/.test(ch);
|
|
1122
1195
|
while (i < len) {
|
|
1123
1196
|
const ch = src[i];
|
|
1124
1197
|
if (isSpace(ch)) {
|
|
@@ -1133,10 +1206,21 @@ var FormulaLinter = class {
|
|
|
1133
1206
|
}
|
|
1134
1207
|
if (ch === '"') {
|
|
1135
1208
|
i++;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1209
|
+
let value = "";
|
|
1210
|
+
for (; ; ) {
|
|
1211
|
+
if (i >= len) throw LinterError.syntax('a text literal opens with " and never closes');
|
|
1212
|
+
if (src[i] === '"') {
|
|
1213
|
+
if (i + 1 < len && src[i + 1] === '"') {
|
|
1214
|
+
value += '"';
|
|
1215
|
+
i += 2;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
i++;
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
value += src[i];
|
|
1222
|
+
i++;
|
|
1223
|
+
}
|
|
1140
1224
|
tokens.push({ type: "STRING", value });
|
|
1141
1225
|
continue;
|
|
1142
1226
|
}
|
|
@@ -1144,7 +1228,7 @@ var FormulaLinter = class {
|
|
|
1144
1228
|
const start = i;
|
|
1145
1229
|
i++;
|
|
1146
1230
|
for (; ; ) {
|
|
1147
|
-
if (i >= len) throw
|
|
1231
|
+
if (i >= len) throw LinterError.syntax("a quoted sheet name opens with ' and never closes");
|
|
1148
1232
|
if (src[i] === "'") {
|
|
1149
1233
|
if (i + 1 < len && src[i + 1] === "'") {
|
|
1150
1234
|
i += 2;
|
|
@@ -1177,7 +1261,9 @@ var FormulaLinter = class {
|
|
|
1177
1261
|
i++;
|
|
1178
1262
|
continue;
|
|
1179
1263
|
}
|
|
1180
|
-
throw
|
|
1264
|
+
throw LinterError.syntax(
|
|
1265
|
+
ch.charCodeAt(0) < 128 ? `unexpected character '${ch}'` : "unexpected non-ASCII character outside a text literal"
|
|
1266
|
+
);
|
|
1181
1267
|
}
|
|
1182
1268
|
return tokens;
|
|
1183
1269
|
}
|
|
@@ -1195,7 +1281,7 @@ var FormulaLinter = class {
|
|
|
1195
1281
|
const op = c.tokens[c.pos].value;
|
|
1196
1282
|
c.pos++;
|
|
1197
1283
|
const right = this.parseConcat(c, sheet, stack);
|
|
1198
|
-
left = this.compare(left, right, op);
|
|
1284
|
+
left = this.skipping > 0 ? null : this.compare(left, right, op);
|
|
1199
1285
|
}
|
|
1200
1286
|
return left;
|
|
1201
1287
|
}
|
|
@@ -1204,7 +1290,7 @@ var FormulaLinter = class {
|
|
|
1204
1290
|
while (this.isOp(c, "&")) {
|
|
1205
1291
|
c.pos++;
|
|
1206
1292
|
const right = this.parseArith(c, sheet, stack);
|
|
1207
|
-
left = this.coerceString(left) + this.coerceString(right);
|
|
1293
|
+
left = this.skipping > 0 ? null : this.coerceString(left) + this.coerceString(right);
|
|
1208
1294
|
}
|
|
1209
1295
|
return left;
|
|
1210
1296
|
}
|
|
@@ -1214,6 +1300,10 @@ var FormulaLinter = class {
|
|
|
1214
1300
|
const op = c.tokens[c.pos].value;
|
|
1215
1301
|
c.pos++;
|
|
1216
1302
|
const right = this.parseTerm(c, sheet, stack);
|
|
1303
|
+
if (this.skipping > 0) {
|
|
1304
|
+
left = null;
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1217
1307
|
const a = this.coerceNumber(left);
|
|
1218
1308
|
const b = this.coerceNumber(right);
|
|
1219
1309
|
left = op === "+" ? a + b : a - b;
|
|
@@ -1221,46 +1311,68 @@ var FormulaLinter = class {
|
|
|
1221
1311
|
return left;
|
|
1222
1312
|
}
|
|
1223
1313
|
parseTerm(c, sheet, stack) {
|
|
1224
|
-
let left = this.
|
|
1225
|
-
while (this.isOp(c, "*", "/"
|
|
1314
|
+
let left = this.parsePower(c, sheet, stack);
|
|
1315
|
+
while (this.isOp(c, "*", "/")) {
|
|
1226
1316
|
const op = c.tokens[c.pos].value;
|
|
1227
1317
|
c.pos++;
|
|
1228
|
-
const right = this.
|
|
1318
|
+
const right = this.parsePower(c, sheet, stack);
|
|
1319
|
+
if (this.skipping > 0) {
|
|
1320
|
+
left = null;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1229
1323
|
const a = this.coerceNumber(left);
|
|
1230
1324
|
const b = this.coerceNumber(right);
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
if (b === 0) throw new LinterError(ERR_DIV0);
|
|
1237
|
-
left = a / b;
|
|
1238
|
-
break;
|
|
1239
|
-
case "%":
|
|
1240
|
-
left = a / 100 * b;
|
|
1241
|
-
break;
|
|
1242
|
-
case "^":
|
|
1243
|
-
left = a ** b;
|
|
1244
|
-
break;
|
|
1325
|
+
if (op === "*") {
|
|
1326
|
+
left = a * b;
|
|
1327
|
+
} else {
|
|
1328
|
+
if (b === 0) throw new LinterError(ERR_DIV0);
|
|
1329
|
+
left = a / b;
|
|
1245
1330
|
}
|
|
1246
1331
|
}
|
|
1247
1332
|
return left;
|
|
1248
1333
|
}
|
|
1334
|
+
/**
|
|
1335
|
+
* `^` binds tighter than `*` and `/` (=2*3^2 is 18) and, as in Excel, is
|
|
1336
|
+
* left-associative (=2^3^2 is 64). Negation binds tighter than `^`, so
|
|
1337
|
+
* =-2^2 is 4: Excel's order, not mathematics'.
|
|
1338
|
+
*/
|
|
1339
|
+
parsePower(c, sheet, stack) {
|
|
1340
|
+
let left = this.parseUnary(c, sheet, stack);
|
|
1341
|
+
while (this.isOp(c, "^")) {
|
|
1342
|
+
c.pos++;
|
|
1343
|
+
const right = this.parseUnary(c, sheet, stack);
|
|
1344
|
+
left = this.skipping > 0 ? null : this.coerceNumber(left) ** this.coerceNumber(right);
|
|
1345
|
+
}
|
|
1346
|
+
return left;
|
|
1347
|
+
}
|
|
1249
1348
|
parseUnary(c, sheet, stack) {
|
|
1250
1349
|
if (this.isOp(c, "-")) {
|
|
1251
1350
|
c.pos++;
|
|
1252
1351
|
const v = this.parseUnary(c, sheet, stack);
|
|
1253
|
-
return -this.coerceNumber(v);
|
|
1352
|
+
return this.skipping > 0 ? null : -this.coerceNumber(v);
|
|
1254
1353
|
}
|
|
1255
1354
|
if (this.isOp(c, "+")) {
|
|
1256
1355
|
c.pos++;
|
|
1257
1356
|
return this.parseUnary(c, sheet, stack);
|
|
1258
1357
|
}
|
|
1259
|
-
return this.
|
|
1358
|
+
return this.parsePercent(c, sheet, stack);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Excel's `%` is a POSTFIX operator that divides by 100: `50%` is 0.5 and
|
|
1362
|
+
* `A1%` is A1/100. It binds tighter than `^`, `*` and `/`. There is no
|
|
1363
|
+
* binary `%`, so `10%2` leaves a stray `2` and is a syntax error.
|
|
1364
|
+
*/
|
|
1365
|
+
parsePercent(c, sheet, stack) {
|
|
1366
|
+
let value = this.parsePrimary(c, sheet, stack);
|
|
1367
|
+
while (this.isOp(c, "%")) {
|
|
1368
|
+
c.pos++;
|
|
1369
|
+
value = this.skipping > 0 ? null : this.coerceNumber(value) / 100;
|
|
1370
|
+
}
|
|
1371
|
+
return value;
|
|
1260
1372
|
}
|
|
1261
1373
|
parsePrimary(c, sheet, stack) {
|
|
1262
1374
|
const tok = this.peek(c);
|
|
1263
|
-
if (!tok) throw
|
|
1375
|
+
if (!tok) throw LinterError.syntax("the formula ends where a value was expected");
|
|
1264
1376
|
if (tok.type === "OP" && tok.value === "(") {
|
|
1265
1377
|
c.pos++;
|
|
1266
1378
|
const val = this.parseExpr(c, sheet, stack);
|
|
@@ -1280,15 +1392,19 @@ var FormulaLinter = class {
|
|
|
1280
1392
|
const next2 = c.tokens[c.pos + 2];
|
|
1281
1393
|
if (next && next.type === "OP" && next.value === "!" && next2 && next2.type === "IDENT") {
|
|
1282
1394
|
const sheetName2 = this.sheetNames.get(this.cleanSheetName(tok.value).toLowerCase());
|
|
1283
|
-
if (sheetName2 === void 0) throw new LinterError(ERR_REF);
|
|
1395
|
+
if (sheetName2 === void 0 && this.skipping === 0) throw new LinterError(ERR_REF);
|
|
1284
1396
|
c.pos += 2;
|
|
1285
1397
|
const startTok = c.tokens[c.pos].value;
|
|
1286
1398
|
c.pos++;
|
|
1287
|
-
return this.resolveRefOrRange(startTok, sheetName2, c, stack);
|
|
1399
|
+
return this.resolveRefOrRange(startTok, sheetName2 ?? tok.value, c, stack);
|
|
1400
|
+
}
|
|
1401
|
+
if (tok.type === "SHEET") {
|
|
1402
|
+
throw LinterError.syntax(`the quoted name ${tok.value} is not followed by ! and a cell reference`);
|
|
1288
1403
|
}
|
|
1289
|
-
if (tok.type === "SHEET") throw new LinterError(ERR_NAME);
|
|
1290
1404
|
if (next && next.type === "OP" && next.value === "(") {
|
|
1291
1405
|
const name = tok.value.toUpperCase();
|
|
1406
|
+
if (!FUNCTIONS.has(name)) throw new LinterError(ERR_NAME, "function", name);
|
|
1407
|
+
if (name === "IF") return this.parseIf(c, sheet, stack);
|
|
1292
1408
|
c.pos += 2;
|
|
1293
1409
|
const args = [];
|
|
1294
1410
|
if (!this.isOp(c, ")")) {
|
|
@@ -1302,7 +1418,7 @@ var FormulaLinter = class {
|
|
|
1302
1418
|
}
|
|
1303
1419
|
}
|
|
1304
1420
|
this.expectOp(c, ")");
|
|
1305
|
-
return this.callFunction(name, args);
|
|
1421
|
+
return this.skipping > 0 ? null : this.callFunction(name, args);
|
|
1306
1422
|
}
|
|
1307
1423
|
const upper = tok.value.toUpperCase();
|
|
1308
1424
|
if (upper === "TRUE" || upper === "FALSE") {
|
|
@@ -1312,13 +1428,65 @@ var FormulaLinter = class {
|
|
|
1312
1428
|
c.pos++;
|
|
1313
1429
|
return this.resolveRefOrRange(tok.value, sheet, c, stack);
|
|
1314
1430
|
}
|
|
1315
|
-
throw
|
|
1431
|
+
throw LinterError.syntax(`unexpected ${this.describeToken(tok)} where a value was expected`);
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* IF, evaluated lazily as Excel does: `=IF(A2=0,0,1/A2)` with A2 = 0 is 0,
|
|
1435
|
+
* not #DIV/0!. Every argument is parsed, so a syntax error or an unknown
|
|
1436
|
+
* function anywhere in it is still reported, but only the condition and the
|
|
1437
|
+
* branch it selects are evaluated.
|
|
1438
|
+
*
|
|
1439
|
+
* The order of the verdicts is deliberate. A syntax error anywhere wins,
|
|
1440
|
+
* because Excel refuses a formula it cannot parse and computes nothing.
|
|
1441
|
+
* Then fewer than two arguments is #VALUE!. Then an error in the condition
|
|
1442
|
+
* propagates, and neither branch is evaluated.
|
|
1443
|
+
*/
|
|
1444
|
+
parseIf(c, sheet, stack) {
|
|
1445
|
+
c.pos += 2;
|
|
1446
|
+
const args = [];
|
|
1447
|
+
let take = null;
|
|
1448
|
+
let conditionError = null;
|
|
1449
|
+
if (!this.isOp(c, ")")) {
|
|
1450
|
+
for (; ; ) {
|
|
1451
|
+
const i = args.length;
|
|
1452
|
+
const skip = i > 0 && i !== take;
|
|
1453
|
+
if (skip) this.skipping++;
|
|
1454
|
+
try {
|
|
1455
|
+
args.push(this.parseExpr(c, sheet, stack));
|
|
1456
|
+
} finally {
|
|
1457
|
+
if (skip) this.skipping--;
|
|
1458
|
+
}
|
|
1459
|
+
if (i === 0 && this.skipping === 0) {
|
|
1460
|
+
try {
|
|
1461
|
+
take = this.toLogical(args[0]) ? 1 : 2;
|
|
1462
|
+
} catch (e) {
|
|
1463
|
+
if (!(e instanceof LinterError)) throw e;
|
|
1464
|
+
conditionError = e;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (this.isOp(c, ",")) {
|
|
1468
|
+
c.pos++;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
this.expectOp(c, ")");
|
|
1475
|
+
if (this.skipping > 0) return null;
|
|
1476
|
+
if (args.length < 2) return ERR_VALUE;
|
|
1477
|
+
if (conditionError !== null) throw conditionError;
|
|
1478
|
+
return take === 1 ? args[1] : args[2] ?? false;
|
|
1316
1479
|
}
|
|
1317
1480
|
resolveRefOrRange(startRef, defaultSheet, c, stack) {
|
|
1481
|
+
const next = c.tokens[c.pos + 1];
|
|
1482
|
+
const isRange = this.isOp(c, ":") && !!next && next.type === "IDENT";
|
|
1483
|
+
if (this.skipping > 0) {
|
|
1484
|
+
if (isRange) c.pos += 2;
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1318
1487
|
const cleanStart = this.cleanRef(startRef);
|
|
1319
1488
|
if (cleanStart === null) throw new LinterError(ERR_REF);
|
|
1320
|
-
|
|
1321
|
-
if (this.isOp(c, ":") && next && next.type === "IDENT") {
|
|
1489
|
+
if (isRange) {
|
|
1322
1490
|
const endRef = next.value;
|
|
1323
1491
|
c.pos += 2;
|
|
1324
1492
|
const cleanEnd = this.cleanRef(endRef);
|
|
@@ -1372,7 +1540,10 @@ var FormulaLinter = class {
|
|
|
1372
1540
|
}
|
|
1373
1541
|
expectOp(c, op) {
|
|
1374
1542
|
const t = c.tokens[c.pos];
|
|
1375
|
-
if (!t
|
|
1543
|
+
if (!t) throw LinterError.syntax(`expected '${op}', but the formula ends`);
|
|
1544
|
+
if (t.type !== "OP" || t.value !== op) {
|
|
1545
|
+
throw LinterError.syntax(`expected '${op}', found ${this.describeToken(t)}`);
|
|
1546
|
+
}
|
|
1376
1547
|
c.pos++;
|
|
1377
1548
|
}
|
|
1378
1549
|
// ---- Coercion / comparison / functions ----
|
|
@@ -1417,17 +1588,28 @@ var FormulaLinter = class {
|
|
|
1417
1588
|
return false;
|
|
1418
1589
|
}
|
|
1419
1590
|
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Excel's comparison. An error on either side propagates. Otherwise the
|
|
1593
|
+
* types rank number < text < logical, so ="5">9 is TRUE: numeric TEXT is
|
|
1594
|
+
* text, never a number. Text compares without regard to case. A blank
|
|
1595
|
+
* takes the other side's type (0, "" or FALSE); two blanks are equal.
|
|
1596
|
+
*/
|
|
1420
1597
|
cmpVal(a, b) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
if (
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1598
|
+
if (this.isError(a)) throw new LinterError(a);
|
|
1599
|
+
if (this.isError(b)) throw new LinterError(b);
|
|
1600
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1601
|
+
return compareCodePoints(String(a), String(b));
|
|
1602
|
+
}
|
|
1603
|
+
const x = a ?? blankLike(b);
|
|
1604
|
+
const y = b ?? blankLike(x);
|
|
1605
|
+
const rank = (v) => typeof v === "boolean" ? 2 : typeof v === "string" ? 1 : 0;
|
|
1606
|
+
if (rank(x) !== rank(y)) return rank(x) < rank(y) ? -1 : 1;
|
|
1607
|
+
if (typeof x === "string") {
|
|
1608
|
+
return compareCodePoints(x.toLowerCase(), String(y).toLowerCase());
|
|
1609
|
+
}
|
|
1610
|
+
const p = Number(x);
|
|
1611
|
+
const q = Number(y);
|
|
1612
|
+
return p < q ? -1 : p > q ? 1 : 0;
|
|
1431
1613
|
}
|
|
1432
1614
|
callFunction(name, args) {
|
|
1433
1615
|
const flat = this.flatten(args);
|
|
@@ -1445,10 +1627,8 @@ var FormulaLinter = class {
|
|
|
1445
1627
|
return this.minMax(flat, true);
|
|
1446
1628
|
case "MAX":
|
|
1447
1629
|
return this.minMax(flat, false);
|
|
1448
|
-
case "IF":
|
|
1449
|
-
return this.ifFn(args);
|
|
1450
1630
|
case "ROUND":
|
|
1451
|
-
return
|
|
1631
|
+
return excelRound(this.coerceNumber(args[0] ?? 0), Math.trunc(this.coerceNumber(args[1] ?? 0)));
|
|
1452
1632
|
case "ABS":
|
|
1453
1633
|
return Math.abs(this.coerceNumber(args[0] ?? 0));
|
|
1454
1634
|
case "LEN":
|
|
@@ -1465,7 +1645,7 @@ var FormulaLinter = class {
|
|
|
1465
1645
|
case "FALSE":
|
|
1466
1646
|
return false;
|
|
1467
1647
|
default:
|
|
1468
|
-
|
|
1648
|
+
throw new LinterError(ERR_NAME, "function", name);
|
|
1469
1649
|
}
|
|
1470
1650
|
}
|
|
1471
1651
|
flatten(args) {
|
|
@@ -1493,21 +1673,34 @@ var FormulaLinter = class {
|
|
|
1493
1673
|
const vals = nums.map((v) => this.maybeNum(v));
|
|
1494
1674
|
return min ? Math.min(...vals) : Math.max(...vals);
|
|
1495
1675
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1676
|
+
/**
|
|
1677
|
+
* Excel's reading of an IF condition: an error propagates, a blank is
|
|
1678
|
+
* FALSE, a number is TRUE unless it is 0, and text is TRUE or FALSE only
|
|
1679
|
+
* when it spells one of them (in any case). Any other text, "" included,
|
|
1680
|
+
* is #VALUE!.
|
|
1681
|
+
*/
|
|
1682
|
+
toLogical(v) {
|
|
1683
|
+
if (this.isError(v)) throw new LinterError(v);
|
|
1684
|
+
if (typeof v === "boolean") return v;
|
|
1685
|
+
if (v === null) return false;
|
|
1686
|
+
if (typeof v === "number") return v !== 0;
|
|
1687
|
+
if (typeof v === "string") {
|
|
1688
|
+
const upper = v.toUpperCase();
|
|
1689
|
+
if (upper === "TRUE") return true;
|
|
1690
|
+
if (upper === "FALSE") return false;
|
|
1691
|
+
throw new LinterError(ERR_VALUE);
|
|
1692
|
+
}
|
|
1693
|
+
return true;
|
|
1501
1694
|
}
|
|
1502
1695
|
// ---- Hints ----
|
|
1503
|
-
hint(error2, formula2, sheet) {
|
|
1696
|
+
hint(error2, key, formula2, sheet) {
|
|
1504
1697
|
switch (error2) {
|
|
1505
1698
|
case ERR_VALUE:
|
|
1506
1699
|
return this.hintValue(formula2, sheet);
|
|
1507
1700
|
case ERR_REF:
|
|
1508
1701
|
return this.hintRef(formula2);
|
|
1509
1702
|
case ERR_NAME:
|
|
1510
|
-
return
|
|
1703
|
+
return this.hintName(key, formula2, sheet);
|
|
1511
1704
|
case ERR_DIV0:
|
|
1512
1705
|
return "Division by zero \u2014 the divisor evaluated to 0.";
|
|
1513
1706
|
case ERR_CIRC:
|
|
@@ -1516,6 +1709,39 @@ var FormulaLinter = class {
|
|
|
1516
1709
|
return "Formula evaluation failed.";
|
|
1517
1710
|
}
|
|
1518
1711
|
}
|
|
1712
|
+
/**
|
|
1713
|
+
* #NAME? has three causes in this formula, plus inheritance, and each hint
|
|
1714
|
+
* says which one it is. A syntax error used to be told it "references an
|
|
1715
|
+
* unknown function", and an agent went hunting for a function that was fine.
|
|
1716
|
+
* The texts are byte-identical to PHP's and Python's: see PHP
|
|
1717
|
+
* `FormulaLinter::hintName` for why a syntax error keeps the #NAME? label.
|
|
1718
|
+
*/
|
|
1719
|
+
hintName(key, formula2, sheet) {
|
|
1720
|
+
const cause = this.nameCauses.get(key);
|
|
1721
|
+
switch (cause?.cause) {
|
|
1722
|
+
case "syntax":
|
|
1723
|
+
return `Syntax error: ${cause.detail}. Excel cannot parse this formula. Check for an unbalanced parenthesis or quote, a missing operator or argument, or a character a formula cannot contain. Text goes in double quotes, and a quote inside text is written twice: "a""b".`;
|
|
1724
|
+
case "function":
|
|
1725
|
+
return `Unknown function ${cause.detail}. Holy Sheet supports: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, IF, ROUND, ABS, LEN, UPPER, LOWER, CONCAT.`;
|
|
1726
|
+
case "internal":
|
|
1727
|
+
return `The linter itself failed on this formula (${cause.detail}). That is a holy-sheet defect, not evidence that the formula is wrong.`;
|
|
1728
|
+
default:
|
|
1729
|
+
return this.hintInheritedName(formula2, sheet);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
hintInheritedName(formula2, sheet) {
|
|
1733
|
+
const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
|
|
1734
|
+
const sources = [];
|
|
1735
|
+
for (const m of formula2.matchAll(re)) {
|
|
1736
|
+
const sheetName2 = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
|
|
1737
|
+
const a1 = m[2].toUpperCase() + m[3];
|
|
1738
|
+
if (this.cache.get(sheetName2 + "!" + a1) === ERR_NAME) {
|
|
1739
|
+
const source = m[1] ? m[1] + "!" + a1 : a1;
|
|
1740
|
+
if (!sources.includes(source)) sources.push(source);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return sources.length === 0 ? "This formula depends on a cell whose value is #NAME?. Fix that cell first." : `This formula depends on ${sources.join(", ")}, whose value is #NAME?. Fix that cell first.`;
|
|
1744
|
+
}
|
|
1519
1745
|
hintRef(formula2) {
|
|
1520
1746
|
for (const m of formula2.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
|
|
1521
1747
|
const name = this.cleanSheetName(m[1]);
|
|
@@ -1553,9 +1779,30 @@ var FormulaLinter = class {
|
|
|
1553
1779
|
return "A non-numeric value was used in arithmetic. Check that all referenced cells contain numbers.";
|
|
1554
1780
|
}
|
|
1555
1781
|
};
|
|
1556
|
-
function
|
|
1557
|
-
|
|
1558
|
-
|
|
1782
|
+
function blankLike(other) {
|
|
1783
|
+
if (typeof other === "boolean") return false;
|
|
1784
|
+
if (typeof other === "string") return "";
|
|
1785
|
+
return 0;
|
|
1786
|
+
}
|
|
1787
|
+
function compareCodePoints(a, b) {
|
|
1788
|
+
const x = Array.from(a);
|
|
1789
|
+
const y = Array.from(b);
|
|
1790
|
+
const n = Math.min(x.length, y.length);
|
|
1791
|
+
for (let i = 0; i < n; i++) {
|
|
1792
|
+
const p = x[i].codePointAt(0);
|
|
1793
|
+
const q = y[i].codePointAt(0);
|
|
1794
|
+
if (p !== q) return p < q ? -1 : 1;
|
|
1795
|
+
}
|
|
1796
|
+
return x.length < y.length ? -1 : x.length > y.length ? 1 : 0;
|
|
1797
|
+
}
|
|
1798
|
+
function excelRound(x, n) {
|
|
1799
|
+
if (!Number.isFinite(x)) return x;
|
|
1800
|
+
const [mantissa, exponent] = Math.abs(x).toExponential().split("e");
|
|
1801
|
+
const shifted = Number(`${mantissa}e${Number(exponent) + n}`);
|
|
1802
|
+
if (!Number.isFinite(shifted)) return x;
|
|
1803
|
+
const rounded = Math.round(shifted);
|
|
1804
|
+
const digits = String(rounded);
|
|
1805
|
+
return Math.sign(x) * (digits.includes("e") ? rounded / 10 ** n : Number(`${digits}e${-n}`));
|
|
1559
1806
|
}
|
|
1560
1807
|
|
|
1561
1808
|
// src/schema/repairer.ts
|
|
@@ -6252,7 +6499,7 @@ var holy_sheet_schema_default = {
|
|
|
6252
6499
|
};
|
|
6253
6500
|
|
|
6254
6501
|
// src/agent.ts
|
|
6255
|
-
var VERSION = "2.11.
|
|
6502
|
+
var VERSION = "2.11.2";
|
|
6256
6503
|
function toU8(input) {
|
|
6257
6504
|
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
6258
6505
|
}
|
|
@@ -6824,5 +7071,5 @@ function unencodable(reason) {
|
|
|
6824
7071
|
}
|
|
6825
7072
|
|
|
6826
7073
|
export { Agent, ArchiveRefused, ArrayBuilder, CellAddress, CsvBuilder, FormatSniffer, FormulaLinter, Inference, Normalizer, OdsReader, OfficeTemplateRefused, Repairer, SchemaException, SheetDiff, SheetOpSchema, SheetReducer, SourceArchive, Theme, UnsupportedFormatException, VERSION, Validator, XlsxReader, XlsxWriter, readFileBounded, sourceKind, unzipSync, zipSync };
|
|
6827
|
-
//# sourceMappingURL=chunk-
|
|
6828
|
-
//# sourceMappingURL=chunk-
|
|
7074
|
+
//# sourceMappingURL=chunk-Y32UXRQ6.js.map
|
|
7075
|
+
//# sourceMappingURL=chunk-Y32UXRQ6.js.map
|