@particle-academy/holy-sheet 2.11.1 → 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 +56 -0
- package/dist/{chunk-NQRZFV32.js → chunk-Y32UXRQ6.js} +308 -72
- package/dist/chunk-Y32UXRQ6.js.map +1 -0
- package/dist/cli.cjs +306 -70
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +306 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -3
- package/dist/index.d.ts +62 -3
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-NQRZFV32.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -1066,13 +1066,46 @@ var ERR_REF = "#REF!";
|
|
|
1066
1066
|
var ERR_NAME = "#NAME?";
|
|
1067
1067
|
var ERR_DIV0 = "#DIV/0!";
|
|
1068
1068
|
var ERR_CIRC = "#CIRC!";
|
|
1069
|
+
var FUNCTIONS = /* @__PURE__ */ new Set([
|
|
1070
|
+
"SUM",
|
|
1071
|
+
"AVERAGE",
|
|
1072
|
+
"AVG",
|
|
1073
|
+
"COUNT",
|
|
1074
|
+
"COUNTA",
|
|
1075
|
+
"MIN",
|
|
1076
|
+
"MAX",
|
|
1077
|
+
"IF",
|
|
1078
|
+
"ROUND",
|
|
1079
|
+
"ABS",
|
|
1080
|
+
"LEN",
|
|
1081
|
+
"UPPER",
|
|
1082
|
+
"LOWER",
|
|
1083
|
+
"CONCAT",
|
|
1084
|
+
"CONCATENATE",
|
|
1085
|
+
"TRUE",
|
|
1086
|
+
"FALSE"
|
|
1087
|
+
]);
|
|
1069
1088
|
var SHEET_QUALIFIER = "('(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!";
|
|
1070
|
-
var LinterError = class extends Error {
|
|
1071
|
-
|
|
1089
|
+
var LinterError = class _LinterError extends Error {
|
|
1090
|
+
/**
|
|
1091
|
+
* @param cause Why a #NAME? arose in THIS formula: "syntax" or "function".
|
|
1092
|
+
* Null for an error that was computed, or inherited from another cell.
|
|
1093
|
+
* @param detail The syntax fault, or the unknown function's name.
|
|
1094
|
+
*/
|
|
1095
|
+
constructor(errorCode, cause = null, detail = "") {
|
|
1072
1096
|
super(errorCode);
|
|
1073
1097
|
this.errorCode = errorCode;
|
|
1098
|
+
this.cause = cause;
|
|
1099
|
+
this.detail = detail;
|
|
1100
|
+
}
|
|
1101
|
+
static syntax(detail) {
|
|
1102
|
+
return new _LinterError(ERR_NAME, "syntax", detail);
|
|
1074
1103
|
}
|
|
1075
1104
|
};
|
|
1105
|
+
var isSpace = (ch) => ch === " " || ch >= " " && ch <= "\r";
|
|
1106
|
+
var isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
1107
|
+
var isAlpha = (ch) => ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z";
|
|
1108
|
+
var isAlnum = (ch) => isAlpha(ch) || isDigit(ch);
|
|
1076
1109
|
var FormulaLinter = class {
|
|
1077
1110
|
constructor() {
|
|
1078
1111
|
this.index = /* @__PURE__ */ new Map();
|
|
@@ -1083,10 +1116,25 @@ var FormulaLinter = class {
|
|
|
1083
1116
|
* `#REF!` rather than a range of blanks.
|
|
1084
1117
|
*/
|
|
1085
1118
|
this.sheetNames = /* @__PURE__ */ new Map();
|
|
1119
|
+
/**
|
|
1120
|
+
* Why a cell's own formula produced #NAME?, keyed `Sheet!A1`: a syntax
|
|
1121
|
+
* error, an unknown function, or a linter fault. A cell with #NAME? and no
|
|
1122
|
+
* entry here inherited it from a cell it references.
|
|
1123
|
+
*/
|
|
1124
|
+
this.nameCauses = /* @__PURE__ */ new Map();
|
|
1125
|
+
/**
|
|
1126
|
+
* Greater than 0 while parsing an IF branch that is not taken. The parser
|
|
1127
|
+
* evaluates as it goes, so a skipped branch is still PARSED (its syntax
|
|
1128
|
+
* errors and unknown functions are reported) but nothing in it is
|
|
1129
|
+
* EVALUATED: no operator runs, no cell is read, and every value is null.
|
|
1130
|
+
*/
|
|
1131
|
+
this.skipping = 0;
|
|
1086
1132
|
}
|
|
1087
1133
|
lint(schema) {
|
|
1088
1134
|
const workbook = new Normalizer().normalize(schema);
|
|
1089
1135
|
this.sheetNames = new Map(workbook.sheets.map((sheet) => [sheet.name.toLowerCase(), sheet.name]));
|
|
1136
|
+
this.nameCauses = /* @__PURE__ */ new Map();
|
|
1137
|
+
this.skipping = 0;
|
|
1090
1138
|
this.index = this.buildIndex(workbook);
|
|
1091
1139
|
this.cache = /* @__PURE__ */ new Map();
|
|
1092
1140
|
const issues = [];
|
|
@@ -1102,7 +1150,7 @@ var FormulaLinter = class {
|
|
|
1102
1150
|
address,
|
|
1103
1151
|
formula: cell.formula,
|
|
1104
1152
|
error: result,
|
|
1105
|
-
hint: this.hint(result, cell.formula, sheet.name)
|
|
1153
|
+
hint: this.hint(result, key, cell.formula, sheet.name)
|
|
1106
1154
|
});
|
|
1107
1155
|
}
|
|
1108
1156
|
}
|
|
@@ -1122,25 +1170,39 @@ var FormulaLinter = class {
|
|
|
1122
1170
|
return typeof v === "string" && v.length > 0 && v[0] === "#";
|
|
1123
1171
|
}
|
|
1124
1172
|
evaluate(formula2, defaultSheet, stack) {
|
|
1173
|
+
const key = stack[stack.length - 1];
|
|
1174
|
+
this.nameCauses.delete(key);
|
|
1125
1175
|
try {
|
|
1126
1176
|
const c = { tokens: this.tokenize(formula2), pos: 0 };
|
|
1127
1177
|
const result = this.parseExpr(c, defaultSheet, stack);
|
|
1128
|
-
if (c.pos < c.tokens.length)
|
|
1178
|
+
if (c.pos < c.tokens.length) {
|
|
1179
|
+
throw LinterError.syntax(`unexpected ${this.describeToken(c.tokens[c.pos])} after a complete expression`);
|
|
1180
|
+
}
|
|
1129
1181
|
return result;
|
|
1130
1182
|
} catch (e) {
|
|
1131
|
-
if (e instanceof LinterError)
|
|
1183
|
+
if (e instanceof LinterError) {
|
|
1184
|
+
if (e.cause !== null) this.nameCauses.set(key, { cause: e.cause, detail: e.detail });
|
|
1185
|
+
return e.errorCode;
|
|
1186
|
+
}
|
|
1187
|
+
this.nameCauses.set(key, { cause: "internal", detail: e instanceof Error ? e.constructor.name : typeof e });
|
|
1132
1188
|
return ERR_NAME;
|
|
1133
1189
|
}
|
|
1134
1190
|
}
|
|
1191
|
+
describeToken(tok) {
|
|
1192
|
+
switch (tok.type) {
|
|
1193
|
+
case "STRING":
|
|
1194
|
+
return 'text "' + tok.value + '"';
|
|
1195
|
+
case "NUMBER":
|
|
1196
|
+
return "number " + tok.value;
|
|
1197
|
+
default:
|
|
1198
|
+
return "'" + tok.value + "'";
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1135
1201
|
// ---- Tokenizer ----
|
|
1136
1202
|
tokenize(src) {
|
|
1137
1203
|
const tokens = [];
|
|
1138
1204
|
let i = 0;
|
|
1139
1205
|
const len = src.length;
|
|
1140
|
-
const isSpace = (ch) => /\s/.test(ch);
|
|
1141
|
-
const isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
1142
|
-
const isAlpha = (ch) => /[A-Za-z]/.test(ch);
|
|
1143
|
-
const isAlnum = (ch) => /[A-Za-z0-9]/.test(ch);
|
|
1144
1206
|
while (i < len) {
|
|
1145
1207
|
const ch = src[i];
|
|
1146
1208
|
if (isSpace(ch)) {
|
|
@@ -1155,10 +1217,21 @@ var FormulaLinter = class {
|
|
|
1155
1217
|
}
|
|
1156
1218
|
if (ch === '"') {
|
|
1157
1219
|
i++;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1220
|
+
let value = "";
|
|
1221
|
+
for (; ; ) {
|
|
1222
|
+
if (i >= len) throw LinterError.syntax('a text literal opens with " and never closes');
|
|
1223
|
+
if (src[i] === '"') {
|
|
1224
|
+
if (i + 1 < len && src[i + 1] === '"') {
|
|
1225
|
+
value += '"';
|
|
1226
|
+
i += 2;
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
i++;
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
value += src[i];
|
|
1233
|
+
i++;
|
|
1234
|
+
}
|
|
1162
1235
|
tokens.push({ type: "STRING", value });
|
|
1163
1236
|
continue;
|
|
1164
1237
|
}
|
|
@@ -1166,7 +1239,7 @@ var FormulaLinter = class {
|
|
|
1166
1239
|
const start = i;
|
|
1167
1240
|
i++;
|
|
1168
1241
|
for (; ; ) {
|
|
1169
|
-
if (i >= len) throw
|
|
1242
|
+
if (i >= len) throw LinterError.syntax("a quoted sheet name opens with ' and never closes");
|
|
1170
1243
|
if (src[i] === "'") {
|
|
1171
1244
|
if (i + 1 < len && src[i + 1] === "'") {
|
|
1172
1245
|
i += 2;
|
|
@@ -1199,7 +1272,9 @@ var FormulaLinter = class {
|
|
|
1199
1272
|
i++;
|
|
1200
1273
|
continue;
|
|
1201
1274
|
}
|
|
1202
|
-
throw
|
|
1275
|
+
throw LinterError.syntax(
|
|
1276
|
+
ch.charCodeAt(0) < 128 ? `unexpected character '${ch}'` : "unexpected non-ASCII character outside a text literal"
|
|
1277
|
+
);
|
|
1203
1278
|
}
|
|
1204
1279
|
return tokens;
|
|
1205
1280
|
}
|
|
@@ -1217,7 +1292,7 @@ var FormulaLinter = class {
|
|
|
1217
1292
|
const op = c.tokens[c.pos].value;
|
|
1218
1293
|
c.pos++;
|
|
1219
1294
|
const right = this.parseConcat(c, sheet, stack);
|
|
1220
|
-
left = this.compare(left, right, op);
|
|
1295
|
+
left = this.skipping > 0 ? null : this.compare(left, right, op);
|
|
1221
1296
|
}
|
|
1222
1297
|
return left;
|
|
1223
1298
|
}
|
|
@@ -1226,7 +1301,7 @@ var FormulaLinter = class {
|
|
|
1226
1301
|
while (this.isOp(c, "&")) {
|
|
1227
1302
|
c.pos++;
|
|
1228
1303
|
const right = this.parseArith(c, sheet, stack);
|
|
1229
|
-
left = this.coerceString(left) + this.coerceString(right);
|
|
1304
|
+
left = this.skipping > 0 ? null : this.coerceString(left) + this.coerceString(right);
|
|
1230
1305
|
}
|
|
1231
1306
|
return left;
|
|
1232
1307
|
}
|
|
@@ -1236,6 +1311,10 @@ var FormulaLinter = class {
|
|
|
1236
1311
|
const op = c.tokens[c.pos].value;
|
|
1237
1312
|
c.pos++;
|
|
1238
1313
|
const right = this.parseTerm(c, sheet, stack);
|
|
1314
|
+
if (this.skipping > 0) {
|
|
1315
|
+
left = null;
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1239
1318
|
const a = this.coerceNumber(left);
|
|
1240
1319
|
const b = this.coerceNumber(right);
|
|
1241
1320
|
left = op === "+" ? a + b : a - b;
|
|
@@ -1243,46 +1322,68 @@ var FormulaLinter = class {
|
|
|
1243
1322
|
return left;
|
|
1244
1323
|
}
|
|
1245
1324
|
parseTerm(c, sheet, stack) {
|
|
1246
|
-
let left = this.
|
|
1247
|
-
while (this.isOp(c, "*", "/"
|
|
1325
|
+
let left = this.parsePower(c, sheet, stack);
|
|
1326
|
+
while (this.isOp(c, "*", "/")) {
|
|
1248
1327
|
const op = c.tokens[c.pos].value;
|
|
1249
1328
|
c.pos++;
|
|
1250
|
-
const right = this.
|
|
1329
|
+
const right = this.parsePower(c, sheet, stack);
|
|
1330
|
+
if (this.skipping > 0) {
|
|
1331
|
+
left = null;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1251
1334
|
const a = this.coerceNumber(left);
|
|
1252
1335
|
const b = this.coerceNumber(right);
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
if (b === 0) throw new LinterError(ERR_DIV0);
|
|
1259
|
-
left = a / b;
|
|
1260
|
-
break;
|
|
1261
|
-
case "%":
|
|
1262
|
-
left = a / 100 * b;
|
|
1263
|
-
break;
|
|
1264
|
-
case "^":
|
|
1265
|
-
left = a ** b;
|
|
1266
|
-
break;
|
|
1336
|
+
if (op === "*") {
|
|
1337
|
+
left = a * b;
|
|
1338
|
+
} else {
|
|
1339
|
+
if (b === 0) throw new LinterError(ERR_DIV0);
|
|
1340
|
+
left = a / b;
|
|
1267
1341
|
}
|
|
1268
1342
|
}
|
|
1269
1343
|
return left;
|
|
1270
1344
|
}
|
|
1345
|
+
/**
|
|
1346
|
+
* `^` binds tighter than `*` and `/` (=2*3^2 is 18) and, as in Excel, is
|
|
1347
|
+
* left-associative (=2^3^2 is 64). Negation binds tighter than `^`, so
|
|
1348
|
+
* =-2^2 is 4: Excel's order, not mathematics'.
|
|
1349
|
+
*/
|
|
1350
|
+
parsePower(c, sheet, stack) {
|
|
1351
|
+
let left = this.parseUnary(c, sheet, stack);
|
|
1352
|
+
while (this.isOp(c, "^")) {
|
|
1353
|
+
c.pos++;
|
|
1354
|
+
const right = this.parseUnary(c, sheet, stack);
|
|
1355
|
+
left = this.skipping > 0 ? null : this.coerceNumber(left) ** this.coerceNumber(right);
|
|
1356
|
+
}
|
|
1357
|
+
return left;
|
|
1358
|
+
}
|
|
1271
1359
|
parseUnary(c, sheet, stack) {
|
|
1272
1360
|
if (this.isOp(c, "-")) {
|
|
1273
1361
|
c.pos++;
|
|
1274
1362
|
const v = this.parseUnary(c, sheet, stack);
|
|
1275
|
-
return -this.coerceNumber(v);
|
|
1363
|
+
return this.skipping > 0 ? null : -this.coerceNumber(v);
|
|
1276
1364
|
}
|
|
1277
1365
|
if (this.isOp(c, "+")) {
|
|
1278
1366
|
c.pos++;
|
|
1279
1367
|
return this.parseUnary(c, sheet, stack);
|
|
1280
1368
|
}
|
|
1281
|
-
return this.
|
|
1369
|
+
return this.parsePercent(c, sheet, stack);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Excel's `%` is a POSTFIX operator that divides by 100: `50%` is 0.5 and
|
|
1373
|
+
* `A1%` is A1/100. It binds tighter than `^`, `*` and `/`. There is no
|
|
1374
|
+
* binary `%`, so `10%2` leaves a stray `2` and is a syntax error.
|
|
1375
|
+
*/
|
|
1376
|
+
parsePercent(c, sheet, stack) {
|
|
1377
|
+
let value = this.parsePrimary(c, sheet, stack);
|
|
1378
|
+
while (this.isOp(c, "%")) {
|
|
1379
|
+
c.pos++;
|
|
1380
|
+
value = this.skipping > 0 ? null : this.coerceNumber(value) / 100;
|
|
1381
|
+
}
|
|
1382
|
+
return value;
|
|
1282
1383
|
}
|
|
1283
1384
|
parsePrimary(c, sheet, stack) {
|
|
1284
1385
|
const tok = this.peek(c);
|
|
1285
|
-
if (!tok) throw
|
|
1386
|
+
if (!tok) throw LinterError.syntax("the formula ends where a value was expected");
|
|
1286
1387
|
if (tok.type === "OP" && tok.value === "(") {
|
|
1287
1388
|
c.pos++;
|
|
1288
1389
|
const val = this.parseExpr(c, sheet, stack);
|
|
@@ -1302,15 +1403,19 @@ var FormulaLinter = class {
|
|
|
1302
1403
|
const next2 = c.tokens[c.pos + 2];
|
|
1303
1404
|
if (next && next.type === "OP" && next.value === "!" && next2 && next2.type === "IDENT") {
|
|
1304
1405
|
const sheetName2 = this.sheetNames.get(this.cleanSheetName(tok.value).toLowerCase());
|
|
1305
|
-
if (sheetName2 === void 0) throw new LinterError(ERR_REF);
|
|
1406
|
+
if (sheetName2 === void 0 && this.skipping === 0) throw new LinterError(ERR_REF);
|
|
1306
1407
|
c.pos += 2;
|
|
1307
1408
|
const startTok = c.tokens[c.pos].value;
|
|
1308
1409
|
c.pos++;
|
|
1309
|
-
return this.resolveRefOrRange(startTok, sheetName2, c, stack);
|
|
1410
|
+
return this.resolveRefOrRange(startTok, sheetName2 ?? tok.value, c, stack);
|
|
1411
|
+
}
|
|
1412
|
+
if (tok.type === "SHEET") {
|
|
1413
|
+
throw LinterError.syntax(`the quoted name ${tok.value} is not followed by ! and a cell reference`);
|
|
1310
1414
|
}
|
|
1311
|
-
if (tok.type === "SHEET") throw new LinterError(ERR_NAME);
|
|
1312
1415
|
if (next && next.type === "OP" && next.value === "(") {
|
|
1313
1416
|
const name = tok.value.toUpperCase();
|
|
1417
|
+
if (!FUNCTIONS.has(name)) throw new LinterError(ERR_NAME, "function", name);
|
|
1418
|
+
if (name === "IF") return this.parseIf(c, sheet, stack);
|
|
1314
1419
|
c.pos += 2;
|
|
1315
1420
|
const args = [];
|
|
1316
1421
|
if (!this.isOp(c, ")")) {
|
|
@@ -1324,7 +1429,7 @@ var FormulaLinter = class {
|
|
|
1324
1429
|
}
|
|
1325
1430
|
}
|
|
1326
1431
|
this.expectOp(c, ")");
|
|
1327
|
-
return this.callFunction(name, args);
|
|
1432
|
+
return this.skipping > 0 ? null : this.callFunction(name, args);
|
|
1328
1433
|
}
|
|
1329
1434
|
const upper = tok.value.toUpperCase();
|
|
1330
1435
|
if (upper === "TRUE" || upper === "FALSE") {
|
|
@@ -1334,13 +1439,65 @@ var FormulaLinter = class {
|
|
|
1334
1439
|
c.pos++;
|
|
1335
1440
|
return this.resolveRefOrRange(tok.value, sheet, c, stack);
|
|
1336
1441
|
}
|
|
1337
|
-
throw
|
|
1442
|
+
throw LinterError.syntax(`unexpected ${this.describeToken(tok)} where a value was expected`);
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* IF, evaluated lazily as Excel does: `=IF(A2=0,0,1/A2)` with A2 = 0 is 0,
|
|
1446
|
+
* not #DIV/0!. Every argument is parsed, so a syntax error or an unknown
|
|
1447
|
+
* function anywhere in it is still reported, but only the condition and the
|
|
1448
|
+
* branch it selects are evaluated.
|
|
1449
|
+
*
|
|
1450
|
+
* The order of the verdicts is deliberate. A syntax error anywhere wins,
|
|
1451
|
+
* because Excel refuses a formula it cannot parse and computes nothing.
|
|
1452
|
+
* Then fewer than two arguments is #VALUE!. Then an error in the condition
|
|
1453
|
+
* propagates, and neither branch is evaluated.
|
|
1454
|
+
*/
|
|
1455
|
+
parseIf(c, sheet, stack) {
|
|
1456
|
+
c.pos += 2;
|
|
1457
|
+
const args = [];
|
|
1458
|
+
let take = null;
|
|
1459
|
+
let conditionError = null;
|
|
1460
|
+
if (!this.isOp(c, ")")) {
|
|
1461
|
+
for (; ; ) {
|
|
1462
|
+
const i = args.length;
|
|
1463
|
+
const skip = i > 0 && i !== take;
|
|
1464
|
+
if (skip) this.skipping++;
|
|
1465
|
+
try {
|
|
1466
|
+
args.push(this.parseExpr(c, sheet, stack));
|
|
1467
|
+
} finally {
|
|
1468
|
+
if (skip) this.skipping--;
|
|
1469
|
+
}
|
|
1470
|
+
if (i === 0 && this.skipping === 0) {
|
|
1471
|
+
try {
|
|
1472
|
+
take = this.toLogical(args[0]) ? 1 : 2;
|
|
1473
|
+
} catch (e) {
|
|
1474
|
+
if (!(e instanceof LinterError)) throw e;
|
|
1475
|
+
conditionError = e;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (this.isOp(c, ",")) {
|
|
1479
|
+
c.pos++;
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
this.expectOp(c, ")");
|
|
1486
|
+
if (this.skipping > 0) return null;
|
|
1487
|
+
if (args.length < 2) return ERR_VALUE;
|
|
1488
|
+
if (conditionError !== null) throw conditionError;
|
|
1489
|
+
return take === 1 ? args[1] : args[2] ?? false;
|
|
1338
1490
|
}
|
|
1339
1491
|
resolveRefOrRange(startRef, defaultSheet, c, stack) {
|
|
1492
|
+
const next = c.tokens[c.pos + 1];
|
|
1493
|
+
const isRange = this.isOp(c, ":") && !!next && next.type === "IDENT";
|
|
1494
|
+
if (this.skipping > 0) {
|
|
1495
|
+
if (isRange) c.pos += 2;
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1340
1498
|
const cleanStart = this.cleanRef(startRef);
|
|
1341
1499
|
if (cleanStart === null) throw new LinterError(ERR_REF);
|
|
1342
|
-
|
|
1343
|
-
if (this.isOp(c, ":") && next && next.type === "IDENT") {
|
|
1500
|
+
if (isRange) {
|
|
1344
1501
|
const endRef = next.value;
|
|
1345
1502
|
c.pos += 2;
|
|
1346
1503
|
const cleanEnd = this.cleanRef(endRef);
|
|
@@ -1394,7 +1551,10 @@ var FormulaLinter = class {
|
|
|
1394
1551
|
}
|
|
1395
1552
|
expectOp(c, op) {
|
|
1396
1553
|
const t = c.tokens[c.pos];
|
|
1397
|
-
if (!t
|
|
1554
|
+
if (!t) throw LinterError.syntax(`expected '${op}', but the formula ends`);
|
|
1555
|
+
if (t.type !== "OP" || t.value !== op) {
|
|
1556
|
+
throw LinterError.syntax(`expected '${op}', found ${this.describeToken(t)}`);
|
|
1557
|
+
}
|
|
1398
1558
|
c.pos++;
|
|
1399
1559
|
}
|
|
1400
1560
|
// ---- Coercion / comparison / functions ----
|
|
@@ -1439,17 +1599,28 @@ var FormulaLinter = class {
|
|
|
1439
1599
|
return false;
|
|
1440
1600
|
}
|
|
1441
1601
|
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Excel's comparison. An error on either side propagates. Otherwise the
|
|
1604
|
+
* types rank number < text < logical, so ="5">9 is TRUE: numeric TEXT is
|
|
1605
|
+
* text, never a number. Text compares without regard to case. A blank
|
|
1606
|
+
* takes the other side's type (0, "" or FALSE); two blanks are equal.
|
|
1607
|
+
*/
|
|
1442
1608
|
cmpVal(a, b) {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
if (
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
|
|
1609
|
+
if (this.isError(a)) throw new LinterError(a);
|
|
1610
|
+
if (this.isError(b)) throw new LinterError(b);
|
|
1611
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1612
|
+
return compareCodePoints(String(a), String(b));
|
|
1613
|
+
}
|
|
1614
|
+
const x = a ?? blankLike(b);
|
|
1615
|
+
const y = b ?? blankLike(x);
|
|
1616
|
+
const rank = (v) => typeof v === "boolean" ? 2 : typeof v === "string" ? 1 : 0;
|
|
1617
|
+
if (rank(x) !== rank(y)) return rank(x) < rank(y) ? -1 : 1;
|
|
1618
|
+
if (typeof x === "string") {
|
|
1619
|
+
return compareCodePoints(x.toLowerCase(), String(y).toLowerCase());
|
|
1620
|
+
}
|
|
1621
|
+
const p = Number(x);
|
|
1622
|
+
const q = Number(y);
|
|
1623
|
+
return p < q ? -1 : p > q ? 1 : 0;
|
|
1453
1624
|
}
|
|
1454
1625
|
callFunction(name, args) {
|
|
1455
1626
|
const flat = this.flatten(args);
|
|
@@ -1467,10 +1638,8 @@ var FormulaLinter = class {
|
|
|
1467
1638
|
return this.minMax(flat, true);
|
|
1468
1639
|
case "MAX":
|
|
1469
1640
|
return this.minMax(flat, false);
|
|
1470
|
-
case "IF":
|
|
1471
|
-
return this.ifFn(args);
|
|
1472
1641
|
case "ROUND":
|
|
1473
|
-
return
|
|
1642
|
+
return excelRound(this.coerceNumber(args[0] ?? 0), Math.trunc(this.coerceNumber(args[1] ?? 0)));
|
|
1474
1643
|
case "ABS":
|
|
1475
1644
|
return Math.abs(this.coerceNumber(args[0] ?? 0));
|
|
1476
1645
|
case "LEN":
|
|
@@ -1487,7 +1656,7 @@ var FormulaLinter = class {
|
|
|
1487
1656
|
case "FALSE":
|
|
1488
1657
|
return false;
|
|
1489
1658
|
default:
|
|
1490
|
-
|
|
1659
|
+
throw new LinterError(ERR_NAME, "function", name);
|
|
1491
1660
|
}
|
|
1492
1661
|
}
|
|
1493
1662
|
flatten(args) {
|
|
@@ -1515,21 +1684,34 @@ var FormulaLinter = class {
|
|
|
1515
1684
|
const vals = nums.map((v) => this.maybeNum(v));
|
|
1516
1685
|
return min ? Math.min(...vals) : Math.max(...vals);
|
|
1517
1686
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1687
|
+
/**
|
|
1688
|
+
* Excel's reading of an IF condition: an error propagates, a blank is
|
|
1689
|
+
* FALSE, a number is TRUE unless it is 0, and text is TRUE or FALSE only
|
|
1690
|
+
* when it spells one of them (in any case). Any other text, "" included,
|
|
1691
|
+
* is #VALUE!.
|
|
1692
|
+
*/
|
|
1693
|
+
toLogical(v) {
|
|
1694
|
+
if (this.isError(v)) throw new LinterError(v);
|
|
1695
|
+
if (typeof v === "boolean") return v;
|
|
1696
|
+
if (v === null) return false;
|
|
1697
|
+
if (typeof v === "number") return v !== 0;
|
|
1698
|
+
if (typeof v === "string") {
|
|
1699
|
+
const upper = v.toUpperCase();
|
|
1700
|
+
if (upper === "TRUE") return true;
|
|
1701
|
+
if (upper === "FALSE") return false;
|
|
1702
|
+
throw new LinterError(ERR_VALUE);
|
|
1703
|
+
}
|
|
1704
|
+
return true;
|
|
1523
1705
|
}
|
|
1524
1706
|
// ---- Hints ----
|
|
1525
|
-
hint(error2, formula2, sheet) {
|
|
1707
|
+
hint(error2, key, formula2, sheet) {
|
|
1526
1708
|
switch (error2) {
|
|
1527
1709
|
case ERR_VALUE:
|
|
1528
1710
|
return this.hintValue(formula2, sheet);
|
|
1529
1711
|
case ERR_REF:
|
|
1530
1712
|
return this.hintRef(formula2);
|
|
1531
1713
|
case ERR_NAME:
|
|
1532
|
-
return
|
|
1714
|
+
return this.hintName(key, formula2, sheet);
|
|
1533
1715
|
case ERR_DIV0:
|
|
1534
1716
|
return "Division by zero \u2014 the divisor evaluated to 0.";
|
|
1535
1717
|
case ERR_CIRC:
|
|
@@ -1538,6 +1720,39 @@ var FormulaLinter = class {
|
|
|
1538
1720
|
return "Formula evaluation failed.";
|
|
1539
1721
|
}
|
|
1540
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* #NAME? has three causes in this formula, plus inheritance, and each hint
|
|
1725
|
+
* says which one it is. A syntax error used to be told it "references an
|
|
1726
|
+
* unknown function", and an agent went hunting for a function that was fine.
|
|
1727
|
+
* The texts are byte-identical to PHP's and Python's: see PHP
|
|
1728
|
+
* `FormulaLinter::hintName` for why a syntax error keeps the #NAME? label.
|
|
1729
|
+
*/
|
|
1730
|
+
hintName(key, formula2, sheet) {
|
|
1731
|
+
const cause = this.nameCauses.get(key);
|
|
1732
|
+
switch (cause?.cause) {
|
|
1733
|
+
case "syntax":
|
|
1734
|
+
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".`;
|
|
1735
|
+
case "function":
|
|
1736
|
+
return `Unknown function ${cause.detail}. Holy Sheet supports: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, IF, ROUND, ABS, LEN, UPPER, LOWER, CONCAT.`;
|
|
1737
|
+
case "internal":
|
|
1738
|
+
return `The linter itself failed on this formula (${cause.detail}). That is a holy-sheet defect, not evidence that the formula is wrong.`;
|
|
1739
|
+
default:
|
|
1740
|
+
return this.hintInheritedName(formula2, sheet);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
hintInheritedName(formula2, sheet) {
|
|
1744
|
+
const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
|
|
1745
|
+
const sources = [];
|
|
1746
|
+
for (const m of formula2.matchAll(re)) {
|
|
1747
|
+
const sheetName2 = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
|
|
1748
|
+
const a1 = m[2].toUpperCase() + m[3];
|
|
1749
|
+
if (this.cache.get(sheetName2 + "!" + a1) === ERR_NAME) {
|
|
1750
|
+
const source = m[1] ? m[1] + "!" + a1 : a1;
|
|
1751
|
+
if (!sources.includes(source)) sources.push(source);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
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.`;
|
|
1755
|
+
}
|
|
1541
1756
|
hintRef(formula2) {
|
|
1542
1757
|
for (const m of formula2.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
|
|
1543
1758
|
const name = this.cleanSheetName(m[1]);
|
|
@@ -1575,9 +1790,30 @@ var FormulaLinter = class {
|
|
|
1575
1790
|
return "A non-numeric value was used in arithmetic. Check that all referenced cells contain numbers.";
|
|
1576
1791
|
}
|
|
1577
1792
|
};
|
|
1578
|
-
function
|
|
1579
|
-
|
|
1580
|
-
|
|
1793
|
+
function blankLike(other) {
|
|
1794
|
+
if (typeof other === "boolean") return false;
|
|
1795
|
+
if (typeof other === "string") return "";
|
|
1796
|
+
return 0;
|
|
1797
|
+
}
|
|
1798
|
+
function compareCodePoints(a, b) {
|
|
1799
|
+
const x = Array.from(a);
|
|
1800
|
+
const y = Array.from(b);
|
|
1801
|
+
const n = Math.min(x.length, y.length);
|
|
1802
|
+
for (let i = 0; i < n; i++) {
|
|
1803
|
+
const p = x[i].codePointAt(0);
|
|
1804
|
+
const q = y[i].codePointAt(0);
|
|
1805
|
+
if (p !== q) return p < q ? -1 : 1;
|
|
1806
|
+
}
|
|
1807
|
+
return x.length < y.length ? -1 : x.length > y.length ? 1 : 0;
|
|
1808
|
+
}
|
|
1809
|
+
function excelRound(x, n) {
|
|
1810
|
+
if (!Number.isFinite(x)) return x;
|
|
1811
|
+
const [mantissa, exponent] = Math.abs(x).toExponential().split("e");
|
|
1812
|
+
const shifted = Number(`${mantissa}e${Number(exponent) + n}`);
|
|
1813
|
+
if (!Number.isFinite(shifted)) return x;
|
|
1814
|
+
const rounded = Math.round(shifted);
|
|
1815
|
+
const digits = String(rounded);
|
|
1816
|
+
return Math.sign(x) * (digits.includes("e") ? rounded / 10 ** n : Number(`${digits}e${-n}`));
|
|
1581
1817
|
}
|
|
1582
1818
|
|
|
1583
1819
|
// src/schema/repairer.ts
|
|
@@ -6692,7 +6928,7 @@ var holy_sheet_schema_default = {
|
|
|
6692
6928
|
};
|
|
6693
6929
|
|
|
6694
6930
|
// src/agent.ts
|
|
6695
|
-
var VERSION = "2.11.
|
|
6931
|
+
var VERSION = "2.11.2";
|
|
6696
6932
|
function toU8(input) {
|
|
6697
6933
|
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
6698
6934
|
}
|