Calculation Expression Reference
Infohub calculations are compiled C#. This is everything an expression has in scope.
An Infohub calculation expression is not a formula in a bespoke mini-language. It is a fragment of C# that Infohub compiles, with a handful of objects placed in scope for you. That has a liberating consequence: anything legal in a C# expression — ternaries, Math, LINQ, string handling, null coalescing — is legal in an Infohub calculation.
An interval calculation must evaluate to a double?. Returning null means "no value for this interval", which is different from returning 0.
Referring to other measures#
Every measure your calculation depends on appears as a variable named Measure_ followed by that measure's ID:
Measure_1043.Value
You do not type the dependency list by hand in Flow Config — adding a measure to the calculation's dependencies creates the symbol. From the CLI, dependencies are passed explicitly alongside the expression.
The number is a measure ID, so expressions are not portable. Measure_1043 means measure 1043 in this database. The same expression copied to another instance refers to whatever measure happens to hold that ID there. This is why templates and model attributes exist — see Model Attributes.
What a measure gives you#
Each Measure_<id> is a MeasureProxy:
| Member | Type | What it is |
|---|---|---|
.Value |
double? |
The value for the period being calculated |
.Quality |
int |
Source quality code for that value |
.Duration |
long? |
Duration in milliseconds associated with the value |
.Events |
collection | Events overlapping this period |
.Attributes |
collection | Event attribute values in scope |
.Limits |
collection | Limits configured on this measure |
.ModelAttributes |
collection | Model attributes resolved for this measure |
.Slices |
collection | The period's slices |
.NullAttribute |
proxy | Fallback used when an attribute is absent |
Reaching other periods#
This is the part that makes Infohub calculations powerful, and it is easy to miss.
A single offset period — index the measure with a relative period number. 0 is the period being calculated, -1 the one before it:
Measure_1043[-1].Value previous period
Measure_1043[-12].Value twelve periods ago
Growth against the previous period, guarded:
Measure_1043[-1].Value == 0 ? 0
: (Measure_1043.Value - Measure_1043[-1].Value) / Measure_1043[-1].Value * 100
A range of periods — index with a start and an end to get a collection:
Measure_1043[-7, 0]
That collection exposes parallel arrays, which LINQ then operates on:
| Member | Type |
|---|---|
.Values |
double?[] |
.Qualities |
int?[] |
.Durations |
long?[] |
.Limits |
collection |
.SliceCollections |
collection per period |
A rolling seven-period average:
Measure_1043[-7, 0].Values.Average()
And because it is real LINQ, so is this:
Measure_1043[-30, 0].Values.Where(v => v.HasValue && v > 0).Average()
Rolling windows beat extra measures. A rolling seven-day average is one calculated measure with a window expression. Building it as seven measures plus a sum is a common early mistake that triples the model size and breaks the first time the window changes.
The period being calculated#
Period describes the interval the calculation is producing. It is how a single expression can behave differently on different days.
| Member | Type | Notes |
|---|---|---|
Period.Start, Period.End |
DateTime |
UTC |
Period.StartLocal, Period.EndLocal |
DateTime |
Converted to the period's time zone |
Period.TimeZoneInfo |
TimeZoneInfo |
The zone in play |
Period.Duration |
TimeSpan |
Length of the period |
Period.IsTimePeriod |
bool |
True for interval periods |
Period.IsEventPeriod |
bool |
True when calculating against an event |
Period.Hour, .DayOfWeek, .DayOfMonth, .DayOfYear |
int? |
Calendar position |
Period.Week, .Month, .Quarter, .Year |
int? |
Calendar position |
Period.Shift |
string |
Shift name, where the period is a shift |
Period.Interval |
int? |
Interval number |
Period.Index |
long? |
Sequential period index |
Period can also be offset, exactly like a measure — Period[-1] is the previous period, and Period[-7, 0] is a range.
Two things this makes easy. Per-day-of-month normalisation, using the real length of the month:
Measure_1043.Value / Period.Duration.TotalDays
Weekend or shift-specific behaviour:
(Period.DayOfWeek == 6 || Period.DayOfWeek == 7) ? 0 : Measure_1043.Value
Period.Start and Period.End are UTC. Use StartLocal and EndLocal when comparing against wall-clock times an operator would recognise. Comparing Period.Start.Hour against a shift start is a classic off-by-the-timezone-offset bug.
Context and model attributes#
Context carries what the calculation is being evaluated for:
| Member | What it is |
|---|---|
Context.Type |
The kind of thing being calculated |
Context.Name |
Name of the object in context |
Context.Event, .Attribute, .AttributeValue |
Event context, where applicable |
Context.Attributes |
Attribute values, by key |
Context.ModelAttributes |
Model attributes, by key |
Model attributes are the mechanism that makes one expression serve many instances. Where a model attribute holds a design capacity or a conversion factor, the expression reads it rather than hard-coding a number:
Convert.ToDouble(Context.ModelAttributes["Design Capacity"].Value)
Model attribute values are strings, so convert them. Guard the conversion if the attribute may be missing or blank.
Measure collections#
Collection gives an expression access to a measure collection — a set of measures defined by a filter rather than listed one by one. That is what allows "the sum of this metric's children" to keep working when someone adds another child.
Collections are defined by their own filter expression, evaluated against Me, MyMetric, MyEvent, MyParent, MyFolder, MyTemplate, and MyModelAttributes.
Custom functions#
Function libraries you define are compiled alongside the expression and their namespaces are imported, so their methods are callable unqualified:
SafeDivide(Measure_1043.Value, Measure_1044.Value)
See Custom Functions. Because a library edit invalidates the cached calculation assembly, changing a function takes effect for every calculation that uses it.
Available namespaces#
System, System.Linq, and System.Collections.Generic are imported, so Math, Convert, TimeSpan, DateTime, and the LINQ operators are all available:
Math.Round(Measure_1043.Value ?? 0, 2)
Math.Max(Measure_1043.Value ?? 0, 0)
Convert.ToDouble(Context.ModelAttributes["Factor"].Value)
Working with nulls#
.Value is double?, and null means the value is genuinely absent. Decide deliberately which you want:
Measure_1043.Value ?? 0 treat missing as zero
Measure_1043.Value.HasValue ? … : (double?)null propagate missing
Propagating null is usually more honest: a gap stays visible instead of being silently reported as zero. Substituting zero is right when zero is genuinely the operational meaning — no production on a shutdown day.
Guard every division#
Measure_1044.Value == 0 ? 0 : Measure_1043.Value / Measure_1044.Value
This is not a stylistic preference. See The Zero Guard.
Timestamp calculations differ#
A calculation on a Tag interval measure runs per raw value rather than per interval, and its symbols differ: Timestamp is in scope for the current point, and .Points and .Duration are available on each measure.
Two behaviours to know. Values are emitted only when the value or quality changes from the previous point, so a timestamp calculation produces a sparse, step-shaped series rather than one point per input. And because each point's duration extends to the next point, the last point in a window takes its duration from the window end.
Practical patterns#
Percentage of target, guarded and bounded:
Measure_2001.Value == 0 ? 0
: Math.Min(Measure_2000.Value / Measure_2001.Value * 100 ?? 0, 100)
Availability from an event-derived downtime measure:
Period.Duration.TotalMinutes == 0 ? 0
: (Period.Duration.TotalMinutes - (Measure_3010.Value ?? 0)) / Period.Duration.TotalMinutes * 100
Month-to-date from a rolling window on a daily measure:
Measure_1043[-Period.DayOfMonth ?? 0, 0].Values.Where(v => v.HasValue).Sum()
Year-on-year comparison for a monthly measure:
Measure_1043[-12].Value