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