Skip to content
Open
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
17 changes: 12 additions & 5 deletions lib/doc/cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -749,9 +749,10 @@ class FormulaValue {
_copyModel(model) {
const copy = {};
const cp = name => {
const value = model[name];
if (value) {
copy[name] = value;
// Use 'in' check so falsy results (0, false, '') survive the copy.
// See exceljs/exceljs#2943.
if (name in model && model[name] !== undefined) {
copy[name] = model[name];
}
};
cp('formula');
Expand Down Expand Up @@ -869,13 +870,19 @@ class FormulaValue {
}

toCsvString() {
return `${this.model.result || ''}`;
// Preserve falsy results (0, false, '') instead of collapsing to ''.
// See exceljs/exceljs#2943.
const {result} = this.model;
return result === null || result === undefined ? '' : `${result}`;
}

release() {}

toString() {
return this.model.result ? this.model.result.toString() : '';
// Preserve falsy results (0, false, '') instead of collapsing to ''.
// See exceljs/exceljs#2943.
const {result} = this.model;
return result === null || result === undefined ? '' : result.toString();
}
}

Expand Down
7 changes: 6 additions & 1 deletion lib/xlsx/xform/sheet/cell-xform.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ class CellXform extends BaseXform {
// first guess on cell type
if (model.formula || model.shareType) {
model.type = Enums.ValueType.Formula;
if (model.value) {
if (model.value !== undefined) {
// Use !== undefined so an empty <v></v> (e.g. result === '')
// is preserved instead of dropped. See exceljs/exceljs#2943.
if (this.t === 'str') {
model.result = utils.xmlDecode(model.value);
} else if (this.t === 'b') {
Expand All @@ -359,6 +361,9 @@ class CellXform extends BaseXform {
model.result = parseFloat(model.value);
}
model.value = undefined;
} else if (this.t === 'str') {
// Empty string formula result: <c t="str"><f>...</f><v/></c>
model.result = '';
}
} else if (model.value !== undefined) {
switch (this.t) {
Expand Down
107 changes: 107 additions & 0 deletions spec/integration/issues/issue-2943-formula-falsy-result.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const ExcelJS = verquire('exceljs');

// Regression test for exceljs/exceljs#2943.
// Copying a formula cell whose cached result is a falsy value (0, false, '')
// previously dropped the `result` field because `_copyModel` used a truthy
// check (`if (value)`).

describe('github issue 2943 - copy formula cell with falsy result', () => {
it('preserves result === 0 when copying formula cell value', () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
ws.getCell('B1').value = {formula: 'A1-A1', result: 0};

ws.getCell('C1').value = ws.getCell('B1').value;

expect(ws.getCell('C1').value).to.deep.equal({formula: 'A1-A1', result: 0});
expect(ws.getCell('C1').value.result).to.equal(0);
});

it('preserves result === false when copying formula cell value', () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
ws.getCell('B1').value = {formula: 'FALSE()', result: false};

ws.getCell('C1').value = ws.getCell('B1').value;

expect(ws.getCell('C1').value).to.deep.equal({formula: 'FALSE()', result: false});
expect(ws.getCell('C1').value.result).to.equal(false);
});

it('preserves result === "" when copying formula cell value', () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
ws.getCell('B1').value = {formula: '""', result: ''};

ws.getCell('C1').value = ws.getCell('B1').value;

expect(ws.getCell('C1').value).to.deep.equal({formula: '""', result: ''});
expect(ws.getCell('C1').value.result).to.equal('');
});

it('preserves result === 0 across xlsx round-trip', async () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
ws.getCell('B1').value = {formula: 'A1-A1', result: 0};

const buffer = await wb.xlsx.writeBuffer();
const wb2 = new ExcelJS.Workbook();
await wb2.xlsx.load(buffer);

expect(wb2.getWorksheet('s').getCell('B1').value).to.deep.equal({formula: 'A1-A1', result: 0});
});

it('preserves result === false across xlsx round-trip', async () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
ws.getCell('B1').value = {formula: 'FALSE()', result: false};

const buffer = await wb.xlsx.writeBuffer();
const wb2 = new ExcelJS.Workbook();
await wb2.xlsx.load(buffer);

expect(wb2.getWorksheet('s').getCell('B1').value).to.deep.equal({
formula: 'FALSE()',
result: false,
});
});

it('preserves result === "" across xlsx round-trip', async () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
ws.getCell('B1').value = {formula: '""', result: ''};

const buffer = await wb.xlsx.writeBuffer();
const wb2 = new ExcelJS.Workbook();
await wb2.xlsx.load(buffer);

expect(wb2.getWorksheet('s').getCell('B1').value).to.deep.equal({formula: '""', result: ''});
});

it('toCsvString returns "0" for falsy numeric result', () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
const cell = ws.getCell('B1');
cell.value = {formula: 'A1-A1', result: 0};

expect(cell.toCsvString()).to.equal('0');
});

it('toString returns "0" for falsy numeric result', () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
const cell = ws.getCell('B1');
cell.value = {formula: 'A1-A1', result: 0};

expect(cell.toString()).to.equal('0');
});

it('toCsvString returns "false" for boolean false result', () => {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet('s');
const cell = ws.getCell('B1');
cell.value = {formula: 'FALSE()', result: false};

expect(cell.toCsvString()).to.equal('false');
});
});