Decimal numbers
Arithmetic is decimal, so 0.1 + 0.2 is 0.3. Every number that leaves an expression has at most 15 significant digits.
Inside an expression, arithmetic is decimal, not binary floating point. So 0.1 + 0.2 is exactly
0.3, in every language. Every number that leaves an expression — a computed value, a message
argument, a command payload — is rounded half even to 15 significant digits. That is the most a JSON
number keeps through any parser unchanged.
Syntax
{ op: add, args: [0.1, 0.2] } # 0.3, exactly
{ op: div, args: [{ var: data.price }, 3] } # leaves the expression with 15 significant digits| Step | Precision |
|---|---|
| A number written in a ruleset or a request | The nearest IEEE 754 double, read as its shortest decimal |
| Arithmetic and comparisons inside an expression | Decimal, 34 significant digits, round half even |
| A number leaving the expression | Rounded half even to 15 significant digits |
Example
The total is 0.1 plus 0.2, and the discount is a third of 0.1. The validation rule checks that
0.1 + 0.2 equals 0.3; it passes.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.numbers, version: 1.0.0, title: "Decimal numbers" }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.total.compute
kind: compute
target: { entity: Order, field: /total }
operations: [create]
assign:
- { field: /total, value: { op: add, args: [{ var: data.price }, { var: data.shipping }] }, mode: always }
- { field: /discount, value: { op: div, args: [{ var: data.price }, 3] }, mode: always }
- id: order.total.exact
kind: validation
target: { entity: Order, field: /total }
operations: [create]
assert: { op: eq, args: [{ op: add, args: [0.1, 0.2] }, 0.3] }
severity: error
finding: { code: LRN-NUM-001, message: order.notDecimal }
messages:
en:
order.notDecimal: "0.1 + 0.2 is not 0.3 here."
tests:
- name: 0.1 plus 0.2 is exactly 0.3, and a third has 15 digits
entity: Order
operation: create
given:
data: { price: 0.1, shipping: 0.2 }
expect:
decision: allow
findings: []
effects:
- { type: value, field: /total, value: 0.3 }
- { type: value, field: /discount, value: 0.0333333333333333 }{
"entity": "Order",
"operation": "create",
"data": {
"price": 0.1,
"shipping": 0.2
}
}Result, from the engine
Decisionallow0 findings, server channel
- computed value
/total=0.3 - computed value
/discount=0.0333333333333333
Common mistakes
- Sending identifiers as numbers. An order number with 17 digits does not survive a double. Send identifiers as strings.
- Writing numbers with more than 15 significant digits in a ruleset. The checker reports them
(
NUMBER_NOT_PORTABLE). - Dividing by zero.
divandmodwith a zero divisor are evaluation errors.
Exercise
Compute /total as the price times the quantity. Test it with 3 items at 19.99: the total is exactly 59.97.
Hint
Replace the compute rule's value with mul of data.price and data.quantity, and expect the value 59.97 in the golden test's effects.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.numbers, version: 1.0.0, title: "Decimal numbers" }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.total.compute
kind: compute
target: { entity: Order, field: /total }
operations: [create]
assign:
- { field: /total, value: { op: mul, args: [{ var: data.price }, { var: data.quantity }] }, mode: always }
tests:
- name: three items at 19.99 cost exactly 59.97
entity: Order
operation: create
given:
data: { price: 19.99, quantity: 3 }
expect:
decision: allow
findings: []
effects:
- { type: value, field: /total, value: 59.97 }{
"entity": "Order",
"operation": "create",
"data": {
"price": 19.99,
"quantity": 3
}
}Result, from the engine
Decisionallow0 findings, server channel
- computed value
/total=59.97