Table of Contents

Computed values

An action's value, and the left-hand side of a condition, can be an expression computed from the fact. Expressions are data too: a closed set of operators, nested as JSON, that a UI can generate and a reviewer can read.

The expression forms

Form Example
a bare scalar 0.1, "Thanks", true
a field { "field": "Order.Total" }
an explicit literal { "literal": 5 } (the same as a bare 5)
an operator { "op": "multiply", "operands": [ { "field": "Order.Total" }, 0.1 ] }
a function call { "call": "RoundTo", "operands": [ { "field": "Order.Total" }, 2 ] } — a value function the engine registered
a param reference { "param": "averageLine" } — a scoped param in scope

Operands are expressions themselves, so they nest freely.

Operator Operands Result
add, multiply 2 or more A number
subtract, divide, modulo exactly 2, in order A number. Dividing by zero gives null.
negate exactly 1 A number
concat 2 or more A string, or null if any operand is null
coalesce 2 or more The first operand that isn't null
count exactly 1 How many elements a collection has, or null if it isn't one

Values computed from the fact

{
  "name": "Computed values",
  "rules": [
    {
      "id": "pricing",
      "condition": { "field": "Order.Total", "operator": "GreaterThan", "value": 0 },
      "actions": [
        {
          "type": "setOutput",
          "target": "Discount",
          "value": {
            "op": "multiply",
            "operands": [ { "field": "Order.Total" }, 0.1 ]
          }
        },
        {
          "type": "setOutput",
          "target": "AverageLinePrice",
          "value": {
            "op": "divide",
            "operands": [
              { "field": "Order.Total" },
              { "field": "Order.ItemCount" }
            ]
          }
        },
        {
          "type": "setOutput",
          "target": "Greeting",
          "value": {
            "op": "concat",
            "operands": [ "Thanks, ", { "field": "Customer.Name" }, "!" ]
          }
        },
        {
          "type": "setOutput",
          "target": "ShipTo",
          "value": {
            "op": "coalesce",
            "operands": [ { "field": "Customer.Country" }, "unknown" ]
          }
        },
        {
          "type": "setOutput",
          "target": "Label",
          "value": {
            "op": "concat",
            "operands": [ "Ship to ", { "field": "Customer.Country" } ]
          }
        },
        {
          "type": "setOutput",
          "target": "LineCount",
          "value": { "op": "count", "operands": [ { "field": "Order.Lines" } ] }
        }
      ]
    },
    {
      "id": "pricey-items",
      "condition": {
        "expression": {
          "op": "divide",
          "operands": [
            { "field": "Order.Total" },
            { "field": "Order.ItemCount" }
          ]
        },
        "operator": "GreaterThan",
        "value": 50
      },
      "actions": [
        { "type": "setOutput", "target": "Segment", "value": "premium" }
      ]
    }
  ]
}
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;

LoadedRuleSet rules = engine.LoadRuleSet(File.ReadAllText("computed-values.json"));

foreach (Checkout checkout in new[] { Facts.Vip(), Facts.Newcomer() })
{
    RuleEvaluationResult result = engine.Evaluate(rules, checkout);

    Console.WriteLine($"{checkout.Customer.Name}:");
    foreach (KeyValuePair<string, object?> output in result.Outputs)
    {
        Console.WriteLine($"  {output.Key,-17} = {output.Value ?? "null"}");
    }
}

Output

Aroha:
  Discount          = 24.0
  AverageLinePrice  = 60
  Greeting          = Thanks, Aroha!
  ShipTo            = NZ
  Label             = Ship to NZ
  LineCount         = 3
  Segment           = premium
Ben:
  Discount          = 3.5
  AverageLinePrice  = 35
  Greeting          = Thanks, Ben!
  ShipTo            = unknown
  Label             = null
  LineCount         = 1

Ben has no country, so coalesce falls back to "unknown" while concat gives null. His average line price is 35, so pricey-items doesn't fire and he gets no Segment.

A computed left-hand side

Use expression in place of field to compare a computed value:

{
  "expression": { "op": "divide", "operands": [ { "field": "Order.Total" }, { "field": "Order.ItemCount" } ] },
  "operator": "GreaterThan",
  "value": 50
}

That reads average item price over 50. A leaf has a field or an expression, never both. The custom operator and the quantifiers take a field only.

Evaluation is total

An expression never throws on data:

  • A null operand makes the result null, except in coalesce.
  • A non-numeric operand to an arithmetic operator makes the result null.
  • divide and modulo by zero give null.
  • count of a null, a string or anything that isn't a collection gives null. A string is text, not a collection of characters.

A null value then flows through the usual rules: a setOutput writes null, an accumulator skips it, and a comparison follows the null rules.

Arithmetic is decimal

Arithmetic runs in decimal unless an operand is a binary floating-point number, in which case it runs in double. So 7 / 2 is 3.5, never integer division, and 0.1 + 0.2 equals 0.3 exactly. JSON numbers become decimal whenever they can, so in practice rule arithmetic is decimal arithmetic.

Note

Rules can't read each other's outputs. An expression reads the fact, never Outputs. Evaluation is a single pass with no chaining, by design. To use one result in another decision, evaluate in stages: the fraud scoring tutorial does exactly that.

Next

Collections: conditions over order lines, tags and other lists.