-
Notifications
You must be signed in to change notification settings - Fork 696
feat: add claudePeakHoursEnabled setting and integrate into UsageMenu… #611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b314126
09a4420
702c0a3
ccf1c46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import Foundation | ||
|
|
||
| public enum ClaudePeakHours: Sendable { | ||
| private static let peakTimeZone = TimeZone(identifier: "America/New_York")! | ||
| private static let peakStartHour = 8 | ||
| private static let peakEndHour = 14 | ||
|
|
||
| public struct Status: Sendable, Equatable { | ||
| public let isPeak: Bool | ||
| public let label: String | ||
| } | ||
|
|
||
| public static func status(at date: Date) -> Status { | ||
| let calendar = self.calendar() | ||
| let components = calendar.dateComponents([.hour, .minute, .weekday], from: date) | ||
|
|
||
| guard let hour = components.hour, | ||
| let minute = components.minute, | ||
| let weekday = components.weekday | ||
| else { | ||
| return Status(isPeak: false, label: "Off-peak") | ||
| } | ||
|
|
||
| let isWeekday = weekday >= 2 && weekday <= 6 | ||
| let nowMinutes = hour * 60 + minute | ||
| let peakStartMinutes = self.peakStartHour * 60 | ||
| let peakEndMinutes = self.peakEndHour * 60 | ||
| let isInPeakWindow = nowMinutes >= peakStartMinutes && nowMinutes < peakEndMinutes | ||
|
|
||
| if isWeekday && isInPeakWindow { | ||
| let remaining = peakEndMinutes - nowMinutes | ||
| return Status( | ||
| isPeak: true, | ||
| label: "Peak · ends in \(self.formatDuration(minutes: remaining))") | ||
| } | ||
|
|
||
| if isWeekday { | ||
| if nowMinutes < peakStartMinutes { | ||
| let until = peakStartMinutes - nowMinutes | ||
| return Status( | ||
| isPeak: false, | ||
| label: "Off-peak · peak in \(self.formatDuration(minutes: until))") | ||
| } else { | ||
| let minutesLeftToday = 24 * 60 - nowMinutes | ||
| let nextPeakMinutes: Int | ||
| if weekday == 6 { | ||
| nextPeakMinutes = minutesLeftToday + 2 * 24 * 60 + peakStartMinutes | ||
| } else { | ||
| nextPeakMinutes = minutesLeftToday + peakStartMinutes | ||
| } | ||
| return Status( | ||
| isPeak: false, | ||
| label: "Off-peak · peak in \(self.formatDuration(minutes: nextPeakMinutes))") | ||
| } | ||
| } | ||
|
|
||
| let daysUntilMonday: Int | ||
| if weekday == 7 { | ||
| daysUntilMonday = 2 | ||
| } else { | ||
| daysUntilMonday = 1 | ||
| } | ||
| let minutesLeftToday = 24 * 60 - nowMinutes | ||
| let totalMinutes = minutesLeftToday + (daysUntilMonday - 1) * 24 * 60 + peakStartMinutes | ||
|
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This countdown is computed with fixed 24-hour day math, which is incorrect in Useful? React with 👍 / 👎. |
||
| return Status( | ||
| isPeak: false, | ||
| label: "Off-peak · peak in \(self.formatDuration(minutes: totalMinutes))") | ||
| } | ||
|
|
||
| private static func formatDuration(minutes: Int) -> String { | ||
| let h = minutes / 60 | ||
| let m = minutes % 60 | ||
| if h == 0 { | ||
| return "\(m)m" | ||
| } | ||
| if m == 0 { | ||
| return "\(h)h" | ||
| } | ||
| return "\(h)h \(m)m" | ||
| } | ||
|
|
||
| private static func calendar() -> Calendar { | ||
| var cal = Calendar(identifier: .gregorian) | ||
| cal.timeZone = self.peakTimeZone | ||
| return cal | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| import CodexBarCore | ||
| import Foundation | ||
| import Testing | ||
|
|
||
| struct ClaudePeakHoursTests { | ||
| private static let eastern = TimeZone(identifier: "America/New_York")! | ||
|
|
||
| private func date( | ||
| year: Int = 2026, | ||
| month: Int = 3, | ||
| day: Int, | ||
| hour: Int, | ||
| minute: Int = 0 | ||
| ) -> Date { | ||
| var cal = Calendar(identifier: .gregorian) | ||
| cal.timeZone = Self.eastern | ||
| return cal.date(from: DateComponents( | ||
| year: year, month: month, day: day, | ||
| hour: hour, minute: minute))! | ||
| } | ||
|
|
||
| // MARK: - Weekday peak hours | ||
|
|
||
| @Test | ||
| func weekdayMorningBeforePeak() { | ||
| // Wednesday 2026-03-25 at 7:00 AM ET → off-peak, 1h until peak | ||
| let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 7)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 1h") | ||
| } | ||
|
|
||
| @Test | ||
| func weekdayJustBeforePeak() { | ||
| // Wednesday 2026-03-25 at 7:45 AM ET → off-peak, 15m until peak | ||
| let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 7, minute: 45)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 15m") | ||
| } | ||
|
|
||
| @Test | ||
| func weekdayPeakStart() { | ||
| // Wednesday 2026-03-25 at 8:00 AM ET → peak, 6h remaining | ||
| let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 8)) | ||
| #expect(status.isPeak) | ||
| #expect(status.label == "Peak · ends in 6h") | ||
| } | ||
|
|
||
| @Test | ||
| func weekdayMidPeak() { | ||
| // Wednesday 2026-03-25 at 11:30 AM ET → peak, 2h 30m remaining | ||
| let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 11, minute: 30)) | ||
| #expect(status.isPeak) | ||
| #expect(status.label == "Peak · ends in 2h 30m") | ||
| } | ||
|
|
||
| @Test | ||
| func weekdayPeakEndBoundary() { | ||
| // Wednesday 2026-03-25 at 1:59 PM ET → peak, 1m remaining | ||
| let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 13, minute: 59)) | ||
| #expect(status.isPeak) | ||
| #expect(status.label == "Peak · ends in 1m") | ||
| } | ||
|
|
||
| @Test | ||
| func weekdayAfterPeak() { | ||
| // Wednesday 2026-03-25 at 2:00 PM ET → off-peak, next peak tomorrow 8 AM (18h) | ||
| let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 14)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 18h") | ||
| } | ||
|
|
||
| @Test | ||
| func weekdayLateEvening() { | ||
| // Thursday 2026-03-26 at 11 PM ET → off-peak, 9h until next peak | ||
| let status = ClaudePeakHours.status(at: self.date(day: 26, hour: 23)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 9h") | ||
| } | ||
|
|
||
| // MARK: - Weekend | ||
|
|
||
| @Test | ||
| func saturdayMorning() { | ||
| // Saturday 2026-03-28 at 10 AM ET → off-peak, ~46h until Monday 8 AM | ||
| let status = ClaudePeakHours.status(at: self.date(day: 28, hour: 10)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 46h") | ||
| } | ||
|
|
||
| @Test | ||
| func sundayEvening() { | ||
| // Sunday 2026-03-22 at 9 PM ET → off-peak, 11h until Monday 8 AM | ||
| let status = ClaudePeakHours.status(at: self.date(day: 22, hour: 21)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 11h") | ||
| } | ||
|
|
||
| // MARK: - Friday → Monday transition | ||
|
|
||
| @Test | ||
| func fridayAfterPeak() { | ||
| // Friday 2026-03-27 at 3 PM ET → off-peak, next peak Monday 8 AM (65h) | ||
| let status = ClaudePeakHours.status(at: self.date(day: 27, hour: 15)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 65h") | ||
| } | ||
|
|
||
| @Test | ||
| func fridayPeak() { | ||
| // Friday 2026-03-27 at 12 PM ET → peak, 2h remaining | ||
| let status = ClaudePeakHours.status(at: self.date(day: 27, hour: 12)) | ||
| #expect(status.isPeak) | ||
| #expect(status.label == "Peak · ends in 2h") | ||
| } | ||
|
|
||
| // MARK: - Edge cases | ||
|
|
||
| @Test | ||
| func mondayMidnight() { | ||
| // Monday 2026-03-23 at 12:00 AM ET → off-peak, 8h until peak | ||
| let status = ClaudePeakHours.status(at: self.date(day: 23, hour: 0)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 8h") | ||
| } | ||
|
|
||
| @Test | ||
| func peakWithMinuteGranularity() { | ||
| // Wednesday 2026-03-25 at 12:15 PM ET → peak, 1h 45m remaining | ||
| let status = ClaudePeakHours.status(at: self.date(day: 25, hour: 12, minute: 15)) | ||
| #expect(status.isPeak) | ||
| #expect(status.label == "Peak · ends in 1h 45m") | ||
| } | ||
|
|
||
| @Test | ||
| func saturdayMidnight() { | ||
| // Saturday 2026-03-28 at 12:00 AM ET → off-peak, 56h until Monday 8 AM | ||
| let status = ClaudePeakHours.status(at: self.date(day: 28, hour: 0)) | ||
| #expect(!status.isPeak) | ||
| #expect(status.label == "Off-peak · peak in 56h") | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.