using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common; /// The AtTime rules, applied to one requested time and its stored neighbours. public static class AtTimeEvaluator { /// /// Applies the AtTime rules to one requested time (the result is stamped t): /// /// prev at exactly t ⇒ the stored value and status, unchanged. /// No prev ⇒ BadNoData, null value. /// prev Bad (incl. an Empty outage marker) ⇒ t lies inside an outage: null value /// carrying prev's Bad status code (info bits stripped) — never better than its source. /// prev usable and next Bad ⇒ prev's value held (stepped, NOT interpolated into the /// outage) with UncertainDataSubNormal + Interpolated bits. /// Double/Int64 with a usable next ⇒ linear interpolation (Int64 rounded to the nearest integer, /// ties away from zero, keeping the type); status Good + Interpolated when both neighbours are Good, /// else UncertainDataSubNormal + Interpolated. /// Boolean/String/DateTime, or no next (after the last sample) ⇒ stepped: prev's value with /// prev's status code + Interpolated bits — except that a time later than now that is stepped from the /// last sample is UncertainDataSubNormal + Interpolated. /// /// /// Requested time. /// The stored sample at or before , if any. /// The stored sample after , if any. /// Server timestamp for the result. /// The current time (for the beyond-now rule). /// The value at . public static DataValueSnapshot Evaluate(DateTime t, StoredSample? prev, StoredSample? next, DateTime serverTime, DateTime now) { if (prev is not { } p) return new DataValueSnapshot(null, HistorianStatusCodes.BadNoData, t, serverTime); if (p.TimeUtc == t) return new DataValueSnapshot(p.Value.ToObject(), p.Status, t, serverTime); // Inside an outage: report the outage's own Bad code, never an upgraded Uncertain. if (!p.IsUsable) return new DataValueSnapshot(null, p.Status & StatusBits.CodeBitsMask, t, serverTime); // Usable previous, Bad next: hold the previous value up to the outage; do not interpolate into it. if (next is { IsUsable: false }) return new DataValueSnapshot(p.Value.ToObject(), HistorianInfoBits.WithInterpolated(HistorianStatusCodes.UncertainDataSubNormal), t, serverTime); if (next is { } n && p.Value.Kind is HistorianValueKind.Double or HistorianValueKind.Int64 && n.Value.TryGetDouble(out var nv)) { var fraction = (double)(t.Ticks - p.TimeUtc.Ticks) / (n.TimeUtc.Ticks - p.TimeUtc.Ticks); var v = p.Value.Number + (nv - p.Value.Number) * fraction; // Each arm boxed on its own: `cond ? (long)x : v` is a DOUBLE expression in C# (the long widens), which returned // every interpolated Int64 as a Double until #693. object value = p.Value.Kind == HistorianValueKind.Int64 ? (object)(long)Math.Round(v, MidpointRounding.AwayFromZero) : v; var status = p.IsGood && n.IsGood ? HistorianStatusCodes.Good : HistorianStatusCodes.UncertainDataSubNormal; return new DataValueSnapshot(value, HistorianInfoBits.WithInterpolated(status), t, serverTime); } // Beyond the last sample AND beyond now: the value is held into the future, so it is not known to be Good. if (next is null && t > now) return new DataValueSnapshot(p.Value.ToObject(), HistorianInfoBits.WithInterpolated(HistorianStatusCodes.UncertainDataSubNormal), t, serverTime); // Stepped (non-numeric kind, or after the last sample): hold the previous value and its status. return new DataValueSnapshot(p.Value.ToObject(), HistorianInfoBits.WithInterpolated(p.Status), t, serverTime); } }