Rule Cascade
LearnOperators

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

arithmetic
{ op: add, args: [<number>, <number>] }
{ op: round, args: [<number>, <places>] }
{ op: min, args: [<number>, <number>, ...] }
OperatorArgumentsResult
add, sub, mul, divnumber, numberSum, difference, product, quotient. Division by zero is an error
modnumber, numberRemainder of a truncated division; the sign follows the first number. Zero is an error
absnumberThe value without its sign
min, maxone or more numbersThe smallest, the largest
roundnumber, 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.

arithmetic.ruleset.yaml
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: [] }
request.json
{
  "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./total
  • ART-002warningnot blockingThese items come in pairs; an odd quantity leaves one alone./quantity
Try it YourselfOpens this ruleset and request in the playground. Nothing to install.

Common mistakes

  • add, sub, mul and div take exactly two numbers. Nest calls to add three.
  • round rounds half to even: 2.5 becomes 2, 3.5 becomes 4.
  • Dividing by zero is an evaluation error. Guard the rule with when if 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
arithmetic.ruleset.yaml
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: [] }
request.json
{
  "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
Course overview

On this page