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.
divideandmoduloby zero give null.countof 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.