@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/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-NQRZFV32.js';
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
- constructor(errorCode) {
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) return ERR_NAME;
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) return e.errorCode;
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
- const start = i;
1155
- while (i < len && src[i] !== '"') i++;
1156
- const value = src.slice(start, i);
1157
- i++;
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 new LinterError(ERR_NAME);
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 new LinterError(ERR_NAME);
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.parseUnary(c, sheet, stack);
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.parseUnary(c, sheet, stack);
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
- switch (op) {
1250
- case "*":
1251
- left = a * b;
1252
- break;
1253
- case "/":
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.parsePrimary(c, sheet, stack);
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 new LinterError(ERR_NAME);
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 new LinterError(ERR_NAME);
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
- const next = c.tokens[c.pos + 1];
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 || t.type !== "OP" || t.value !== op) throw new LinterError(ERR_NAME);
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
- const an = typeof a === "number" || typeof a === "string" && isNumericString(a);
1440
- const bn = typeof b === "number" || typeof b === "string" && isNumericString(b);
1441
- if (an && bn) {
1442
- const x = Number(a);
1443
- const y = Number(b);
1444
- return x < y ? -1 : x > y ? 1 : 0;
1445
- }
1446
- const sa = String(a);
1447
- const sb = String(b);
1448
- return sa < sb ? -1 : sa > sb ? 1 : 0;
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 phpRound(this.coerceNumber(args[0] ?? 0), Math.trunc(this.coerceNumber(args[1] ?? 0)));
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
- return ERR_NAME;
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
- ifFn(args) {
1515
- if (args.length < 2) return ERR_VALUE;
1516
- const cond = args[0];
1517
- const truthy = typeof cond === "boolean" ? cond : cond !== null && cond !== 0 && cond !== "" && cond !== "FALSE";
1518
- return truthy ? args[1] : args[2] ?? false;
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 "The formula references an unknown function or has a syntax error. Holy Sheet supports: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, IF, ROUND, ABS, LEN, UPPER, LOWER, CONCAT.";
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 phpRound(x, n) {
1575
- const factor = 10 ** n;
1576
- return Math.sign(x) * Math.round(Math.abs(x) * factor) / factor;
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.1";
6927
+ var VERSION = "2.11.2";
6692
6928
  function toU8(input) {
6693
6929
  return input instanceof Uint8Array ? input : new Uint8Array(input);
6694
6930
  }