T11: Custom Functions
Move the zero guard into a function, refactor your KPIs to use it, then discover what a function edit does to every caller.
What you will build: a small function library, refactored KPIs, and an understanding of the blast radius of a function change.
Time: about 25 minutes.
You need: T10 completed — you need several expressions to refactor.
Step 1 — Look at what you have written#
Open your Layer 3 KPIs from T10. Every one of them contains a variation on the same guard:
(Measure_<x>.Value ?? 0) == 0 ? (double?)null : Measure_<y>.Value / Measure_<x>.Value
Four expressions, four copies of the same logic, four opportunities to forget it. In a real model it is two hundred copies.
Step 2 — Write the function#
In the Toolbox, create a function library and add:
public static double? SafeDivide(double? numerator, double? denominator)
{
if (!denominator.HasValue || denominator.Value == 0) return null;
return (numerator ?? 0) / denominator.Value;
}
Note the choices:
double?in and out, because measure values are nullable.- Returns
null, not0, because these results get averaged and a false zero understates the average. - No exception thrown. A throw would fail the whole calculation for that interval with an unhelpful message.
Step 3 — Refactor a single KPI first#
Change one expression — specific energy:
SafeDivide(Measure_<energyDailyId>.Value, Measure_<bottlesDailyId>.Value) * 1000
Compare against the four-line version it replaced. The intent is now obvious, and the guard cannot be forgotten.
Check the values, over several days including one with no production, before refactoring the rest. Prove the function on one measure before spreading it to twenty.
Step 4 — Refactor the rest#
Now the other ratios:
SafeDivide(Measure_<bottlesDailyId>.Value, Measure_<targetDailyId>.Value) * 100
The second shows the pattern composing usefully — the denominator is itself an expression, and the guard still applies.
Step 5 — Add two more functions#
Percentage of target, bounded so a chart axis stays sensible:
public static double? PercentOfTarget(double? actual, double? target, bool cap = true)
{
if (!target.HasValue || target.Value == 0) return null;
var pct = (actual ?? 0) / target.Value * 100;
return cap ? Math.Min(pct, 100) : pct;
}
Reading a model attribute safely, which otherwise clutters every expression that uses one:
public static double AttrAsDouble(ModelAttributeCollection attrs, string key, double fallback = 0)
{
if (attrs == null || !attrs.ContainsKey(key)) return fallback;
return double.TryParse(attrs[key].Value, out var v) ? v : fallback;
}
You will use the second in T15. Model attribute values are always strings, so every expression reading one needs a conversion and a fallback — exactly the kind of repetitive, easy-to-get-wrong code that belongs in a function.
Spreading a period value across days, without assuming a month length:
public static double? PerDay(double? periodValue, double daysInPeriod)
{
if (daysInPeriod <= 0) return null;
return (periodValue ?? 0) / daysInPeriod;
}
Called as PerDay(Measure_<monthlyTargetId>.Value, Period.Duration.TotalDays) — which replaces the T03 daily target expression.
Step 6 — Now change a function deliberately#
Edit SafeDivide to return 0 instead of null:
if (!denominator.HasValue || denominator.Value == 0) return 0;
Recalculate a range that includes a non-production day, and look at what happened.
Every ratio in your model changed at once. Attainment, quality rate, specific energy — all of them now report 0 on days they previously reported nothing. And your monthly averages moved, because those zeros are now included.
Know the blast radius before editing a widely-used function. That is the power and the danger. A function edit invalidates the cached calculation assemblies of every calculation that calls it, so the change reaches everything immediately.
Before editing a function used across a model, know how many expressions call it and what the change does to each. This is the single most consequential kind of edit available in Infohub's modelling layer.
Change it back to null and recalculate again.
Step 7 — Note what recalculation did and did not do#
When you changed the function, future intervals used the new logic immediately. Intervals already calculated kept their old values until you recalculated them.
That is consistent with every other correction in Infohub, and it matters here more than usual: a function change silently splits your history into "before the change" and "after", and the split is invisible on a chart.
After a meaningful function change, recalculate the affected range — or accept and document that the model holds two behaviours.
Step 8 — Rename one, and see it break#
Rename PercentOfTarget to AttainmentPct.
Every expression calling the old name now fails to compile. That is the correct behaviour — the alternative would be silent breakage — but it means a rename is a coordinated change, not a tidy-up.
Rename it back, or fix the callers. The Config CLI can list calculation expressions, which makes finding callers practical rather than a manual trawl.
Step 9 — Guidelines worth adopting#
Return null for "unknown", not a sentinel. Returning -1 or -999 pollutes every average and chart downstream.
Never throw. Validate inputs and return null.
Keep them pure. Depend only on the arguments. A function reading ambient state makes calculations non-reproducible and very hard to debug.
Name for intent. SafeDivide beats DivideWithZeroCheck. PercentOfTarget beats Calc2.
Document the units. KgToTonnes is unambiguous; Convert is not.
Group by domain. Separate libraries for maths helpers, unit conversions, and site-specific business rules — it makes ownership clear and limits the blast radius of a change.
Test on one measure before spreading it.
Step 10 — Know when not to write one#
A function that wraps a + b adds indirection and no value.
More importantly: prefer several intermediate measures over one function that does everything. A three-hundred-line state machine in a function library is invisible on a dashboard, hard to review, and known only to whoever wrote it. Intermediate measures are inspectable, chartable, and self-documenting.
Reach for a function when the logic genuinely is a reusable calculation. If it is really a modelling structure in disguise, model it.
What you have learned#
Functions remove repetition and make expressions read as intent.
A function edit reaches every caller immediately, because cached calculation assemblies are invalidated.
Behaviour changes forward, not retrospectively. Recalculate after a meaningful change.
A rename breaks callers. Coordinate it.
Return null, never throw, keep them pure, name them for intent.
Prove a function on one measure first.
Do not hide modelling structure inside a function.
Try this before moving on#
Standardise your whole model on SafeDivide. Search every calculated measure for a / and confirm each one either uses the function or has an explicit inline guard.
Mixed conventions make review slower and make an unguarded division harder to spot among guarded ones. Pick one form — the function, ideally — and hold to it as a house rule.
Next: T12: Templates — build once, deploy twelve times.