diff --git a/lib/doc/cell.js b/lib/doc/cell.js index 251dded27..cd67ff6ae 100644 --- a/lib/doc/cell.js +++ b/lib/doc/cell.js @@ -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'); @@ -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(); } } diff --git a/lib/xlsx/xform/sheet/cell-xform.js b/lib/xlsx/xform/sheet/cell-xform.js index 41715695a..a9e2f8396 100644 --- a/lib/xlsx/xform/sheet/cell-xform.js +++ b/lib/xlsx/xform/sheet/cell-xform.js @@ -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 (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') { @@ -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: ... + model.result = ''; } } else if (model.value !== undefined) { switch (this.t) { diff --git a/spec/integration/issues/issue-2943-formula-falsy-result.spec.js b/spec/integration/issues/issue-2943-formula-falsy-result.spec.js new file mode 100644 index 000000000..28b42b1b8 --- /dev/null +++ b/spec/integration/issues/issue-2943-formula-falsy-result.spec.js @@ -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'); + }); +});