Skip to content
Merged
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
9 changes: 6 additions & 3 deletions src/foam/mlang/Constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ foam.CLASS({
return '\"' + this.value + '\"';

if ( typeof this.value === 'number' && ! Number.isInteger(this.value) && isFinite(this.value) ) {
var s = this.value.toFixed(20);
s = s.replace(/0+$/, '');
if ( s.endsWith('.') ) s += '0';
var s = String(this.value);
if ( s.indexOf('e') !== -1 || s.indexOf('E') !== -1 ) {
s = this.value.toFixed(20);
s = s.replace(/0+$/, '');
if ( s.endsWith('.') ) s += '0';
}
return s;
}

Expand Down
14 changes: 9 additions & 5 deletions src/foam/parse/SimpleQueryParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,21 @@ private foam.lib.parse.Grammar buildGrammar() {
// ───────── Primitive Symbols ─────────

private void buildPrimitiveSymbols(foam.lib.parse.Grammar g) {
// digits: one or more digit chars joined into a string
g.addSymbol("digits", new Join(new Repeat(Range.create('0', '9'), 1)));
// rawDigits: base grammar for one or more digit chars, no action (preserves leading zeros)
g.addSymbol("rawDigits", new Join(new Repeat(Range.create('0', '9'), 1)));

// float: optional negative, digits, optional decimal part → joined string
// digits: delegates to rawDigits, but has a parseInt action (see buildActions)
g.addSymbol("digits", g.sym("rawDigits"));

// float: optional negative, rawDigits, optional decimal part → joined string
// Uses rawDigits (not digits) to avoid parseInt stripping leading zeros (e.g., "001" → 1)
g.addSymbol("float",
new Seq1(1,
g.sym("ws"),
new Join(new Seq(
new Optional(Literal.create("-")),
g.sym("digits"),
new Optional(new Join(new Seq(Literal.create("."), new Optional(g.sym("digits")))))))));
g.sym("rawDigits"),
new Optional(new Join(new Seq(Literal.create("."), new Optional(g.sym("rawDigits")))))))));

// floats: two or more floats separated by comma (for IN RANGE)
g.addSymbol("floats", new Repeat(g.sym("float"), Literal.create(","), 2));
Expand Down
8 changes: 6 additions & 2 deletions src/foam/parse/SimpleQueryParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,14 @@ foam.CLASS({

'range float': seq1(1, sym('ws'), sym('floats'), sym('ws'), ')'),

digits: str(repeat(range('0', '9'), null, 1)),
// rawDigits: base grammar for one or more digit chars, no action (preserves leading zeros)
rawDigits: str(repeat(range('0', '9'), null, 1)),

// digits: delegates to rawDigits, but has a parseInt action (see actions below)
digits: sym('rawDigits'),

// TODO replace '.' with an internationalized decimal point, or have the input preprocessed
float: seq1(1, sym('ws'), str(seq(optional('-'), sym('digits'), optional(str(seq('.', optional(sym('digits')))))))),
float: seq1(1, sym('ws'), str(seq(optional('-'), sym('rawDigits'), optional(str(seq('.', optional(sym('rawDigits')))))))),

numberArray: seq1(1, sym('ws'), sym('numbers'), sym('ws'), ')'),

Expand Down
29 changes: 29 additions & 0 deletions src/foam/parse/test/SimpleQueryParserJavaTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,35 @@ foam.CLASS({
ft12.toLowerCase().contains("latitude"),
"Float Test12: Inner property NOT IN RANGE produces OR — got: " + ft12
);

// Float Test13: Small float IN RANGE preserves decimal precision
String ft13 = buildPredicate("address.latitude IN RANGE (-0.0001, 0.0001)");
test(ft13 != null, "Float Test13: Small float IN RANGE (-0.0001, 0.0001) parses");
test(
ft13.contains("-1.000001E-4") || ft13.contains("-0.0001000001") || ft13.contains("-1.0001E-4"),
"Float Test13: Small float IN RANGE lower bound preserves magnitude — got: " + ft13
);
test(
! ft13.contains("0.1") || ft13.contains("0.0001"),
"Float Test13: Small float IN RANGE does NOT corrupt to 0.1 — got: " + ft13
);

// Float Test14: Small float equality preserves decimal precision
String ft14 = buildPredicate("address.longitude = 0.001");
test(ft14 != null, "Float Test14: Small float = 0.001 parses");
test(
! ft14.contains("0.1,") && ! ft14.contains(", 0.1"),
"Float Test14: Small float = 0.001 does NOT corrupt to 0.1 — got: " + ft14
);

// Float Test15: Small float NOT IN RANGE preserves precision
String ft15 = buildPredicate("address.latitude NOT IN RANGE (-0.0001, 0.0001)");
test(ft15 != null, "Float Test15: Small float NOT IN RANGE parses");
test(
ft15.toLowerCase().contains("or(") &&
ft15.toLowerCase().contains("gte(") && ft15.toLowerCase().contains("lt("),
"Float Test15: Small float NOT IN RANGE produces OR(GTE,LT) — got: " + ft15
);
`
},

Expand Down
53 changes: 49 additions & 4 deletions src/foam/parse/test/SimpleQueryParserMQLTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ foam.CLASS({
var EPSILON = 0.0000000001;
var f = function(num) { return num - EPSILON; };
var g = function(num) { return num + EPSILON; };
// Format a float the same way Constant.toMQL does
// Expected float formatting: String() for normal decimals, toFixed for scientific notation
var fmt = function(num) {
var s = num.toFixed(20);
s = s.replace(/0+$/, '');
if ( s.endsWith('.') ) s += '0';
var s = String(num);
if ( s.indexOf('e') !== -1 || s.indexOf('E') !== -1 ) {
s = num.toFixed(20);
s = s.replace(/0+$/, '');
if ( s.endsWith('.') ) s += '0';
}
return s;
};
// Format a date the same way Constant.toMQL does (truncate at second colon)
Expand Down Expand Up @@ -120,6 +123,43 @@ foam.CLASS({
'(latitude>=' + fmt(g(8.5)) + ' OR latitude<' + fmt(f(6.5)) + ')'),
'Float MQL 8: NOT IN RANGE');

// ── Float small-value precision (Constant.toMQL must not corrupt small floats) ──
x.test(this.constantToMQL(0.001) === '0.001',
'Float MQL constant 1: 0.001 formats correctly, got: ' + this.constantToMQL(0.001));
x.test(this.constantToMQL(0.0001) === '0.0001',
'Float MQL constant 2: 0.0001 formats correctly, got: ' + this.constantToMQL(0.0001));
x.test(this.constantToMQL(-0.0001) === '-0.0001',
'Float MQL constant 3: -0.0001 formats correctly, got: ' + this.constantToMQL(-0.0001));
x.test(this.constantToMQL(0.5) === '0.5',
'Float MQL constant 4: 0.5 formats correctly, got: ' + this.constantToMQL(0.5));
x.test(this.constantToMQL(0.1) === '0.1',
'Float MQL constant 5: 0.1 formats correctly, got: ' + this.constantToMQL(0.1));
x.test(this.constantToMQL(1.23) === '1.23',
'Float MQL constant 6: 1.23 formats correctly, got: ' + this.constantToMQL(1.23));
x.test(this.constantToMQL(0.00000001) === '0.00000001',
'Float MQL constant 7: 0.00000001 formats correctly, got: ' + this.constantToMQL(0.00000001));

// ── Float epsilon-adjusted small values (the values that actually appear in MQL) ──
x.test(this.constantToMQL(f(0.0001)) === String(f(0.0001)),
'Float MQL epsilon 1: f(0.0001) no corruption, got: ' + this.constantToMQL(f(0.0001)));
x.test(this.constantToMQL(g(0.0001)) === String(g(0.0001)),
'Float MQL epsilon 2: g(0.0001) no corruption, got: ' + this.constantToMQL(g(0.0001)));
x.test(this.constantToMQL(f(-0.0001)) === String(f(-0.0001)),
'Float MQL epsilon 3: f(-0.0001) no corruption, got: ' + this.constantToMQL(f(-0.0001)));
x.test(this.constantToMQL(g(-0.0001)) === String(g(-0.0001)),
'Float MQL epsilon 4: g(-0.0001) no corruption, got: ' + this.constantToMQL(g(-0.0001)));

// ── Float IN RANGE with small values (user-reported bug) ──
x.test(this.isValidMQL('address.latitude IN RANGE (-0.0001, 0.0001)',
'latitude>=' + String(f(-0.0001)) + ' AND latitude<' + String(g(0.0001))),
'Float MQL small range 1: IN RANGE (-0.0001, 0.0001)');
x.test(this.isValidMQL('address.latitude NOT IN RANGE (-0.0001, 0.0001)',
'(latitude>=' + String(g(0.0001)) + ' OR latitude<' + String(f(-0.0001)) + ')'),
'Float MQL small range 2: NOT IN RANGE (-0.0001, 0.0001)');
x.test(this.isValidMQL('address.latitude IN RANGE (0.001, 0.01)',
'latitude>=' + String(f(0.001)) + ' AND latitude<' + String(g(0.01))),
'Float MQL small range 3: IN RANGE (0.001, 0.01)');

// ── Float scientific notation guard ──
x.test(this.hasNoScientificNotation(this.buildMQL('address.longitude = 0')),
'Float MQL 9: equals zero has no scientific notation');
Expand Down Expand Up @@ -235,6 +275,11 @@ foam.CLASS({
return mql === expectedMQL;
},

function constantToMQL(value) {
var c = foam.mlang.Constant.create({ value: value });
return c.toMQL();
},

function hasNoScientificNotation(mql) {
if ( mql == null ) return false;
console.log('Scientific notation check: ' + mql);
Expand Down
14 changes: 14 additions & 0 deletions src/foam/parse/test/SimpleQueryParserTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ foam.CLASS({
x.test(this.isValid("address.latitude IN RANGE (6.5, 8.5)", "AND(GTE(foam.core.auth.User.address.foam.core.auth.Address.latitude, " + testFloatStart(6.5) + "),LT(foam.core.auth.User.address.foam.core.auth.Address.latitude, " + testFloatEnd(8.5) + "))"), "Float Test11: Inner property is within range 6.5 to 8.5");
x.test(this.isValid('address.latitude NOT IN RANGE (6.5, 8.5)', "OR(GTE(foam.core.auth.User.address.foam.core.auth.Address.latitude, " + testFloatEnd(8.5) + "),LT(foam.core.auth.User.address.foam.core.auth.Address.latitude, " + testFloatStart(6.5) + "))"), 'Float Test12: Inner property is not within range 6.5 to 8.5');

// Float small-value precision tests — parser must preserve leading zeros after decimal
x.test(this.isValidSymbol('float', '0.001', '0.001', true), 'Float Test13: Small float 0.001 preserves precision');

// Float small-value IN RANGE — the user-reported bug
x.test(this.isValid("address.latitude IN RANGE (-0.0001, 0.0001)",
"AND(GTE(foam.core.auth.User.address.foam.core.auth.Address.latitude, " + testFloatStart(-0.0001) + "),LT(foam.core.auth.User.address.foam.core.auth.Address.latitude, " + testFloatEnd(0.0001) + "))"),
"Float Test16: Small float IN RANGE (-0.0001, 0.0001)");
x.test(this.isValid('address.latitude NOT IN RANGE (-0.0001, 0.0001)',
"OR(GTE(foam.core.auth.User.address.foam.core.auth.Address.latitude, " + testFloatEnd(0.0001) + "),LT(foam.core.auth.User.address.foam.core.auth.Address.latitude, " + testFloatStart(-0.0001) + "))"),
'Float Test17: Small float NOT IN RANGE (-0.0001, 0.0001)');
x.test(this.isValid("address.longitude = 0.001",
"AND(GTE(foam.core.auth.User.address.foam.core.auth.Address.longitude, " + testFloatStart(0.001) + "),LT(foam.core.auth.User.address.foam.core.auth.Address.longitude, " + testFloatEnd(0.001) + "))"),
"Float Test18: Small float equals 0.001");

// Date format tests
x.test(this.isValidSymbol('date', '2025-01-01', [testDate([2025, 0, 1, 12]), testDate([2025, 0, 2, 12])].toString()), 'Date Test1: ISO date YYYY-MM-DD');
x.test(this.isValidSymbol('date', '25/10/01', [testDate([2025, 9, 1, 12]), testDate([2025, 9, 2, 12])].toString()), 'Date Test2: Short date YY/MM/DD');
Expand Down
Loading