Skip to content
Talk to our solutions team

`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.

Whether val is between min and max, inclusive. Whole numbers.

ParameterTypeMeaning
valint64The value to test
minint64Lower bound, inclusive
maxint64Upper 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);
}
facts
{ "loan": { "termMonths": 480 } }
out
{ "termValid": false }

The same for floats, and the one to use for money, rates and ratios.

ParameterTypeMeaning
valfloat64The value to test
minfloat64Lower bound, inclusive
maxfloat64Upper 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);
}
facts
{ "note": { "rate": "6.125%" } }
out
{ "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.

ParameterTypeMeaning
vanyThe value to convert

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.

FunctionReturns
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", "")));
}
facts
{ "unit": { "line": "Unit 4 of 12" } }
out
{ "unit": 4.0, "unitsTotal": 12.0 }

Every function here takes and returns floats, except the decimal count of math.Round.

ParameterTypeMeaning
xfloat64The 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);
}
facts
{ "invoice": { "statedTotal": 1500.0, "computedTotal": 1499.5 } }
out
{ "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”
ParameterTypeMeaning
a, bfloat64The two values

math.Min(10, 20) aborts; write math.Min(10.0, 20.0).

Rounds to decimals places, halves away from zero.

ParameterTypeMeaning
xfloat64The value
decimalsint64How 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”
ParameterTypeMeaning
xfloat64The value

part as a percentage of whole, or 0 when whole is zero.

ParameterTypeMeaning
partfloat64The portion
wholefloat64The 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));
}
facts
{ "loan": { "amount": 340000.0 }, "property": { "value": 400000.0 } }
out
{ "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.

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.

Parses value using layout, or returns the zero time when it does not parse.

ParameterTypeMeaning
layoutstringThe reference layout, e.g. "2006-01-02"
valuestringThe 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.

ParameterTypeMeaning
ttimeThe time
layoutstringThe 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.

ParameterTypeMeaning
t1, t2timeThe 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.

ParameterTypeMeaning
t1, t2timeThe two times
FunctionCounts
DaysBetweenWhole days elapsed, truncated
MonthsBetweenCalendar months crossed, ignoring the day: 31 January to 1 March is 2
YearsBetweenMonthsBetween 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);
}
facts
{ "loan": { "disclosedOn": "03/02/2026", "closingOn": "2026-03-04" } }
out
{ "rescissionMet": false }

time.AddDays(t, days) → time · time.AddMonths(t, months) → time

Section titled “time.AddDays(t, days) → time · time.AddMonths(t, months) → time”
ParameterTypeMeaning
ttimeThe starting time
days / monthsint64How 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"));
}
facts
{ "lock": { "date": "2026-03-01" } }
out
{ "lockExpiry": "2026-04-15" }