diff --git a/lib/doc/cell.js b/lib/doc/cell.js index 251dded27..6f7363116 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'); @@ -840,6 +841,9 @@ class FormulaValue { if (v instanceof Date) { return Enums.ValueType.Date; } + if (typeof v === 'boolean') { + return Enums.ValueType.Boolean; + } if (v.text && v.hyperlink) { return Enums.ValueType.Hyperlink; } @@ -869,13 +873,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..1413154a3 100644 --- a/lib/xlsx/xform/sheet/cell-xform.js +++ b/lib/xlsx/xform/sheet/cell-xform.js @@ -107,9 +107,8 @@ class CellXform extends BaseXform { } else if (model.sharedFormula) { const master = options.formulae[model.sharedFormula]; if (!master) { - throw new Error( - `Shared Formula master must exist above and or left of clone for cell ${model.address}` - ); + const msg = `Shared Formula master must exist above and or left of clone for cell ${model.address}`; + throw new Error(msg); } if (master.si === undefined) { master.shareType = 'shared'; @@ -348,7 +347,9 @@ class CellXform extends BaseXform { // first guess on cell type if (model.formula || model.shareType) { model.type = Enums.ValueType.Formula; - if (model.value) { + // Use !== undefined so an empty (e.g. result === '') + // is preserved instead of dropped. See exceljs/exceljs#2943. + if (model.value !== undefined) { if (this.t === 'str') { model.result = utils.xmlDecode(model.value); } else if (this.t === 'b') { @@ -359,6 +360,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..8848ac000 --- /dev/null +++ b/spec/integration/issues/issue-2943-formula-falsy-result.spec.js @@ -0,0 +1,124 @@ +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('A1').value = 5; + 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('A1').value = 5; + 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'); + }); +});