mulmoclaude 2.6.0 → 2.8.0
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/client/assets/Graph-ChAxkn9o.js +1 -0
- package/client/assets/{JsonEditor-FKfzzMkt.js → JsonEditor-COQcmjLQ.js} +1 -1
- package/client/assets/dist-Bij44LdC.js +2 -0
- package/client/assets/echarts-GV3U9sVf.js +1 -0
- package/client/assets/echarts-gl-BXx4On0Y.js +4282 -0
- package/client/assets/echarts-wordcloud-_9uQUAfp.js +1 -0
- package/client/assets/emscripten-module-Q67P5WYC-BU3Hlttz.js +2160 -0
- package/client/assets/ffi-4V2b7xu3.js +1 -0
- package/client/assets/{html2canvas-D0m9Y8pD-y1tDAhR3.js → html2canvas-NBfz6Rb9-CIdEBoH2.js} +1 -1
- package/client/assets/{index-gnbD2OHU.js → index-D6xzCZhP.js} +605 -604
- package/client/assets/index-DzMsXDnj.css +2 -0
- package/client/assets/{index.es-HSMDtZ_a-CYtNwG_G.js → index.es-STZX6pYP-BGvt3MI2.js} +3 -3
- package/client/assets/install-C1moNcrn.js +15 -0
- package/client/assets/japan-CyDc-cz7-BgORDJsl.js +1 -0
- package/client/assets/{marp-BVojbGre.js → marp-Du5Sym5b.js} +57 -57
- package/client/assets/module-ES6BEMUI-CDTIpMiA.js +6 -0
- package/client/assets/purify.es-DkTnAgLw-BZxk_wWO.js +3 -0
- package/client/assets/style-BZwVK1dO-BFtilmZh.js +1 -0
- package/client/assets/vue-i18n-YREco4q1.js +3 -0
- package/client/assets/world-R-tMZ4nZ-B1c4T9X2.js +1 -0
- package/client/index.html +6 -4
- package/package.json +13 -12
- package/server/api/routes/plugins.ts +9 -59
- package/server/build/mcp-server.mjs +806 -714
- package/server/index.ts +1 -0
- package/server/plugins/confined-file-ops.ts +56 -0
- package/server/plugins/spreadsheet-builtin.ts +26 -0
- package/server/plugins/spreadsheet-save-outcome.ts +7 -0
- package/server/utils/files/store-resolvers.ts +1 -1
- package/src/lang/de.ts +0 -20
- package/src/lang/en.ts +0 -20
- package/src/lang/es.ts +0 -20
- package/src/lang/fr.ts +0 -20
- package/src/lang/ja.ts +0 -20
- package/src/lang/ko.ts +0 -20
- package/src/lang/pt-BR.ts +0 -20
- package/src/lang/zh.ts +0 -20
- package/src/plugins/spreadsheet/definition.ts +9 -127
- package/src/plugins/spreadsheet/index.ts +7 -4
- package/src/plugins/spreadsheet/meta.ts +2 -4
- package/client/assets/index-BA0fpWIZ.css +0 -2
- package/client/assets/purify.es-BaSjDiWD-Dbm5oOcm.js +0 -3
- package/client/assets/style-CCV5GAnU-D55tgtzm.js +0 -1
- package/client/assets/vue-i18n-2bgWpywp.js +0 -3
- package/server/utils/files/spreadsheet-store.ts +0 -26
- package/src/plugins/spreadsheet/Preview.vue +0 -31
- package/src/plugins/spreadsheet/View.vue +0 -1007
- package/src/plugins/spreadsheet/cellHighlights.ts +0 -78
- package/src/plugins/spreadsheet/engine/calculator.ts +0 -427
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +0 -89
- package/src/plugins/spreadsheet/engine/cellEmpty.ts +0 -26
- package/src/plugins/spreadsheet/engine/cellFormatting.ts +0 -59
- package/src/plugins/spreadsheet/engine/coerce-boolean.ts +0 -26
- package/src/plugins/spreadsheet/engine/condition.ts +0 -182
- package/src/plugins/spreadsheet/engine/date-locale.ts +0 -47
- package/src/plugins/spreadsheet/engine/date-parser.ts +0 -234
- package/src/plugins/spreadsheet/engine/date-utils.ts +0 -73
- package/src/plugins/spreadsheet/engine/datedif.ts +0 -93
- package/src/plugins/spreadsheet/engine/engine.ts +0 -177
- package/src/plugins/spreadsheet/engine/evaluator.ts +0 -494
- package/src/plugins/spreadsheet/engine/financial-math.ts +0 -114
- package/src/plugins/spreadsheet/engine/formatter.ts +0 -158
- package/src/plugins/spreadsheet/engine/formulaError.ts +0 -67
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +0 -202
- package/src/plugins/spreadsheet/engine/functions/date.ts +0 -212
- package/src/plugins/spreadsheet/engine/functions/financial.ts +0 -244
- package/src/plugins/spreadsheet/engine/functions/index.ts +0 -16
- package/src/plugins/spreadsheet/engine/functions/logical.ts +0 -221
- package/src/plugins/spreadsheet/engine/functions/lookup-math.ts +0 -28
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +0 -257
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +0 -283
- package/src/plugins/spreadsheet/engine/functions/statistical-math.ts +0 -91
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +0 -290
- package/src/plugins/spreadsheet/engine/functions/text.ts +0 -398
- package/src/plugins/spreadsheet/engine/index.ts +0 -34
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +0 -111
- package/src/plugins/spreadsheet/engine/math-ops.ts +0 -98
- package/src/plugins/spreadsheet/engine/numericCoercion.ts +0 -61
- package/src/plugins/spreadsheet/engine/parser.ts +0 -40
- package/src/plugins/spreadsheet/engine/registry.ts +0 -201
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +0 -68
- package/src/plugins/spreadsheet/engine/spreadsheet-errors.ts +0 -82
- package/src/plugins/spreadsheet/engine/textFormat.ts +0 -108
- package/src/plugins/spreadsheet/engine/translateFormula.ts +0 -69
- package/src/plugins/spreadsheet/engine/types.ts +0 -81
- package/src/plugins/spreadsheet/keyboardNav.ts +0 -38
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Annuity math for the financial functions, as pure numeric helpers.
|
|
3
|
-
*
|
|
4
|
-
* Excel's cash-flow sign convention: money received is positive, money paid out
|
|
5
|
-
* is negative — so a loan's `pv` is positive and its payment and interest come
|
|
6
|
-
* back negative. Keeping the per-period split as plain number-in / number-out
|
|
7
|
-
* functions lets it be tested against known Excel values without routing string
|
|
8
|
-
* arguments back through the formula evaluator.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { NUM_ERROR, type SpreadsheetError } from "./spreadsheet-errors";
|
|
12
|
-
|
|
13
|
-
/** Future value after `nper` periods. `type` is 0 (end of period) or 1 (begin). */
|
|
14
|
-
export function computeFv(rate: number, nper: number, pmt: number, pv: number, type: number): number {
|
|
15
|
-
if (rate === 0) return -(pv + pmt * nper);
|
|
16
|
-
const growth = Math.pow(1 + rate, nper);
|
|
17
|
-
return -pv * growth - (pmt * (growth - 1) * (1 + rate * type)) / rate;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Constant per-period payment amortising a present value `pv` to `fv`. */
|
|
21
|
-
export function computePmt(rate: number, nper: number, pv: number, fv: number, type: number): number {
|
|
22
|
-
if (rate === 0) return -(fv + pv) / nper;
|
|
23
|
-
const growth = Math.pow(1 + rate, nper);
|
|
24
|
-
return (-rate * (fv + pv * growth)) / ((growth - 1) * (1 + rate * type));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Interest portion of the `per`-th payment. */
|
|
28
|
-
export function computeIpmt(rate: number, per: number, nper: number, pv: number, fv: number, type: number): number {
|
|
29
|
-
const pmt = computePmt(rate, nper, pv, fv, type);
|
|
30
|
-
if (per === 1 && type === 1) return 0;
|
|
31
|
-
const priorPeriods = type === 1 ? per - 2 : per - 1;
|
|
32
|
-
// Interest is the rate applied to the balance outstanding at the start of the
|
|
33
|
-
// period. computeFv already carries the payment-negative sign, so this product
|
|
34
|
-
// is the interest as a (negative) cash outflow — negating it again inverted the
|
|
35
|
-
// sign (IPMT came back +1250 for a -1250 payment) and PPMT amplified the error.
|
|
36
|
-
const interest = computeFv(rate, priorPeriods, pmt, pv, type) * rate;
|
|
37
|
-
return type === 1 ? interest / (1 + rate) : interest;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Principal portion of the `per`-th payment (total payment minus interest). */
|
|
41
|
-
export function computePpmt(rate: number, per: number, nper: number, pv: number, fv: number, type: number): number {
|
|
42
|
-
return computePmt(rate, nper, pv, fv, type) - computeIpmt(rate, per, nper, pv, fv, type);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Present value of an annuity that grows to `fv` after `nper` payments of `pmt`. */
|
|
46
|
-
export function computePv(rate: number, nper: number, pmt: number, fv: number, type: number): number {
|
|
47
|
-
if (rate === 0) return -(fv + pmt * nper);
|
|
48
|
-
const growth = Math.pow(1 + rate, nper);
|
|
49
|
-
return (-fv - (pmt * (growth - 1) * (1 + rate * type)) / rate) / growth;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Number of periods needed to amortise `pv` to `fv` at constant `pmt`. */
|
|
53
|
-
export function computeNper(rate: number, pmt: number, pv: number, fv: number, type: number): number {
|
|
54
|
-
if (rate === 0) return -(fv + pv) / pmt;
|
|
55
|
-
const pmtWithType = pmt * (1 + rate * type);
|
|
56
|
-
return Math.log((pmtWithType - fv * rate) / (pmtWithType + pv * rate)) / Math.log(1 + rate);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Newton-Raphson is iterative; a valid annuity / cash-flow series converges well
|
|
60
|
-
// inside these bounds. A series with no real solution never does, so exhausting
|
|
61
|
-
// the loop means "no answer" — Excel reports #NUM! there, and returning the last
|
|
62
|
-
// iterate instead would hand back a divergent value or NaN as if it were a rate.
|
|
63
|
-
const NEWTON_MAX_ITERATIONS = 100;
|
|
64
|
-
const NEWTON_TOLERANCE = 1e-7;
|
|
65
|
-
|
|
66
|
-
/** Interest rate per period solving the annuity equation, via Newton-Raphson. */
|
|
67
|
-
export function computeRate(nper: number, pmt: number, pv: number, fv: number, type: number, guess: number): number | SpreadsheetError {
|
|
68
|
-
let rate = guess;
|
|
69
|
-
for (let i = 0; i < NEWTON_MAX_ITERATIONS; i++) {
|
|
70
|
-
if (Math.abs(rate) < NEWTON_TOLERANCE) rate = NEWTON_TOLERANCE; // avoid division by zero
|
|
71
|
-
const growth = Math.pow(1 + rate, nper);
|
|
72
|
-
const f = pv * growth + pmt * ((growth - 1) / rate) * (1 + rate * type) + fv;
|
|
73
|
-
const df =
|
|
74
|
-
nper * pv * Math.pow(1 + rate, nper - 1) +
|
|
75
|
-
(pmt * (1 + rate * type) * (nper * Math.pow(1 + rate, nper - 1) * rate - (growth - 1))) / (rate * rate) +
|
|
76
|
-
pmt * type * ((growth - 1) / rate);
|
|
77
|
-
const newRate = rate - f / df;
|
|
78
|
-
if (Math.abs(newRate - rate) < NEWTON_TOLERANCE) return Number.isFinite(newRate) ? newRate : NUM_ERROR;
|
|
79
|
-
rate = newRate;
|
|
80
|
-
}
|
|
81
|
-
return NUM_ERROR;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Net present value of `cashflows`, each discounted one period further into the future. */
|
|
85
|
-
export function computeNpv(rate: number, cashflows: number[]): number {
|
|
86
|
-
return cashflows.reduce((npv, value, index) => npv + value / Math.pow(1 + rate, index + 1), 0);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The present value of `values` (element 0 at period 0) discounted at `rate`,
|
|
90
|
-
* together with its derivative — the pair one Newton-Raphson step needs. */
|
|
91
|
-
function discountedSeries(values: number[], rate: number): { npv: number; dnpv: number } {
|
|
92
|
-
return values.reduce(
|
|
93
|
-
(totals, value, period) => {
|
|
94
|
-
const factor = Math.pow(1 + rate, period);
|
|
95
|
-
return { npv: totals.npv + value / factor, dnpv: totals.dnpv - (period * value) / (factor * (1 + rate)) };
|
|
96
|
-
},
|
|
97
|
-
{ npv: 0, dnpv: 0 },
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Internal rate of return of `values` (element 0 at period 0), via Newton-Raphson. */
|
|
102
|
-
export function computeIrr(values: number[], guess: number): number | SpreadsheetError {
|
|
103
|
-
let rate = guess;
|
|
104
|
-
for (let i = 0; i < NEWTON_MAX_ITERATIONS; i++) {
|
|
105
|
-
const { npv, dnpv } = discountedSeries(values, rate);
|
|
106
|
-
if (Math.abs(npv) < NEWTON_TOLERANCE) return rate;
|
|
107
|
-
// A flat derivative leaves nowhere to step: the series has no root here.
|
|
108
|
-
if (Math.abs(dnpv) < NEWTON_TOLERANCE) return NUM_ERROR;
|
|
109
|
-
const newRate = rate - npv / dnpv;
|
|
110
|
-
if (Math.abs(newRate - rate) < NEWTON_TOLERANCE) return Number.isFinite(newRate) ? newRate : NUM_ERROR;
|
|
111
|
-
rate = newRate;
|
|
112
|
-
}
|
|
113
|
-
return NUM_ERROR;
|
|
114
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Number Formatting Utilities
|
|
3
|
-
*
|
|
4
|
-
* Handles Excel-style format codes for currency, percentages, decimals, dates, etc.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { serialToDate, MONTH_NAMES_SHORT, MONTH_NAMES_FULL, DAY_NAMES_SHORT, DAY_NAMES_FULL } from "./date-utils";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Check if a format code is for dates
|
|
11
|
-
*/
|
|
12
|
-
function isDateFormat(format: string): boolean {
|
|
13
|
-
// Date formats contain date/time tokens: Y, M, D, h, m, s
|
|
14
|
-
// But not percentage (which also has 'm' in format like #,##0)
|
|
15
|
-
// Look for specific date patterns
|
|
16
|
-
return /[YMD]|MMM|DD|YYYY|h:mm|AM\/PM/i.test(format);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Format a number as a date according to Excel format code
|
|
21
|
-
*
|
|
22
|
-
* Supported formats:
|
|
23
|
-
* - MM/DD/YYYY, M/D/YYYY
|
|
24
|
-
* - DD/MM/YYYY, D/M/YYYY
|
|
25
|
-
* - YYYY-MM-DD
|
|
26
|
-
* - DD-MMM-YYYY, D-MMM-YYYY
|
|
27
|
-
* - MMM D, YYYY, MMMM D, YYYY
|
|
28
|
-
* - h:mm AM/PM, HH:mm:ss
|
|
29
|
-
*
|
|
30
|
-
* @param serial - Excel serial number
|
|
31
|
-
* @param format - Date format code
|
|
32
|
-
* @returns Formatted date string
|
|
33
|
-
*/
|
|
34
|
-
// Every token, longest-first within each family so `MMMM` wins over `MMM` and
|
|
35
|
-
// `MM`. Matched in ONE pass: a sequence of `replace` calls re-scans its own
|
|
36
|
-
// output, so an inserted "March" had its `M` rewritten by the later month-number
|
|
37
|
-
// step and "AM/PM" was destroyed before the meridiem branch could see it.
|
|
38
|
-
const DATE_TOKEN_RE = /YYYY|YY|MMMM|MMM|MM|M|dddd|ddd|DD|D|AM\/PM|am\/pm|HH|H|hh|h|mm|ss/g;
|
|
39
|
-
|
|
40
|
-
function formatDate(serial: number, format: string): string {
|
|
41
|
-
const date = serialToDate(serial);
|
|
42
|
-
|
|
43
|
-
const year = date.getUTCFullYear();
|
|
44
|
-
const month = date.getUTCMonth(); // 0-11
|
|
45
|
-
const day = date.getUTCDate();
|
|
46
|
-
const hours = date.getUTCHours();
|
|
47
|
-
const minutes = date.getUTCMinutes();
|
|
48
|
-
const seconds = date.getUTCSeconds();
|
|
49
|
-
const dayOfWeek = date.getUTCDay(); // 0-6
|
|
50
|
-
|
|
51
|
-
// `h` means the 12-hour clock only when the format also asks for a meridiem;
|
|
52
|
-
// decided from the ORIGINAL format, before any substitution.
|
|
53
|
-
const uses12Hour = /AM\/PM|am\/pm/.test(format);
|
|
54
|
-
const hours12 = hours % 12 || 12; // 0 becomes 12
|
|
55
|
-
const isPM = hours >= 12;
|
|
56
|
-
|
|
57
|
-
const replacements: Record<string, string> = {
|
|
58
|
-
YYYY: year.toString(),
|
|
59
|
-
YY: (year % 100).toString().padStart(2, "0"),
|
|
60
|
-
MMMM: MONTH_NAMES_FULL[month] ?? MONTH_NAMES_FULL[0],
|
|
61
|
-
MMM: MONTH_NAMES_SHORT[month] ?? MONTH_NAMES_SHORT[0],
|
|
62
|
-
MM: (month + 1).toString().padStart(2, "0"),
|
|
63
|
-
M: (month + 1).toString(),
|
|
64
|
-
dddd: DAY_NAMES_FULL[dayOfWeek] ?? DAY_NAMES_FULL[0],
|
|
65
|
-
ddd: DAY_NAMES_SHORT[dayOfWeek] ?? DAY_NAMES_SHORT[0],
|
|
66
|
-
DD: day.toString().padStart(2, "0"),
|
|
67
|
-
D: day.toString(),
|
|
68
|
-
"AM/PM": isPM ? "PM" : "AM",
|
|
69
|
-
"am/pm": isPM ? "pm" : "am",
|
|
70
|
-
HH: hours.toString().padStart(2, "0"),
|
|
71
|
-
H: hours.toString(),
|
|
72
|
-
hh: (uses12Hour ? hours12 : hours).toString().padStart(2, "0"),
|
|
73
|
-
h: (uses12Hour ? hours12 : hours).toString(),
|
|
74
|
-
mm: minutes.toString().padStart(2, "0"),
|
|
75
|
-
ss: seconds.toString().padStart(2, "0"),
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
return format.replace(DATE_TOKEN_RE, (token) => replacements[token] ?? token);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const THOUSANDS_GROUP_SIZE = 3;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Insert thousands separators into a run of digits ("1234567" → "1,234,567").
|
|
85
|
-
* Regex free on purpose: the classic `\B(?=(\d{3})+(?!\d))` lookahead
|
|
86
|
-
* backtracks badly on a long run of digits.
|
|
87
|
-
*/
|
|
88
|
-
export const groupThousands = (digits: string): string =>
|
|
89
|
-
Array.from(digits).reduce((grouped, digit, index) => {
|
|
90
|
-
const needsSeparator = index > 0 && (digits.length - index) % THOUSANDS_GROUP_SIZE === 0;
|
|
91
|
-
return needsSeparator ? `${grouped},${digit}` : `${grouped}${digit}`;
|
|
92
|
-
}, "");
|
|
93
|
-
|
|
94
|
-
/** Group the integer part of a formatted number ("1234567.89" → "1,234,567.89"),
|
|
95
|
-
* leaving any fractional part after the decimal point untouched. */
|
|
96
|
-
export const addThousandSeparators = (formatted: string): string => {
|
|
97
|
-
const [whole, ...fraction] = formatted.split(".");
|
|
98
|
-
if (whole === undefined) return formatted;
|
|
99
|
-
return [groupThousands(whole), ...fraction].join(".");
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Format a number according to Excel format code
|
|
104
|
-
*
|
|
105
|
-
* Supported formats:
|
|
106
|
-
* - Currency: $#,##0.00, $#,##0
|
|
107
|
-
* - Percentage: 0.00%, 0.0%
|
|
108
|
-
* - Integer with commas: #,##0
|
|
109
|
-
* - Decimal: 0.00, 0.000
|
|
110
|
-
* - Dates: MM/DD/YYYY, DD-MMM-YYYY, etc.
|
|
111
|
-
*
|
|
112
|
-
* @param value - The numeric value to format
|
|
113
|
-
* @param format - The Excel format code
|
|
114
|
-
* @returns Formatted string representation
|
|
115
|
-
*/
|
|
116
|
-
export function formatNumber(value: number, format: string): string {
|
|
117
|
-
if (!format) return value.toString();
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
// Check if it's a date format
|
|
121
|
-
if (isDateFormat(format)) {
|
|
122
|
-
return formatDate(value, format);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Handle currency formats
|
|
126
|
-
if (format.includes("$")) {
|
|
127
|
-
const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
|
|
128
|
-
const magnitude = Math.abs(value).toFixed(decimals >= 0 ? decimals : 0);
|
|
129
|
-
const grouped = format.includes(",") ? addThousandSeparators(magnitude) : magnitude;
|
|
130
|
-
const withSymbol = "$" + grouped;
|
|
131
|
-
return value < 0 ? "-" + withSymbol : withSymbol;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Handle percentage
|
|
135
|
-
if (format.includes("%")) {
|
|
136
|
-
const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
|
|
137
|
-
return (value * 100).toFixed(decimals >= 0 ? decimals : 2) + "%";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Handle comma separator
|
|
141
|
-
if (format.includes(",")) {
|
|
142
|
-
const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
|
|
143
|
-
const grouped = addThousandSeparators(Math.abs(value).toFixed(decimals >= 0 ? decimals : 0));
|
|
144
|
-
return value < 0 ? "-" + grouped : grouped;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Handle decimal places
|
|
148
|
-
const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
|
|
149
|
-
if (decimals >= 0) {
|
|
150
|
-
return value.toFixed(decimals);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return value.toString();
|
|
154
|
-
} catch (error) {
|
|
155
|
-
console.error("Format error:", error);
|
|
156
|
-
return value.toString();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typed formula-evaluation errors
|
|
3
|
-
*
|
|
4
|
-
* A failed formula must surface as a typed error with an Excel-style error value
|
|
5
|
-
* in the cell, never as a swallowed bare string (issue #2359). This module holds
|
|
6
|
-
* the error taxonomy, the throwable carrier, and the pure helpers that classify a
|
|
7
|
-
* thrown error and propagate an error value across cell references. No engine
|
|
8
|
-
* state is captured — every export is input → output only.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { CalculationError } from "./types";
|
|
12
|
-
import type { SpreadsheetErrorCode } from "./spreadsheet-errors";
|
|
13
|
-
|
|
14
|
-
/** The evaluator-facing error kinds: every `CalculationError["type"]` except
|
|
15
|
-
* `circular`, which the calculator raises directly (not via a thrown error). */
|
|
16
|
-
export type FormulaErrorType = Exclude<CalculationError["type"], "circular">;
|
|
17
|
-
|
|
18
|
-
/** The error code shown for each kind, matching Excel's error literals. */
|
|
19
|
-
export const FORMULA_ERROR_VALUES: Record<FormulaErrorType, SpreadsheetErrorCode> = {
|
|
20
|
-
div_zero: "#DIV/0!",
|
|
21
|
-
invalid_ref: "#REF!",
|
|
22
|
-
syntax: "#NAME?",
|
|
23
|
-
unknown: "#ERROR!",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/** Reverse map for propagation: an error code back to the kind it represents.
|
|
27
|
-
* Codes without a dedicated kind (`#N/A`, `#NUM!`, …) propagate as `unknown`. */
|
|
28
|
-
const ERROR_VALUE_TO_TYPE: Record<string, FormulaErrorType> = {
|
|
29
|
-
[FORMULA_ERROR_VALUES.div_zero]: "div_zero",
|
|
30
|
-
[FORMULA_ERROR_VALUES.invalid_ref]: "invalid_ref",
|
|
31
|
-
[FORMULA_ERROR_VALUES.syntax]: "syntax",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** A recoverable formula failure carrying the kind and the code to show. */
|
|
35
|
-
export class FormulaError extends Error {
|
|
36
|
-
constructor(
|
|
37
|
-
readonly errorType: FormulaErrorType,
|
|
38
|
-
readonly display: SpreadsheetErrorCode,
|
|
39
|
-
message?: string,
|
|
40
|
-
) {
|
|
41
|
-
super(message ?? display);
|
|
42
|
-
this.name = "FormulaError";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const isFormulaError = (error: unknown): error is FormulaError => error instanceof FormulaError;
|
|
47
|
-
|
|
48
|
-
export const divZeroError = (): FormulaError => new FormulaError("div_zero", FORMULA_ERROR_VALUES.div_zero, "Division by zero");
|
|
49
|
-
|
|
50
|
-
export const invalidRefError = (ref: string): FormulaError => new FormulaError("invalid_ref", FORMULA_ERROR_VALUES.invalid_ref, `Invalid reference: ${ref}`);
|
|
51
|
-
|
|
52
|
-
export const nameError = (funcName: string): FormulaError => new FormulaError("syntax", FORMULA_ERROR_VALUES.syntax, `Unknown function: ${funcName}`);
|
|
53
|
-
|
|
54
|
-
export const unknownError = (message?: string): FormulaError => new FormulaError("unknown", FORMULA_ERROR_VALUES.unknown, message);
|
|
55
|
-
|
|
56
|
-
/** The FormulaError that re-raises an error read from a referenced cell or a
|
|
57
|
-
* nested call, so `=A1+1` inherits A1's error instead of corrupting into
|
|
58
|
-
* `"#DIV/0!"+1`. */
|
|
59
|
-
export const propagatedError = (value: SpreadsheetErrorCode): FormulaError =>
|
|
60
|
-
new FormulaError(ERROR_VALUE_TO_TYPE[value] ?? "unknown", value, `Propagated error: ${value}`);
|
|
61
|
-
|
|
62
|
-
/** Map any thrown value onto the typed entry the calculator records. A
|
|
63
|
-
* `FormulaError` keeps its own kind and display; anything else is `unknown`. */
|
|
64
|
-
export const classifyThrownError = (error: unknown): { type: FormulaErrorType; display: SpreadsheetErrorCode } => {
|
|
65
|
-
if (isFormulaError(error)) return { type: error.errorType, display: error.display };
|
|
66
|
-
return { type: "unknown", display: FORMULA_ERROR_VALUES.unknown };
|
|
67
|
-
};
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extract the set of cells that a formula references.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from `src/plugins/spreadsheet/View.vue` (was the body of
|
|
5
|
-
* `extractCellReferences`, cognitive complexity 32). The original
|
|
6
|
-
* function combined regex scanning, range expansion, single-cell
|
|
7
|
-
* parsing, and deduplication all in one body; splitting each concern
|
|
8
|
-
* into a named helper brings the top-level function well under the
|
|
9
|
-
* sonarjs/cognitive-complexity threshold of 15 and makes the pure
|
|
10
|
-
* logic unit-testable in isolation (see
|
|
11
|
-
* `test/plugins/spreadsheet/engine/test_formulaRefs.ts`).
|
|
12
|
-
*
|
|
13
|
-
* Tracks #175. No behavioural change — the wrapper in View.vue
|
|
14
|
-
* still returns exactly the same `{ row, col }` list as before.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { columnToIndex } from "./parser.js";
|
|
18
|
-
|
|
19
|
-
export interface CellCoord {
|
|
20
|
-
row: number;
|
|
21
|
-
col: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// `A1:B10`, `$A$1:$B$10`, `Sheet` refs are out of scope here — the
|
|
25
|
-
// caller only passes the formula body, and cross-sheet ranges never
|
|
26
|
-
// reached the original regex anyway. Keeping the patterns identical
|
|
27
|
-
// to the pre-refactor code preserves behaviour exactly.
|
|
28
|
-
const RANGE_REGEX = /\$?[A-Z]+\$?\d+:\$?[A-Z]+\$?\d+/g;
|
|
29
|
-
const CELL_REGEX = /\$?[A-Z]+\$?\d+/g;
|
|
30
|
-
|
|
31
|
-
// Excel formulas start with `=`. Strip it for uniform handling.
|
|
32
|
-
// Keeps any inner `=` intact (Excel does not allow them but the
|
|
33
|
-
// caller may pass partial text during live editing).
|
|
34
|
-
export function stripFormulaPrefix(formula: string): string {
|
|
35
|
-
return formula.startsWith("=") ? formula.slice(1) : formula;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const A1_REGEX = /^([A-Z]+)(\d+)$/;
|
|
39
|
-
|
|
40
|
-
// One `A1` token as its column index (0-based) and its row NUMBER as written
|
|
41
|
-
// (1-based). The single place a column letter + row digit pair is read, so the
|
|
42
|
-
// range and single-cell parsers below cannot drift apart.
|
|
43
|
-
function parseA1Token(token: string): { col: number; rowNumber: number } | null {
|
|
44
|
-
const [, column, row] = token.match(A1_REGEX) ?? [];
|
|
45
|
-
if (column === undefined || row === undefined) return null;
|
|
46
|
-
return { col: columnToIndex(column), rowNumber: parseInt(row, 10) };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// The two endpoints of an `A1:B3` range, or null for anything that is not
|
|
50
|
-
// exactly two `A1` tokens joined by one colon.
|
|
51
|
-
function parseRangeEndpoints(body: string): { start: { col: number; rowNumber: number }; end: { col: number; rowNumber: number } } | null {
|
|
52
|
-
const [startToken, endToken, ...extra] = body.split(":");
|
|
53
|
-
if (startToken === undefined || endToken === undefined || extra.length > 0) return null;
|
|
54
|
-
const start = parseA1Token(startToken);
|
|
55
|
-
const end = parseA1Token(endToken);
|
|
56
|
-
return start && end ? { start, end } : null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Expand a single range token (`A1:B3`, `$A$1:$C$5`) into every
|
|
60
|
-
// coordinate the range covers. Returns an empty array for malformed
|
|
61
|
-
// input so callers never have to handle exceptions; the worst case
|
|
62
|
-
// is "we silently ignored a weird-looking substring," which matches
|
|
63
|
-
// the original inline behaviour.
|
|
64
|
-
export function expandRange(rangeStr: string): CellCoord[] {
|
|
65
|
-
const endpoints = parseRangeEndpoints(rangeStr.replace(/\$/g, ""));
|
|
66
|
-
if (!endpoints) return [];
|
|
67
|
-
const { start, end } = endpoints;
|
|
68
|
-
const cells: CellCoord[] = [];
|
|
69
|
-
for (let row = start.rowNumber - 1; row <= end.rowNumber - 1; row++) {
|
|
70
|
-
for (let col = start.col; col <= end.col; col++) {
|
|
71
|
-
cells.push({ row, col });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return cells;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Expand a range OR a single cell into coordinates, upcasing first so
|
|
78
|
-
// lowercase references (`a1:b2`, which spreadsheets accept) are not dropped,
|
|
79
|
-
// and falling back to a single cell when there is no colon. `collectRangeValues`
|
|
80
|
-
// in the calculator used a range-only, case-sensitive regex, so `A1`,
|
|
81
|
-
// `$A$1:$A$10` and `a1:a10` all silently produced no values (#2356). Ordering
|
|
82
|
-
// matches `expandRange`: top-to-bottom, left-to-right.
|
|
83
|
-
export function expandRangeOrCell(ref: string): CellCoord[] | null {
|
|
84
|
-
const upper = ref.trim().toUpperCase();
|
|
85
|
-
if (upper.includes(":")) {
|
|
86
|
-
const cells = expandRange(upper);
|
|
87
|
-
return cells.length > 0 ? cells : null;
|
|
88
|
-
}
|
|
89
|
-
const single = parseSingleCellRef(upper);
|
|
90
|
-
return single ? [single] : null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Parse a single cell ref (`A1`, `$A$1`, `AA100`) into a coord.
|
|
94
|
-
// Returns null for malformed input rather than throwing — keeps the
|
|
95
|
-
// caller's loop flat (the engine-layer `parseCellRef` throws, which
|
|
96
|
-
// is fine for the evaluator but wrong for a best-effort scanner).
|
|
97
|
-
export function parseSingleCellRef(refStr: string): CellCoord | null {
|
|
98
|
-
const parsed = parseA1Token(refStr.replace(/\$/g, ""));
|
|
99
|
-
return parsed ? { col: parsed.col, row: parsed.rowNumber - 1 } : null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Numeric bounds of a `A2:C10` range, with any `Sheet1!` / `'My Sheet'!`
|
|
103
|
-
// prefix kept verbatim so callers can rebuild sheet-qualified refs. Columns
|
|
104
|
-
// are 0-based (via `columnToIndex`); rows stay 1-based, matching A1 notation.
|
|
105
|
-
export interface RangeBounds {
|
|
106
|
-
sheetPrefix: string;
|
|
107
|
-
startCol: number;
|
|
108
|
-
startRow: number;
|
|
109
|
-
endCol: number;
|
|
110
|
-
endRow: number;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Split an optional sheet prefix from a range, then parse the `A2:C10` body.
|
|
114
|
-
// The prefix is everything up to and including the last `!`, so a quoted sheet
|
|
115
|
-
// name that itself contains no `!` (the common case) is preserved intact. The
|
|
116
|
-
// lookup functions each carried their own copy of this parse; one copy ran a
|
|
117
|
-
// sheet-unaware regex against the whole string and threw on `Sheet1!A2:C10`
|
|
118
|
-
// before the sheet-aware copy could run (#2390). Returns null for anything that
|
|
119
|
-
// is not a two-endpoint range so callers surface one "Invalid range" message.
|
|
120
|
-
export function parseRangeBounds(range: string): RangeBounds | null {
|
|
121
|
-
const bang = range.lastIndexOf("!");
|
|
122
|
-
const sheetPrefix = bang >= 0 ? range.slice(0, bang + 1) : "";
|
|
123
|
-
const body = bang >= 0 ? range.slice(bang + 1) : range;
|
|
124
|
-
const endpoints = parseRangeEndpoints(body);
|
|
125
|
-
if (!endpoints) return null;
|
|
126
|
-
const { start, end } = endpoints;
|
|
127
|
-
return { sheetPrefix, startCol: start.col, startRow: start.rowNumber, endCol: end.col, endRow: end.rowNumber };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Excel's `0` row/column index selects the entire row/column. This scalar engine
|
|
131
|
-
// returns a single cell, so `0` is representable only when that dimension is one
|
|
132
|
-
// line long; otherwise it is out of range.
|
|
133
|
-
const WHOLE_LINE_INDEX = 0;
|
|
134
|
-
const FIRST_INDEX = 1;
|
|
135
|
-
|
|
136
|
-
// Map a 1-based INDEX position onto a 0-based offset within a `size`-long line,
|
|
137
|
-
// truncating toward zero as Excel does. Returns null when the position falls
|
|
138
|
-
// outside the line — including a `0` (whole-line) request the scalar engine
|
|
139
|
-
// cannot collapse to one cell. Callers turn null into #REF!.
|
|
140
|
-
function lineOffset(position: number, size: number): number | null {
|
|
141
|
-
const index = Math.trunc(position);
|
|
142
|
-
if (!Number.isFinite(index)) return null;
|
|
143
|
-
if (index === WHOLE_LINE_INDEX) return size === 1 ? 0 : null;
|
|
144
|
-
if (index < FIRST_INDEX || index > size) return null;
|
|
145
|
-
return index - FIRST_INDEX;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Resolve INDEX(range, rowNum, colNum)'s 1-based position to an absolute cell
|
|
149
|
-
// within the range, or null (→ #REF!) when it falls outside. The former handler
|
|
150
|
-
// computed the target with no bounds check, so an out-of-range index silently
|
|
151
|
-
// read a cell outside the range (#2390: `INDEX(A1:A3,5)` read A5, `INDEX(A2:B5,
|
|
152
|
-
// 0,1)` read A1). Returned column is 0-based; row is 1-based (A1 notation).
|
|
153
|
-
export function resolveIndexTarget(bounds: RangeBounds, rowNum: number, colNum: number): { colIndex: number; row: number } | null {
|
|
154
|
-
const rowOffset = lineOffset(rowNum, bounds.endRow - bounds.startRow + 1);
|
|
155
|
-
const colOffset = lineOffset(colNum, bounds.endCol - bounds.startCol + 1);
|
|
156
|
-
if (rowOffset === null || colOffset === null) return null;
|
|
157
|
-
return { colIndex: bounds.startCol + colOffset, row: bounds.startRow + rowOffset };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// VLOOKUP's col_index_num / HLOOKUP's row_index_num as a 0-based offset inside
|
|
161
|
-
// the table, or null (→ #REF!) when it points outside. Excel rejects an index
|
|
162
|
-
// past the table's width; without the check the handler addressed a cell beyond
|
|
163
|
-
// the range and returned whatever lived there — usually a silent 0 (#2360).
|
|
164
|
-
export function resolveTableOffset(position: number, size: number): number | null {
|
|
165
|
-
// Not `lineOffset`: INDEX reads a `0` position as "the whole line", which it
|
|
166
|
-
// can collapse to one cell when the line is one long. A lookup index has no
|
|
167
|
-
// such meaning — VLOOKUP's columns are numbered from 1 — so `0` is out of
|
|
168
|
-
// range even for a single-column table (Codex review).
|
|
169
|
-
const index = Math.trunc(position);
|
|
170
|
-
if (!Number.isFinite(index)) return null;
|
|
171
|
-
if (index < FIRST_INDEX || index > size) return null;
|
|
172
|
-
return index - FIRST_INDEX;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Top-level: scan the formula, expand any ranges, then pick up
|
|
176
|
-
// remaining single-cell refs, deduplicating as we go. Kept short
|
|
177
|
-
// (~15 lines) so the cognitive-complexity signal lands on the
|
|
178
|
-
// helpers if anything grows here.
|
|
179
|
-
export function extractCellReferences(formula: string): CellCoord[] {
|
|
180
|
-
const clean = stripFormulaPrefix(formula);
|
|
181
|
-
const refs: CellCoord[] = [];
|
|
182
|
-
const seen = new Set<string>();
|
|
183
|
-
const addUnique = (coord: CellCoord): void => {
|
|
184
|
-
const key = `${coord.row},${coord.col}`;
|
|
185
|
-
if (seen.has(key)) return;
|
|
186
|
-
seen.add(key);
|
|
187
|
-
refs.push(coord);
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
for (const range of clean.match(RANGE_REGEX) ?? []) {
|
|
191
|
-
for (const coord of expandRange(range)) addUnique(coord);
|
|
192
|
-
}
|
|
193
|
-
// Strip matched ranges so the cell-regex doesn't re-emit their
|
|
194
|
-
// endpoints as standalone refs (mirrors the original's second
|
|
195
|
-
// `.replace(rangeRegex, "")` pass).
|
|
196
|
-
const withoutRanges = clean.replace(RANGE_REGEX, "");
|
|
197
|
-
for (const cellStr of withoutRanges.match(CELL_REGEX) ?? []) {
|
|
198
|
-
const coord = parseSingleCellRef(cellStr);
|
|
199
|
-
if (coord) addUnique(coord);
|
|
200
|
-
}
|
|
201
|
-
return refs;
|
|
202
|
-
}
|