From ffdf0ce3adba00ed9e303d6a0471ed3f694a3365 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Wed, 4 Mar 2026 13:13:47 +1100 Subject: [PATCH 1/7] Throwing Day init, inline properties, remove DayComponents/CalendarDay, fix macro deprecations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Day init(year:month:day:) now throws DayError for invalid inputs - Day stores year, month, dayOfMonth directly alongside daysSince1970 - Removed DayComponents and CalendarDay — CalendarDays is now OrderedDictionary - Fixed deprecated MemberMacro expansion signatures (added conformingTo) - Updated README and CLAUDE.md Co-Authored-By: Claude Opus 4.6 From 203f15ef7c77679b46db2136db7543cdddaed2e5 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Wed, 4 Mar 2026 18:29:25 +1100 Subject: [PATCH 2/7] Truncating day(byAdding:) for month and year rolling Month and year rolling now uses modular arithmetic to calculate the target month/year, then clamps the day to the last day of that month. This means Jan 31 + 1 month = Feb 28 (or 29) instead of overshooting into March, and Feb 29 + 1 year = Feb 28 in non-leap years. Co-Authored-By: Claude Opus 4.6 From 9e9bf608d8ae16cd95ba21be288d3cbe959ef70f Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sat, 7 Mar 2026 20:40:51 +1100 Subject: [PATCH 3/7] Reintroduce DayComponents as computed property for SwiftData compatibility Day now has a single stored property (daysSince1970) to prevent SwiftData decomposing it into multiple columns. Year, month, and dayOfMonth are accessed via the dayComponents computed property. CalendarDays values changed from [Day] to [DayComponents]. Co-Authored-By: Claude Opus 4.6 From 3f5e24ab6fcb17c22eda3641aaa020a2da4d64cd Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sun, 8 Mar 2026 13:25:27 +1100 Subject: [PATCH 4/7] Adding raw support for SwiftData From 7d590402d664b375339da757a14e2a5128ee5884 Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sun, 8 Mar 2026 17:29:32 +1100 Subject: [PATCH 5/7] Fixing SwiftData issues --- README.md | 83 +++++++++++++++++-- .../Conformance/Day+RawRepresentable.swift | 17 ---- Sources/DayComponents.swift | 4 + .../Day+RawRepresentableTests.swift | 45 ---------- 4 files changed, 80 insertions(+), 69 deletions(-) delete mode 100644 Sources/Conformance/Day+RawRepresentable.swift delete mode 100644 Tests/Conformance/Day+RawRepresentableTests.swift diff --git a/README.md b/README.md index 4fb0250..2a9714b 100755 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ _An API for dates and nothing else. No calendars, no timezones, no hours, minutes or seconds. **Just dates!**_ -Sure 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. +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. @@ -11,6 +11,39 @@ 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](#installation) +- [Introducing Day](#introducing-day) + - [Initialisers](#initialisers) + - [Properties](#properties) + - [var daysSince1970: Int { get }](#var-dayssince1970-int-get) + - [var dayComponents: DayComponents { get }](#var-daycomponents-daycomponents-get) + - [static var today: Day { get }](#static-var-today-day-get) + - [var weekday: Weekday { get }](#var-weekday-weekday-get) + - [Mathematical operators](#mathematical-operators) + - [Functions](#functions) + - [func date(inCalendar calendar: Calendar = .current, timeZone: TimeZone? = nil) -> Date](#func-date-incalendar-calendar-calendar-current-timezone-timezone-nil-date) + - [func day(byAdding component: Day.Component, value: Int) -> Day](#func-day-byadding-component-day-component-value-int-day) + - [func formatted(_ day: Date.FormatStyle.DateStyle = .abbreviated) -> String](#func-formatted-day-date-formatstyle-datestyle-abbreviated-string) +- [Calendar generation](#calendar-generation) + - [Generating a calendar month](#generating-a-calendar-month) + - [Merging calendar months](#merging-calendar-months) +- [Protocol conformance](#protocol-conformance) + - [Codable](#codable) + - [Equatable](#equatable) + - [Comparable](#comparable) + - [Hashable](#hashable) + - [Strideable](#strideable) +- [Property wrappers](#property-wrappers) + - [`@DayString.DMY`, `@DayString.MDY` & `@DayString.YMD`](#daystring-dmy-daystring-mdy-daystring-ymd) + - [`@Epoch.Seconds` & `@Epoch.Milliseconds`](#epoch-seconds-epoch-milliseconds) + - [`@ISO8601.Default` and `@ISO8601.SansTimezone`](#iso8601-default-and-iso8601-sanstimezone) + - [Encoding and decoding nulls](#encoding-and-decoding-nulls) +- [DayType and SwiftData](#daytype-and-swiftdata) +- [References and thanks](#references-and-thanks) +- [Future additions](#future-additions) + ## Installation `DayType` is a SPM package only. So install it as you would install any other package. @@ -104,13 +137,9 @@ Uses Apple's `Date.formatted(date:time:)` function to format the day into a `Str # Calendar generation -DayType provides calendar generation specifically for building calendar UIs. - -## CalendarDays - -A typealias for `OrderedDictionary` (using Apple's [swift-collections](https://github.com/apple/swift-collections)) where each key is the first `Day` of a week and the value is a 7-element array of `DayComponents` values. One per day of the week starting from either Sunday or Monday. Depending on your preference. +DayType can also generate a data structure specifically for building calendar UIs. It has a `CalendarDays` typealias which maps to a `OrderedDictionary` (Apple's [swift-collections](https://github.com/apple/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 it to be mapped into a UI without any complicated processing. Simply loop through the values which will be in order and then loop through the arrays to create the Sunday to Saturday or Monday to Sunday cells. +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 @@ -268,6 +297,46 @@ Will write the following JSON when all the properties are `nil`: } ``` +# 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: + +```swift +@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: + +```swift +struct SomeView: View { + @Query(sort: \Holiday.startDate.daysSince1970) private var holidays: [Holiday] +} +``` + # References and thanks * Can't thank [Howard Hinnant](http://howardhinnant.github.io) enough. Using his math instead of Apple's APIs produced a significant speed boost when converting to and from years, months and days. diff --git a/Sources/Conformance/Day+RawRepresentable.swift b/Sources/Conformance/Day+RawRepresentable.swift deleted file mode 100644 index eb01c8c..0000000 --- a/Sources/Conformance/Day+RawRepresentable.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -/// `RawRepresentable` conformance using `daysSince1970` as the raw value. -/// -/// This is required for SwiftData compatibility. Without it, SwiftData decomposes `Day` -/// into its internal stored property (`daysSince1970`) and uses that as the column name -/// instead of the model's property name. Conforming to `RawRepresentable` tells SwiftData -/// to treat `Day` as a single scalar value, producing correctly named columns -/// (e.g. `ZSTARTDATE`) and enabling keypath-based queries like `@Query(sort: \.startDate)`. -extension Day: RawRepresentable { - - public init?(rawValue: Int) { - self.init(daysSince1970: rawValue) - } - - public var rawValue: Int { daysSince1970 } -} diff --git a/Sources/DayComponents.swift b/Sources/DayComponents.swift index 0572b80..a68def9 100644 --- a/Sources/DayComponents.swift +++ b/Sources/DayComponents.swift @@ -14,4 +14,8 @@ public struct DayComponents { self.month = month self.dayOfMonth = dayOfMonth } + + func day() throws -> Day { + try Day(self) + } } diff --git a/Tests/Conformance/Day+RawRepresentableTests.swift b/Tests/Conformance/Day+RawRepresentableTests.swift deleted file mode 100644 index aaeaee4..0000000 --- a/Tests/Conformance/Day+RawRepresentableTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -import DayType -import Foundation -import Testing - -extension ProtocolConformanceSuites { - - @Test("RawRepresentable init from rawValue") - func rawRepresentableInit() throws { - let day = try Day(2026, 3, 8) - let restored = Day(rawValue: day.daysSince1970) - #expect(restored == day) - } - - @Test("RawRepresentable rawValue matches daysSince1970") - func rawRepresentableRawValue() throws { - let day = try Day(2026, 3, 8) - #expect(day.rawValue == day.daysSince1970) - } - - @Test("RawRepresentable round trip") - func rawRepresentableRoundTrip() throws { - let day = try Day(2024, 2, 29) - let rawValue = day.rawValue - let restored = Day(rawValue: rawValue) - #expect(restored == day) - #expect(restored?.dayComponents.dayOfMonth == 29) - } - - @Test("RawRepresentable with negative daysSince1970") - func rawRepresentableNegative() { - let day = Day(daysSince1970: -1) - let restored = Day(rawValue: day.rawValue) - #expect(restored == day) - #expect(restored?.dayComponents.year == 1969) - } - - @Test("RawRepresentable epoch zero") - func rawRepresentableEpochZero() { - let day = Day(rawValue: 0) - #expect(day?.daysSince1970 == 0) - #expect(day?.dayComponents.year == 1970) - #expect(day?.dayComponents.month == 1) - #expect(day?.dayComponents.dayOfMonth == 1) - } -} From 767274cf768a9713f47c47516f36f4cad562c05f Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sun, 8 Mar 2026 17:37:45 +1100 Subject: [PATCH 6/7] Testing --- Sources/DayComponents.swift | 2 +- Tests/DayComponentsTests.swift | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Tests/DayComponentsTests.swift diff --git a/Sources/DayComponents.swift b/Sources/DayComponents.swift index a68def9..9dc94b1 100644 --- a/Sources/DayComponents.swift +++ b/Sources/DayComponents.swift @@ -15,7 +15,7 @@ public struct DayComponents { self.dayOfMonth = dayOfMonth } - func day() throws -> Day { + public func day() throws -> Day { try Day(self) } } diff --git a/Tests/DayComponentsTests.swift b/Tests/DayComponentsTests.swift new file mode 100644 index 0000000..df51397 --- /dev/null +++ b/Tests/DayComponentsTests.swift @@ -0,0 +1,27 @@ +import DayType +import Foundation +import OrderedCollections +import Testing + +@Suite("Day calendar month") +struct DayComponentsTests { + + // MARK: - Structure + + @Test("Components generation") + func rowLengths() throws { + let components = try Day(2026, 3, 15).dayComponents + #expect(components.year == 2026) + #expect(components.month == 3) + #expect(components.dayOfMonth == 15) + } + + @Test("Components generation and reversion") + func componentsReversion() throws { + let originalDay = try Day(2026, 3, 15) + let components = try Day(2026, 3, 15).dayComponents + let componentsDay = try components.day() + #expect(originalDay == componentsDay) + } + +} From 198c9c938bdb1ea801fdf5def10b23f8b849cd4e Mon Sep 17 00:00:00 2001 From: Derek Clarkson Date: Sun, 8 Mar 2026 17:39:49 +1100 Subject: [PATCH 7/7] Update swift.yml --- .github/workflows/swift.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index be2df49..7cfc141 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -6,7 +6,7 @@ name: Test on: workflow_dispatch: push: - pull_request: + # pull_request: branches: [ "main" ] jobs: