drekka/daytype
_An API for dates and nothing else. No calendars, no timezones, no hours, minutes or seconds. **Just dates!**_
Installation
DayType is a SPM package only. So install it as you would install any other package.
Introducing Day
DayType's core type is Day which has all the necessary properties and functions to read, create and manipulate them. Most of which have been modelled off Apple's APIs for consistency and that sense of familiarity.
Initialisers
A Day has a number of convenience initialisers. Most of which are pretty self-explanatory and similar what you would be used to seeing with a Swift Date:
init() // Creates a `Day` based on the current time.
init(daysSince1970: DayInterval) // Creates a `Day` using the number of days since 1970.
init(timeIntervalSince1970: TimeInterval) // Creates a `Day` from a `TimeInterval`.
init(date: Date, usingCalendar calendar: Calendar = .current) // Creates a `Day` from a `Date` with an optional calendar.
init(_ dayComponents: DayComponents) throws // Creates a `Day` from a `DayComponents` value.
init(_ year: Int, _ month: Int, _ day: Int) throws // Creates a `Day` from individual year, month and day values. Short form.
init(year: Int, month: Int, day: Int) throws // Creates a `Day` from individual year, month and day values.Properties
var daysSince1970: Int { get }
Literally the number of days since Swift's base date of 00:00:00 UTC on 1 January 1970. Note this is the number of whole days, dropping any spare hours, minutes and seconds.
Note: This matches the number of days produced by this API 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!
var dayComponents: DayComponents { get }
Returns a DayComponents value containing the year, month, and dayOfMonth properties of the day. Computed on access using Hinnant's civil_from_days algorithm.
let components = try Day(2025, 8, 29).dayComponents
components.year // 2025
components.month // 8
components.dayOfMonth // 29static var today: Day { get }
Convenience property that returns a Day representing today's date. Equivalent to Day().
var weekday: Weekday { get }
Returns the day of the week as a Weekday enum value.
try Day(2026, 3, 2).weekday // .monday
try Day(1970, 1, 1).weekday // .thursdayMathematical operators
Day has a range of mathematical operators for adding and subtracting days from a Day:
// Adding days
var day = try Day(2000,1,1) + 5 // -> 2000-01-06
day += 5 // -> 2000-01-11
// Subtracting days
var day = try Day(2000,1,1) - 10 // -> 1999-12-21
day -= 5 // -> 1999-12-16
// Obtaining a duration in days
try Day(2000,1,10) - Day(2000,1,5) // -> 5 days duration.Functions
func date(inCalendar calendar: Calendar = .current, timeZone: TimeZone? = nil) -> Date
Using the passed Calendar and TimeZone this function converts a Day to a Swift Date in a specific timezone with the time components set to 00:00 (midnight).
func day(byAdding component: Day.Component, value: Int) -> Day
Adds any number of years, months or days to a Day and returns a new Day. This is convenient for doing things like producing a sequence of dates for the same day on each month.
func formatted(_ day: Date.FormatStyle.DateStyle = .abbreviated) -> String
Uses Apple's Date.formatted(date:time:) function to format the day into a String using the formatting specified in Date.FormatStyle.DateStyle.
Calendar generation
DayType can also generate a data structure specifically for building calendar UIs. It has a CalendarDays typealias which maps to a OrderedDictionary<Day, [DayComponents]> (Apple's swift-collections) where the key is the first Day of a single week in the calendar and the value is a an array of DayComponents values representing the days in that week. Starting from either Sunday or Monday.
The intent of this data structure is to allow easy mapping into a UI. Simply loop through the array values to create the Sunday to Saturday or Monday to Sunday cells.
Generating a calendar month
Use calendarMonth(startingOn:) to generate the grid for the month containing a given day. It doesn't matter which day you give it, the function will work out the month to build out. Because it's focused on calendars the first and last week arrays may also contains some days from the prior or next month to ensure those week arrays have the full 7 days.
// Instance method
let month = try Day(2026, 3, 15).calendarMonth(startingOn: .monday)
// Static convenience
let month = try Day.calendarMonth(containing: Day(2026, 3, 15), startingOn: .sunday)
// Defaults to today and Sunday start
let month = Day.calendarMonth()The StartOfWeek enum controls which day begins each week row (.sunday or .monday).
Merging calendar months
The + and += operators merge calendar months together, automatically deduplicating overlapping boundary weeks:
let march = try Day(2026, 3, 15).calendarMonth(startingOn: .monday)
let april = try Day(2026, 4, 2).calendarMonth(startingOn: .monday)
// Merge two CalendarDays dictionaries
let twoMonths = march + april
// or …
var year = march
year += aprilProtocol conformance
Codable
Day is fully Codable.
When encoded or decoded it uses an Int representing the number of days since 1 January 1970. This value can also be accessed via the .daysSince1970 property.
Equatable
Day is Equatable so days can be compared:
try Day(2001, 2, 3) == Day(2001, 2, 3) // trueComparable
Day is Comparable which enables the comparable operators: >, <, >= and <= for comparing days.
Hashable
Day is also Hashable which allows it to be used as a dictionary key or in a set.
Strideable
Day is Strideable which means you can use it in for loops as well as with the stride(from:to:by:) function. For example:
for day in try Day(2000, 1, 1) ... Day(2000, 1, 5) {
/// do something with the 1st, 2nd, 3rd, 4th and 5th.
}
for day in try Day(2000, 1, 1) ..< Day(2000, 1, 5) {
/// do something with the 1st, 2nd, 3rd and 4th.
}
for day in stride(from: try Day(2000, 1, 1), to: try Day(2000, 1, 5), by: 2) {
/// do something with the 1st and 3rd.
}Property wrappers
DayType also provides a number of property wrappers which implement Codable, the intent being to allow easy conversions from all sorts of date formats that are often returned from servers.
All of the supplied property wrappers can read and write both Day and optional Day? properties and are grouped by the format of the data they expect to encode and decode.
`@DayString.DMY`, `@DayString.MDY` & `@DayString.YMD`
These property wrappers are designed to encode and decode dates in the dd/MM/yyyy, MM/dd/yyyy and yyyy-MM-dd formats. For example:
struct MyData {
@DayString.DMY var dmyDay: Day // "30/04/2025"
@DayString.MDY var mdyDay: Day // "04/30/2025"
@DayString.YMD var ymdOptionalDay: Day? // "2025-04-30"
}`@Epoch.Seconds` & `@Epoch.Milliseconds`
Encodes and decodes days as epoch timestamps. For example:
struct MyData {
@Epoch.Seconds var optionalSeconds: Day? // 1746059246
@Epoch.Milliseconds var milliseconds: Day // 1746059246123
}`@ISO8601.Default` and `@ISO8601.SansTimezone`
Encodes and decodes standard ISO 8601 date strings. The only difference is that @ISO8601.SansTimezone is, as its name suggests, intended for reading strings written without a timezone value. For example:
struct MyData {
@ISO8601.Default var iso8601: Day // "2025-04-30T12:01:00Z"
@ISO8601.SansTimezone var optionalSansTimezone: Day? // "2025-04-30T12:01:00"
}Encoding and decoding nulls
By default all of DayType's property wrappers can handle decoding where the passed value is a null or if there is no value at all. For example:
struct MyData {
@DayString.DMY var dmy: Day?
}Will read both of these JSONs, setting dmy to nil:
// Null value.
{
"dmy": null
}
// Missing value.
{}When encoding, DayType will skip encoding nil values (producing {}), however some APIs require null values. To handle these APIs, DayType provides nested property wrappers which will write null values instead of skipping the keys altogether. For example:
struct MyData {
@DayString.DMY.Nullable var dmy: Day?
@Epoch.Seconds.Nullable var seconds: Day?
@ISO8601.Default.Nullable var iso8601: Day?
}Will write the following JSON when all the properties are nil:
{
"dmy": null,
"seconds": null,
"iso8601": null
}DayType and SwiftData
DayType works within SwiftData up to a point. That point being where you wish to use a Day in a SwiftData @Query. As an example you might do something like this:
@Model
class Holiday {
let startDatye: Day
let endEnd: Day
}
struct SomeView: View {
@Query(sort: \Holiday.startDate) private var holidays: [Holiday]
}This makes sense however when SwiftData writes the schema out to the database it will actually flatten the two Day fields into the Holiday Table as something like:
CREATE TABLE ZHOLIDAY (
Z_PK INTEGER PRIMARY KEY,
ZDAYSSINCE1970 INTEGER,
ZDAYSSINCE19701 INTEGER
) The result of this is that when you run the @Query(…) it fails, claiming it's unable to resolve the startDate key path.
This is due to the way SwiftData works. When flattening it uses mirrors and raw types, reaching into each Day to get the name of the internal property holding the number of days since 1970.
There is no way (currently) around this. SwiftData has no facility to specifically name the database field and due to it's use or mirrors there's no swift trickery we can use to make it work.
Instead what we have to do is explicity use Day's daysSince1970 property in the query like this:
struct SomeView: View {
@Query(sort: \Holiday.startDate.daysSince1970) private var holidays: [Holiday]
}References and thanks
- 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.
Future additions
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 the Hinnant date algorithms page. However I plan to add these as it becomes clear they will provide a useful addition rather than reimplementing a large number of functions that may not be needed.
Please feel free to drop a request for anything you'd like added.
Package Metadata
Repository: drekka/daytype
Default branch: main
README: README.md