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');
+ });
+});