Skip to content

Instantly share code, notes, and snippets.

@JadenGeller
Last active July 29, 2025 07:59
Show Gist options
  • Save JadenGeller/4152349aa90f7a4b6af2e0efc0eb708e to your computer and use it in GitHub Desktop.
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
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