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.
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.
- Provides types which accurately represents the information you want to store, and semantically conveys what is being stored to the reader of the code.
- Provides types which group contextual information such as the
TimeZone
. - Makes performing calendar arithmetic fast and simple.
- Simplifies serialising and parsing date and time information.
- 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
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.
print(calendarDate.month)
print(calendarDate.dayOfWeek)
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
.
We’ve added some conveniences for writing TimeInterval
s 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 = 18⁝30 // Use the "tricolon" operator
let example5 = 12⁝34⁝56 // Also supports seconds
You can do arithmetic with a TimeOfDay
and TimeInterval
:
let midday = 11⁝30 + 30.minutes
let elevenAM = 11⁝30 - 30.minutes
let fiveMins: TimeInterval = 06⁝00 - 05⁝55
let oneAM = 23⁝30 + (1.hours + 30.minutes) // Can span over midnight
It also supports various string conversions:
let timeOfDay = 15⁝34⁝56
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 = 11⁝30 ..< 18⁝00
let timeRangeOverMidnight: TimeRange = 23⁝30 ..< 01⁝00
// Can test is a time exists in the range
timeRange.contains(12⁝30) // 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.
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 aDateTime
2022-01-01
for aCalendarDate
12:30:00
for aTimeOfDay
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)
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
dateFormatter.setLocalizedDateFormatFromTemplate(dateFormat)
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.
- Website: ctrl-group.com
- Twitter: @ctrl_group
Time is available under the MIT license. See the LICENSE file for more info.
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!