Skip to content

Null logic

Reports leave cells empty, and rules must not fail because of it. DPM-XL handles missing values with defaults, null propagation and three-valued logic.

from dpmxl import MemoryProvider, Number, TableLayout, evaluate_rule

provider = MemoryProvider(
    [
        TableLayout("T", rows=("0010", "0020", "0030"), columns=("0010",)),
        TableLayout("O", columns=("0010", "0020"), open_keys=("qEBB",)),
    ],
    {
        ("T", "0010", "0010", None, ()): Number.of(10, 0),
        ("T", "0020", "0010", None, ()): Number.of(4, 0),
        # T r0030 is not reported
        ("O", None, "0010", None, (("qEBB", "A"),)): Number.of(5, 0),
        ("O", None, "0010", None, (("qEBB", "B"),)): Number.of(-1, 0),
        ("O", None, "0020", None, (("qEBB", "A"),)): Number.of(2, 0),
        ("O", None, "0020", None, (("qEBB", "B"),)): Number.of(0, 0),
    },
)


def show(expression):
    result = evaluate_rule(expression, provider)
    print(
        result.status, result.message or [outcome.passed for outcome in result.outcomes]
    )

Missing facts and defaults

A cell with no fact takes the selection's default; with no default it is null. A provider must therefore return every defined cell of a closed selection, reported or not (Connect your data). The default also stands in where another operand has an open key the selection lacks (Records and keys).

show("{tT, r0030, c0010} > 0")
show("{tT, r0030, c0010, default: 0} >= 0")
show("isnull({tT, r0030, c0010})")
not_applicable [None]
passed [True]
passed [True]

Null propagates

Null propagates through arithmetic, comparisons and functions, except isnull and nvl(x, replacement). A record whose result is null is "not evaluated", never a failure; a rule whose records are all null is not_applicable.

and, or and not use three-valued (Kleene) logic: null or true is true, null and false is false, anything else with null is null.

show("{tT, r0030, c0010} > 0 or true")
show("{tT, r0030, c0010} > 0 and false")
passed [True]
failed [False]

Conditionals

if takes the else branch when the condition is null. Without else, a Boolean then (filing indicators {v_…} and Boolean facts included) yields true where the condition is false or null, so a guarded check passes when its guard does not hold.

show("if {tT, r0030, c0010} > 0 then false endif")
show("if {tT, r0030, c0010} > 0 then 1 else 2 endif = 2")
passed [True]
passed [True]

The result has the condition's record structure. Branch keys the condition lacks widen it, so each condition record applies to every branch record that shares its keys. A condition with a default also covers the key values only a branch has (Records and keys).

Errors stay in their record

Division by zero is a run-time error, as the specification requires. Like a type error (a non-Boolean if condition included), it fails only its own record: the rule is an error only if that record reaches the result. A guard therefore works:

show("{tT, r0010, c0010} / {tT, r0030, c0010, default: 0} > 1")
show(
    "if {tT, r0030, c0010, default: 0} != 0 then {tT, r0010, c0010} / {tT, r0030, c0010, default: 0} > 1 endif"
)
error division by zero
passed [True]

Aggregates

Aggregates over no records are null, except count, which is 0. count counts non-null values; the others skip nulls.

show("count({tT, r0030, c0010}) = 0")
show("sum({tT, r0030, c0010}) = 0")
passed [True]
not_applicable [None]

Preconditions

A precondition makes the rule not_applicable unless it yields at least one true record; null and false records do not count. See Preconditions and results.