sdocs-dev 1.15.0 → 1.19.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.
@@ -6,17 +6,17 @@
6
6
  // text starts with "=") against a grid of other cells, and recalc() resolves
7
7
  // a whole model at once with cycle detection.
8
8
  //
9
- // Supported, deliberately small but useful:
9
+ // Supported core syntax:
10
10
  // numbers 12, 3.5, -2, 1e3
11
- // operators + - * / ^ % and unary minus, with parentheses
12
- // cell references A1, B12 (column letters + 1-based row, like the UI)
11
+ // text / booleans "text", TRUE, FALSE, concatenation with &
12
+ // operators + - * / ^ % and comparisons, with parentheses
13
+ // cell references A1, $A1, A$1, $A$1 and quoted sheet names
13
14
  // ranges A1:B3 (only inside a function's arguments)
14
- // functions SUM AVERAGE/AVG MIN MAX COUNT COUNTA PRODUCT
15
- // ROUND ABS IF
16
- // comparisons = <> < <= > >= (mainly for IF)
15
+ // functions aggregates, math, logic, text, conditional aggregates,
16
+ // lookup, date, and common finance functions
17
17
  //
18
- // Values: a referenced empty cell is 0 in arithmetic; referenced text is an
19
- // error (#VALUE!) in arithmetic but is counted by COUNTA and ignored by SUM.
18
+ // Values: a referenced empty cell is 0 in arithmetic. Text, booleans, and
19
+ // errors stay typed through calculation and Excel export.
20
20
  // Anything that goes wrong yields an error string (#VALUE!, #DIV/0!, #NAME?,
21
21
  // #REF!, #CIRC!) which the renderer shows in the cell, just like a real sheet.
22
22
  (function (exports) {
@@ -49,29 +49,123 @@
49
49
 
50
50
  function mkErr(code) { var e = new Error(code); e.isFormulaError = true; e.code = code; return e; }
51
51
 
52
+ // A formula is safe to export as live Excel code only when every node in
53
+ // its fully parsed tree is part of this computational grammar. Evaluation
54
+ // is not proof: IF is lazy, so an unchosen branch may contain a function
55
+ // that never ran locally but would run later in Excel.
56
+ var SAFE_FUNCTIONS = {
57
+ SUM: 1, PRODUCT: 1, MIN: 1, MAX: 1, AVERAGE: 1, AVG: 1,
58
+ COUNT: 1, COUNTA: 1, ROUND: 1, ROUNDUP: 1, ROUNDDOWN: 1,
59
+ ABS: 1, INT: 1, MOD: 1, POWER: 1, SQRT: 1, SIGN: 1,
60
+ CEILING: 1, FLOOR: 1, IF: 1, IFERROR: 1, IFNA: 1,
61
+ AND: 1, OR: 1, NOT: 1,
62
+ CONCAT: 1, CONCATENATE: 1, LEFT: 1, RIGHT: 1, MID: 1,
63
+ LEN: 1, LOWER: 1, UPPER: 1, TRIM: 1, VALUE: 1,
64
+ COUNTIF: 1, SUMIF: 1, AVERAGEIF: 1,
65
+ INDEX: 1, MATCH: 1, VLOOKUP: 1, XLOOKUP: 1,
66
+ DATE: 1, YEAR: 1, MONTH: 1, DAY: 1, TODAY: 1,
67
+ NPV: 1, PMT: 1,
68
+ };
69
+
70
+ function isSafeAst(node) {
71
+ if (!node || typeof node !== 'object') return false;
72
+ if (node.k === 'num' || node.k === 'str' || node.k === 'bool' || node.k === 'ref' || node.k === 'range' || node.k === 'err') return true;
73
+ if (node.k === 'unary' || node.k === 'percent') return isSafeAst(node.a);
74
+ if (node.k === 'bin' || node.k === 'cmp' || node.k === 'concat') return isSafeAst(node.a) && isSafeAst(node.b);
75
+ if (node.k === 'call') {
76
+ if (!SAFE_FUNCTIONS[node.name]) return false;
77
+ for (var i = 0; i < node.args.length; i++) if (!isSafeAst(node.args[i])) return false;
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+
52
83
  // ── Tokenizer ────────────────────────────────────────────
53
84
  function tokenize(src) {
54
85
  var toks = [];
55
86
  var i = 0, n = src.length;
87
+ // Excel stores at most 8,192 characters in one formula. Matching that
88
+ // limit also bounds tokenizer and parser work for untrusted documents.
89
+ if (n > 8192) throw mkErr('#VALUE!');
56
90
  function isDigit(c) { return c >= '0' && c <= '9'; }
57
91
  function isAlpha(c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); }
92
+ function isNameStart(c) { return isAlpha(c) || c === '_'; }
93
+ function isNamePart(c) { return isAlpha(c) || isDigit(c) || c === '_' || c === '.'; }
94
+ function readRef(at) {
95
+ var j = at, absCol = false, absRow = false, letters = '', digits = '';
96
+ if (src[j] === '$') { absCol = true; j++; }
97
+ while (j < n && isAlpha(src[j])) letters += src[j++];
98
+ if (!letters) return null;
99
+ if (src[j] === '$') { absRow = true; j++; }
100
+ while (j < n && isDigit(src[j])) digits += src[j++];
101
+ if (!digits) return null;
102
+ var col = colIndex(letters.toUpperCase()), row = parseInt(digits, 10) - 1;
103
+ // Excel's grid ends at XFD1048576. Reject invalid addresses before a
104
+ // range can turn into unbounded work.
105
+ if (col < 0 || col > 16383 || row < 0 || row > 1048575) throw mkErr('#REF!');
106
+ return { end: j, col: col, row: row, absCol: absCol, absRow: absRow };
107
+ }
58
108
  while (i < n) {
59
109
  var c = src[i];
60
110
  if (c === ' ' || c === '\t') { i++; continue; }
61
- if (isDigit(c) || (c === '.' && isDigit(src[i + 1]))) {
62
- var num = '';
63
- while (i < n && (isDigit(src[i]) || src[i] === '.')) num += src[i++];
64
- if (i < n && (src[i] === 'e' || src[i] === 'E')) {
65
- num += src[i++];
66
- if (src[i] === '+' || src[i] === '-') num += src[i++];
67
- while (i < n && isDigit(src[i])) num += src[i++];
111
+ if (c === '"') {
112
+ var str = ''; i++;
113
+ var closed = false;
114
+ while (i < n) {
115
+ if (src[i] === '"') {
116
+ if (src[i + 1] === '"') { str += '"'; i += 2; continue; }
117
+ i++; closed = true; break;
118
+ }
119
+ str += src[i++];
68
120
  }
69
- toks.push({ t: 'num', v: parseFloat(num) });
121
+ if (!closed) throw mkErr('#VALUE!');
122
+ toks.push({ t: 'str', v: str });
70
123
  continue;
71
124
  }
72
- if (isAlpha(c)) {
125
+ if (c === "'") {
126
+ var quoted = ''; i++;
127
+ var qclosed = false;
128
+ while (i < n) {
129
+ if (src[i] === "'") {
130
+ if (src[i + 1] === "'") { quoted += "'"; i += 2; continue; }
131
+ i++; qclosed = true; break;
132
+ }
133
+ quoted += src[i++];
134
+ }
135
+ if (!qclosed || src[i] !== '!') throw mkErr('#REF!');
136
+ i++;
137
+ var qref = readRef(i);
138
+ if (!qref) throw mkErr('#REF!');
139
+ toks.push({ t: 'ref', sheet: quoted, col: qref.col, row: qref.row,
140
+ absCol: qref.absCol, absRow: qref.absRow });
141
+ i = qref.end;
142
+ continue;
143
+ }
144
+ if (isDigit(c) || (c === '.' && isDigit(src[i + 1]))) {
145
+ var nm = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/.exec(src.slice(i));
146
+ if (!nm) throw mkErr('#VALUE!');
147
+ toks.push({ t: 'num', v: Number(nm[0]) });
148
+ i += nm[0].length;
149
+ continue;
150
+ }
151
+ if (c === '$') {
152
+ var dollarRef = readRef(i);
153
+ if (!dollarRef) throw mkErr('#REF!');
154
+ toks.push({ t: 'ref', col: dollarRef.col, row: dollarRef.row,
155
+ absCol: dollarRef.absCol, absRow: dollarRef.absRow });
156
+ i = dollarRef.end;
157
+ continue;
158
+ }
159
+ if (isNameStart(c)) {
160
+ var leadingRef = readRef(i);
161
+ if (leadingRef && src[leadingRef.end] !== '!') {
162
+ toks.push({ t: 'ref', col: leadingRef.col, row: leadingRef.row,
163
+ absCol: leadingRef.absCol, absRow: leadingRef.absRow });
164
+ i = leadingRef.end;
165
+ continue;
166
+ }
73
167
  var word = '';
74
- while (i < n && (isAlpha(src[i]) || isDigit(src[i]))) word += src[i++];
168
+ while (i < n && isNamePart(src[i])) word += src[i++];
75
169
  // A sheet-qualified reference: Sheet!A1. The word before '!' is the
76
170
  // sheet name (any letters/digits run - Sales, Summary, Sheet1, Q1);
77
171
  // the part after '!' must be a plain cell ref. Emitted as ONE ref
@@ -80,16 +174,18 @@
80
174
  if (src[i] === '!') {
81
175
  var sheet = word;
82
176
  i++; // consume '!'
83
- var cellWord = '';
84
- while (i < n && (isAlpha(src[i]) || isDigit(src[i]))) cellWord += src[i++];
85
- var cm = /^([A-Za-z]+)([0-9]+)$/.exec(cellWord);
86
- if (!cm) throw mkErr('#REF!');
87
- toks.push({ t: 'ref', sheet: sheet, col: colIndex(cm[1].toUpperCase()), row: parseInt(cm[2], 10) - 1 });
177
+ var sheetRef = readRef(i);
178
+ if (!sheetRef) throw mkErr('#REF!');
179
+ toks.push({ t: 'ref', sheet: sheet, col: sheetRef.col, row: sheetRef.row,
180
+ absCol: sheetRef.absCol, absRow: sheetRef.absRow });
181
+ i = sheetRef.end;
88
182
  continue;
89
183
  }
90
184
  var m = /^([A-Za-z]+)([0-9]+)$/.exec(word);
91
185
  if (m && src[i] !== '(') {
92
- toks.push({ t: 'ref', col: colIndex(m[1].toUpperCase()), row: parseInt(m[2], 10) - 1 });
186
+ var plainCol = colIndex(m[1].toUpperCase()), plainRow = parseInt(m[2], 10) - 1;
187
+ if (plainCol > 16383 || plainRow < 0 || plainRow > 1048575) throw mkErr('#REF!');
188
+ toks.push({ t: 'ref', col: plainCol, row: plainRow, absCol: false, absRow: false });
93
189
  } else {
94
190
  toks.push({ t: 'name', v: word.toUpperCase() });
95
191
  }
@@ -101,10 +197,16 @@
101
197
  toks.push({ t: 'op', v: op });
102
198
  continue;
103
199
  }
104
- if ('+-*/^%(),:='.indexOf(c) !== -1) { toks.push({ t: 'op', v: c }); i++; continue; }
105
- // A literal #REF! (left behind by shiftFormula when a reference was
106
- // pushed off the sheet) evaluates to that error.
107
- if (c === '#') throw mkErr(src.slice(i, i + 5).toUpperCase() === '#REF!' ? '#REF!' : '#NAME?');
200
+ if ('+-*/^%(),:=&'.indexOf(c) !== -1) { toks.push({ t: 'op', v: c }); i++; continue; }
201
+ // Error literals are tokens, not early throws. The parser must consume
202
+ // the complete formula so export vetting sees every later branch.
203
+ if (c === '#') {
204
+ var em = /^(#REF!|#DIV\/0!|#VALUE!|#NAME\?|#N\/A|#NUM!|#NULL!|#CIRC!)/i.exec(src.slice(i));
205
+ if (!em) throw mkErr('#NAME?');
206
+ toks.push({ t: 'err', v: em[1].toUpperCase() });
207
+ i += em[1].length;
208
+ continue;
209
+ }
108
210
  throw mkErr('#NAME?');
109
211
  }
110
212
  return toks;
@@ -118,14 +220,21 @@
118
220
  function expectOp(v) { var t = next(); if (!t || t.t !== 'op' || t.v !== v) throw mkErr('#VALUE!'); }
119
221
 
120
222
  function parseCompare() {
121
- var left = parseAdd();
223
+ var left = parseConcat();
122
224
  var t = peek();
123
225
  if (t && t.t === 'op' && ['=', '<>', '<', '<=', '>', '>='].indexOf(t.v) !== -1) {
124
226
  next();
125
- return { k: 'cmp', op: t.v, a: left, b: parseAdd() };
227
+ return { k: 'cmp', op: t.v, a: left, b: parseConcat() };
126
228
  }
127
229
  return left;
128
230
  }
231
+ function parseConcat() {
232
+ var node = parseAdd();
233
+ while (peek() && peek().t === 'op' && peek().v === '&') {
234
+ next(); node = { k: 'concat', a: node, b: parseAdd() };
235
+ }
236
+ return node;
237
+ }
129
238
  function parseAdd() {
130
239
  var node = parseMul();
131
240
  while (peek() && peek().t === 'op' && (peek().v === '+' || peek().v === '-')) {
@@ -163,6 +272,8 @@
163
272
  var t = next();
164
273
  if (!t) throw mkErr('#VALUE!');
165
274
  if (t.t === 'num') return { k: 'num', v: t.v };
275
+ if (t.t === 'str') return { k: 'str', v: t.v };
276
+ if (t.t === 'err') return { k: 'err', code: t.v };
166
277
  if (t.t === 'ref') {
167
278
  if (peek() && peek().t === 'op' && peek().v === ':') {
168
279
  next(); var end = next();
@@ -188,8 +299,8 @@
188
299
  expectOp(')');
189
300
  return { k: 'call', name: t.v, args: args };
190
301
  }
191
- if (t.v === 'TRUE') return { k: 'num', v: 1 };
192
- if (t.v === 'FALSE') return { k: 'num', v: 0 };
302
+ if (t.v === 'TRUE') return { k: 'bool', v: true };
303
+ if (t.v === 'FALSE') return { k: 'bool', v: false };
193
304
  throw mkErr('#NAME?');
194
305
  }
195
306
  if (t.t === 'op' && t.v === '(') { var e = parseCompare(); expectOp(')'); return e; }
@@ -205,6 +316,9 @@
205
316
  function evalAst(node, ctx) {
206
317
  switch (node.k) {
207
318
  case 'num': return node.v;
319
+ case 'str': return node.v;
320
+ case 'bool': return node.v;
321
+ case 'err': throw mkErr(node.code);
208
322
  case 'unary': { var v = num(evalAst(node.a, ctx)); return node.op === '-' ? -v : v; }
209
323
  case 'percent': return num(evalAst(node.a, ctx)) / 100;
210
324
  case 'bin': {
@@ -218,8 +332,14 @@
218
332
  }
219
333
  throw mkErr('#VALUE!');
220
334
  }
335
+ case 'concat': return toText(evalAst(node.a, ctx)) + toText(evalAst(node.b, ctx));
221
336
  case 'cmp': {
222
- var x = num(evalAst(node.a, ctx)), y = num(evalAst(node.b, ctx)), r;
337
+ var x = evalAst(node.a, ctx), y = evalAst(node.b, ctx), r;
338
+ if (typeof x === 'string' && typeof y === 'string') {
339
+ x = x.toLowerCase(); y = y.toLowerCase();
340
+ } else if (typeof x !== typeof y) {
341
+ x = num(x); y = num(y);
342
+ }
223
343
  switch (node.op) {
224
344
  case '=': r = x === y; break;
225
345
  case '<>': r = x !== y; break;
@@ -229,7 +349,7 @@
229
349
  case '>=': r = x >= y; break;
230
350
  default: throw mkErr('#VALUE!');
231
351
  }
232
- return r ? 1 : 0;
352
+ return r;
233
353
  }
234
354
  case 'ref': return refValue(ctx.cell(node.col, node.row, node.sheet));
235
355
  case 'range': throw mkErr('#VALUE!');
@@ -241,14 +361,35 @@
241
361
  function refValue(cell) {
242
362
  if (!cell || cell.kind === 'empty') return 0;
243
363
  if (cell.kind === 'number') return cell.value;
364
+ if (cell.kind === 'text') return cell.value;
365
+ if (cell.kind === 'boolean') return cell.value;
244
366
  if (cell.kind === 'error') throw mkErr(cell.code || '#VALUE!');
245
367
  throw mkErr('#VALUE!');
246
368
  }
247
369
 
248
- function num(v) { if (typeof v !== 'number' || !isFinite(v)) throw mkErr('#VALUE!'); return v; }
370
+ function num(v) {
371
+ if (typeof v === 'boolean') return v ? 1 : 0;
372
+ if (typeof v === 'string' && v.trim() !== '' && isFinite(Number(v))) return Number(v);
373
+ if (typeof v !== 'number' || !isFinite(v)) throw mkErr('#VALUE!');
374
+ return v;
375
+ }
376
+
377
+ function toText(v) {
378
+ if (v == null) return '';
379
+ if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE';
380
+ return String(v);
381
+ }
382
+
383
+ function truthy(v) {
384
+ if (typeof v === 'boolean') return v;
385
+ if (typeof v === 'number') return v !== 0;
386
+ if (typeof v === 'string') return v !== '' && v.toUpperCase() !== 'FALSE';
387
+ return false;
388
+ }
249
389
 
250
390
  function argCells(arg, ctx) {
251
391
  if (arg.k === 'range') {
392
+ if (ctx.range) return ctx.range(arg.c0, arg.r0, arg.c1, arg.r1, arg.sheet);
252
393
  var out = [];
253
394
  var r0 = Math.min(arg.r0, arg.r1), r1 = Math.max(arg.r0, arg.r1);
254
395
  var c0 = Math.min(arg.c0, arg.c1), c1 = Math.max(arg.c0, arg.c1);
@@ -257,7 +398,86 @@
257
398
  }
258
399
  return out;
259
400
  }
260
- return [{ kind: 'number', value: num(evalAst(arg, ctx)) }];
401
+ var value = evalAst(arg, ctx);
402
+ if (typeof value === 'number') return [{ kind: 'number', value: value }];
403
+ if (typeof value === 'boolean') return [{ kind: 'boolean', value: value }];
404
+ return [{ kind: 'text', value: toText(value) }];
405
+ }
406
+
407
+ function cellValue(cell) {
408
+ if (!cell || cell.kind === 'empty') return '';
409
+ if (cell.kind === 'error') throw mkErr(cell.code || '#VALUE!');
410
+ return cell.value;
411
+ }
412
+
413
+ function argMatrix(arg, ctx) {
414
+ if (arg.k !== 'range') return [[cellValue(argCells(arg, ctx)[0])]];
415
+ if (ctx.rangeMatrix) return ctx.rangeMatrix(arg.c0, arg.r0, arg.c1, arg.r1, arg.sheet);
416
+ var rows = [], r0 = Math.min(arg.r0, arg.r1), r1 = Math.max(arg.r0, arg.r1);
417
+ var c0 = Math.min(arg.c0, arg.c1), c1 = Math.max(arg.c0, arg.c1);
418
+ for (var r = r0; r <= r1; r++) {
419
+ var row = [];
420
+ for (var c = c0; c <= c1; c++) row.push(cellValue(ctx.cell(c, r, arg.sheet)));
421
+ rows.push(row);
422
+ }
423
+ return rows;
424
+ }
425
+
426
+ function flattenMatrix(matrix) {
427
+ var out = [];
428
+ for (var r = 0; r < matrix.length; r++) {
429
+ for (var c = 0; c < matrix[r].length; c++) out.push(matrix[r][c]);
430
+ }
431
+ return out;
432
+ }
433
+
434
+ function criterionMatches(value, criterion) {
435
+ if (typeof criterion === 'number' || typeof criterion === 'boolean') return value === criterion;
436
+ var s = String(criterion), m = /^(<=|>=|<>|=|<|>)(.*)$/.exec(s);
437
+ var op = m ? m[1] : '=', rhs = m ? m[2] : s;
438
+ var left = value, right = rhs;
439
+ if (rhs.trim() !== '' && isFinite(Number(rhs))) { left = Number(value); right = Number(rhs); }
440
+ if (op === '=' && typeof right === 'string' && /[?*]/.test(right)) {
441
+ var pattern = right.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\?/g, '.').replace(/\*/g, '.*');
442
+ return new RegExp('^' + pattern + '$', 'i').test(String(value));
443
+ }
444
+ if (typeof left === 'string' && typeof right === 'string') {
445
+ left = left.toLowerCase(); right = right.toLowerCase();
446
+ }
447
+ if (op === '=') return left === right;
448
+ if (op === '<>') return left !== right;
449
+ if (op === '<') return left < right;
450
+ if (op === '<=') return left <= right;
451
+ if (op === '>') return left > right;
452
+ return left >= right;
453
+ }
454
+
455
+ function valuesEqual(a, b) {
456
+ if (typeof a === 'string' && typeof b === 'string') return a.toLowerCase() === b.toLowerCase();
457
+ return a === b;
458
+ }
459
+
460
+ function excelRound(value, digits, mode) {
461
+ var factor = Math.pow(10, digits), scaled = (Math.abs(value) + Number.EPSILON) * factor;
462
+ var rounded;
463
+ if (mode === 'up') rounded = Math.ceil(scaled - Number.EPSILON);
464
+ else if (mode === 'down') rounded = Math.floor(scaled + Number.EPSILON);
465
+ else rounded = Math.round(scaled + Number.EPSILON);
466
+ return (value < 0 ? -1 : 1) * rounded / factor;
467
+ }
468
+
469
+ var DAY_MS = 86400000;
470
+ var EXCEL_EPOCH = Date.UTC(1899, 11, 31);
471
+ function dateSerial(year, month, day) {
472
+ var utc = Date.UTC(year, month - 1, day);
473
+ var serial = (utc - EXCEL_EPOCH) / DAY_MS;
474
+ if (utc >= Date.UTC(1900, 2, 1)) serial++;
475
+ return serial;
476
+ }
477
+ function serialDate(serial) {
478
+ var n = num(serial);
479
+ if (n >= 60) n--;
480
+ return new Date(EXCEL_EPOCH + n * DAY_MS);
261
481
  }
262
482
 
263
483
  function callFn(node, ctx) {
@@ -270,17 +490,18 @@
270
490
  var cells = argCells(args[i], ctx);
271
491
  for (var j = 0; j < cells.length; j++) {
272
492
  var cl = cells[j];
273
- if (cl.kind === 'error') { errored = cl.code || '#VALUE!'; }
493
+ if (cl.kind === 'error') { errored = cl.code || '#VALUE!'; counted++; }
274
494
  else if (cl.kind === 'number') { nums.push(cl.value); counted++; }
275
- else if (cl.kind === 'text') { counted++; }
495
+ else if (cl.kind === 'text' || cl.kind === 'boolean') { counted++; }
276
496
  }
277
497
  }
278
- if (errored && name !== 'COUNTA') throw mkErr(errored);
498
+ if (errored && name !== 'COUNTA' && name !== 'COUNT') throw mkErr(errored);
279
499
  if (name === 'COUNT') return nums.length;
280
500
  if (name === 'COUNTA') return counted;
281
501
  if (!nums.length) {
282
502
  if (name === 'SUM') return 0;
283
503
  if (name === 'PRODUCT') return 0;
504
+ if (name === 'MIN' || name === 'MAX') return 0;
284
505
  throw mkErr('#DIV/0!');
285
506
  }
286
507
  if (name === 'SUM') return nums.reduce(function (a, b) { return a + b; }, 0);
@@ -289,22 +510,178 @@
289
510
  if (name === 'MAX') return Math.max.apply(null, nums);
290
511
  return nums.reduce(function (a, b) { return a + b; }, 0) / nums.length; // AVERAGE
291
512
  }
292
- case 'ROUND': {
513
+ case 'ROUND': case 'ROUNDUP': case 'ROUNDDOWN': {
293
514
  if (args.length < 1) throw mkErr('#VALUE!');
294
515
  var x = num(evalAst(args[0], ctx));
295
516
  var d = args.length > 1 ? num(evalAst(args[1], ctx)) : 0;
296
- var f = Math.pow(10, d);
297
- return Math.round(x * f) / f;
517
+ return excelRound(x, d, name === 'ROUNDUP' ? 'up' : (name === 'ROUNDDOWN' ? 'down' : 'round'));
298
518
  }
299
519
  case 'ABS':
300
520
  if (args.length !== 1) throw mkErr('#VALUE!');
301
521
  return Math.abs(num(evalAst(args[0], ctx)));
522
+ case 'INT': return Math.floor(num(evalAst(args[0], ctx)));
523
+ case 'MOD': {
524
+ var modDivisor = num(evalAst(args[1], ctx));
525
+ if (modDivisor === 0) throw mkErr('#DIV/0!');
526
+ return num(evalAst(args[0], ctx)) - modDivisor * Math.floor(num(evalAst(args[0], ctx)) / modDivisor);
527
+ }
528
+ case 'POWER': return Math.pow(num(evalAst(args[0], ctx)), num(evalAst(args[1], ctx)));
529
+ case 'SQRT': {
530
+ var root = num(evalAst(args[0], ctx));
531
+ if (root < 0) throw mkErr('#NUM!');
532
+ return Math.sqrt(root);
533
+ }
534
+ case 'SIGN': { var sign = num(evalAst(args[0], ctx)); return sign === 0 ? 0 : (sign < 0 ? -1 : 1); }
535
+ case 'CEILING': case 'FLOOR': {
536
+ var cv = num(evalAst(args[0], ctx));
537
+ var significance = args.length > 1 ? Math.abs(num(evalAst(args[1], ctx))) : 1;
538
+ if (!significance) return 0;
539
+ return (name === 'CEILING' ? Math.ceil(cv / significance) : Math.floor(cv / significance)) * significance;
540
+ }
302
541
  case 'IF': {
303
542
  if (args.length < 2) throw mkErr('#VALUE!');
304
- var cond = num(evalAst(args[0], ctx));
305
- return cond !== 0 ? evalAst(args[1], ctx)
543
+ var cond = truthy(evalAst(args[0], ctx));
544
+ return cond ? evalAst(args[1], ctx)
306
545
  : (args.length > 2 ? evalAst(args[2], ctx) : 0);
307
546
  }
547
+ case 'IFERROR': case 'IFNA': {
548
+ if (args.length < 2) throw mkErr('#VALUE!');
549
+ try { return evalAst(args[0], ctx); }
550
+ catch (e) {
551
+ if (name === 'IFNA' && (!e || e.code !== '#N/A')) throw e;
552
+ return evalAst(args[1], ctx);
553
+ }
554
+ }
555
+ case 'AND': case 'OR': {
556
+ var bool = name === 'AND';
557
+ var boolError = null;
558
+ for (var ai = 0; ai < args.length; ai++) {
559
+ try {
560
+ var av = truthy(evalAst(args[ai], ctx));
561
+ if (name === 'AND') bool = bool && av;
562
+ else bool = bool || av;
563
+ } catch (e) {
564
+ if (!boolError) boolError = e;
565
+ }
566
+ }
567
+ if (boolError) throw boolError;
568
+ return bool;
569
+ }
570
+ case 'NOT': return !truthy(evalAst(args[0], ctx));
571
+ case 'CONCAT': case 'CONCATENATE': {
572
+ var joined = '';
573
+ for (var ci = 0; ci < args.length; ci++) {
574
+ var concatCells = argCells(args[ci], ctx);
575
+ for (var cj = 0; cj < concatCells.length; cj++) joined += toText(cellValue(concatCells[cj]));
576
+ }
577
+ return joined;
578
+ }
579
+ case 'LEFT': {
580
+ var leftText = toText(evalAst(args[0], ctx));
581
+ var leftCount = args.length > 1 ? num(evalAst(args[1], ctx)) : 1;
582
+ if (leftCount < 0) throw mkErr('#VALUE!');
583
+ return leftText.slice(0, leftCount);
584
+ }
585
+ case 'RIGHT': {
586
+ var rightText = toText(evalAst(args[0], ctx));
587
+ var rightCount = args.length > 1 ? num(evalAst(args[1], ctx)) : 1;
588
+ if (rightCount < 0) throw mkErr('#VALUE!');
589
+ return rightText.slice(Math.max(0, rightText.length - rightCount));
590
+ }
591
+ case 'MID': {
592
+ var midStart = num(evalAst(args[1], ctx)), midCount = num(evalAst(args[2], ctx));
593
+ if (midStart < 1 || midCount < 0) throw mkErr('#VALUE!');
594
+ return toText(evalAst(args[0], ctx)).substr(midStart - 1, midCount);
595
+ }
596
+ case 'LEN': return toText(evalAst(args[0], ctx)).length;
597
+ case 'LOWER': return toText(evalAst(args[0], ctx)).toLowerCase();
598
+ case 'UPPER': return toText(evalAst(args[0], ctx)).toUpperCase();
599
+ case 'TRIM': return toText(evalAst(args[0], ctx)).trim().replace(/\s+/g, ' ');
600
+ case 'VALUE': return num(evalAst(args[0], ctx));
601
+ case 'COUNTIF': case 'SUMIF': case 'AVERAGEIF': {
602
+ if (args.length < 2) throw mkErr('#VALUE!');
603
+ var testValues = flattenMatrix(argMatrix(args[0], ctx));
604
+ var criterion = evalAst(args[1], ctx);
605
+ var sumValues = args.length > 2 ? flattenMatrix(argMatrix(args[2], ctx)) : testValues;
606
+ var matched = 0, numericMatched = 0, total = 0;
607
+ for (var ti = 0; ti < testValues.length; ti++) {
608
+ if (!criterionMatches(testValues[ti], criterion)) continue;
609
+ matched++;
610
+ if (ti < sumValues.length && typeof sumValues[ti] === 'number') {
611
+ total += sumValues[ti]; numericMatched++;
612
+ }
613
+ }
614
+ if (name === 'COUNTIF') return matched;
615
+ if (name === 'AVERAGEIF') {
616
+ if (!numericMatched) throw mkErr('#DIV/0!');
617
+ return total / numericMatched;
618
+ }
619
+ return total;
620
+ }
621
+ case 'INDEX': {
622
+ var indexMatrix = argMatrix(args[0], ctx);
623
+ var indexRow = num(evalAst(args[1], ctx)) - 1;
624
+ var indexCol = args.length > 2 ? num(evalAst(args[2], ctx)) - 1 : 0;
625
+ if (!indexMatrix[indexRow] || indexMatrix[indexRow][indexCol] === undefined) throw mkErr('#REF!');
626
+ return indexMatrix[indexRow][indexCol];
627
+ }
628
+ case 'MATCH': {
629
+ var needle = evalAst(args[0], ctx), matchValues = flattenMatrix(argMatrix(args[1], ctx));
630
+ var matchMode = args.length > 2 ? num(evalAst(args[2], ctx)) : 1;
631
+ var found = -1;
632
+ for (var mi = 0; mi < matchValues.length; mi++) {
633
+ if (valuesEqual(matchValues[mi], needle)) { found = mi; break; }
634
+ if (matchMode === 1 && matchValues[mi] <= needle) found = mi;
635
+ if (matchMode === -1 && found < 0 && matchValues[mi] >= needle) found = mi;
636
+ }
637
+ if (found < 0) throw mkErr('#N/A');
638
+ return found + 1;
639
+ }
640
+ case 'VLOOKUP': {
641
+ var lookup = evalAst(args[0], ctx), table = argMatrix(args[1], ctx);
642
+ var returnCol = num(evalAst(args[2], ctx)) - 1;
643
+ var approximate = args.length < 4 || truthy(evalAst(args[3], ctx));
644
+ var candidate = -1;
645
+ for (var vr = 0; vr < table.length; vr++) {
646
+ if (valuesEqual(table[vr][0], lookup)) { candidate = vr; break; }
647
+ if (approximate && table[vr][0] <= lookup) candidate = vr;
648
+ }
649
+ if (candidate < 0 || !table[candidate] || table[candidate][returnCol] === undefined) throw mkErr('#N/A');
650
+ return table[candidate][returnCol];
651
+ }
652
+ case 'XLOOKUP': {
653
+ var xneedle = evalAst(args[0], ctx);
654
+ var search = flattenMatrix(argMatrix(args[1], ctx));
655
+ var returns = flattenMatrix(argMatrix(args[2], ctx));
656
+ for (var xi = 0; xi < search.length; xi++) if (valuesEqual(search[xi], xneedle)) return returns[xi];
657
+ if (args.length > 3) return evalAst(args[3], ctx);
658
+ throw mkErr('#N/A');
659
+ }
660
+ case 'DATE': return dateSerial(num(evalAst(args[0], ctx)), num(evalAst(args[1], ctx)), num(evalAst(args[2], ctx)));
661
+ case 'YEAR': return serialDate(evalAst(args[0], ctx)).getUTCFullYear();
662
+ case 'MONTH': return serialDate(evalAst(args[0], ctx)).getUTCMonth() + 1;
663
+ case 'DAY': return serialDate(evalAst(args[0], ctx)).getUTCDate();
664
+ case 'TODAY': {
665
+ var now = new Date();
666
+ return dateSerial(now.getUTCFullYear(), now.getUTCMonth() + 1, now.getUTCDate());
667
+ }
668
+ case 'NPV': {
669
+ var rate = num(evalAst(args[0], ctx)), cash = [];
670
+ for (var ni = 1; ni < args.length; ni++) {
671
+ var nc = argCells(args[ni], ctx);
672
+ for (var nj = 0; nj < nc.length; nj++) if (nc[nj].kind === 'number') cash.push(nc[nj].value);
673
+ }
674
+ return cash.reduce(function (sum, value, index) { return sum + value / Math.pow(1 + rate, index + 1); }, 0);
675
+ }
676
+ case 'PMT': {
677
+ var pr = num(evalAst(args[0], ctx)), periods = num(evalAst(args[1], ctx));
678
+ var present = num(evalAst(args[2], ctx));
679
+ var future = args.length > 3 ? num(evalAst(args[3], ctx)) : 0;
680
+ var timing = args.length > 4 ? num(evalAst(args[4], ctx)) : 0;
681
+ if (pr === 0) return -(present + future) / periods;
682
+ var pow = Math.pow(1 + pr, periods);
683
+ return -(pr * (future + pow * present)) / ((1 + pr * timing) * (pow - 1));
684
+ }
308
685
  }
309
686
  throw mkErr('#NAME?');
310
687
  }
@@ -323,30 +700,64 @@
323
700
  var i = 0, n = src.length;
324
701
  function isAlpha(ch) { return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); }
325
702
  function isDigit(ch) { return ch >= '0' && ch <= '9'; }
703
+ function isNameStart(ch) { return isAlpha(ch) || ch === '_'; }
704
+ function isNamePart(ch) { return isAlpha(ch) || isDigit(ch) || ch === '_' || ch === '.'; }
705
+ function readRef(at) {
706
+ var j = at, absCol = false, absRow = false, letters = '', digits = '';
707
+ if (src[j] === '$') { absCol = true; j++; }
708
+ while (j < n && isAlpha(src[j])) letters += src[j++];
709
+ if (!letters) return null;
710
+ if (src[j] === '$') { absRow = true; j++; }
711
+ while (j < n && isDigit(src[j])) digits += src[j++];
712
+ if (!digits) return null;
713
+ return { end: j, letters: letters, row: parseInt(digits, 10) - 1,
714
+ col: colIndex(letters.toUpperCase()), absCol: absCol, absRow: absRow };
715
+ }
326
716
  while (i < n) {
327
717
  var c = src[i];
328
- if (isAlpha(c)) {
329
- var j = i;
330
- while (j < n && (isAlpha(src[j]) || isDigit(src[j]))) j++;
331
- var word = src.slice(i, j);
332
- // A sheet qualifier (Sheet!A1) - emit the name + '!' untouched and let
333
- // the next iteration shift the cell ref that follows. Without this a
334
- // numeric-suffixed sheet name (Q1, Sheet1) would be shifted as if it
335
- // were a cell reference.
336
- if (src[j] === '!') {
337
- out += word + '!';
338
- i = j + 1;
339
- continue;
718
+ if (c === '"') {
719
+ var ds = i++;
720
+ while (i < n) {
721
+ if (src[i] === '"') {
722
+ i++;
723
+ if (src[i] === '"') { i++; continue; }
724
+ break;
725
+ }
726
+ i++;
340
727
  }
341
- var m = /^([A-Za-z]+)([0-9]+)$/.exec(word);
342
- if (m && src[j] !== '(') {
343
- var col = colIndex(m[1].toUpperCase()) + dc;
344
- var row = parseInt(m[2], 10) - 1 + dr;
345
- out += (col < 0 || row < 0) ? '#REF!' : colName(col) + (row + 1);
346
- } else {
347
- out += word;
728
+ out += src.slice(ds, i);
729
+ continue;
730
+ }
731
+ if (c === "'") {
732
+ var qs = i++;
733
+ while (i < n) {
734
+ if (src[i] === "'") {
735
+ i++;
736
+ if (src[i] === "'") { i++; continue; }
737
+ break;
738
+ }
739
+ i++;
348
740
  }
349
- i = j;
741
+ if (src[i] === '!') i++;
742
+ out += src.slice(qs, i);
743
+ continue;
744
+ }
745
+ if (isNameStart(c)) {
746
+ var nameEnd = i + 1;
747
+ while (nameEnd < n && isNamePart(src[nameEnd])) nameEnd++;
748
+ if (src[nameEnd] === '!') {
749
+ out += src.slice(i, nameEnd + 1);
750
+ i = nameEnd + 1;
751
+ continue;
752
+ }
753
+ }
754
+ var ref = (c === '$' || isAlpha(c)) ? readRef(i) : null;
755
+ if (ref && src[ref.end] !== '(') {
756
+ var col = ref.col + (ref.absCol ? 0 : dc);
757
+ var row = ref.row + (ref.absRow ? 0 : dr);
758
+ if (col < 0 || row < 0 || col > 16383 || row > 1048575) out += '#REF!';
759
+ else out += (ref.absCol ? '$' : '') + colName(col) + (ref.absRow ? '$' : '') + (row + 1);
760
+ i = ref.end;
350
761
  continue;
351
762
  }
352
763
  out += c;
@@ -357,14 +768,20 @@
357
768
 
358
769
  // Evaluate one formula string against ctx. Returns { value } or { error }.
359
770
  function evaluate(formula, ctx) {
771
+ var ast;
360
772
  try {
361
773
  var src = formula.charAt(0) === '=' ? formula.slice(1) : formula;
362
- var ast = parse(tokenize(src));
774
+ ast = parse(tokenize(src));
775
+ var safe = isSafeAst(ast);
363
776
  var v = evalAst(ast, ctx);
364
- if (typeof v !== 'number' || !isFinite(v)) return { error: '#VALUE!' };
365
- return { value: v };
777
+ if (typeof v === 'number' && !isFinite(v)) return { error: '#NUM!', safe: safe };
778
+ if (typeof v !== 'number' && typeof v !== 'string' && typeof v !== 'boolean') {
779
+ return { error: '#VALUE!', safe: safe };
780
+ }
781
+ return { value: v, kind: typeof v === 'number' ? 'number'
782
+ : (typeof v === 'boolean' ? 'boolean' : 'text'), safe: safe };
366
783
  } catch (e) {
367
- return { error: e && e.isFormulaError ? e.code : '#VALUE!' };
784
+ return { error: e && e.isFormulaError ? e.code : '#VALUE!', safe: ast ? isSafeAst(ast) : false };
368
785
  }
369
786
  }
370
787
 
@@ -435,22 +852,77 @@
435
852
  var raw = rawAt(si, c, r);
436
853
  if (!isFormula(raw)) return (rRes[c] = baseKind(si, c, r));
437
854
  rState[c] = 1;
855
+ var dependenciesSafe = true;
856
+ function trackDependency(cell) {
857
+ if (cell && cell.safe === false) dependenciesSafe = false;
858
+ return cell;
859
+ }
438
860
  // ctx.cell(col, row, sheet): an undefined `sheet` means the formula's
439
861
  // own sheet (si); a named sheet is looked up in nameToIndex; a name with
440
862
  // no matching sheet is #REF! and never enters resolve.
441
863
  var ctx = {
442
864
  cell: function (cc, rr, sheetName) {
443
- if (sheetName == null) return resolve(si, cc, rr);
865
+ if (sheetName == null) return trackDependency(resolve(si, cc, rr));
444
866
  var key = String(sheetName).toLowerCase();
445
867
  if (!(key in nameToIndex)) return { kind: 'error', code: '#REF!' };
446
- return resolve(nameToIndex[key], cc, rr);
868
+ return trackDependency(resolve(nameToIndex[key], cc, rr));
869
+ },
870
+ // Aggregate functions only need populated cells. Clamp a range to the
871
+ // target model before walking it, so a huge sparse reference does not
872
+ // expand billions of empty coordinates in the browser.
873
+ range: function (c0, r0, c1, r1, sheetName) {
874
+ var target = si;
875
+ if (sheetName != null) {
876
+ var key = String(sheetName).toLowerCase();
877
+ if (!(key in nameToIndex)) throw mkErr('#REF!');
878
+ target = nameToIndex[key];
879
+ }
880
+ var model = sheets[target].model;
881
+ var maxRows = model && model.cells ? model.cells.length : 0;
882
+ var maxCols = model && model.cols ? model.cols : 0;
883
+ if (!maxCols && model && model.cells) {
884
+ for (var mr = 0; mr < model.cells.length; mr++) {
885
+ maxCols = Math.max(maxCols, (model.cells[mr] || []).length);
886
+ }
887
+ }
888
+ var ra = Math.max(0, Math.min(r0, r1));
889
+ var rb = Math.min(maxRows - 1, Math.max(r0, r1));
890
+ var ca = Math.max(0, Math.min(c0, c1));
891
+ var cb = Math.min(maxCols - 1, Math.max(c0, c1));
892
+ var out = [];
893
+ for (var r3 = ra; r3 <= rb; r3++) {
894
+ for (var c3 = ca; c3 <= cb; c3++) out.push(trackDependency(resolve(target, c3, r3)));
895
+ }
896
+ return out;
897
+ },
898
+ rangeMatrix: function (c0, r0, c1, r1, sheetName) {
899
+ var target = si;
900
+ if (sheetName != null) {
901
+ var key = String(sheetName).toLowerCase();
902
+ if (!(key in nameToIndex)) throw mkErr('#REF!');
903
+ target = nameToIndex[key];
904
+ }
905
+ var ra = Math.max(0, Math.min(r0, r1));
906
+ var rb = Math.max(r0, r1);
907
+ var ca = Math.max(0, Math.min(c0, c1));
908
+ var cb = Math.max(c0, c1);
909
+ if ((rb - ra + 1) * (cb - ca + 1) > 100000) throw mkErr('#VALUE!');
910
+ var matrix = [];
911
+ for (var r4 = ra; r4 <= rb; r4++) {
912
+ var matrixRow = [];
913
+ for (var c4 = ca; c4 <= cb; c4++) matrixRow.push(trackDependency(resolve(target, c4, r4)));
914
+ matrix.push(matrixRow);
915
+ }
916
+ return matrix.map(function (row) { return row.map(cellValue); });
447
917
  },
448
918
  };
449
919
  var out = evaluate(raw, ctx);
920
+ out.safe = out.safe === true && dependenciesSafe;
450
921
  rState[c] = 2;
451
- return (rRes[c] = out.error
452
- ? { kind: 'error', code: out.error }
453
- : { kind: 'number', value: out.value });
922
+ if (out.error) return (rRes[c] = { kind: 'error', code: out.error, safe: out.safe });
923
+ var kind = out.kind || (typeof out.value === 'number' ? 'number'
924
+ : (typeof out.value === 'boolean' ? 'boolean' : 'text'));
925
+ return (rRes[c] = { kind: kind, value: out.value, safe: out.safe });
454
926
  }
455
927
 
456
928
  for (var si2 = 0; si2 < n; si2++) {
@@ -478,6 +950,8 @@
478
950
  exports.isFormula = isFormula;
479
951
  exports.tokenize = tokenize;
480
952
  exports.parse = parse;
953
+ exports.isSafeAst = isSafeAst;
954
+ exports.supportedFunctions = Object.keys(SAFE_FUNCTIONS);
481
955
  exports.evaluate = evaluate;
482
956
  exports.recalc = recalc;
483
957
  exports.recalcWorkbook = recalcWorkbook;