Skip to content

drekka/DayType

Repository files navigation

Calendar

DayType

An API for dates and nothing else. No calendars, no timezones, no hours, minutes or seconds. Just dates!

Swift provides excellent date support with its Date, Calendar, TimeZone and related types. But there's a catch — they're all designed to work with a specific point in time. And that's not always how people think, and sometimes not even what we get from a server.

For example we never refer to a person's birthday as being a specific point in time. We don't say "Hey, it's Dave's birthday on the 29th of August at 2:14 am AEST". We simply say the 29th of August and everyone knows what we mean. But Apple's time APIs don't have that generalisation and that means extra work for developers to strip times, adjust time zones, and compare sanitised values. All of which is easy to get wrong.

This is where DayType steps in.

Basically DayType simplifies date handling through a Day type which represents of a 24-hour period independent of any timezone. There's no hours, minutes, seconds and milliseconds. Nor is there any time zones or even calendars to deal with. In other words, it does dates as people think about them.

Table of contents

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 // 29

static 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 // .thursday

Mathematical 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 += april

Protocol 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) // true

Comparable

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.

About

A Swift API for dates (days) rather than dates and times

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages