using StackExchange.Redis; public static class TrendCalculator { // NEWS2-relevant codes that support trend detection. public static readonly IReadOnlyList TrendCodes = new[] { "HEART_RATE", "RESP_RATE", "SYSTOLIC_BP", "TEMP_C", "SPO2" }; public static bool IsTrendCode(string observationCode) => TrendCodes.Contains(observationCode); public static string HistoryKey(Guid encounterId, string code) => $"trend:{encounterId}:{code}"; public static RedisKey[] AllHistoryKeys(Guid encounterId) => TrendCodes.Select(c => (RedisKey)HistoryKey(encounterId, c)).ToArray(); /// /// Computes rate of change (units per minute) between the oldest and newest /// entries within the window. Returns null if fewer than 2 entries or window exceeded. /// public static decimal? ComputeRatePerMinute( IReadOnlyList entries, int windowMinutes) { if (entries.Count < 2) return null; var newest = entries[^1]; var oldest = entries[0]; var deltaMinutes = (newest.RecordedAt - oldest.RecordedAt).TotalMinutes; if (deltaMinutes <= 0 || deltaMinutes > windowMinutes) return null; return (newest.Value - oldest.Value) / (decimal)deltaMinutes; } /// /// Returns true if the rate exceeds the configured threshold. /// For SPO2 and SYSTOLIC_BP, checks negative rate (decline) as clinically significant. /// public static bool ExceedsThreshold( string observationCode, decimal ratePerMinute, decimal thresholdPerMinute) => observationCode switch { "SPO2" or "SYSTOLIC_BP" => ratePerMinute <= -thresholdPerMinute, _ => ratePerMinute >= thresholdPerMinute }; public static string DescribeTrend( string observationCode, decimal ratePerMinute, decimal currentValue) => observationCode switch { "SPO2" or "SYSTOLIC_BP" => $"Rapid decline: {observationCode} falling at {Math.Abs(ratePerMinute):F2}/min (current {currentValue})", _ => $"Rapid rise: {observationCode} rising at {ratePerMinute:F2}/min (current {currentValue})" }; }