|
| 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 = 18⁝30 // Use the "tricolon" operator |
| 110 | +let example5 = 12⁝34⁝56 // Also supports seconds |
| 111 | +``` |
| 112 | + |
| 113 | +You can do arithmetic with a `TimeOfDay` and `TimeInterval`: |
| 114 | +```swift |
| 115 | +let midday = 11⁝30 + 30.minutes |
| 116 | +let elevenAM = 11⁝30 - 30.minutes |
| 117 | +let fiveMins: TimeInterval = 06⁝00 - 05⁝55 |
| 118 | +let oneAM = 23⁝30 + (1.hours + 30.minutes) // Can span over midnight |
| 119 | +``` |
| 120 | + |
| 121 | +It also supports various string conversions: |
| 122 | +```swift |
| 123 | +let timeOfDay = 15⁝34⁝56 |
| 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 = 11⁝30 ..< 18⁝00 |
| 161 | +let timeRangeOverMidnight: TimeRange = 23⁝30 ..< 01⁝00 |
| 162 | + |
| 163 | +// Can test is a time exists in the range |
| 164 | +timeRange.contains(12⁝30) // 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! |
0 commit comments