using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common; /// /// The bucket layout of a processed (aggregate) read. Bucket i = [start + i·interval, /// start + (i+1)·interval) (start-inclusive, end-exclusive to the tick; the last bucket is truncated at /// end). An interval <= 0 ⇒ one interval [start, end). start == end ⇒ no intervals. /// end < start (reverse) ⇒ the window is swapped, computed forward, and assembled in descending bucket /// order (buckets stay anchored at the earlier time). /// public sealed class BucketPlan { /// Spec §9.7: a processed read covering more intervals than this is refused. public const int MaxBuckets = 100_000; private BucketPlan(DateTime startUtc, DateTime endUtc, bool reversed, long intervalTicks, long bucketCount) { StartUtc = startUtc; EndUtc = endUtc; Reversed = reversed; IntervalTicks = intervalTicks; BucketCount = bucketCount; } /// The earlier end of the window (the forward start). public DateTime StartUtc { get; } /// The later end of the window (the forward, exclusive end). public DateTime EndUtc { get; } /// True when the request came in reversed (end < start). public bool Reversed { get; } /// Bucket width in 100 ns ticks (clamped to the window; 0 for an empty window). public long IntervalTicks { get; } /// Number of buckets (0 for an empty window). public long BucketCount { get; } /// as 100 ns ticks since the Unix epoch. public long StartUnixTicks => (StartUtc - DateTime.UnixEpoch).Ticks; /// Plans the buckets of a processed read. /// Requested start. /// Requested end (earlier than start for a reverse read). /// Bucket width; <= 0 means one bucket over the whole window. /// The plan. /// The window holds more than intervals /// (parameter name interval). public static BucketPlan Create(DateTime startUtc, DateTime endUtc, TimeSpan interval) { var reverse = endUtc < startUtc; var (start, end) = reverse ? (endUtc, startUtc) : (startUtc, endUtc); var windowTicks = end.Ticks - start.Ticks; // Everything stays in 100 ns ticks (≤ ≈3.2e18 across 0001–9999, inside a long) — no ns scaling that could // wrap. An interval wider than the window is one bucket, so clamp it to the window; the ceiling division // is written without "+ interval - 1" so a TimeSpan near MaxValue cannot overflow either. var intervalTicks = interval > TimeSpan.Zero ? Math.Min(interval.Ticks, windowTicks) : windowTicks; long bucketCount = windowTicks == 0 ? 0 : windowTicks / intervalTicks + (windowTicks % intervalTicks == 0 ? 0 : 1); if (bucketCount > MaxBuckets) throw new ArgumentOutOfRangeException(nameof(interval), bucketCount, $"A processed read may cover at most {MaxBuckets} intervals; this window holds {bucketCount}."); return new BucketPlan(start, end, reverse, intervalTicks, bucketCount); } /// The start time of bucket (UTC kind). /// Zero-based bucket index. /// The bucket start. public DateTime BucketStart(long index) => DateTime.SpecifyKind(new DateTime(StartUtc.Ticks + index * IntervalTicks), DateTimeKind.Utc); /// One snapshot per bucket in window order; missing buckets get ; reversed if the window was. /// Evaluated snapshots of the non-empty buckets, by bucket index. /// Builds the snapshot of a bucket with no row, from its start time. /// snapshots. public IReadOnlyList Assemble(IReadOnlyDictionary byBucket, Func empty) { ArgumentNullException.ThrowIfNull(byBucket); ArgumentNullException.ThrowIfNull(empty); var result = new List((int)BucketCount); for (long i = 0; i < BucketCount; i++) { var bucketStart = BucketStart(i); result.Add(byBucket.TryGetValue(i, out var v) ? v : empty(bucketStart)); } if (Reversed) result.Reverse(); return result; } }