A swift package to simplify calendar, date and time handling.

What's New



Initial release


Swift Package Manager iOS + macOS Twitter: @ctrl_group

Welcome to Time, a swift package to simplify calendar, date and time handling.


Time was built by Ctrl Group to make working with date and time simple and to avoid the common mistakes made when using the standard Date object.

What does Time provide over the Foundation library

The Foundation library provides a Date object which represents a single point in time, independent of any calendar or time zone. This object needs to be used in conjunction with a DateFormatter, TimeZone, Locale, DateComponents and Calendar in order to perform calendar arithmetic and create meaningful representations of dates and times.

Time uses these tools and provides a simple API to allow you to represent a CalendarDate, TimeOfDay, Timestamp, CalendarDateRange, TimeRange and more.

Key features

  1. Provides types which accurately represents the information you want to store, and semantically conveys what is being stored to the reader of the code.
  2. Provides types which group contextual information such as the TimeZone.
  3. Makes performing calendar arithmetic fast and simple.
  4. Simplifies serialising and parsing date and time information.
  5. Provides an easy way to stub the time for unit tests.


Time is distributed using the Swift Package Manager. To install it into a project, add it as a dependency within your Package.swift manifest:

let package = Package(
    dependencies: [
        .package(url: "https://github.com/ctrlgroup/Time.git", from: "1.0.0")

Then import Time wherever you’d like to use it:

import Time


Calendar Dates

CalendarDate allows you to represent a date in the gregorian calendar, without the time, and irrespective of the time zone.

You can create them with a day, month and year explicitly, or you can create them using a Date and TimeZone.

let europeLondon: TimeZone = ...
let date: Date = ...

// April 4th, 1994
let dateOfBirth = try? CalendarDate(day: 03, month: 04, year: 1994)

// Can also be created from a `Date`
let calendarDate = CalendarDate(date: date, timeZone: europeLondon)

Once instantiated, you can query them for their day, month, year, and even weekday.


They can also be converted back to a Date (representing the moment of midnight on that day)

// Can be converted back to a `Date` using a time zone
let date: Date = calendarDate.date(in: europeLondon)

You can also easily do calendar arithmetic, and usually without having to do any complex Calendar conversions.

let nextWeek = dateOfBirth + 7 // Adds 7 days
let difference: Int = nextWeek - dateOfBirth // 7

// Calendar dates are comparable
let firstOfMay = try? CalendarDate(day: 01, month: 05, year: 2022)
let tenthOfJune = try? CalendarDate(day: 10, month: 06, year: 2022)
tenthOfJune > firstOfMay // true

// Easily find weekdays (uses a Calendar to compute the weekday)
dateOfBirth.previous(weekday: .sunday) // Finds the previous Sunday
dateOfBirth.next(weekday: .sunday) // Finds the next Sunday

Arithmetic with calendar dates is a trivial operation as the underlying raw value is just an Int. This means you don’t need to worry about the overhead of using a Calendar, and can easily do things like iterate over a CalendarDateRange.

Time Interval

We’ve added some conveniences for writing TimeIntervals in an easily readable way.

let seconds: TimeInterval = 12.hours + 34.minutes + 56.seconds
let fiveDays: TimeInterval = 5.days


To represent just time (without a date) you can use TimeOfDay. This is useful for things like recording the time for a repeating reminder or alarm.

A TimeOfDay can be created in various different ways:

let example1 = try? TimeOfDay(hour: 18, minute: 30, second: 0)
let example2 = try? TimeOfDay(secondsSinceMidnight: 3600 * 12)
let example3 = TimeOfDay(date: Date(), timeZone: europeLondon)
let example4 = 1830 // Use the "tricolon" operator
let example5 = 123456 // Also supports seconds

You can do arithmetic with a TimeOfDay and TimeInterval:

let midday = 1130 + 30.minutes
let elevenAM = 1130 - 30.minutes
let fiveMins: TimeInterval = 0600 - 0555
let oneAM = 2330 + (1.hours + 30.minutes) // Can span over midnight

It also supports various string conversions:

let timeOfDay = 153456
let locale = Locale(identifier: "en-US")
timeOfDay.string(style: (.short, .twelveHour), locale: locale) // 3:34 PM
timeOfDay.string(style: (.medium, .twentyFourHour)) // 15:34:56

// Converting from a string
let result = TimeOfDay(string: "3:34 PM", 
                       style: (.short, .twelveHour),
                       locale: locale)

TimeOfDay is also Equatable, Hashable, Codable and Comparable.


CalendarDateRange allows you to easily represent a range of calendar dates which you can iterate over, and use all the commonly available operators of a Range.

// A calendar date range is simply a Swift Range
public typealias CalendarDateRange = Range<CalendarDate>

// Can be created in the usual way
let range = startDate ..< endDate

// It can be indexed using integers
sut[0] == startDate
sut[1] == startDate + 1

// Convenience initializers make calendar date ranges easy to understand
CalendarDateRange(firstDate: startDate, lastIncludedDate: lastDate)
CalendarDateRange(startDate: startDate, endDate: endDate)


A TimeRange is slightly more complex as a TimeOfDay isn’t strictly sequential (they loop when continually adding seconds). However it’s still useful to be able to form ranges over them.

// Can be created in the usual way
let timeRange: TimeRange = 1130 ..< 1800
let timeRangeOverMidnight: TimeRange = 2330 ..< 0100

// Can test is a time exists in the range
timeRange.contains(1230) // true


DateTime can be used when you want to represent a specific time on a specific calendar date, but in any time zone.

let calendarDate: CalendarDate = ...
let timeOfDay: timeOfDay = ...
let dateTime = DateTime(calendarDate: calendarDate, timeOfDay: timeOfDay)

// Can be created from a `Date` and `TimeZone`
let dateTime = DateTime(date: Date(), timeZone: europeLondon)

// Can be converted to a `Date` or `Timestamp`
let date = dateTime.date(in: europeLondon)
let timestamp = dateTime.timestamp(in: europeLondon)


Most of the time a Timestamp would be a more appropriate type to replace Date. It is simply an ecapsulation of a Date with the TimeZone it was recorded at, so similarly it represents a single point in time, but contains the necessary context to understand what date and time it represents as well.

let timestamp = Timestamp(date: Date(), timeZone: europeLondon)
timestamp.day // 1st March
timestamp.timeOfDay // 12pm
timestamp.dateTime = // 1st March at 12pm

One other difference between Timestamp and Date is in the way it implements Equatable. As Date is based on a Double it suffers from inaccuracies of floating point arithmetic. This means comparing two Date objects can somtimes give unexpected results…

let date1 = dateFormatter.date(from: "2022-01-01T12:34:56.1234Z")!
let date2 = Date(timeIntervalSinceReferenceDate: 662733296.123)
print(date1) // 2022-01-01 12:34:56 +0000
print(date2) // 2022-01-01 12:34:56 +0000
date1 == date2 // false

Timestamp fixes this by considering the dates equal if they are within a millisecond of each other.


Clock is a class which can be read to learn the current date and time. It should be used instead of every using the Date() initialiser. By injecting a Clock instance into your code you are able to stub it out in tests in order to unit test code which depends on the current time.

Other functionality

Many of the types (CalendarDate, TimeOfDay etc.) are also Equatable, Comparable, Hashable, and Codable.

When serializing with Codable the package will prefer to save types as a standard ISO8601 string e.g.

  • 2022-01-01T12:30:00Z for a DateTime
  • 2022-01-01 for a CalendarDate
  • 12:30:00 for a TimeOfDay

A Timestamp need the context of a TimeZone so will encode like this in JSON:

  "timestamp": "...",
  "timeZone": "Europe/London"

Here, the timestamp property is an encoded Date. This will encode in the default way (as a double, number of seconds since the reference date) unless you specify to use an ISO8601 representation using the date encoding strategy property:

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let date = try? encoder.encode(timestamp)

String Formatting

Time tries to provide a simpler way to create strings from dates and times without having to use a DateFormatter class directly.

  • It simplifys localizing your date formats by always preferring to use a localized template string or date and time style parameters.
  • It cuts down on lines of code by providing a simple to use API
  • It tries to improve on performance by caching DateFormatter classes that are commonly used.
let dateFormat = "dd-MM-yyyy"

// Using Foundation
let date: Date = ...
let timeZone: TimeZone = ...
let dateFormatter = DateFormatter()
dateFormatter.timeZone = timeZone
dateFormatter.locale = .current
let result = dateFormatter.string(from: date)

// Using Time
let calendarDate: CalendarDate = ...
let result = calendarDate.string(withFormat: dateFormat, locale: .current)

Similar methods also exist for TimeOfDay and additional methods exist on CalendarDate, TimeOfDay, DateTime, and Timestamp for creating ISO8601 strings.


Time was designed and implemented by the team at Ctrl Group.


Time is available under the MIT license. See the LICENSE file for more info.

Contributions and support

We would like Time to developed completely in the open going forward, and your contributions are more than welcome!

If you find any problems, have any suggestions, or wish to improve on the documentaion then we encourage you to open a Pull Request and we will be actively reviewing and accepting contributions.

We hope that you’ll find this package useful and enjoyable!


  • Swift Tools 5.5.0
View More Packages from this Author


Last updated: Mon Mar 13 2023 04:15:27 GMT-0500 (GMT-05:00)