Arithmetic operators
add, sub, mul, div, mod, abs, min, max and round compute with exact decimal numbers.
Arithmetic operators compute numbers. The engine computes in decimal, not in binary floating
point, so 0.1 + 0.2 is exactly 0.3 in every language. Use the result in a comparison or in a
message.
Syntax
{ op: add, args: [<number>, <number>] }
{ op: round, args: [<number>, <places>] }
{ op: min, args: [<number>, <number>, ...] }| Operator | Arguments | Result |
|---|---|---|
add, sub, mul, div | number, number | Sum, difference, product, quotient. Division by zero is an error |
mod | number, number | Remainder of a truncated division; the sign follows the first number. Zero is an error |
abs | number | The value without its sign |
min, max | one or more numbers | The smallest, the largest |
round | number, optional places (0 to 15, default 0) | Rounded half to even |
Example
The request has a total of 36 for three items of 9.99 and a shipping cost of 6. The message shows the total the rule expected.
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.arithmetic, version: 1.0.0, title: Arithmetic operators }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.total.matches
kind: validation
target: { entity: Order, field: /total }
operations: [create]
assert:
op: eq
args:
- { var: data.total }
- { op: round, args: [{ op: sub, args: [{ op: add, args: [{ op: mul, args: [{ var: data.price }, { var: data.quantity }] }, { var: data.shipping }] }, { var: data.discount }] }, 2] }
severity: error
finding:
code: ART-001
message: total.matches
args:
expected: { op: round, args: [{ op: sub, args: [{ op: add, args: [{ op: mul, args: [{ var: data.price }, { var: data.quantity }] }, { var: data.shipping }] }, { var: data.discount }] }, 2] }
- id: order.quantity.pairs
kind: validation
target: { entity: Order, field: /quantity }
operations: [create]
assert: { op: eq, args: [{ op: mod, args: [{ var: data.quantity }, 2] }, 0] }
severity: warning
finding: { code: ART-002, message: quantity.pairs }
- id: order.average.max
kind: validation
target: { entity: Order, field: /total }
operations: [create]
assert: { op: lte, args: [{ op: div, args: [{ var: data.total }, { var: data.quantity }] }, 1000] }
severity: error
finding: { code: ART-003, message: average.max }
- id: order.discount.not-negative
kind: validation
target: { entity: Order, field: /discount }
operations: [create]
assert: { op: eq, args: [{ var: data.discount }, { op: abs, args: [{ var: data.discount }] }] }
severity: error
finding: { code: ART-004, message: discount.notNegative }
- id: order.shipping.by-weight
kind: validation
target: { entity: Order, field: /shipping }
operations: [create]
assert:
op: eq
args:
- { var: data.shipping }
- { op: max, args: [0, { op: min, args: [20, { op: mul, args: [{ var: data.weight }, 2] }] }] }
severity: error
finding: { code: ART-005, message: shipping.byWeight }
messages:
en:
total.matches: "The total should be {expected}."
quantity.pairs: "These items come in pairs; an odd quantity leaves one alone."
average.max: "One item may cost at most 1000 on average."
discount.notNegative: "A discount cannot be negative."
shipping.byWeight: "Shipping is 2 per kg, at most 20."
tests:
- name: a wrong total and an odd quantity
entity: Order
operation: create
given:
data: { price: 9.99, quantity: 3, shipping: 6, discount: 0, total: 36, weight: 3 }
expect:
decision: deny
findings:
- { rule: order.total.matches, message: The total should be 35.97. }
- { rule: order.quantity.pairs, blocking: false }
- name: a correct order
entity: Order
operation: create
given:
data: { price: 9.99, quantity: 2, shipping: 20, discount: 1.5, total: 38.48, weight: 12 }
expect: { decision: allow, findings: [] }{
"entity": "Order",
"operation": "create",
"data": {
"price": 9.99,
"quantity": 3,
"shipping": 6,
"discount": 0,
"total": 36,
"weight": 3
}
}Result, from the engine
Decisiondeny2 findings, server channel
ART-001errorblockingThe total should be 35.97./totalART-002warningnot blockingThese items come in pairs; an odd quantity leaves one alone./quantity
Common mistakes
add,sub,mulanddivtake exactly two numbers. Nest calls to add three.roundrounds half to even:2.5becomes2,3.5becomes4.- Dividing by zero is an evaluation error. Guard the rule with
whenif the divisor can be 0.
Exercise
A discount may be at most 10% of the goods (price times quantity). Write the rule. A discount of 5 on goods worth 40 is denied; 4 is allowed.
Hint
The goods are worth price times quantity. Multiply that by 0.1 and compare it with lte.
Show answer
ruleCascade: 1.0.0
kind: RuleSet
metadata: { id: learn.arithmetic, version: 1.0.0, title: Arithmetic operators }
scope:
- { level: organization, id: learn }
entities:
Order:
schema: { $ref: "./learn.openapi.yaml#/components/schemas/Order" }
rules:
- id: order.discount.max
kind: validation
target: { entity: Order, field: /discount }
operations: [create]
assert:
op: lte
args:
- { var: data.discount }
- { op: mul, args: [{ op: mul, args: [{ var: data.price }, { var: data.quantity }] }, 0.1] }
severity: error
finding: { code: ART-001, message: discount.max }
messages:
en:
discount.max: "The discount may be at most 10% of the goods."
tests:
- name: a discount of 5 on goods worth 40 is denied
entity: Order
operation: create
given:
data: { price: 10, quantity: 4, discount: 5 }
expect:
decision: deny
findings:
- { rule: order.discount.max, fields: [/discount] }
- name: a discount of 4 on goods worth 40 is allowed
entity: Order
operation: create
given:
data: { price: 10, quantity: 4, discount: 4 }
expect: { decision: allow, findings: [] }{
"entity": "Order",
"operation": "create",
"data": {
"price": 10,
"quantity": 4,
"discount": 5
}
}Result, from the engine
Decisiondeny1 finding, server channel
ART-001errorblockingThe discount may be at most 10% of the goods./discount