Announcing Time 1.0.0: Type-safe Calendar Calculations

Four years ago I introduced Time 0.9.0. It is appropriate that today, on its first birthday, it finally graduates to 1.0.0! :partying_face:

Time is a package that provides extensive functionality for performing date and time calculations. It is built with safety and convenience in mind, so that "correct" calculations are easy, and "wrong" operations are either not possible (thanks to Swift's type system) or throw an error.

Time is available on Github, and its extensive documentation is hosted at the Swift Package Index.

Here's a brief overview of what Time can do for you:

// get the current device time:
let instantaneousNow = Clocks.system.now

// get the current calendar minute, a "Fixed<Minute>":
let currentMinute = Clocks.system.currentMinute

// round the current minute to the nearest quarter-hour:
let quarterHour = currentMinute.roundedToNearestMultiple(of: .minutes(15))

// advance the quarter hour by two months (accurate to the minute):
let thisTimeInTwoMonths = quarterHour + .months(2)

// from the quarter hour, get its calendar day:
let today = currentMinute.fixedDay

// get a Sequence of all the hours in the day:
let hours = today.hours

// print information about the hours in the day:
for hour in hours {
    // there are many ".format(...)" methods to handle just about any use-case
    print(hour.format(date: .medium, time: .long))
}

// be notified about the next 10 clock seconds:
for try await aSecond in Clocks.system.strike(every: Second.self).asyncValues.prefix(10) {

    let formatted = aSecond.format(year: .naturalDigits, month: .naturalName, day: .naturalDigits, weekday: .naturalName, hour: .naturalDigits, minute: .twoDigits, second: .twoDigits)
    
    print("The time is now: ", formatted)
}

All "fixed values" keep track of their calendar, locale, and time zone, enabling you to easily work with calendar values from around the world with the knowledge that their relative calculations are correct.

Beyond this, Time has numerous other capabilities, including:

  • Creating clocks in any combination of calendar, locale, or time zone
  • Creating clocks that move faster or slower than real time to facilitate testing time-dependent code
  • Adopting the RegionalClock protocol to create your own clocks for controlling time
  • Listening for time changes via a Combine publisher or AsyncSequence
  • Retrieving the Range<Instant> for any calendar value
  • Converting calendar values between time zones, locales, and calendars
  • Truncating calendar values to get their containing units
  • Finding differences between calendar values
  • Offsetting (adjusting) calendar values by specific amounts
  • And so much more!
62 Likes

@davedelong I must say I love the timing of the releases, looking forward to 1.1 in 4 years :joy:

11 Likes

Off course we had a sev 2 incident at work today, because a (non Swift) system could not cope with todays leap day. :sweat:

Sooo... due to the auspicious timing of this release, this is not something we should have to worry about with this package?

1 Like

Very nice, this is really great! Given the release date, I had to run some experiments, of course:

import Time

let startingPoint = try! Fixed(region: .current, year: 2024, month: 2, day: 29)
print("\(startingPoint.year)-\(startingPoint.month)-\(startingPoint.day)") // 2024-2-29

let twentyFive = startingPoint + .years(1)
print("\(twentyFive.year)-\(twentyFive.month)-\(twentyFive.day)") // 2025-2-28

let twentyFive2 = startingPoint.adding(years: 1)
print("\(twentyFive2.year)-\(twentyFive2.month)-\(twentyFive2.day)") // 2025-2-28

let twentyFive3 = startingPoint + .days(366)
print("\(twentyFive3.year)-\(twentyFive3.month)-\(twentyFive3.day)") // 2025-3-1

let twentyEight = startingPoint + .years(4)
print("\(twentyEight.year)-\(twentyEight.month)-\(twentyEight.day)") // 2028-2-29

let twentyEight2 = startingPoint + .years(1) + .years(1) + .years(1) + .years(1)
print("\(twentyEight2.year)-\(twentyEight2.month)-\(twentyEight2.day)") // 2028-2-28

Is there a way to do leap year aware calculations based on context? I.e., I would have hoped that twentyFive2 might give me the same result as twentyFive3.

Essentially, no. There's a semantic distinction between "adding one year" and "adding 366 days". The implementation of that distinction is left up to the underlying libicu library which Foundation wraps.

(It's also worth pointing out that Time supports calendars where the years are much shorter than 365 days)

1 Like

Whoa, calendrical computations without the optionals. Looks like the calendar API we all wish Foundation offered. Thanks for sharing your effort, @davedelong!

4 Likes

@davedelong Time to update the very last entry of your https://yourcalendricalfallacyis.com/ site to suggest using Time instead of NSCalendar API :stuck_out_tongue:

4 Likes

Done! It's also using some nifty new stuff from @benscheirman to dynamically format timestamps in different calendars :grin: