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);
}
}