Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workspace-dep-closures.json
Original file line number Diff line number Diff line change
Expand Up @@ -4877,6 +4877,7 @@
"crates/macro_env_var",
"crates/macro_event_broker",
"crates/macro_event_topics",
"crates/macro_queues",
"crates/macro_service_urls",
"crates/macro_sync_service_jwt",
"crates/macro_tower_layers",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ Notices:
- U.S. federal works (SEC, Census, BLS, BEA, IRS, SBA, GSA) are in the public domain; the FRED export carries BLS series data.

Templates whose terms forbid redistribution (Microsoft Create, Vertex42, Breaking Into Wall Street, Damodaran, SBDC) were used only for local testing and are not committed. Add a file by copying it here unmodified and adding its `file`, `title`, `source` and `license` to the manifest; set `skipCalculation` with a reason only when the engine cannot finish it.

`upgraded-from-xls.xlsx` is a legacy Excel 97 workbook (`services/convert_service/fixtures/legacy/budget.xls`) converted to `.xlsx` by LibreOffice with the `Calc MS Excel 2007 XML` filter, exactly as the convert service upgrades uploaded `.xls` files. `../xlsx-upgraded-legacy.test.ts` checks that it imports into a native spreadsheet with its sheets, formulas, merges and number formats.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { initSync } from '@ironcalc/wasm';
import { createInitializedSpreadsheetCalculator } from '@macro-inc/spreadsheet/calculation';
import {
importSpreadsheetSheets,
readSpreadsheetWorkbook,
} from '@macro-inc/spreadsheet/workbook-document';
import ExcelJS from 'exceljs';
import { LoroDoc } from 'loro-crdt';
import { beforeAll, describe, expect, it } from 'vitest';
import { decodeXlsx, encodeXlsx } from './xlsx-codec';

/**
* An uploaded legacy `.xls` is upgraded to `.xlsx` by LibreOffice on the
* server; this is that output for a two-sheet budget. It must import into a
* native spreadsheet like any Excel-written workbook.
*/
const upgraded = () =>
new Uint8Array(
readFileSync(
createRequire(import.meta.url).resolve(
'./xlsx-fixtures/upgraded-from-xls.xlsx'
)
)
);

beforeAll(() =>
initSync({
module: readFileSync(
createRequire(import.meta.url).resolve('@ironcalc/wasm/wasm_bg.wasm')
),
})
);

describe('workbook upgraded from a legacy .xls', () => {
it('imports both sheets with their cells and formulas', async () => {
const imported = await decodeXlsx(upgraded());

expect(imported.sheets.map((sheet) => sheet.name)).toEqual([
'Budget',
'Notes',
]);
const [budget, notes] = imported.sheets;
expect(budget.cells.A1.value).toBe('Month');
// Stored as text in the .xls, so it stays text rather than becoming a date.
expect(budget.cells.A2.value).toBe("'2026-01");
expect(budget.cells.B2.value).toBe('1050');
expect(budget.cells.D2.value).toBe('=B2-C2');
expect(budget.cells.D13.value).toBe('=B13-C13');
expect(budget.cells.A15.value).toBe('Total');
expect(budget.cells.B15.value).toBe('=SUM(B2:B13)');
expect(budget.cells.D15.value).toBe('=SUM(D2:D13)');
expect(budget.cells.F1.value).toBe('Merged header');
expect(notes.cells.B1.value).toBe('Finance');
expect(notes.cells.B2.value).toBe('=Budget!D15/Budget!B15');
});

it('keeps header styles, number formats and the merged header', async () => {
const imported = await decodeXlsx(upgraded());
const budget = imported.sheets[0];

expect(budget.cells.A1).toMatchObject({
bold: true,
fillColor: '#DDEBF7',
});
// `#,##0.00` imports as Macro's thousands-separated number format.
expect(budget.cells.B15).toMatchObject({ format: 'number', decimals: 2 });
expect(budget.metadata?.merges).toEqual(['F1:H1']);
expect(imported.warnings).toEqual([
'Merged ranges are shown as individual cells in Macro and restored on Excel export when their covered cells remain empty.',
]);
});

it('calculates the same results after native save and reopen', async () => {
const imported = await decodeXlsx(upgraded());
const doc = new LoroDoc();
const reopened = new LoroDoc();
const engine = createInitializedSpreadsheetCalculator();
try {
importSpreadsheetSheets(doc, imported.sheets, true);
reopened.import(doc.export({ mode: 'snapshot' }));
const sheets = readSpreadsheetWorkbook(reopened);
const results = engine.calculateWorkbook(
sheets.map((sheet) => ({ ...sheet, rowCount: sheet.layout.rowCount })),
{ includeTypes: true }
);

const budget = results[sheets[0].id];
// Revenue 1000 + 50i and cost 600 + 20i for months 1..12.
expect(budget.B15.number).toBe(15_900);
expect(budget.C15.number).toBe(8_760);
expect(budget.D15.number).toBe(7_140);
expect(budget.D2.number).toBe(430);
expect(budget.B15.display).toBe('15,900.00');
expect(results[sheets[1].id].B2.number).toBeCloseTo(7_140 / 15_900, 12);
for (const values of Object.values(results)) {
expect(
Object.entries(values).filter(([, value]) => value.error)
).toEqual([]);
}
} finally {
engine.dispose();
}
});

it('exports back to an .xlsx Excel reads with the same formulas', async () => {
const imported = await decodeXlsx(upgraded());

const exported = await encodeXlsx({ sheets: imported.sheets });

const external = new ExcelJS.Workbook();
await external.xlsx.load(exported.bytes.slice().buffer);
const budget = external.getWorksheet('Budget')!;
expect(budget.getCell('B15').formula).toBe('SUM(B2:B13)');
expect(budget.getCell('D2').formula).toBe('B2-C2');
expect(budget.getCell('F1').isMerged).toBe(true);
expect(external.getWorksheet('Notes')!.getCell('B2').formula).toBe(
'Budget!D15/Budget!B15'
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -529,9 +529,16 @@ export function SpreadsheetEditor(props: {

async function importFile(file: File | undefined) {
if (!file || !editable()) return;
if (/\.xls$/i.test(file.name)) {
// Converting .xls to .xlsx needs the server; uploads are upgraded.
actions.setNotice(
'Legacy .xls workbooks are converted when uploaded. Upload the file, then choose Edit in Macro.'
);
return;
}
if (!/\.(csv|xlsx)$/i.test(file.name)) {
actions.setNotice(
'Choose a .csv or .xlsx file. Legacy .xls and macro-enabled workbooks are not supported.'
'Choose a .csv or .xlsx file. Macro-enabled workbooks are not supported.'
);
return;
}
Expand Down
Loading
Loading