remindwall / RWPureSwift / Sources / AppTypes / AlertWindow.swift
Back to remindwall — Current RemindWall
import Foundation
import Tagged
/// A weekday-mask + time-range window gating any time-sensitive alert
/// surface. Born as BusWindow; the typealias keeps existing call sites and
/// the stored "busWindow" setting string valid.
public enum AlertWindowTag {}
public typealias AlertWindow = Tagged<AlertWindowTag, String>
public typealias BusWindow = AlertWindow
extension AlertWindow {
/// Creates a window from a set of active weekdays and a start/end time.
/// Encoding: "<mask>|<startHour>:<startMinute>-<endHour>:<endMinute>"
/// Mask is a 7-bit field; bit (DaysOfWeek.rawValue - 1) is set when active.
public init(
weekdays: Set<DaysOfWeek>,
startHour: Int, startMinute: Int,
endHour: Int, endMinute: Int
) {
var mask = 0
for day in weekdays {
mask |= 1 << (day.rawValue - 1)
}
self.init(rawValue: "\(mask)|\(startHour):\(startMinute)-\(endHour):\(endMinute)")
}
/// "MTWTF · 6:30 AM–9:00 AM" - dense enough for a settings row.
public var summary: String {
let initials = DaysOfWeek.allCases
.filter { weekdays.contains($0) }
.map { String(String(describing: $0).prefix(1)) }
.joined()
return "\(initials) · \(startTimeDisplay)–\(endTimeDisplay)"
}
/// Mon–Fri, 06:30 to 09:00.
public static let `default` = AlertWindow(
weekdays: [.Monday, .Tuesday, .Wednesday, .Thursday, .Friday],
startHour: 6, startMinute: 30,
endHour: 9, endMinute: 0
)
public var weekdays: Set<DaysOfWeek> {
var result: Set<DaysOfWeek> = []
let mask = parsedMask
for day in DaysOfWeek.allCases where (mask & (1 << (day.rawValue - 1))) != 0 {
result.insert(day)
}
return result
}
public var startHour: Int { parsedTime(part: 0, field: 0) ?? 6 }
public var startMinute: Int { parsedTime(part: 0, field: 1) ?? 30 }
public var endHour: Int { parsedTime(part: 1, field: 0) ?? 9 }
public var endMinute: Int { parsedTime(part: 1, field: 1) ?? 0 }
public var startTotalMinutes: Int { startHour * 60 + startMinute }
public var endTotalMinutes: Int { endHour * 60 + endMinute }
public var startTimeDisplay: String { formatTime(hour: startHour, minute: startMinute) }
public var endTimeDisplay: String { formatTime(hour: endHour, minute: endMinute) }
/// Returns true when the given date falls on an active weekday and the time-of-day
/// is within the start/end window (overnight ranges like 22:00–06:00 supported).
public func isInWindow(date: Date, calendar: Calendar) -> Bool {
let weekday = calendar.component(.weekday, from: date)
let mask = parsedMask
guard (mask & (1 << (weekday - 1))) != 0 else { return false }
let hour = calendar.component(.hour, from: date)
let minute = calendar.component(.minute, from: date)
let current = hour * 60 + minute
let start = startTotalMinutes
let end = endTotalMinutes
if start == end {
return false
} else if start < end {
return current >= start && current < end
} else {
return current >= start || current < end
}
}
public func withWeekdays(_ weekdays: Set<DaysOfWeek>) -> AlertWindow {
AlertWindow(
weekdays: weekdays,
startHour: startHour, startMinute: startMinute,
endHour: endHour, endMinute: endMinute
)
}
public func withStart(hour: Int, minute: Int) -> AlertWindow {
AlertWindow(
weekdays: weekdays,
startHour: hour, startMinute: minute,
endHour: endHour, endMinute: endMinute
)
}
public func withEnd(hour: Int, minute: Int) -> AlertWindow {
AlertWindow(
weekdays: weekdays,
startHour: startHour, startMinute: startMinute,
endHour: hour, endMinute: minute
)
}
private var parsedMask: Int {
let parts = rawValue.split(separator: "|", maxSplits: 1)
guard parts.count == 2, let mask = Int(parts[0]) else { return 0 }
return mask & 0b111_1111
}
private func parsedTime(part: Int, field: Int) -> Int? {
let pipeParts = rawValue.split(separator: "|", maxSplits: 1)
guard pipeParts.count == 2 else { return nil }
let timeParts = pipeParts[1].split(separator: "-")
guard timeParts.count == 2 else { return nil }
let fields = timeParts[part].split(separator: ":")
guard fields.count == 2 else { return nil }
return Int(fields[field])
}
private func formatTime(hour: Int, minute: Int) -> String {
let hour12 = hour % 12
let displayHour = hour12 == 0 ? 12 : hour12
let period = hour < 12 ? "AM" : "PM"
return String(format: "%d:%02d %@", displayHour, minute, period)
}
}