@a3s-lab/office 0.35.0 → 0.37.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/README.md +34 -10
- package/dist/0~7048.js +590 -3
- package/dist/0~spreadsheet-editor.js +515 -44
- package/dist/0~work-office-diagnostics.js +1 -1
- package/dist/3266.js +549 -10
- package/dist/4121.js +2202 -1942
- package/dist/8715.js +299 -299
- package/dist/core.js +41 -4
- package/dist/internal/features/work/editors/spreadsheet-calculation-model.d.ts +1 -1
- package/dist/internal/features/work/editors/spreadsheet-calculation-projection.d.ts +5 -2
- package/dist/internal/features/work/editors/spreadsheet-table-calculated-columns.d.ts +42 -0
- package/dist/internal/features/work/editors/spreadsheet-table-totals.d.ts +39 -0
- package/dist/internal/features/work/editors/spreadsheet-table.d.ts +11 -1
- package/dist/internal/features/work/work-types.d.ts +20 -1
- package/dist/internal/kernel/office-kernel-protocol.d.ts +1 -1
- package/dist/internal/kernel/office-kernel-spreadsheet-fallback-formula.d.ts +11 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-protocol.d.ts +18 -0
- package/dist/internal/kernel/office-kernel-spreadsheet-structured-reference.d.ts +48 -0
- package/dist/office-kernel.wasm +0 -0
- package/dist/office-kernel.worker.js +598 -3
- package/dist/styles.css +120 -0
- package/docs/latest/en/browser-editor-architecture.md +27 -8
- package/package.json +4 -1
|
@@ -13727,6 +13727,13 @@
|
|
|
13727
13727
|
1
|
|
13728
13728
|
]
|
|
13729
13729
|
],
|
|
13730
|
+
[
|
|
13731
|
+
'SUBTOTAL',
|
|
13732
|
+
[
|
|
13733
|
+
2,
|
|
13734
|
+
255
|
|
13735
|
+
]
|
|
13736
|
+
],
|
|
13730
13737
|
[
|
|
13731
13738
|
'SUM',
|
|
13732
13739
|
[
|
|
@@ -13747,6 +13754,90 @@
|
|
|
13747
13754
|
while(normalized.startsWith('_XLFN.') || normalized.startsWith('_XLWS.'))normalized = normalized.slice(6);
|
|
13748
13755
|
return normalized;
|
|
13749
13756
|
}
|
|
13757
|
+
function evaluateParserSubtotal(parameters) {
|
|
13758
|
+
const codeValue = parserNumericValue(parameters[0]);
|
|
13759
|
+
if (void 0 === codeValue) return 'VALUE!';
|
|
13760
|
+
const code = Math.trunc(codeValue);
|
|
13761
|
+
const values = parameters.slice(1).flatMap((parameter)=>flattenParserValues(parameter));
|
|
13762
|
+
for (const value of values){
|
|
13763
|
+
const error = parserErrorValue(value);
|
|
13764
|
+
if (error) return error;
|
|
13765
|
+
}
|
|
13766
|
+
switch(code){
|
|
13767
|
+
case 1:
|
|
13768
|
+
case 101:
|
|
13769
|
+
return parserSubtotalNumeric(values, 'average');
|
|
13770
|
+
case 2:
|
|
13771
|
+
case 102:
|
|
13772
|
+
return values.filter((value)=>'number' == typeof value).length;
|
|
13773
|
+
case 3:
|
|
13774
|
+
case 103:
|
|
13775
|
+
return values.filter((value)=>null != value).length;
|
|
13776
|
+
case 4:
|
|
13777
|
+
case 104:
|
|
13778
|
+
return parserSubtotalNumeric(values, 'max');
|
|
13779
|
+
case 5:
|
|
13780
|
+
case 105:
|
|
13781
|
+
return parserSubtotalNumeric(values, 'min');
|
|
13782
|
+
case 6:
|
|
13783
|
+
case 106:
|
|
13784
|
+
return parserSubtotalNumeric(values, 'product');
|
|
13785
|
+
case 7:
|
|
13786
|
+
case 107:
|
|
13787
|
+
return parserSubtotalNumeric(values, 'stddev');
|
|
13788
|
+
case 8:
|
|
13789
|
+
case 108:
|
|
13790
|
+
return parserSubtotalNumeric(values, 'stddevp');
|
|
13791
|
+
case 9:
|
|
13792
|
+
case 109:
|
|
13793
|
+
return parserSubtotalNumeric(values, 'sum');
|
|
13794
|
+
case 10:
|
|
13795
|
+
case 110:
|
|
13796
|
+
return parserSubtotalNumeric(values, 'var');
|
|
13797
|
+
case 11:
|
|
13798
|
+
case 111:
|
|
13799
|
+
return parserSubtotalNumeric(values, 'varp');
|
|
13800
|
+
default:
|
|
13801
|
+
return 'VALUE!';
|
|
13802
|
+
}
|
|
13803
|
+
}
|
|
13804
|
+
function parserSubtotalNumeric(values, operation) {
|
|
13805
|
+
const numbers = values.filter((value)=>'number' == typeof value && Number.isFinite(value));
|
|
13806
|
+
const count = numbers.length;
|
|
13807
|
+
const sum = numbers.reduce((total, value)=>total + value, 0);
|
|
13808
|
+
if ('sum' === operation) return finiteParserNumber(sum);
|
|
13809
|
+
if ('average' === operation) return count ? finiteParserNumber(sum / count) : 'DIV/0!';
|
|
13810
|
+
if ('max' === operation) return finiteParserNumber(Math.max(...numbers, 0));
|
|
13811
|
+
if ('min' === operation) return finiteParserNumber(Math.min(...numbers, 0));
|
|
13812
|
+
if ('product' === operation) return finiteParserNumber(count ? numbers.reduce((total, value)=>total * value, 1) : 0);
|
|
13813
|
+
if ('stddev' === operation || 'var' === operation) {
|
|
13814
|
+
if (count < 2) return 'DIV/0!';
|
|
13815
|
+
} else if (0 === count) return 'DIV/0!';
|
|
13816
|
+
const mean = sum / count;
|
|
13817
|
+
const divisor = 'stddev' === operation || 'var' === operation ? count - 1 : count;
|
|
13818
|
+
const variance = numbers.reduce((total, value)=>total + (value - mean) ** 2, 0) / divisor;
|
|
13819
|
+
return finiteParserNumber('stddev' === operation || 'stddevp' === operation ? Math.sqrt(variance) : variance);
|
|
13820
|
+
}
|
|
13821
|
+
function flattenParserValues(value) {
|
|
13822
|
+
if (!Array.isArray(value)) return [
|
|
13823
|
+
value
|
|
13824
|
+
];
|
|
13825
|
+
return value.flatMap((entry)=>flattenParserValues(entry));
|
|
13826
|
+
}
|
|
13827
|
+
function parserNumericValue(value) {
|
|
13828
|
+
if ('number' == typeof value) return Number.isFinite(value) ? value : void 0;
|
|
13829
|
+
if ('boolean' == typeof value) return value ? 1 : 0;
|
|
13830
|
+
if ('string' != typeof value || !value.trim()) return;
|
|
13831
|
+
const parsed = Number(value);
|
|
13832
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
13833
|
+
}
|
|
13834
|
+
function parserErrorValue(value) {
|
|
13835
|
+
if (value instanceof Error) return value.message.startsWith('#') ? value.message.slice(1) : 'VALUE!';
|
|
13836
|
+
return 'string' == typeof value && value.startsWith('#') ? value.slice(1) : void 0;
|
|
13837
|
+
}
|
|
13838
|
+
function finiteParserNumber(value) {
|
|
13839
|
+
return Number.isFinite(value) ? value : 'NUM!';
|
|
13840
|
+
}
|
|
13750
13841
|
function normalizeFormulaForFortuneParser(formula) {
|
|
13751
13842
|
const source = formula.replace(/^=/, '');
|
|
13752
13843
|
let output = '';
|
|
@@ -13916,6 +14007,7 @@
|
|
|
13916
14007
|
if (!sheet.id.trim() || utf8ByteLength(sheet.id) > MAX_SPREADSHEET_IDENTIFIER_BYTES || !sheet.name.trim() || utf8ByteLength(sheet.name) > MAX_SPREADSHEET_IDENTIFIER_BYTES || sheetIds.has(sheet.id) || sheetNames.has(normalizedName)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.sheet_invalid', 'Spreadsheet sheet IDs and names must be unique and non-empty.');
|
|
13917
14008
|
sheetIds.add(sheet.id);
|
|
13918
14009
|
sheetNames.add(normalizedName);
|
|
14010
|
+
validateSpreadsheetTables(sheet, request.sheets);
|
|
13919
14011
|
const coordinates = new Set();
|
|
13920
14012
|
for (const cell of sheet.cells){
|
|
13921
14013
|
const key = `${cell.row}:${cell.column}`;
|
|
@@ -13927,6 +14019,68 @@
|
|
|
13927
14019
|
}
|
|
13928
14020
|
for (const target of request.targets ?? [])if (!sheetIds.has(target.sheetId) || !boundedSpreadsheetIndex(target.row, OFFICE_KERNEL_SPREADSHEET_MAX_ROWS) || !boundedSpreadsheetIndex(target.column, 16384)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.target_invalid', 'Spreadsheet calculation targets must reference an existing, bounded cell.');
|
|
13929
14021
|
}
|
|
14022
|
+
function validateSpreadsheetTables(sheet, sheets) {
|
|
14023
|
+
const tables = sheet.tables ?? [];
|
|
14024
|
+
const tableCount = sheets.reduce((count, candidate)=>count + (candidate.tables?.length ?? 0), 0);
|
|
14025
|
+
if (tableCount > 1024) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_limit_exceeded', 'A Spreadsheet calculation request may contain at most 1024 tables.');
|
|
14026
|
+
const aliases = new Map();
|
|
14027
|
+
for (const candidate of sheets)for (const [tableIndex, table] of (candidate.tables ?? []).entries()){
|
|
14028
|
+
const identity = `${candidate.id}\u0000${tableIndex}`;
|
|
14029
|
+
for (const alias of [
|
|
14030
|
+
table.name,
|
|
14031
|
+
table.displayName
|
|
14032
|
+
]){
|
|
14033
|
+
if (!alias) continue;
|
|
14034
|
+
const normalized = alias.toLocaleLowerCase();
|
|
14035
|
+
const existing = aliases.get(normalized);
|
|
14036
|
+
if (existing && existing !== identity) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table name '${alias}' is ambiguous.`);
|
|
14037
|
+
aliases.set(normalized, identity);
|
|
14038
|
+
}
|
|
14039
|
+
}
|
|
14040
|
+
const ranges = [];
|
|
14041
|
+
for (const table of tables){
|
|
14042
|
+
for (const [kind, value] of [
|
|
14043
|
+
[
|
|
14044
|
+
'startRow',
|
|
14045
|
+
table.startRow
|
|
14046
|
+
],
|
|
14047
|
+
[
|
|
14048
|
+
'endRow',
|
|
14049
|
+
table.endRow
|
|
14050
|
+
],
|
|
14051
|
+
[
|
|
14052
|
+
'startColumn',
|
|
14053
|
+
table.startColumn
|
|
14054
|
+
],
|
|
14055
|
+
[
|
|
14056
|
+
'endColumn',
|
|
14057
|
+
table.endColumn
|
|
14058
|
+
]
|
|
14059
|
+
])if (!boundedSpreadsheetIndex(value, kind.endsWith('Row') ? OFFICE_KERNEL_SPREADSHEET_MAX_ROWS : 16384)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' has an out-of-bounds ${kind}.`);
|
|
14060
|
+
if (table.startRow > table.endRow || table.startColumn > table.endColumn || table.columns.length !== table.endColumn - table.startColumn + 1) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' has an invalid range or column count.`);
|
|
14061
|
+
validateTableName(table.name, 'name');
|
|
14062
|
+
if (void 0 !== table.displayName) validateTableName(table.displayName, 'displayName');
|
|
14063
|
+
const columnNames = new Set();
|
|
14064
|
+
for (const column of table.columns){
|
|
14065
|
+
validateTableName(column, 'column');
|
|
14066
|
+
const normalized = column.toLocaleLowerCase();
|
|
14067
|
+
if (columnNames.has(normalized)) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' contains duplicate column names.`);
|
|
14068
|
+
columnNames.add(normalized);
|
|
14069
|
+
}
|
|
14070
|
+
if (table.headerRow && table.totalsRow && table.startRow === table.endRow) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table '${table.name}' cannot use header and totals rows in one row.`);
|
|
14071
|
+
for (const previous of ranges)if (table.startRow <= previous.endRow && table.endRow >= previous.startRow && table.startColumn <= previous.endColumn && table.endColumn >= previous.startColumn) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet tables '${previous.name}' and '${table.name}' overlap.`);
|
|
14072
|
+
ranges.push({
|
|
14073
|
+
startRow: table.startRow,
|
|
14074
|
+
endRow: table.endRow,
|
|
14075
|
+
startColumn: table.startColumn,
|
|
14076
|
+
endColumn: table.endColumn,
|
|
14077
|
+
name: table.name
|
|
14078
|
+
});
|
|
14079
|
+
}
|
|
14080
|
+
}
|
|
14081
|
+
function validateTableName(value, kind) {
|
|
14082
|
+
if (!value.trim() || utf8ByteLength(value) > MAX_SPREADSHEET_IDENTIFIER_BYTES) throw office_kernel_spreadsheet_fallback_validation_kernelError('office.kernel.spreadsheet.table_invalid', `Spreadsheet table ${kind} must contain 1-${MAX_SPREADSHEET_IDENTIFIER_BYTES} UTF-8 bytes.`);
|
|
14083
|
+
}
|
|
13930
14084
|
function boundedSpreadsheetIndex(value, exclusiveMaximum) {
|
|
13931
14085
|
return Number.isSafeInteger(value) && value >= 0 && value < exclusiveMaximum;
|
|
13932
14086
|
}
|
|
@@ -14020,6 +14174,428 @@
|
|
|
14020
14174
|
const withSuffix = '#DIV/0' === normalized ? '#DIV/0!' : normalized;
|
|
14021
14175
|
return isOfficeKernelSpreadsheetError(withSuffix);
|
|
14022
14176
|
}
|
|
14177
|
+
class SpreadsheetStructuredReferenceError extends Error {
|
|
14178
|
+
kind;
|
|
14179
|
+
constructor(kind, message){
|
|
14180
|
+
super(message);
|
|
14181
|
+
this.name = 'SpreadsheetStructuredReferenceError';
|
|
14182
|
+
this.kind = kind;
|
|
14183
|
+
}
|
|
14184
|
+
}
|
|
14185
|
+
function parseSpreadsheetStructuredReference(reference) {
|
|
14186
|
+
const open = reference.indexOf('[');
|
|
14187
|
+
if (open < 0) throw invalidReference(reference);
|
|
14188
|
+
const tableName = reference.slice(0, open) || void 0;
|
|
14189
|
+
const content = outerGroup(reference.slice(open));
|
|
14190
|
+
if (null === content) throw invalidReference(reference);
|
|
14191
|
+
const rows = {
|
|
14192
|
+
all: false,
|
|
14193
|
+
headers: false,
|
|
14194
|
+
data: false,
|
|
14195
|
+
totals: false,
|
|
14196
|
+
current: false
|
|
14197
|
+
};
|
|
14198
|
+
let firstColumn;
|
|
14199
|
+
let lastColumn;
|
|
14200
|
+
if (content.startsWith('@')) {
|
|
14201
|
+
rows.current = true;
|
|
14202
|
+
const column = parseCurrentColumn(content.slice(1), reference);
|
|
14203
|
+
firstColumn = column;
|
|
14204
|
+
lastColumn = column;
|
|
14205
|
+
} else if (content.startsWith('[')) [firstColumn, lastColumn] = parseNestedSelection(content, reference, rows);
|
|
14206
|
+
else {
|
|
14207
|
+
const item = tableItem(content);
|
|
14208
|
+
if (item) applyTableItem(rows, item, reference);
|
|
14209
|
+
else {
|
|
14210
|
+
const column = parsePlainColumn(content, reference);
|
|
14211
|
+
firstColumn = column;
|
|
14212
|
+
lastColumn = column;
|
|
14213
|
+
}
|
|
14214
|
+
}
|
|
14215
|
+
if (!rows.all && !rows.headers && !rows.data && !rows.totals && !rows.current) rows.data = true;
|
|
14216
|
+
return {
|
|
14217
|
+
tableName,
|
|
14218
|
+
firstColumn,
|
|
14219
|
+
lastColumn,
|
|
14220
|
+
rows
|
|
14221
|
+
};
|
|
14222
|
+
}
|
|
14223
|
+
class SpreadsheetStructuredReferenceCatalog {
|
|
14224
|
+
sheets;
|
|
14225
|
+
definitions = [];
|
|
14226
|
+
byName = new Map();
|
|
14227
|
+
bySheet = new Map();
|
|
14228
|
+
constructor(sheets){
|
|
14229
|
+
this.sheets = sheets;
|
|
14230
|
+
for (const sheet of sheets){
|
|
14231
|
+
const indexes = [];
|
|
14232
|
+
for (const table of sheet.tables ?? []){
|
|
14233
|
+
const definition = {
|
|
14234
|
+
...table,
|
|
14235
|
+
sheetId: sheet.id,
|
|
14236
|
+
sheetName: sheet.name
|
|
14237
|
+
};
|
|
14238
|
+
const index = this.definitions.length;
|
|
14239
|
+
this.definitions.push(definition);
|
|
14240
|
+
indexes.push(index);
|
|
14241
|
+
for (const alias of [
|
|
14242
|
+
table.name,
|
|
14243
|
+
table.displayName
|
|
14244
|
+
]){
|
|
14245
|
+
if (!alias) continue;
|
|
14246
|
+
const key = alias.toLocaleLowerCase();
|
|
14247
|
+
if (!this.byName.has(key)) this.byName.set(key, index);
|
|
14248
|
+
}
|
|
14249
|
+
}
|
|
14250
|
+
this.bySheet.set(sheet.id, indexes);
|
|
14251
|
+
}
|
|
14252
|
+
}
|
|
14253
|
+
resolve(qualifier, reference, currentSheet, currentColumn, currentRow) {
|
|
14254
|
+
const parsed = parseSpreadsheetStructuredReference(reference);
|
|
14255
|
+
const table = this.resolveTable(parsed, reference, currentSheet, currentColumn, currentRow);
|
|
14256
|
+
if (qualifier && table.sheetName.toLocaleLowerCase() !== qualifier.toLocaleLowerCase()) throw new SpreadsheetStructuredReferenceError('missing-table', `Spreadsheet table '${table.name}' is not on worksheet '${qualifier}'.`);
|
|
14257
|
+
if (parsed.rows.current && currentSheet.id !== table.sheetId) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference #This Row requires the current formula cell to be on table '${table.name}'.`);
|
|
14258
|
+
const [startColumn, endColumn] = this.resolveColumns(table, parsed);
|
|
14259
|
+
return this.resolveRows(table, parsed.rows, currentRow).map(([startRow, endRow])=>({
|
|
14260
|
+
sheetId: table.sheetId,
|
|
14261
|
+
sheetName: table.sheetName,
|
|
14262
|
+
startRow,
|
|
14263
|
+
endRow,
|
|
14264
|
+
startColumn,
|
|
14265
|
+
endColumn
|
|
14266
|
+
}));
|
|
14267
|
+
}
|
|
14268
|
+
resolveTable(parsed, reference, currentSheet, currentColumn, currentRow) {
|
|
14269
|
+
if (parsed.tableName) {
|
|
14270
|
+
const index = this.byName.get(parsed.tableName.toLocaleLowerCase());
|
|
14271
|
+
if (void 0 === index) throw new SpreadsheetStructuredReferenceError('missing-table', `Spreadsheet table '${parsed.tableName}' does not exist.`);
|
|
14272
|
+
const table = this.definitions[index];
|
|
14273
|
+
if (!table) throw invalidReference(reference);
|
|
14274
|
+
return table;
|
|
14275
|
+
}
|
|
14276
|
+
const matching = (this.bySheet.get(currentSheet.id) ?? []).map((index)=>this.definitions[index]).filter((table)=>void 0 !== table && currentRow >= table.startRow && currentRow <= table.endRow && currentColumn >= table.startColumn && currentColumn <= table.endColumn);
|
|
14277
|
+
if (!matching.length) throw new SpreadsheetStructuredReferenceError('missing-table', `Table-local structured reference '${reference}' requires the current formula cell to be inside a Spreadsheet table.`);
|
|
14278
|
+
if (matching.length > 1) throw new SpreadsheetStructuredReferenceError('unsupported', `Table-local structured reference '${reference}' is ambiguous at the current formula cell.`);
|
|
14279
|
+
return matching[0];
|
|
14280
|
+
}
|
|
14281
|
+
resolveColumns(table, parsed) {
|
|
14282
|
+
let first = 0;
|
|
14283
|
+
let last = table.columns.length - 1;
|
|
14284
|
+
if (void 0 !== parsed.firstColumn && void 0 !== parsed.lastColumn) {
|
|
14285
|
+
first = table.columns.findIndex((column)=>column.toLocaleLowerCase() === parsed.firstColumn.toLocaleLowerCase());
|
|
14286
|
+
last = table.columns.findIndex((column)=>column.toLocaleLowerCase() === parsed.lastColumn.toLocaleLowerCase());
|
|
14287
|
+
if (first < 0) throw new SpreadsheetStructuredReferenceError('missing-column', `Spreadsheet table '${table.name}' has no column '${parsed.firstColumn}'.`);
|
|
14288
|
+
if (last < 0) throw new SpreadsheetStructuredReferenceError('missing-column', `Spreadsheet table '${table.name}' has no column '${parsed.lastColumn}'.`);
|
|
14289
|
+
if (first > last) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured-reference column range '${parsed.firstColumn}:${parsed.lastColumn}' is reversed.`);
|
|
14290
|
+
}
|
|
14291
|
+
if (first < 0 || last < first || table.startColumn + last > table.endColumn) throw invalidReference(table.name);
|
|
14292
|
+
return [
|
|
14293
|
+
table.startColumn + first,
|
|
14294
|
+
table.startColumn + last
|
|
14295
|
+
];
|
|
14296
|
+
}
|
|
14297
|
+
resolveRows(table, rows, currentRow) {
|
|
14298
|
+
const selected = [];
|
|
14299
|
+
if (rows.all) selected.push([
|
|
14300
|
+
table.startRow,
|
|
14301
|
+
table.endRow
|
|
14302
|
+
]);
|
|
14303
|
+
if (rows.headers) {
|
|
14304
|
+
if (!table.headerRow) throw missingRows(table, '#Headers');
|
|
14305
|
+
selected.push([
|
|
14306
|
+
table.startRow,
|
|
14307
|
+
table.startRow
|
|
14308
|
+
]);
|
|
14309
|
+
}
|
|
14310
|
+
if (rows.data) selected.push(this.dataRows(table));
|
|
14311
|
+
if (rows.totals) {
|
|
14312
|
+
if (!table.totalsRow) throw missingRows(table, '#Totals');
|
|
14313
|
+
selected.push([
|
|
14314
|
+
table.endRow,
|
|
14315
|
+
table.endRow
|
|
14316
|
+
]);
|
|
14317
|
+
}
|
|
14318
|
+
if (rows.current) {
|
|
14319
|
+
const [start, end] = this.dataRows(table);
|
|
14320
|
+
if (currentRow < start || currentRow > end) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference #This Row requires the current formula row to be inside table '${table.name}'.`);
|
|
14321
|
+
selected.push([
|
|
14322
|
+
currentRow,
|
|
14323
|
+
currentRow
|
|
14324
|
+
]);
|
|
14325
|
+
}
|
|
14326
|
+
selected.sort((left, right)=>left[0] - right[0]);
|
|
14327
|
+
const merged = [];
|
|
14328
|
+
for (const [start, end] of selected){
|
|
14329
|
+
const previous = merged.at(-1);
|
|
14330
|
+
if (previous && start <= previous[1] + 1) previous[1] = Math.max(previous[1], end);
|
|
14331
|
+
else merged.push([
|
|
14332
|
+
start,
|
|
14333
|
+
end
|
|
14334
|
+
]);
|
|
14335
|
+
}
|
|
14336
|
+
if (!merged.length) throw new SpreadsheetStructuredReferenceError('unsupported', 'Structured reference selects no table rows.');
|
|
14337
|
+
return merged;
|
|
14338
|
+
}
|
|
14339
|
+
dataRows(table) {
|
|
14340
|
+
const start = table.startRow + (table.headerRow ? 1 : 0);
|
|
14341
|
+
const end = table.endRow - (table.totalsRow ? 1 : 0);
|
|
14342
|
+
if (start > end) throw new SpreadsheetStructuredReferenceError('unsupported', `Spreadsheet table '${table.name}' has no data rows.`);
|
|
14343
|
+
return [
|
|
14344
|
+
start,
|
|
14345
|
+
end
|
|
14346
|
+
];
|
|
14347
|
+
}
|
|
14348
|
+
}
|
|
14349
|
+
function expandSpreadsheetStructuredReferences(formula, catalog, currentSheet, currentRow, currentColumn) {
|
|
14350
|
+
const hasEquals = formula.startsWith('=');
|
|
14351
|
+
const source = hasEquals ? formula.slice(1) : formula;
|
|
14352
|
+
let output = '';
|
|
14353
|
+
let cursor = 0;
|
|
14354
|
+
while(cursor < source.length){
|
|
14355
|
+
const character = source[cursor] ?? '';
|
|
14356
|
+
if ('"' === character) {
|
|
14357
|
+
const end = quotedStringEnd(source, cursor);
|
|
14358
|
+
output += source.slice(cursor, end);
|
|
14359
|
+
cursor = end;
|
|
14360
|
+
continue;
|
|
14361
|
+
}
|
|
14362
|
+
const token = scanStructuredReference(source, cursor);
|
|
14363
|
+
if (!token) {
|
|
14364
|
+
output += character;
|
|
14365
|
+
cursor += 1;
|
|
14366
|
+
continue;
|
|
14367
|
+
}
|
|
14368
|
+
const areas = catalog.resolve(token.qualifier, token.reference, currentSheet, currentColumn, currentRow);
|
|
14369
|
+
if (areas.length > 1) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference '${token.reference}' resolves to disjoint row areas; the JavaScript fallback requires a contiguous range.`);
|
|
14370
|
+
const ranges = areas.map((area)=>{
|
|
14371
|
+
const prefix = area.sheetId === currentSheet.id ? '' : `${quoteSheetName(area.sheetName)}!`;
|
|
14372
|
+
const start = cellAddress(area.startRow, area.startColumn);
|
|
14373
|
+
const end = cellAddress(area.endRow, area.endColumn);
|
|
14374
|
+
return `${prefix}${start}${start === end ? '' : `:${end}`}`;
|
|
14375
|
+
});
|
|
14376
|
+
output += 1 === ranges.length ? ranges[0] : `(${ranges.join(',')})`;
|
|
14377
|
+
cursor = token.end;
|
|
14378
|
+
}
|
|
14379
|
+
return hasEquals ? `=${output}` : output;
|
|
14380
|
+
}
|
|
14381
|
+
function scanStructuredReference(source, start) {
|
|
14382
|
+
const qualified = scanQualifier(source, start);
|
|
14383
|
+
let cursor = qualified?.end ?? start;
|
|
14384
|
+
const qualifier = qualified?.name;
|
|
14385
|
+
if ('[' === source[cursor]) {
|
|
14386
|
+
const end = matchingBracket(source, cursor);
|
|
14387
|
+
if (null === end) return null;
|
|
14388
|
+
const content = source.slice(cursor + 1, end - 1);
|
|
14389
|
+
if (!content.startsWith('@') && !content.startsWith('#') && !content.startsWith('[')) return null;
|
|
14390
|
+
return {
|
|
14391
|
+
end,
|
|
14392
|
+
qualifier,
|
|
14393
|
+
reference: source.slice(cursor, end)
|
|
14394
|
+
};
|
|
14395
|
+
}
|
|
14396
|
+
const nameStart = cursor;
|
|
14397
|
+
if (!isNameStart(source[cursor] ?? '')) return null;
|
|
14398
|
+
cursor += 1;
|
|
14399
|
+
while(cursor < source.length && isNameContinue(source[cursor]))cursor += 1;
|
|
14400
|
+
if ('[' !== source[cursor]) return null;
|
|
14401
|
+
const end = matchingBracket(source, cursor);
|
|
14402
|
+
if (null === end) throw invalidReference(source.slice(nameStart, source.length));
|
|
14403
|
+
return {
|
|
14404
|
+
end,
|
|
14405
|
+
qualifier,
|
|
14406
|
+
reference: source.slice(nameStart, end)
|
|
14407
|
+
};
|
|
14408
|
+
}
|
|
14409
|
+
function scanQualifier(source, start) {
|
|
14410
|
+
if ("'" === source[start]) {
|
|
14411
|
+
let cursor = start + 1;
|
|
14412
|
+
let decoded = '';
|
|
14413
|
+
while(cursor < source.length){
|
|
14414
|
+
const character = source[cursor];
|
|
14415
|
+
if ("'" === character) {
|
|
14416
|
+
if ("'" === source[cursor + 1]) {
|
|
14417
|
+
decoded += "'";
|
|
14418
|
+
cursor += 2;
|
|
14419
|
+
continue;
|
|
14420
|
+
}
|
|
14421
|
+
if ('!' === source[cursor + 1]) return {
|
|
14422
|
+
name: decoded,
|
|
14423
|
+
end: cursor + 2
|
|
14424
|
+
};
|
|
14425
|
+
break;
|
|
14426
|
+
}
|
|
14427
|
+
decoded += character;
|
|
14428
|
+
cursor += 1;
|
|
14429
|
+
}
|
|
14430
|
+
return null;
|
|
14431
|
+
}
|
|
14432
|
+
let cursor = start;
|
|
14433
|
+
while(cursor < source.length && isQualifierCharacter(source[cursor]))cursor += 1;
|
|
14434
|
+
if ('!' !== source[cursor] || cursor === start) return null;
|
|
14435
|
+
return {
|
|
14436
|
+
name: source.slice(start, cursor),
|
|
14437
|
+
end: cursor + 1
|
|
14438
|
+
};
|
|
14439
|
+
}
|
|
14440
|
+
function matchingBracket(source, start) {
|
|
14441
|
+
let depth = 0;
|
|
14442
|
+
for(let cursor = start; cursor < source.length; cursor += 1){
|
|
14443
|
+
const character = source[cursor];
|
|
14444
|
+
if ("'" === character) {
|
|
14445
|
+
cursor += "'" === source[cursor + 1] ? 1 : 0;
|
|
14446
|
+
continue;
|
|
14447
|
+
}
|
|
14448
|
+
if ('[' === character) depth += 1;
|
|
14449
|
+
else if (']' === character) {
|
|
14450
|
+
depth -= 1;
|
|
14451
|
+
if (0 === depth) return cursor + 1;
|
|
14452
|
+
if (depth < 0) break;
|
|
14453
|
+
}
|
|
14454
|
+
}
|
|
14455
|
+
return null;
|
|
14456
|
+
}
|
|
14457
|
+
function outerGroup(value) {
|
|
14458
|
+
if (!value.startsWith('[')) return null;
|
|
14459
|
+
const end = matchingBracket(value, 0);
|
|
14460
|
+
return end === value.length ? value.slice(1, -1) : null;
|
|
14461
|
+
}
|
|
14462
|
+
function bracketAtom(value) {
|
|
14463
|
+
if (!value.startsWith('[')) return null;
|
|
14464
|
+
const end = matchingBracket(value, 0);
|
|
14465
|
+
return null === end ? null : {
|
|
14466
|
+
atom: value.slice(1, end - 1),
|
|
14467
|
+
consumed: end
|
|
14468
|
+
};
|
|
14469
|
+
}
|
|
14470
|
+
function parseCurrentColumn(value, reference) {
|
|
14471
|
+
if (value.startsWith('[')) {
|
|
14472
|
+
const atom = bracketAtom(value);
|
|
14473
|
+
if (!atom || atom.consumed !== value.length) throw invalidReference(reference);
|
|
14474
|
+
const column = decodeAtom(atom.atom);
|
|
14475
|
+
if (!column) throw invalidReference(reference);
|
|
14476
|
+
return column;
|
|
14477
|
+
}
|
|
14478
|
+
return parsePlainColumn(value, reference);
|
|
14479
|
+
}
|
|
14480
|
+
function parseNestedSelection(content, reference, rows) {
|
|
14481
|
+
const atoms = [];
|
|
14482
|
+
const separators = [];
|
|
14483
|
+
let cursor = 0;
|
|
14484
|
+
while(cursor < content.length){
|
|
14485
|
+
const atom = bracketAtom(content.slice(cursor));
|
|
14486
|
+
if (!atom) throw invalidReference(reference);
|
|
14487
|
+
atoms.push(atom.atom);
|
|
14488
|
+
cursor += atom.consumed;
|
|
14489
|
+
if (cursor === content.length) break;
|
|
14490
|
+
const separator = content[cursor];
|
|
14491
|
+
if (',' !== separator && ':' !== separator) throw invalidReference(reference);
|
|
14492
|
+
separators.push(separator);
|
|
14493
|
+
cursor += 1;
|
|
14494
|
+
}
|
|
14495
|
+
const columns = [];
|
|
14496
|
+
atoms.forEach((atom, index)=>{
|
|
14497
|
+
const item = tableItem(atom);
|
|
14498
|
+
if (item) applyTableItem(rows, item, reference);
|
|
14499
|
+
else {
|
|
14500
|
+
const column = decodeAtom(atom);
|
|
14501
|
+
if (!column) throw invalidReference(reference);
|
|
14502
|
+
columns.push({
|
|
14503
|
+
index,
|
|
14504
|
+
name: column
|
|
14505
|
+
});
|
|
14506
|
+
}
|
|
14507
|
+
});
|
|
14508
|
+
if (0 === columns.length) {
|
|
14509
|
+
if (separators.includes(':')) throw invalidReference(reference);
|
|
14510
|
+
return [
|
|
14511
|
+
void 0,
|
|
14512
|
+
void 0
|
|
14513
|
+
];
|
|
14514
|
+
}
|
|
14515
|
+
if (1 === columns.length) {
|
|
14516
|
+
if (separators.includes(':')) throw invalidReference(reference);
|
|
14517
|
+
return [
|
|
14518
|
+
columns[0].name,
|
|
14519
|
+
columns[0].name
|
|
14520
|
+
];
|
|
14521
|
+
}
|
|
14522
|
+
const first = columns[0];
|
|
14523
|
+
const last = columns[1];
|
|
14524
|
+
if (2 === columns.length && last.index === first.index + 1 && ':' === separators[first.index] && separators.every((separator, index)=>index === first.index || ',' === separator)) return [
|
|
14525
|
+
first.name,
|
|
14526
|
+
last.name
|
|
14527
|
+
];
|
|
14528
|
+
throw new SpreadsheetStructuredReferenceError('unsupported', 'Disjoint structured-reference columns are not supported.');
|
|
14529
|
+
}
|
|
14530
|
+
function tableItem(value) {
|
|
14531
|
+
const normalized = value.toLocaleLowerCase();
|
|
14532
|
+
if ('#all' === normalized) return 'all';
|
|
14533
|
+
if ('#headers' === normalized) return 'headers';
|
|
14534
|
+
if ('#data' === normalized) return 'data';
|
|
14535
|
+
if ('#totals' === normalized) return 'totals';
|
|
14536
|
+
if ('#this row' === normalized) return 'current';
|
|
14537
|
+
}
|
|
14538
|
+
function applyTableItem(rows, item, reference) {
|
|
14539
|
+
rows['current' === item ? 'current' : item] = true;
|
|
14540
|
+
if (rows.current && (rows.all || rows.headers || rows.data || rows.totals)) throw new SpreadsheetStructuredReferenceError('unsupported', `Structured reference '${reference}' cannot combine #This Row with another item.`);
|
|
14541
|
+
}
|
|
14542
|
+
function parsePlainColumn(value, reference) {
|
|
14543
|
+
if (!value || /[[\],:]/u.test(value)) throw invalidReference(reference);
|
|
14544
|
+
const column = decodeAtom(value);
|
|
14545
|
+
if (!column) throw invalidReference(reference);
|
|
14546
|
+
return column;
|
|
14547
|
+
}
|
|
14548
|
+
function decodeAtom(value) {
|
|
14549
|
+
let output = '';
|
|
14550
|
+
for(let cursor = 0; cursor < value.length; cursor += 1){
|
|
14551
|
+
const character = value[cursor];
|
|
14552
|
+
if ("'" === character) {
|
|
14553
|
+
const escaped = value[cursor + 1];
|
|
14554
|
+
if (void 0 === escaped) throw invalidReference(value);
|
|
14555
|
+
output += escaped;
|
|
14556
|
+
cursor += 1;
|
|
14557
|
+
} else output += character;
|
|
14558
|
+
}
|
|
14559
|
+
return output;
|
|
14560
|
+
}
|
|
14561
|
+
function missingRows(table, item) {
|
|
14562
|
+
return new SpreadsheetStructuredReferenceError('unsupported', `Structured reference ${item} requires table '${table.name}' to contain that row.`);
|
|
14563
|
+
}
|
|
14564
|
+
function invalidReference(reference) {
|
|
14565
|
+
return new SpreadsheetStructuredReferenceError('invalid', `Structured reference '${reference}' is not in a supported canonical form.`);
|
|
14566
|
+
}
|
|
14567
|
+
function quotedStringEnd(source, start) {
|
|
14568
|
+
for(let cursor = start + 1; cursor < source.length; cursor += 1)if ('"' === source[cursor]) {
|
|
14569
|
+
if ('"' === source[cursor + 1]) {
|
|
14570
|
+
cursor += 1;
|
|
14571
|
+
continue;
|
|
14572
|
+
}
|
|
14573
|
+
return cursor + 1;
|
|
14574
|
+
}
|
|
14575
|
+
return source.length;
|
|
14576
|
+
}
|
|
14577
|
+
function cellAddress(row, column) {
|
|
14578
|
+
let value = column + 1;
|
|
14579
|
+
let label = '';
|
|
14580
|
+
while(value > 0){
|
|
14581
|
+
value -= 1;
|
|
14582
|
+
label = String.fromCharCode(65 + value % 26) + label;
|
|
14583
|
+
value = Math.floor(value / 26);
|
|
14584
|
+
}
|
|
14585
|
+
return `${label}${row + 1}`;
|
|
14586
|
+
}
|
|
14587
|
+
function quoteSheetName(name) {
|
|
14588
|
+
return /^[A-Za-z_][A-Za-z0-9_.]*$/u.test(name) ? name : `'${name.replaceAll("'", "''")}'`;
|
|
14589
|
+
}
|
|
14590
|
+
function isNameStart(value) {
|
|
14591
|
+
return /^[A-Za-z_\\?]$/u.test(value);
|
|
14592
|
+
}
|
|
14593
|
+
function isNameContinue(value) {
|
|
14594
|
+
return /^[A-Za-z0-9_.\\?]$/u.test(value);
|
|
14595
|
+
}
|
|
14596
|
+
function isQualifierCharacter(value) {
|
|
14597
|
+
return /^[A-Za-z0-9_.\\?[\]:$]$/u.test(value);
|
|
14598
|
+
}
|
|
14023
14599
|
async function calculateSpreadsheetInJavaScript(request) {
|
|
14024
14600
|
validateSpreadsheetCalculationRequest(request);
|
|
14025
14601
|
const formulaParser = await Promise.resolve().then(__webpack_require__.bind(__webpack_require__, 909));
|
|
@@ -14035,9 +14611,11 @@
|
|
|
14035
14611
|
stack = [];
|
|
14036
14612
|
calculationOrder = [];
|
|
14037
14613
|
issues = [];
|
|
14614
|
+
tableCatalog;
|
|
14038
14615
|
constructor(request, formulaParser){
|
|
14039
14616
|
this.request = request;
|
|
14040
14617
|
this.formulaParser = formulaParser;
|
|
14618
|
+
this.tableCatalog = new SpreadsheetStructuredReferenceCatalog(request.sheets);
|
|
14041
14619
|
for (const sheet of request.sheets){
|
|
14042
14620
|
this.sheetsByName.set(sheet.name.toLowerCase(), sheet);
|
|
14043
14621
|
for (const cell of sheet.cells)this.cells.set(cellKey(sheet.id, cell.row, cell.column), {
|
|
@@ -14108,16 +14686,24 @@
|
|
|
14108
14686
|
}
|
|
14109
14687
|
evaluateFormula(coordinate, indexed) {
|
|
14110
14688
|
const formula = indexed.cell.formula ?? '';
|
|
14689
|
+
let expandedFormula;
|
|
14690
|
+
try {
|
|
14691
|
+
expandedFormula = expandSpreadsheetStructuredReferences(formula, this.tableCatalog, indexed.sheet, coordinate.row, coordinate.column);
|
|
14692
|
+
} catch (error) {
|
|
14693
|
+
const message = error instanceof SpreadsheetStructuredReferenceError ? error.message : 'Structured reference expansion failed.';
|
|
14694
|
+
return failedEvaluation(indexed.cell.value, calculationIssue(coordinate, 'office.kernel.spreadsheet.formula_unsupported', message));
|
|
14695
|
+
}
|
|
14111
14696
|
const parser = new this.formulaParser.Parser();
|
|
14112
14697
|
let unresolvedDependency = false;
|
|
14113
14698
|
let unsupportedReference = false;
|
|
14114
14699
|
let unsupportedFunction;
|
|
14115
14700
|
let materializedRangeCells = 0;
|
|
14116
14701
|
parser.setFunction('IFERROR', evaluateParserIfError).setFunction('ROW', (parameters)=>parameters.length ? null : coordinate.row + 1).setFunction('COLUMN', (parameters)=>parameters.length ? null : coordinate.column + 1);
|
|
14117
|
-
parser.on('callFunction', (name, parameters)=>{
|
|
14702
|
+
parser.on('callFunction', (name, parameters, done)=>{
|
|
14118
14703
|
const normalized = normalizeSpreadsheetFunctionName(name);
|
|
14119
14704
|
const arity = browserScalarFunctionArities.get(normalized);
|
|
14120
14705
|
if (!arity || parameters.length < arity[0] || parameters.length > arity[1]) unsupportedFunction ??= name.toUpperCase();
|
|
14706
|
+
if ('SUBTOTAL' === normalized) done(evaluateParserSubtotal(parameters));
|
|
14121
14707
|
});
|
|
14122
14708
|
parser.on('callCellValue', (cell, _options, done)=>{
|
|
14123
14709
|
const dependency = this.resolveCoordinate(indexed.sheet, cell);
|
|
@@ -14141,7 +14727,7 @@
|
|
|
14141
14727
|
return true;
|
|
14142
14728
|
}));
|
|
14143
14729
|
});
|
|
14144
|
-
const parsed = parser.parse(normalizeFormulaForFortuneParser(
|
|
14730
|
+
const parsed = parser.parse(normalizeFormulaForFortuneParser(expandedFormula), {
|
|
14145
14731
|
sheetId: indexed.sheet.id
|
|
14146
14732
|
});
|
|
14147
14733
|
if (unsupportedFunction) return failedEvaluation(indexed.cell.value, calculationIssue(coordinate, 'office.kernel.spreadsheet.formula_unsupported', `Formula function '${unsupportedFunction}' is not supported.`));
|
|
@@ -14165,7 +14751,8 @@
|
|
|
14165
14751
|
}
|
|
14166
14752
|
rangeValues(currentSheet, start, end, onUnresolvedDependency, onUnsupportedReference, reserveRangeCells) {
|
|
14167
14753
|
const startCoordinate = this.resolveCoordinate(currentSheet, start);
|
|
14168
|
-
const
|
|
14754
|
+
const rangeSheet = start.sheetName ? this.sheetsByName.get(start.sheetName.toLowerCase()) : currentSheet;
|
|
14755
|
+
const endCoordinate = rangeSheet ? this.resolveCoordinate(rangeSheet, end) : null;
|
|
14169
14756
|
if (!startCoordinate || !endCoordinate || startCoordinate.sheetId !== endCoordinate.sheetId) {
|
|
14170
14757
|
onUnsupportedReference();
|
|
14171
14758
|
return [];
|
|
@@ -14344,6 +14931,14 @@
|
|
|
14344
14931
|
return sheets.map((sheet)=>({
|
|
14345
14932
|
id: sheet.id,
|
|
14346
14933
|
name: sheet.name,
|
|
14934
|
+
...sheet.tables?.length ? {
|
|
14935
|
+
tables: sheet.tables.map((table)=>({
|
|
14936
|
+
...table,
|
|
14937
|
+
columns: [
|
|
14938
|
+
...table.columns
|
|
14939
|
+
]
|
|
14940
|
+
}))
|
|
14941
|
+
} : {},
|
|
14347
14942
|
cells: sheet.cells.map((cell)=>({
|
|
14348
14943
|
...cell,
|
|
14349
14944
|
value: {
|