Skip to content

Commit ee1250f

Browse files
committed
Initial commit
0 parents  commit ee1250f

File tree

65 files changed

+3518
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3518
-0
lines changed

.github/workflows/test.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Run tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
8+
jobs:
9+
build:
10+
11+
runs-on: macos-latest
12+
13+
steps:
14+
- uses: actions/checkout@v3
15+
- name: Build
16+
run: swift build -v
17+
- name: Run tests
18+
run: swift test -v

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.swiftpm
2+
.build
3+
Package.resolved

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Ctrl Group
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Logo.png

7.29 KB
Loading

Package.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// swift-tools-version:5.5
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "Time",
8+
platforms: [ .iOS(.v13), .macOS(.v10_14) ],
9+
products: [
10+
.library(name: "Time", targets: ["Time"]),
11+
.library(name: "TimeTestHelpers", targets: ["TimeTestHelpers"])
12+
],
13+
dependencies: [
14+
.package(url: "https://github.com/ctrlgroup/Resolver.git",
15+
branch: "feature/moc-view-context-constant"),
16+
.package(url: "https://github.com/Quick/Quick.git", from: "4.0.0"),
17+
.package(url: "https://github.com/Quick/Nimble.git", from: "9.2.1")
18+
],
19+
targets: [
20+
.target(name: "Time", dependencies: ["Resolver"]),
21+
.target(name: "TimeTestHelpers", dependencies: ["Time"]),
22+
.testTarget(name: "TimeTests", dependencies: [
23+
"Quick",
24+
"Nimble",
25+
"Time",
26+
"TimeTestHelpers"
27+
])
28+
]
29+
)

README.md

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
<img src="Logo.png" width="178" max-width="60%" alt="Time" />
2+
3+
<p>
4+
<img src="https://img.shields.io/badge/Swift-5.5-orange.svg" />
5+
<a href="https://swift.org/package-manager">
6+
<img src="https://img.shields.io/badge/swiftpm-compatible-brightgreen.svg?style=flat" alt="Swift Package Manager" />
7+
</a>
8+
<img src="https://img.shields.io/badge/platforms-iOS+macOS-brightgreen.svg?style=flat" alt="iOS + macOS" />
9+
<a href="https://twitter.com/ctrl_group">
10+
<img src="https://img.shields.io/badge/twitter-@ctrl_group-blue.svg?style=flat" alt="Twitter: @ctrl_group" />
11+
</a>
12+
</p>
13+
14+
Welcome to **Time**, a swift package to simplify calendar, date and time handling.
15+
16+
## Introduction
17+
Time was built by [Ctrl Group](https://www.ctrl-group.com) to make working with date and time simple and to avoid the common mistakes made when using the standard `Date` object.
18+
19+
### What does Time provide over the Foundation library
20+
The Foundation library provides a [`Date`](https://developer.apple.com/documentation/foundation/date) object which represents a single point in time, independent of any calendar or time zone. This object needs to be used in conjunction with a [`DateFormatter`](https://developer.apple.com/documentation/foundation/dateformatter), [`TimeZone`](https://developer.apple.com/documentation/foundation/timezone), [`Locale`](https://developer.apple.com/documentation/foundation/locale), [`DateComponents`](https://developer.apple.com/documentation/foundation/datecomponents) and [`Calendar`](https://developer.apple.com/documentation/foundation/calendar) in order to perform calendar arithmetic and create meaningful representations of dates and times.
21+
22+
**Time** uses these tools and provides a simple API to allow you to represent a `CalendarDate`, `TimeOfDay`, `Timestamp`, `CalendarDateRange`, `TimeRange` and more.
23+
24+
### Key features
25+
1. Provides types which accurately represents the information you want to store, and semantically conveys what is being stored to the reader of the code.
26+
2. Provides types which group contextual information such as the `TimeZone`.
27+
3. Makes performing calendar arithmetic fast and simple.
28+
4. Simplifies serialising and parsing date and time information.
29+
5. Provides an easy way to stub the time for unit tests.
30+
31+
## Installation
32+
Time is distributed using the [Swift Package Manager](https://swift.org/package-manager). To install it into a project, add it as a dependency within your Package.swift manifest:
33+
```swift
34+
let package = Package(
35+
...
36+
dependencies: [
37+
.package(url: "https://github.com/ctrlgroup/Time.git", from: "1.0.0")
38+
],
39+
...
40+
)
41+
```
42+
43+
Then import Time wherever you’d like to use it:
44+
```swift
45+
import Time
46+
```
47+
48+
## Examples
49+
### Calendar Dates
50+
`CalendarDate` allows you to represent a date in the gregorian calendar, without the time, and irrespective of the time zone.
51+
52+
You can create them with a day, month and year explicitly, or you can create them using a `Date` and `TimeZone`.
53+
```swift
54+
let europeLondon: TimeZone = ...
55+
let date: Date = ...
56+
57+
// April 4th, 1994
58+
let dateOfBirth = try? CalendarDate(day: 03, month: 04, year: 1994)
59+
60+
// Can also be created from a `Date`
61+
let calendarDate = CalendarDate(date: date, timeZone: europeLondon)
62+
```
63+
64+
Once instantiated, you can query them for their day, month, year, and even weekday.
65+
```swift
66+
print(calendarDate.month)
67+
print(calendarDate.dayOfWeek)
68+
```
69+
70+
They can also be converted back to a `Date` (representing the moment of midnight on that day)
71+
```swift
72+
// Can be converted back to a `Date` using a time zone
73+
let date: Date = calendarDate.date(in: europeLondon)
74+
```
75+
76+
You can also easily do calendar arithmetic, and usually without having to do any complex `Calendar` conversions.
77+
```swift
78+
let nextWeek = dateOfBirth + 7 // Adds 7 days
79+
let difference: Int = nextWeek - dateOfBirth // 7
80+
81+
// Calendar dates are comparable
82+
let firstOfMay = try? CalendarDate(day: 01, month: 05, year: 2022)
83+
let tenthOfJune = try? CalendarDate(day: 10, month: 06, year: 2022)
84+
tenthOfJune > firstOfMay // true
85+
86+
// Easily find weekdays (uses a Calendar to compute the weekday)
87+
dateOfBirth.previous(weekday: .sunday) // Finds the previous Sunday
88+
dateOfBirth.next(weekday: .sunday) // Finds the next Sunday
89+
```
90+
91+
Arithmetic with calendar dates is a trivial operation as the underlying raw value is just an `Int`. This means you don’t need to worry about the overhead of using a `Calendar`, and can easily do things like iterate over a [`CalendarDateRange`](#CalendarDateRange).
92+
93+
### Time Interval
94+
We’ve added some conveniences for writing `TimeInterval`s in an easily readable way.
95+
96+
```swift
97+
let seconds: TimeInterval = 12.hours + 34.minutes + 56.seconds
98+
let fiveDays: TimeInterval = 5.days
99+
```
100+
101+
### TimeOfDay
102+
To represent just time (without a date) you can use `TimeOfDay`. This is useful for things like recording the time for a repeating reminder or alarm.
103+
104+
A `TimeOfDay` can be created in various different ways:
105+
```swift
106+
let example1 = try? TimeOfDay(hour: 18, minute: 30, second: 0)
107+
let example2 = try? TimeOfDay(secondsSinceMidnight: 3600 * 12)
108+
let example3 = TimeOfDay(date: Date(), timeZone: europeLondon)
109+
let example4 = 1830 // Use the "tricolon" operator
110+
let example5 = 123456 // Also supports seconds
111+
```
112+
113+
You can do arithmetic with a `TimeOfDay` and `TimeInterval`:
114+
```swift
115+
let midday = 1130 + 30.minutes
116+
let elevenAM = 1130 - 30.minutes
117+
let fiveMins: TimeInterval = 0600 - 0555
118+
let oneAM = 2330 + (1.hours + 30.minutes) // Can span over midnight
119+
```
120+
121+
It also supports various string conversions:
122+
```swift
123+
let timeOfDay = 153456
124+
let locale = Locale(identifier: "en-US")
125+
timeOfDay.string(style: (.short, .twelveHour), locale: locale) // 3:34 PM
126+
timeOfDay.string(style: (.medium, .twentyFourHour)) // 15:34:56
127+
128+
// Converting from a string
129+
let result = TimeOfDay(string: "3:34 PM",
130+
style: (.short, .twelveHour),
131+
locale: locale)
132+
```
133+
134+
`TimeOfDay` is also `Equatable`, `Hashable`, `Codable` and `Comparable`.
135+
136+
### CalendarDateRange
137+
`CalendarDateRange` allows you to easily represent a range of calendar dates which you can iterate over, and use all the commonly available operators of a `Range`.
138+
139+
```swift
140+
// A calendar date range is simply a Swift Range
141+
public typealias CalendarDateRange = Range<CalendarDate>
142+
143+
// Can be created in the usual way
144+
let range = startDate ..< endDate
145+
146+
// It can be indexed using integers
147+
sut[0] == startDate
148+
sut[1] == startDate + 1
149+
150+
// Convenience initializers make calendar date ranges easy to understand
151+
CalendarDateRange(firstDate: startDate, lastIncludedDate: lastDate)
152+
CalendarDateRange(startDate: startDate, endDate: endDate)
153+
```
154+
155+
### TimeRange
156+
A `TimeRange` is slightly more complex as a `TimeOfDay` isn’t strictly sequential (they loop when continually adding seconds). However it’s still useful to be able to form ranges over them.
157+
158+
```swift
159+
// Can be created in the usual way
160+
let timeRange: TimeRange = 1130 ..< 1800
161+
let timeRangeOverMidnight: TimeRange = 2330 ..< 0100
162+
163+
// Can test is a time exists in the range
164+
timeRange.contains(1230) // true
165+
```
166+
167+
### DateTime
168+
`DateTime` can be used when you want to represent a specific time on a specific calendar date, but in any time zone.
169+
170+
```swift
171+
let calendarDate: CalendarDate = ...
172+
let timeOfDay: timeOfDay = ...
173+
let dateTime = DateTime(calendarDate: calendarDate, timeOfDay: timeOfDay)
174+
175+
// Can be created from a `Date` and `TimeZone`
176+
let dateTime = DateTime(date: Date(), timeZone: europeLondon)
177+
178+
// Can be converted to a `Date` or `Timestamp`
179+
let date = dateTime.date(in: europeLondon)
180+
let timestamp = dateTime.timestamp(in: europeLondon)
181+
```
182+
183+
### Timestamp
184+
Most of the time a `Timestamp` would be a more appropriate type to replace `Date`. It is simply an ecapsulation of a `Date` with the `TimeZone` it was recorded at, so similarly it represents a single point in time, but contains the necessary context to understand what date and time it represents as well.
185+
186+
```swift
187+
let timestamp = Timestamp(date: Date(), timeZone: europeLondon)
188+
timestamp.day // 1st March
189+
timestamp.timeOfDay // 12pm
190+
timestamp.dateTime = // 1st March at 12pm
191+
```
192+
193+
One other difference between `Timestamp` and `Date` is in the way it implements `Equatable`. As `Date` is based on a `Double` it suffers from inaccuracies of floating point arithmetic. This means comparing two `Date` objects can somtimes give unexpected results…
194+
195+
```swift
196+
let date1 = dateFormatter.date(from: "2022-01-01T12:34:56.1234Z")!
197+
let date2 = Date(timeIntervalSinceReferenceDate: 662733296.123)
198+
print(date1) // 2022-01-01 12:34:56 +0000
199+
print(date2) // 2022-01-01 12:34:56 +0000
200+
date1 == date2 // false
201+
```
202+
203+
`Timestamp` fixes this by considering the dates equal if they are within a millisecond of each other.
204+
205+
### Clock
206+
`Clock` is a class which can be read to learn the current date and time. It should be used instead of every using the `Date()` initialiser. By injecting a `Clock` instance into your code you are able to stub it out in tests in order to unit test code which depends on the current time.
207+
208+
### Other functionality
209+
Many of the types (`CalendarDate`, `TimeOfDay` etc.) are also `Equatable`, `Comparable`, `Hashable`, and `Codable`.
210+
211+
When serializing with `Codable` the package will prefer to save types as a standard ISO8601 string e.g.
212+
- `2022-01-01T12:30:00Z` for a `DateTime`
213+
- `2022-01-01` for a `CalendarDate`
214+
- `12:30:00` for a `TimeOfDay`
215+
216+
A `Timestamp` need the context of a `TimeZone` so will encode like this in JSON:
217+
218+
```json
219+
{
220+
"timestamp": "...",
221+
"timeZone": "Europe/London"
222+
}
223+
```
224+
225+
Here, the `timestamp` property is an encoded `Date`. This will encode in the default way (as a double, number of seconds since the reference date) unless you specify to use an ISO8601 representation using the date encoding strategy property:
226+
227+
```swift
228+
let encoder = JSONEncoder()
229+
encoder.dateEncodingStrategy = .iso8601
230+
let date = try? encoder.encode(timestamp)
231+
```
232+
233+
## String Formatting
234+
Time tries to provide a simpler way to create strings from dates and times without having to use a `DateFormatter` class directly.
235+
- It simplifys localizing your date formats by always preferring to use a _localized template string_ or date and time style parameters.
236+
- It cuts down on lines of code by providing a simple to use API
237+
- It tries to improve on performance by caching `DateFormatter` classes that are commonly used.
238+
239+
```swift
240+
let dateFormat = "dd-MM-yyyy"
241+
242+
// Using Foundation
243+
let date: Date = ...
244+
let timeZone: TimeZone = ...
245+
let dateFormatter = DateFormatter()
246+
dateFormatter.timeZone = timeZone
247+
dateFormatter.locale = .current
248+
dateFormatter.setLocalizedDateFormatFromTemplate(dateFormat)
249+
let result = dateFormatter.string(from: date)
250+
251+
// Using Time
252+
let calendarDate: CalendarDate = ...
253+
let result = calendarDate.string(withFormat: dateFormat, locale: .current)
254+
```
255+
256+
Similar methods also exist for `TimeOfDay` and additional methods exist on `CalendarDate`, `TimeOfDay`, `DateTime`, and `Timestamp` for creating ISO8601 strings.
257+
258+
## Author
259+
Time was designed and implemented by the team at [Ctrl Group](https://www.ctrl-group.com).
260+
* Website: [ctrl-group.com](https://www.ctrl-group.com)
261+
* Twitter: [@ctrl_group](https://twitter.com/ctrl_group)
262+
263+
## License
264+
Time is available under the MIT license. See the LICENSE file for more info.
265+
266+
## Contributions and support
267+
We would like `Time` to developed completely in the open going forward, and your contributions are more than welcome!
268+
269+
If you find any problems, have any suggestions, or wish to improve on the documentaion then we encourage you to [open a Pull Request](https://github.com/ctrlgroup/Time/compare) and we will be actively reviewing and accepting contributions.
270+
271+
We hope that you’ll find this package useful and enjoyable!

Sources/Time/Calendar.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// Calendar.swift
3+
// Time
4+
//
5+
// Copyright © 2022 Ctrl Group Ltd. All rights reserved.
6+
// MIT license, see LICENSE file for details
7+
//
8+
9+
import Foundation
10+
11+
public extension Calendar {
12+
13+
static let gregorianInUTC: Calendar = gregorianInUTC(withLocale: nil)
14+
15+
static func gregorianInUTC(withLocale locale: Locale? = nil) -> Calendar {
16+
var calendar = Calendar(identifier: .gregorian)
17+
calendar.timeZone = .utc
18+
calendar.locale = locale
19+
return calendar
20+
}
21+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// CalendarDate+Arithmetic.swift
3+
// Time
4+
//
5+
// Copyright © 2022 Ctrl Group Ltd. All rights reserved.
6+
// MIT license, see LICENSE file for details
7+
//
8+
9+
import Foundation
10+
11+
public extension CalendarDate {
12+
func dateByAdding(_ value: Int, unit: Calendar.Component) -> CalendarDate {
13+
let resultDate = gregorianCalendar.date(byAdding: unit, value: value, to: date(in: .utc))!
14+
return CalendarDate(date: resultDate, timeZone: .utc)
15+
}
16+
}

0 commit comments

Comments
 (0)