Skip to content

Commit

Permalink
Merge pull request #3 from drekka/develop
Browse files Browse the repository at this point in the history
Adding support for date strings.
  • Loading branch information
drekka authored Aug 6, 2024
2 parents fcdfdc8 + 9d633c1 commit 2dc6c7a
Show file tree
Hide file tree
Showing 18 changed files with 406 additions and 91 deletions.
32 changes: 32 additions & 0 deletions .swiftpm/configuration/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"pins" : [
{
"identity" : "cwlcatchexception",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlCatchException.git",
"state" : {
"revision" : "3ef6999c73b6938cc0da422f2c912d0158abb0a0",
"version" : "2.2.0"
}
},
{
"identity" : "cwlpreconditiontesting",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"state" : {
"revision" : "2ef56b2caf25f55fa7eef8784c30d5a767550f54",
"version" : "2.2.1"
}
},
{
"identity" : "nimble",
"kind" : "remoteSourceControl",
"location" : "https://github.com/quick/nimble",
"state" : {
"revision" : "eb5e3d717224fa0d1f6aff3fc2c5e8e81fa1f728",
"version" : "11.2.2"
}
}
],
"version" : 2
}
145 changes: 93 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
![Calendar](media/Calendar.png)
# DayType

(A friendly API for working with dates without the time of day)
_An API for dates that doesn't involve hours, minutes, seconds and timezones._

Swift provides the excellent date support through `Date`, `Calendar`, `TimeZone` and other types. However these are all designed to work with specific points in time, rather than the generalisations that people often refer to. For example, a person's date of birth is often used without any reference to the exact time they were born. The same goes for a variety of other dates, an employee's person's leave, various religious holidays, retail sales, festivals, etc.
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.

As a result developers often find themselves stripping time components from Swift's `Date` to force it to act like a date, often with mixed results as there are many technical issues to consider when coercing a specific point to such a generalisation. Especially with the complexities of time zones and sometime questionable input from external sources.
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.

`DayType` sets out to simplify date handling by providing a new `Day` type which represents a general 24 hours period instead of a specific point in time. ie. no hours, minutes, etc and no time zones. This allows date only code to be simpler because it no longer needs to sanitise time components which in turn removes the angst of accidental bugs as well as making date based calculations simpler.
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.

## Installation

`DayType` is a SPM package.
`DayType` is a SPM package only.

# Day

# Creating a Day
The common type you'll use is `Day`.

`Day` has a number of convenience initialisers which are pretty self explanatory and mostly similar to Swift's `Date`:
## Initialisers

`Day` has a number of convenience initialisers which are pretty self explanatory and similar to Swift's `Date` initialisers:

```swift
init()
Expand All @@ -26,9 +32,9 @@ init(components: DayComponents)
init(_ year: Int, _ month: Int, _ day: Int)
init(year: Int, month: Int, day: Int)
```
# Properties
## Properties

## .daysSince1970
### .daysSince1970

Literally the number of days since Swift's base date of 00:00:00 UTC on 1 January 1970.

Expand All @@ -42,11 +48,11 @@ let numberOfDays = Calendar.current.dateComponents([.day], from: fromDate, to: t

# Property wrappers

`Day`'s internal value isn't something that external APIs are typically aware of. So to support the typical values that external APIs tend to use `DayType` provides a variety of property wrappers implementing `Codable` to automatically handle the conversions.
DayType's property wrappers are designed to address the mostly commonly seen issues when coding and decoding data from external sources.

_Note: All of these wrappers support both `Day` and `Day?` properties through the use of the `DayCodable` protocol which is applied to both. Technically this protocol could be added to other types to make them convertible to `Day`._
_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`._

## @CodableAsEpochSeconds
## @EpochSeconds

Converts [epoch timestamps](https://www.epochconverter.com) to `Day`. For example the JSON data structure:

Expand All @@ -60,13 +66,13 @@ Can be read by:

```swift
struct MyType: Codable {
@CodableAsEpochSeconds var dob: Day // or Day?
@EpochSeconds var dob: Day // or Day?
}
```

## @CodableAsEpochMilliseconds
## @EpochMilliseconds

Essentially the same as `@CodableAsEpochSeconds` but expects the epoch time to be in millisecond [epoch timestamps](https://www.epochconverter.com). For example the JSON data structure:
Essentially the same as `@EpochSeconds` but expects the epoch time to be in millisecond [epoch timestamps](https://www.epochconverter.com).

```json
{
Expand All @@ -78,13 +84,13 @@ Can be read by:

```swift
struct MyType: Codable {
@CodableAsEpochMilliseconds var dob: Day // or Day?
@EpochMilliseconds var dob: Day // or Day?
}
```

## @CodableAsISO8601
## @ISO8601

Converts [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date strings to `Day`. For example:
Converts [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date strings to `Day`.

```json
{
Expand All @@ -96,18 +102,16 @@ Can be read by:

```swift
struct MyType: Codable {
@CodableAsISO8601 var dob: Day // or Day?
@ISO8601 var dob: Day // or Day?
}
```

## @CodableAsConfiguredISO8601<T, Configurator>
## @CustomISO8601<T, Configurator>

Where `T: DayCodable` and `Configurator: ISO8601Configurator`.

Internally `DayType` uses an `ISO8601DateFormatter` to read and write [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) strings. As there are a variety of ISO8601 formats, this property wrapper allows you to pre-configure the formatter before it processes the string.

For example:

```json
{
"dob": "20120202 133323"
Expand All @@ -125,33 +129,70 @@ enum MinimalFormat: ISO8601Configurator {
}

struct MyType: Codable {
@CodableAsConfiguredISO8601<Day, MinimalFormat> var dob: Day
@CustomISO8601<Day, MinimalFormat> var dob: Day
// or ...
@CodableAsConfiguredISO8601<Day?, MinimalFormat> var dob: Day?
@CustomISO8601<Day?, MinimalFormat> var dob: Day?
}
```

The `ISO8601Configurator` 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.
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, `@CodableAsConfiguredISO8601<T, Configurator>` requires you to specify the `DayCodable` type (`Day` or `Day?`) which must match the type of the property._
_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._

## Supplied ISO8601 configurators
### Supplied ISO8601 configurators

### ISO8601Config.Default
#### ISO8601Config.Default

This configurator does not change the formatter. It's main purpose is to support the `@CodableAsISO8601` property wrapper.
This configurator does not change the formatter. It's main purpose is to support the `@ISO8601` property wrapper.

### ISO8601Config.SansTimeZone
#### ISO8601Config.SansTimeZone

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"`.

# Manipulating Day types
## @DateString<T, Configurator>

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.

```json
{
"dob": "2012-12-02"
}
```

Can be read by:

```swift
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?`)._

### Supplied date string configurators

#### DateStringConfig.ISO

## Operators
Reads date strings that follow the ISO8601 format but don't have any time components. ie. `2012-12-01'

There are a variety of functions that can be performed on `Day` types. `Day` has `+`, `-`, `+=` and `-=` operators which can be used to add or subtract a number of days from a day.
#### DateStringConfig.DMY

For example:
Reads date strings using the `dd/MM/yyyy` date format. ie. `01/12/2012'

#### DateStringConfig.MDY

Reads date strings using the `MM/dd/yyyy` date format. ie. `12/01/2012'

# Manipulating Day types

`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.

```swift
let day = Day(2000,1,1) + 5 // -> 2000-01-06
Expand All @@ -164,17 +205,31 @@ let day = Day(2000,1,1)
day -= 5 // -> 1999-12-21
```

In addition you can also subtract one day from another to get the duration between them.
And you can subtract one day from another to get the duration between them.

```swift
Day(2000,1,10) - Day(2000,1,5) // -> 5 days duration.
```

## Functions

### .date(inCalendar:timeZone:) -> Date

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.

### .day(byAdding:, value:) -> Day

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.

### .formatted(_:) -> String

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.

# DayComponents

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.

# Conformance
# Protocol conformance

## Codable

Expand Down Expand Up @@ -217,27 +272,13 @@ for day in stride(from: Day(2000,1,1), to: Day(2000,1,5), by: 2) {
}
```

# Other Day functions

## .date(inCalendar:timeZone:) -> Date

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.

## .day(byAdding:, value:) -> Day

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.

## .formatted(_:) -> String

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.

# 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.
* Quick thank you to the guys behind the excellent [Nimble test assertion framework](https://github.com/Quick/Nimble).
* A second thanks to the guys behind the excellent [Nimble test assertion framework](https://github.com/Quick/Nimble) which I prefer over Apple's XCTest asserts. Sorry Apple.

# 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 [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.

So please feel free to drop a request for thing you'd like added.
Please feel free to drop a request for any thing you'd like added.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

/// Identifies a ``Day`` property that reads and writes from an ISO8601 formatted string.
@propertyWrapper
public struct CodableAsConfiguredISO8601<T, Configurator>: Codable where T: ISO8601Codable, Configurator: ISO8601Configurator {
public struct CustomISO8601<T, Configurator>: Codable where T: ISO8601Codable, Configurator: CustomISO8601Configurator {

public var wrappedValue: T

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@
import Foundation

/// Sets up the ``ISO8601DateFormatter`` used for decoding and encoding date strings.
public protocol ISO8601Configurator {
public protocol CustomISO8601Configurator {

/// Called when setting up to decode or encode an ISO8601 date string.
static func configure(formatter: ISO8601DateFormatter)
}

/// USeful common configurations of ISO8601 formatters.
/// Useful common configurations of ISO8601 formatters.
public enum ISO8601Config {

/// A default implementation that leaves the formatted untouched from it's defaults.
/// in the default property wrappers.
public enum Default: ISO8601Configurator {
public enum Default: CustomISO8601Configurator {
public static func configure(formatter _: ISO8601DateFormatter) {}
}

/// Removes the time zone element from the string.
public enum SansTimeZone: ISO8601Configurator {
public enum SansTimeZone: CustomISO8601Configurator {
public static func configure(formatter: ISO8601DateFormatter) {
formatter.formatOptions.remove(.withTimeZone)
}
Expand Down
27 changes: 27 additions & 0 deletions Sources/Property wrappers/DateString.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// CustomISO8601Day.swift
//
//
// Created by Derek Clarkson on 9/1/2024.
//

import Foundation

/// Identifies a ``Day`` property that reads and writes from date strings using a configured ``DateFormatter``.
@propertyWrapper
public struct DateString<T, Configurator>: Codable where T: DateStringCodable, Configurator: DateStringConfigurator {

public var wrappedValue: T

public init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}

public init(from decoder: Decoder) throws {
wrappedValue = try T(dateDecoder: decoder, configurator: Configurator.self)
}

public func encode(to encoder: Encoder) throws {
try wrappedValue.encode(dateEncoder: encoder, configurator: Configurator.self)
}
}
Loading

0 comments on commit 2dc6c7a

Please sign in to comment.