// LoopFollow // AlarmCondition.swift import Foundation protocol AlarmCondition { static var type: AlarmType { get } init() /// pure, per-alarm logic against `AlarmData` func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool /// `true` when `belowBG`/`aboveBG` are this condition's own trigger /// threshold, checked in `evaluate`; `false` (the default) treats them /// as activation limits enforced by `passesBGLimits`. var checksOwnBGLimits: Bool { get } } extension AlarmCondition { var checksOwnBGLimits: Bool { false } /// Returns `true` when the alarm is allowed to continue evaluating /// after BG-limit checks; `false` blocks it immediately. func passesBGLimits(alarm: Alarm, data: AlarmData) -> Bool { let bgReading = data.bgReadings.last?.sgv let haveBG = (bgReading ?? 0) > 0 let bgValue = Double(bgReading ?? 0) // ──────────────────────────────────── // 1. BG-based alarms always need data // ──────────────────────────────────── if alarm.type.isBGBased && !haveBG { return false } // ──────────────────────────────────── // 2. Conditions that check their own threshold in `evaluate` // skip the limit gate (the data requirement above applies // regardless). // ──────────────────────────────────── if checksOwnBGLimits { return true } // ──────────────────────────────────── // 3. No limits? we’re done. // ──────────────────────────────────── if alarm.belowBG == nil && alarm.aboveBG == nil { return true } // If we reach here, there *are* limits. // Non-BG alarms without a reading must fail; // BG-based alarms already bailed out above. guard haveBG else { return false } switch (alarm.belowBG, alarm.aboveBG) { case let (lo?, hi?): return lo < hi ? (bgValue <= lo || bgValue >= hi) // fire outside band : (hi <= bgValue && bgValue <= lo) // fire inside band case let (lo?, nil): return bgValue <= lo case let (nil, hi?): return bgValue >= hi default: return true } } /// applies every global & per-alarm guard exactly once func shouldFire(alarm: Alarm, data: AlarmData, now: Date, config: AlarmConfiguration) -> Bool { // master on/off guard alarm.isEnabled else { return false } // per-alarm snooze if let snooze = alarm.snoozedUntil, snooze > now { return false } if !passesBGLimits(alarm: alarm, data: data) { return false } // time-of-day guard let comps = Calendar.current.dateComponents([.hour, .minute], from: now) let nowMin = (comps.hour! * 60) + comps.minute! let dStart = config.dayStart.minutesSinceMidnight let nStart = config.nightStart.minutesSinceMidnight let isNight = (nowMin < dStart) || (nowMin >= nStart) switch alarm.activeOption { case .always: break case .day: // only fire in day guard !isNight else { return false } case .night: // only fire in night guard isNight else { return false } } // finally, run the type-specific logic return evaluate(alarm: alarm, data: data, now: now) } }