`num`, `math` and `time`
Three small namespaces: num for range tests and numbers inside text, math for arithmetic,
time for dates.
Division needs no care: 7 / 2 is 3.5, never 3.
num.Between(val, min, max) → bool
Section titled “num.Between(val, min, max) → bool”Whether val is between min and max, inclusive. Whole numbers.
| Parameter | Type | Meaning |
|---|---|---|
val | int64 | The value to test |
min | int64 | Lower bound, inclusive |
max | int64 | Upper bound, inclusive |
rule TermWithinRange "loan term between 12 and 360 months" { when !num.Between(in.Int("loan.termMonths", 0), 12, 360) then findings.Error("TermWithinRange", "underwriting", "Term outside the permitted 12-360 months"); out.Set("termValid", false);}{ "loan": { "termMonths": 480 } }{ "termValid": false }num.BetweenF(val, min, max) → bool
Section titled “num.BetweenF(val, min, max) → bool”The same for floats, and the one to use for money, rates and ratios.
| Parameter | Type | Meaning |
|---|---|---|
val | float64 | The value to test |
min | float64 | Lower bound, inclusive |
max | float64 | Upper bound, inclusive |
rule RateWithinBand "rate must sit between 2% and 12%" { when num.BetweenF(to.Num(in.Str("note.rate", "")), 2.0, 12.0) then out.Set("rateInBand", true);}{ "note": { "rate": "6.125%" } }{ "rateInBand": true }num.ToInt64(v) → int64 · num.ToFloat64(v) → float64
Section titled “num.ToInt64(v) → int64 · num.ToFloat64(v) → float64”The same conversions as to.Int and
to.Num, under older names: they read
currency symbols, separators and accounting parentheses too. Prefer the to forms in new rules.
| Parameter | Type | Meaning |
|---|---|---|
v | any | The value to convert |
Numbers inside text
Section titled “Numbers inside text”to.Num requires the whole text to be one number. These find numbers inside noisy text, a
line such as "g$____-8,987.00 due" or "Units 4 of 12". Each takes any value and reads it as
text.
| Function | Returns |
|---|---|
num.Has(v) | Whether the text carries at least one number |
num.Count(v) | How many numbers the text carries |
num.First(v) | The first number, as float64, or 0 |
num.Last(v) | The last number, or 0 |
num.Nth(v, n) | The nth number, from 1; a negative n counts from the end. Out of range is 0 |
A miss is 0, so pair First/Last/Nth with num.Has or num.Count where a real zero is
possible.
rule UnitOfTotal "the unit number and the total from one line" { when num.Count(in.Str("unit.line", "")) >= 2 then out.Set("unit", num.First(in.Str("unit.line", ""))); out.Set("unitsTotal", num.Last(in.Str("unit.line", "")));}{ "unit": { "line": "Unit 4 of 12" } }{ "unit": 4.0, "unitsTotal": 12.0 }Every function here takes and returns floats, except the decimal count of math.Round.
math.Abs(x) → float64
Section titled “math.Abs(x) → float64”| Parameter | Type | Meaning |
|---|---|---|
x | float64 | The value |
The tolerance idiom: compare a difference against a threshold rather than testing equality on values that were read off a page.
rule TotalsRecompute "the stated total matches the sum of parts" { when math.Abs(in.Double("invoice.statedTotal", 0.0) - in.Double("invoice.computedTotal", 0.0)) > 0.01 then findings.Error("TotalsRecompute", "consistency", strings.Sprintf("Stated total %s does not match computed %s", to.Fixed(in.Double("invoice.statedTotal", 0.0), 2), to.Fixed(in.Double("invoice.computedTotal", 0.0), 2))); out.Set("totalsMatch", false);}{ "invoice": { "statedTotal": 1500.0, "computedTotal": 1499.5 } }{ "totalsMatch": false }A cent of tolerance is deliberate: two figures rounded independently on the same document routinely differ in the last place, and a rule that fires on that is noise.
math.Min(a, b) → float64 · math.Max(a, b) → float64
Section titled “math.Min(a, b) → float64 · math.Max(a, b) → float64”| Parameter | Type | Meaning |
|---|---|---|
a, b | float64 | The two values |
math.Min(10, 20) aborts; write math.Min(10.0, 20.0).
math.Round(x, decimals) → float64
Section titled “math.Round(x, decimals) → float64”Rounds to decimals places, halves away from zero.
| Parameter | Type | Meaning |
|---|---|---|
x | float64 | The value |
decimals | int64 | How many decimal places. A bare integer here |
Note the mixed types: x needs its decimal point, decimals must not have one.
math.Floor(x) → float64 · math.Ceil(x) → float64
Section titled “math.Floor(x) → float64 · math.Ceil(x) → float64”| Parameter | Type | Meaning |
|---|---|---|
x | float64 | The value |
math.Percent(part, whole) → float64
Section titled “math.Percent(part, whole) → float64”part as a percentage of whole, or 0 when whole is zero.
| Parameter | Type | Meaning |
|---|---|---|
part | float64 | The portion |
whole | float64 | The total |
Returns a percentage, not a fraction: math.Percent(25.0, 100.0) is 25, not 0.25. That matches
to.Pct, which also does not scale.
rule LoanToValue "LTV above 80% requires mortgage insurance" { when math.Percent(in.Double("loan.amount", 0.0), in.Double("property.value", 0.0)) > 80.0 && !in.Bool("loan.mortgageInsurance", false) then findings.Critical("LoanToValue", "underwriting", strings.Sprintf("LTV of %s%% requires mortgage insurance", to.Fixed(math.Percent(in.Double("loan.amount", 0.0), in.Double("property.value", 0.0)), 1))); out.Set("ltv", math.Round(math.Percent(in.Double("loan.amount", 0.0), in.Double("property.value", 0.0)), 1));}{ "loan": { "amount": 340000.0 }, "property": { "value": 400000.0 } }{ "ltv": 85.0 }A zero or unread denominator makes math.Percent return 0, which passes a “greater than” test
silently. Guard it where that matters: in.Double("property.value", 0.0) > 0.0.
The time functions take and return times. Get one from to.Date
(text in any common layout), in.Date(path).Time(),
time.ParseTime or time.Now.
time.Now() → time
Section titled “time.Now() → time”The current time. Use sparingly: a rule that depends on the wall clock produces a different result on a rerun, which makes a decision hard to reproduce when someone asks why it was made. Prefer a date carried on the input.
time.ParseTime(layout, value) → time
Section titled “time.ParseTime(layout, value) → time”Parses value using layout, or returns the zero time when it does not parse.
| Parameter | Type | Meaning |
|---|---|---|
layout | string | The reference layout, e.g. "2006-01-02" |
value | string | The text to parse |
to.Date is usually better. It tries the
common layouts in order, so it reads what documents actually contain without you naming the format.
time.FormatTime(t, layout) → string
Section titled “time.FormatTime(t, layout) → string”| Parameter | Type | Meaning |
|---|---|---|
t | time | The time |
layout | string | The output layout, e.g. "2006-01-02" or "02 Jan 2006" |
to.DateStr does the parse and the
format in one call, and returns the empty string rather than a formatted zero date on failure.
time.IsBefore(t1, t2) → bool · time.IsAfter(t1, t2) → bool
Section titled “time.IsBefore(t1, t2) → bool · time.IsAfter(t1, t2) → bool”Whether t1 is before / after t2.
| Parameter | Type | Meaning |
|---|---|---|
t1, t2 | time | The two times |
Guard both sides with to.IsDate first. An unreadable date is the zero time, and the zero time
compares as before everything, so an unguarded comparison silently passes or silently fires,
depending which side failed.
time.DaysBetween(t1, t2) → int64 · time.MonthsBetween(t1, t2) → int64 · time.YearsBetween(t1, t2) → int64
Section titled “time.DaysBetween(t1, t2) → int64 · time.MonthsBetween(t1, t2) → int64 · time.YearsBetween(t1, t2) → int64”The interval from t1 to t2, negative when t2 is earlier.
| Parameter | Type | Meaning |
|---|---|---|
t1, t2 | time | The two times |
| Function | Counts |
|---|---|
DaysBetween | Whole days elapsed, truncated |
MonthsBetween | Calendar months crossed, ignoring the day: 31 January to 1 March is 2 |
YearsBetween | MonthsBetween divided by 12, truncated |
rule RescissionPeriod "closing must be at least three days after disclosure" { when to.IsDate(in.Str("loan.disclosedOn", "")) && to.IsDate(in.Str("loan.closingOn", "")) && time.DaysBetween(to.Date(in.Str("loan.disclosedOn", "")), to.Date(in.Str("loan.closingOn", ""))) < 3 then findings.Critical("RescissionPeriod", "compliance", "Fewer than three days between disclosure and closing"); out.Set("rescissionMet", false);}{ "loan": { "disclosedOn": "03/02/2026", "closingOn": "2026-03-04" } }{ "rescissionMet": false }time.AddDays(t, days) → time · time.AddMonths(t, months) → time
Section titled “time.AddDays(t, days) → time · time.AddMonths(t, months) → time”| Parameter | Type | Meaning |
|---|---|---|
t | time | The starting time |
days / months | int64 | How many to add: negative to subtract |
AddMonths overflows rather than clamping: one month after 31 January 2026 is 3 March, because
31 February does not exist and the surplus days carry over. Where month-end matters, add the
months to the first of the month and step back a day.
rule RateLockExpiry "record when the rate lock runs out" { when to.IsDate(in.Str("lock.date", "")) then out.Set("lockExpiry", time.FormatTime(time.AddDays(to.Date(in.Str("lock.date", "")), 45), "2006-01-02"));}{ "lock": { "date": "2026-03-01" } }{ "lockExpiry": "2026-04-15" }See also
Section titled “See also”to: conversion: reading numbers and dates out of textstrings, text: formatting a message around a figure- The rule language: operators and type dispatch