An API for dates that doesn't involve hours, minutes, seconds and timezones.
Swift provides excellent date support through it's Date
, Calendar
, TimeZone
and other types. However there's a catch, they're all designed to with with specific points in time rather than the generalisations people often use.
For example, the APIS cannot refer to a person's birthday without anchoring it to a specific hour, minute, second and even partial second within a specific timezone. Yet people when discussion a person's birthday only think of the date in whatever timezone they are in. Not the exact moment of a person's birth which sometime's even the person being discussed doesn't know.The same goes for other dates people often work with, an employee leave, religious holidays, retail sales, festivals, etc all typically have a date associated, but not a time.
As a result developers often find themselves writing code to strip time from Swift's Date
in order to trick it into acting like a date. Often with mixed results as there are many technical issues to consider when coercing a point in time to such a generalisation. Especially with time zones and sometime questionable input from external sources.
DayType
provides simplify date handling through it's Day
type. A Day
is a representation of a 24 hours period instead of a specific point in time. ie. it doesn't have any hours, minutes, timezones, etc. This allows date code to be simpler because the developer no longer needs to sanitise time components, and that removes the angst of accidental bugs as well as making date based code considerably simpler.
DayType
is a SPM package only.
The common type you'll use is Day
.
Day
has a number of convenience initialisers which are pretty self explanatory and similar to Swift's Date
initialisers:
init()
init(daysSince1970: DayInterval)
init(timeIntervalSince1970: TimeInterval)
init(date: Date, usingCalendar calendar: Calendar = .current)
init(components: DayComponents)
init(_ year: Int, _ month: Int, _ day: Int)
init(year: Int, month: Int, day: Int)
Literally the number of days since Swift's base date of 00:00:00 UTC on 1 January 1970.
Note that matches the number of days produced by this Apple API based code:
let fromDate = Calendar.current.startOfDay(for: Date(timeIntervalSince1970: 0))
let toDate = Calendar.current.startOfDay(for: Date())
let numberOfDays = Calendar.current.dateComponents([.day], from: fromDate, to: toDate).day!
DayType's property wrappers are designed to address the mostly commonly seen issues when coding and decoding data from external sources.
Note: Whilst all of these wrappers support both Day
and Day?
properties through the use of the DayCodable
protocol, it's also technically possible to apply this protocol to other types to make them convertible to a Day
.
Converts epoch timestamps to Day
. For example the JSON data structure:
{
"dob":856616400
}
Can be read by:
struct MyType: Codable {
@EpochSeconds var dob: Day // or Day?
}
Essentially the same as @EpochSeconds
but expects the epoch time to be in millisecond epoch timestamps.
{
"dob":856616400123
}
Can be read by:
struct MyType: Codable {
@EpochMilliseconds var dob: Day // or Day?
}
Converts ISO8601 date strings to Day
.
{
"dob": "1997-02-22T13:00:00+11:00"
}
Can be read by:
struct MyType: Codable {
@ISO8601 var dob: Day // or Day?
}
Where T: DayCodable
and Configurator: ISO8601Configurator
.
Internally DayType
uses an ISO8601DateFormatter
to read and write ISO8601 strings. As there are a variety of ISO8601 formats, this property wrapper allows you to pre-configure the formatter before it processes the string.
{
"dob": "20120202 133323"
}
Can be read by:
enum MinimalFormat: ISO8601Configurator {
static func configure(formatter: ISO8601DateFormatter) {
formatter.timeZone = TimeZone(secondsFromGMT: 11 * 60 * 60)
formatter.formatOptions.insert(.withSpaceBetweenDateAndTime)
formatter.formatOptions.subtract([.withTimeZone, .withColonSeparatorInTime, .withDashSeparatorInDate]) }
}
struct MyType: Codable {
@CustomISO8601<Day, MinimalFormat> var dob: Day
// or ...
@CustomISO8601<Day?, MinimalFormat> var dob: Day?
}
The property wrapper is configured trough a ISO8601Configurator
protocol instance. There's only one function so implementing the protocol is pretty easy.
Note that because Swift does not current support specifying a default type for a generic argument, @CustomISO8601<T, Configurator>
requires you to specify the DayCodable
type (Day
or Day?
) which must match the type of the property.
This configurator does not change the formatter. It's main purpose is to support the @ISO8601
property wrapper.
This configurator is for the common situation where the ISO8601 string does not have the time zone specified. For example "1997-02-22T13:00:00"
.
Where T: DayCodable
and Configurator: DateStringConfigurator
.
This property wrapper handles dates stored as strings. It makes use of a custom configurator to specify the format of the date string with a number of common formats supplied.
{
"dob": "2012-12-02"
}
Can be read by:
struct MyType: Codable {
@DateString<Day, DateStringConfig.DMY> var dob: Day
// or ...
@DateString<Day?, DateStringConfig.DMY> var dob: Day?
}
The DateStringConfigurator
protocol specifies only a single function which is static
. That function is used to configure the formatter used to read and write the date strings.
Note: Because Swift does not current support specifying a default type for a generic argument, @DateString<T, Configurator>
requires you to specify the DayCodable
type (Day
or Day?
).
Reads date strings that follow the ISO8601 format but don't have any time components. ie. `2012-12-01'
Reads date strings using the dd/MM/yyyy
date format. ie. `01/12/2012'
Reads date strings using the MM/dd/yyyy
date format. ie. `12/01/2012'
Day
has also been extended to support a variety of functions and operators. it has +
, -
, +=
and -=
operators which can be used to add or subtract a number of days from a day.
let day = Day(2000,1,1) + 5 // -> 2000-01-06
let day = Day(2000,1,1) - 10 // -> 1999-12-21
let day = Day(2000,1,1)
day += 5 // -> 2000-01-06
let day = Day(2000,1,1)
day -= 5 // -> 1999-12-21
And you can subtract one day from another to get the duration between them.
Day(2000,1,10) - Day(2000,1,5) // -> 5 days duration.
Using a passed Calendar
and TimeZone
, this function coverts a Day
to a Swift Date
with the Day
's year, month and day, and a time of 00:00
(midnight). With no arguments this function uses the current calendar and time zone.
Lets you add any number of years, months or days to a Day
and get a new day
back. This is convenient for doing things like producing a sequence of dates for the same day on each month.
Wrapping Date.formatted(date:time:)
this function formats a day using the standard formatting specified by the Date.FormatStyle.DateStyle
styles. The time component of Date.formatted(date:time:)
is omitted.
Similar to the way Date
has a matching DateComponents
, Day
has a matching DayComponents
. In this case mostly as a convenient wrapper for passing the individual values for a year, month and day.
Day
is fully Codable
.
It's base value is an Int
representing the number of days since 1 January 1970 which can accessed via the .daysSince1970
property.
Day
is Equatable
so
Day(2001,2,3) == Day(2001,2,3) // true
Day
is Comparable
which lets you use all the comparable operators to compare dates. ie. >
, <
, >=
and <=
.
Day
is Hashable
so it can be used as dictionary keys and in sets.
Day
is Stridable
which means you can use it in for loops as well as with the stride(from:to:by:)
function.
for day in Day(2000,1,1)...Day(2000,1,5) {
/// do something with the 1st, 2nd, 3rd, 4th and 5th.
}
for day in Day(2000,1,1)..<Day(2000,1,5) {
/// do something with the 1st, 2nd, 3rd and 4th.
}
for day in stride(from: Day(2000,1,1), to: Day(2000,1,5), by: 2) {
/// do something with the 1st and 3rd.
}
- Can't thank Howard Hinnant enough. Using his math instead of Apple's APIs produced a significant speed boost when converting to and from years, months and days.
- A second thanks to the guys behind the excellent Nimble test assertion framework which I prefer over Apple's XCTest asserts. Sorry Apple.
Obviously there are a large number of useful functions that can be added to this API, many of which could come from various other calculations on http://howardhinnant.github.io/date_algorithms.html#weekday_from_days. However I plan to add these as it becomes clear they will provide a useful addition rather than re-implementing a large number of functions that may not ben needed.
Please feel free to drop a request for any thing you'd like added.