Skip to content

Add JSON Logic comparison operators (<, <=, >, >=)#3484

Draft
ajpallares wants to merge 4 commits into
pallares/json-logic-evaluatorfrom
pallares/json-logic-comparison-operators
Draft

Add JSON Logic comparison operators (<, <=, >, >=)#3484
ajpallares wants to merge 4 commits into
pallares/json-logic-evaluatorfrom
pallares/json-logic-comparison-operators

Conversation

@ajpallares
Copy link
Copy Markdown
Member

@ajpallares ajpallares commented May 14, 2026

Resolves SDK-4328

Motivation

Extends the JSON Logic operator set with the comparison operators rule authors need to express numeric thresholds and ranges.

Summary

  • Implements <, <=, >, >= per the JSON Logic spec.
  • < and <= accept the 3-arg between form ({"<=": [1, x, 10]} reads as 1 <= x <= 10). > and >= are binary only, matching the JS reference.
  • All operators coerce operands through Value.toNumberOrNull and compare as Double. Non-numeric operands (ObjectValue, ArrayValue, unparseable strings) become Double.NaN; per IEEE 754 every comparison against NaN is false, so a malformed operand makes the predicate fail closed.

Tests

  • ComparisonOperatorsTest covers each operator (basic, between form, coercion, NaN propagation, no-between for > / >=, arity errors).
  • EvaluatorTest adds two integration tests through dispatch: var >= 3 and the 3-arg 1 <= var <= 10 between form.

Notes

  • String-vs-string semantics deviate from the JS reference. JS compares two strings lexicographically ("10" < "9" is true) and only coerces when types mix. We always coerce numerically, which gives the more intuitive "10" < "9" is false. Documented in the type's KDoc.
  • Stacked on top of RulesEngine: add JSON Logic predicate evaluator #3482. Re-target to main once that lands.

Made with Cursor

Extends the JSON Logic operator set with the comparison operators rule
authors need to express numeric thresholds and ranges.

What's new:

- `<`, `<=`, `>`, `>=` per the JSON Logic spec.
- `<` and `<=` accept the 3-arg between form (`{"<=": [1, x, 10]}`
  reads as `1 <= x <= 10`). `>` and `>=` are binary only, matching
  the JS reference.
- All operators coerce operands through `Value.toNumberOrNull` and
  compare as `Double`. Non-numeric operands (`ObjectValue`,
  `ArrayValue`, unparseable strings) become `Double.NaN`; per
  IEEE 754 every comparison against NaN is `false`, so a malformed
  operand makes the predicate fail closed.

String semantics deviate from the JS reference (which compares two
strings lexicographically, e.g. `"10" < "9"` is true). We always
coerce numerically — `"10" < "9"` is false. Documented in the type's
KDoc.

Tests:

- `ComparisonOperatorsTest` covers each operator (basic, between
  form, coercion, NaN propagation, no-between for `>` / `>=`, arity
  errors).
- `EvaluatorTest` adds two integration tests through dispatch:
  `var >= 3` and the 3-arg `1 <= var <= 10` between form.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ajpallares ajpallares added pr:feat A new feature pr:other and removed pr:feat A new feature labels May 14, 2026
ajpallares and others added 3 commits May 21, 2026 17:37
…nto pallares/json-logic-comparison-operators
Mirrors the iOS counterpart `a60dac602`. The original implementation
documented a deliberate deviation from the JSON Logic / JS spec and
always coerced operands to `Double` for `<`, `<=`, `>`, `>=`. The spec
(ECMAScript Abstract Relational Comparison) actually splits on operand
type:

- **Both operands are strings** → lexicographic comparison
  (`"10" < "9"` is `true` because `'1'` < `'9'` byte-wise).
- **Otherwise** → numeric coercion (with `Double.NaN` fall-back for
  non-coercible operands so comparisons fail closed per IEEE 754).

Refactor:

- `ComparisonOperators` gains a private `Comparator` enum that
  dispatches to two explicit `apply` overloads (one `Double`, one
  `String`) — Kotlin's primitive `<` on `Double` is IEEE-conformant
  while a generic `Comparable<Double>.compareTo` route would silently
  flip to total-order semantics (NaN becomes greater than +Infinity).
  Pinned in a doc comment on the enum.
- `compare(lhs, rhs, cmp)` picks the lex path when both operands are
  `Value.StringValue` and the numeric path otherwise.
- `evalChain` and `evalBinary` now take a `Comparator` (not a
  `(Double, Double) -> Boolean` lambda) and route through `compare`.

Tests:

- `lt string compared numerically not lexicographically` is replaced
  by `lt compares two strings lexicographically` (the new pin) and
  `lt mixed string and number coerces numerically` (mixed-type fall
  through to numeric coercion).
- New `le compares two strings lexicographically inclusive` and
  `gt compares two strings lexicographically` round out the
  cross-operator coverage.

Drive-by, from the merge of `pallares/json-logic-evaluator`: drop the
now-unused `RulesEngineLogger` parameter (and the `PrintlnLogger`
import) from `ComparisonOperators` / `ComparisonOperatorsTest`, and
fix the stale `, logger` references the auto-merge left in
`Operators.dispatch` for the comparison cases. Engine internals route
warnings through the module-level `Rules.logger` now, so threading the
logger through every operator call is no longer needed.

Verified: `:rules-engine:testDefaultsDebugUnitTest`, `detektAll`, and
`scripts/check-rules-engine-internal-only.sh` all green.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nto pallares/json-logic-comparison-operators
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant