Skip to content

Commit b025f16

Browse files
committed
[FIX] evaluation: fix floating point precision
Steps to reproduce =0.1+0.1+0.1=0.3 => FALSE but it should be TRUE This is of course the well known issue of floating point precision. Excel is rounding up to 1e-15 (for basic operators at least) =0.3+1E-16=0.3 => TRUE =0.3+1E-15=0.3 => FALSE There are some places where Excel gets it wrong (=DELTA(0.3,0.1+0.1+0.1)) We'll never get rid of floating point precision issues, unless we use a decimal representation instead of binary. Using decimals is unrealistic: huge impact on the codebase, performance. Excel doesn't do it, accounting doesn't do it. We shouldn't do it either. Let's fix the basic arithmetic operators eq, lt, lte, gt and gte. closes #6391 Task: 4766910 Signed-off-by: Pierre Rousseau (pro) <pro@odoo.com> Signed-off-by: Lucas Lefèvre (lul) <lul@odoo.com>
1 parent 8e88baf commit b025f16

File tree

2 files changed

+59
-0
lines changed

2 files changed

+59
-0
lines changed

src/functions/module_operators.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ function isEmpty(data: Maybe<FunctionResultObject>): boolean {
8383

8484
const getNeutral = { number: 0, string: "", boolean: false };
8585

86+
function areAlmostEqual(value1: number, value2: number, epsilon: number = 2e-16): boolean {
87+
return Math.abs(value1 - value2) < epsilon;
88+
}
89+
8690
export const EQ = {
8791
description: _t("Equal."),
8892
args: [
@@ -107,6 +111,9 @@ export const EQ = {
107111
if (isEvaluationError(_value2)) {
108112
throw value2;
109113
}
114+
if (typeof _value1 === "number" && typeof _value2 === "number") {
115+
return areAlmostEqual(_value1, _value2);
116+
}
110117
return _value1 === _value2;
111118
},
112119
} satisfies AddFunctionDescription;
@@ -155,6 +162,9 @@ export const GT = {
155162
value2: Maybe<FunctionResultObject>
156163
): boolean {
157164
return applyRelationalOperator(value1, value2, (v1, v2) => {
165+
if (typeof v1 === "number" && typeof v2 === "number") {
166+
return !areAlmostEqual(v1, v2) && v1 > v2;
167+
}
158168
return v1 > v2;
159169
});
160170
},
@@ -174,6 +184,9 @@ export const GTE = {
174184
value2: Maybe<FunctionResultObject>
175185
): boolean {
176186
return applyRelationalOperator(value1, value2, (v1, v2) => {
187+
if (typeof v1 === "number" && typeof v2 === "number") {
188+
return areAlmostEqual(v1, v2) || v1 > v2;
189+
}
177190
return v1 >= v2;
178191
});
179192
},

tests/functions/module_operators.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ describe("EQ formula", () => {
147147
expect(evaluateCell("A1", { A1: "=EQ(FALSE, )" })).toBe(true);
148148
});
149149

150+
test("floating point rounding", () => {
151+
expect(evaluateCell("A1", { A1: "=EQ(0.3, 0.1+0.1+0.1)" })).toBe(true);
152+
expect(evaluateCell("A1", { A1: "=EQ(0.3, 0.3+1e-16)" })).toBe(true);
153+
expect(evaluateCell("A1", { A1: "=EQ(0.3, 0.3+1e-15)" })).toBe(false);
154+
});
155+
150156
test("functional tests on cell arguments", () => {
151157
expect(evaluateCell("A1", { A1: "=EQ(A2, A3)" })).toBe(true);
152158
expect(evaluateCell("A1", { A1: "=EQ(A2, A3)", A3: "0" })).toBe(true);
@@ -252,6 +258,13 @@ describe("GT formula", () => {
252258
expect(evaluateCell("A1", { A1: "=GT(A2, A3)", A2: '="1"', A3: "99999" })).toBe(true);
253259
expect(evaluateCell("A1", { A1: "=GT(A2, A3)", A2: '="1"', A3: '="99999"' })).toBe(false);
254260
});
261+
262+
test("floating point rounding", () => {
263+
expect(evaluateCell("A1", { A1: "=GT(0.3, 0.1+0.1+0.1)" })).toBe(false);
264+
expect(evaluateCell("A1", { A1: "=GT(0.3, 0.3+1e-16)" })).toBe(false);
265+
expect(evaluateCell("A1", { A1: "=GT(0.3, 0.3-1e-16)" })).toBe(false);
266+
expect(evaluateCell("A1", { A1: "=GT(0.3, 0.3-1e-15)" })).toBe(true);
267+
});
255268
});
256269

257270
describe("GTE formula", () => {
@@ -304,6 +317,14 @@ describe("GTE formula", () => {
304317
expect(evaluateCell("A1", { A1: '=GTE("1", "99999")' })).toBe(false);
305318
});
306319

320+
test("floating point rounding", () => {
321+
expect(evaluateCell("A1", { A1: "=GTE(0.3, 0.1+0.1+0.1)" })).toBe(true);
322+
expect(evaluateCell("A1", { A1: "=GTE(0.3, 0.3+1e-16)" })).toBe(true);
323+
expect(evaluateCell("A1", { A1: "=GTE(0.3, 0.3-1e-16)" })).toBe(true);
324+
expect(evaluateCell("A1", { A1: "=GTE(0.3, 0.3-1e-15)" })).toBe(true);
325+
expect(evaluateCell("A1", { A1: "=GTE(0.3, 0.3+1e-15)" })).toBe(false);
326+
});
327+
307328
test("functional tests on cell arguments", () => {
308329
expect(evaluateCell("A1", { A1: "=GTE(A2, A3)" })).toBe(true);
309330
expect(evaluateCell("A1", { A1: "=GTE(A2, A3)", A2: "1" })).toBe(true);
@@ -431,6 +452,13 @@ describe("LT formula", () => {
431452
expect(evaluateCell("A1", { A1: "=LT(A2, 42)", A2: "=KABOUM" })).toBe("#BAD_EXPR");
432453
expect(evaluateCell("A1", { A1: "=LT(KABOUM, KABOUM)" })).toBe("#BAD_EXPR");
433454
});
455+
456+
test("floating point rounding", () => {
457+
expect(evaluateCell("A1", { A1: "=LT(0.3, 0.1+0.1+0.1)" })).toBe(false);
458+
expect(evaluateCell("A1", { A1: "=LT(0.3, 0.3+1e-16)" })).toBe(false);
459+
expect(evaluateCell("A1", { A1: "=LT(0.3, 0.3-1e-16)" })).toBe(false);
460+
expect(evaluateCell("A1", { A1: "=LT(0.3, 0.3+1e-15)" })).toBe(true);
461+
});
434462
});
435463

436464
describe("LTE formula", () => {
@@ -483,6 +511,10 @@ describe("LTE formula", () => {
483511
expect(evaluateCell("A1", { A1: '=LTE("1", "99999")' })).toBe(true);
484512
});
485513

514+
test("floating point rounding", () => {
515+
expect(evaluateCell("A1", { A1: "=LTE(0.3, 0.1+0.1+0.1)" })).toBe(true);
516+
});
517+
486518
test("functional tests on cell arguments", () => {
487519
expect(evaluateCell("A1", { A1: "=LTE(A2, A3)" })).toBe(true);
488520
expect(evaluateCell("A1", { A1: "=LTE(A2, A3)", A2: "1" })).toBe(false);
@@ -521,6 +553,14 @@ describe("LTE formula", () => {
521553
expect(evaluateCell("A1", { A1: "=LTE(A2, 42)", A2: "=KABOUM" })).toBe("#BAD_EXPR");
522554
expect(evaluateCell("A1", { A1: "=LTE(KABOUM, KABOUM)" })).toBe("#BAD_EXPR");
523555
});
556+
557+
test("floating point rounding", () => {
558+
expect(evaluateCell("A1", { A1: "=LTE(0.3, 0.1+0.1+0.1)" })).toBe(true);
559+
expect(evaluateCell("A1", { A1: "=LTE(0.3, 0.3+1e-16)" })).toBe(true);
560+
expect(evaluateCell("A1", { A1: "=LTE(0.3, 0.3-1e-16)" })).toBe(true);
561+
expect(evaluateCell("A1", { A1: "=LTE(0.3, 0.3-1e-15)" })).toBe(false);
562+
expect(evaluateCell("A1", { A1: "=LTE(0.3, 0.3+1e-15)" })).toBe(true);
563+
});
524564
});
525565

526566
describe("MINUS formula", () => {
@@ -656,6 +696,12 @@ describe("NE formula", () => {
656696
expect(evaluateCell("A1", { A1: "=NE(A2, 42)", A2: "=KABOUM" })).toBe("#BAD_EXPR");
657697
expect(evaluateCell("A1", { A1: "=NE(KABOUM, KABOUM)" })).toBe("#BAD_EXPR");
658698
});
699+
700+
test("floating point rounding", () => {
701+
expect(evaluateCell("A1", { A1: "=NE(0.3, 0.1+0.1+0.1)" })).toBe(false);
702+
expect(evaluateCell("A1", { A1: "=NE(0.3, 0.3+1e-16)" })).toBe(false);
703+
expect(evaluateCell("A1", { A1: "=NE(0.3, 0.3+1e-15)" })).toBe(true);
704+
});
659705
});
660706

661707
describe("POW formula", () => {

0 commit comments

Comments
 (0)