Last active
July 29, 2025 07:59
-
-
Save JadenGeller/4152349aa90f7a4b6af2e0efc0eb708e to your computer and use it in GitHub Desktop.
represent a particular day on the calendar that's stable across time zones
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
/// Represents a specific day in the Gregorian calendar as an integer offset. | |
/// | |
/// `CalendarDay` provides a timezone-stable way to identify calendar days. | |
/// When created, it captures which Gregorian calendar day a date falls on | |
/// according to the specified timezone, then stores this as an integer offset | |
/// from Apple's reference date (January 1, 2001). | |
/// | |
/// This type is useful when you need to: | |
/// - Track events that occur on specific calendar days | |
/// - Calculate day-based intervals that remain stable across timezone changes | |
/// - Schedule recurring daily events | |
/// | |
/// Example: | |
/// ```swift | |
/// // User takes medication starting Monday in London | |
/// let startDay = CalendarDay(from: Date()) | |
/// let nextDose = startDay.adding(days: 30) | |
/// | |
/// // 30 days later in New York, the calculation remains consistent | |
/// if CalendarDay.today >= nextDose { | |
/// // Time for next dose | |
/// } | |
/// ``` | |
public struct CalendarDay: Comparable, Hashable, Codable { | |
/// The number of days since January 1, 2001 00:00:00 UTC. | |
public let daysSinceReferenceDate: Int | |
/// Creates a calendar day from a raw offset. | |
/// | |
/// - Parameter daysSinceReferenceDate: The number of days since January 1, 2001 | |
public init(daysSinceReferenceDate: Int) { | |
self.daysSinceReferenceDate = daysSinceReferenceDate | |
} | |
/// Creates a calendar day from a date, interpreted in the specified timezone. | |
/// | |
/// - Parameters: | |
/// - date: The date to convert to a calendar day | |
/// - timeZone: The timezone for interpreting which day the date falls on. | |
public init(from date: Date, in timeZone: TimeZone) { | |
var calendar = Calendar(identifier: .gregorian) | |
calendar.timeZone = timeZone | |
// Get the year/month/day in the specified timezone | |
let components = calendar.dateComponents([.year, .month, .day], from: date) | |
// Create a date from these components interpreted as UTC | |
var utcCalendar = Calendar(identifier: .gregorian) | |
utcCalendar.timeZone = TimeZone(identifier: "UTC")! | |
let utcDate = utcCalendar.date(from: components)! | |
// Now calculate days from reference date | |
let referenceDate = Date(timeIntervalSinceReferenceDate: 0) | |
self.daysSinceReferenceDate = utcCalendar.dateComponents([.day], from: referenceDate, to: utcDate).day! | |
} | |
/// Today in the system's current timezone. | |
public static var today: CalendarDay { | |
CalendarDay(from: .now, in: .current) | |
} | |
/// Creates a new calendar day by adding days. | |
/// | |
/// - Parameter days: The number of days to add (can be negative) | |
/// - Returns: A new calendar day offset by the specified number of days | |
public func adding(days: Int) -> CalendarDay { | |
CalendarDay(daysSinceReferenceDate: daysSinceReferenceDate + days) | |
} | |
/// Calculates the number of days from another calendar day to this one. | |
/// | |
/// - Parameter other: The starting calendar day | |
/// - Returns: The number of days between the two days. | |
/// Positive if this day is later, negative if earlier. | |
public func days(since other: CalendarDay) -> Int { | |
daysSinceReferenceDate - other.daysSinceReferenceDate | |
} | |
/// The Gregorian date components (year, month, day) for this calendar day. | |
/// | |
/// Use these components to: | |
/// - Display the date to users | |
/// - Create calendar notification triggers | |
/// - Convert back to a Date with specific time components | |
public var dateComponents: DateComponents { | |
let referenceDate = Date(timeIntervalSinceReferenceDate: 0) | |
let date = Calendar.gregorianUTC.date(byAdding: .day, value: daysSinceReferenceDate, to: referenceDate)! | |
return Calendar.gregorianUTC.dateComponents([.era, .year, .month, .day], from: date) | |
} | |
// MARK: - Comparable | |
public static func < (lhs: CalendarDay, rhs: CalendarDay) -> Bool { | |
lhs.daysSinceReferenceDate < rhs.daysSinceReferenceDate | |
} | |
} | |
// MARK: - Private Helpers | |
private extension Calendar { | |
static let gregorianUTC: Calendar = { | |
var calendar = Calendar(identifier: .gregorian) | |
calendar.timeZone = TimeZone(identifier: "UTC")! | |
return calendar | |
}() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment