├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Package.swift
├── README.md
├── Sources
├── NaiveDate.swift
├── NaiveDateFormatStyle.swift
└── NaiveDateFormatter.swift
└── Tests
├── NaiveDateFormatStyleTests.swift
├── NaiveDateFormatterTest.swift
└── NaiveDateTests.swift
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | macos:
13 | name: macOS (Xcode ${{ matrix.xcode }})
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | xcode: ["13.4.1", "13.2.1"]
18 | include:
19 | - xcode: "13.4.1"
20 | macos: macOS-12
21 | - xcode: "13.2.1"
22 | macos: macOS-11
23 | runs-on: ${{ matrix.macos }}
24 | env:
25 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer
26 | steps:
27 | - name: Checkout Repo
28 | uses: actions/checkout@v3
29 | - name: Run Tests
30 | run: swift test
31 | linux:
32 | name: Linux (Swift ${{ matrix.swift }})
33 | strategy:
34 | fail-fast: false
35 | matrix:
36 | swift: ["5.5", "5.6"]
37 | runs-on: ubuntu-latest
38 | container: swift:${{ matrix.swift }}
39 | steps:
40 | - name: Checkout Repo
41 | uses: actions/checkout@v3
42 | - name: Run Tests
43 | run: swift test
44 |
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 | .swiftpm/
5 |
6 | ## Build generated
7 | build/
8 | DerivedData/
9 |
10 | ## Various settings
11 | *.pbxuser
12 | !default.pbxuser
13 | *.mode1v3
14 | !default.mode1v3
15 | *.mode2v3
16 | !default.mode2v3
17 | *.perspectivev3
18 | !default.perspectivev3
19 | xcuserdata/
20 |
21 | ## Other
22 | *.moved-aside
23 | *.xccheckout
24 | *.xcscmblueprint
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 | *.ipa
29 | *.dSYM.zip
30 | *.dSYM
31 |
32 | ## Playgrounds
33 | timeline.xctimeline
34 | playground.xcworkspace
35 |
36 | # Swift Package Manager
37 | #
38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
39 | # Packages/
40 | # Package.pins
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots
68 | fastlane/test_output
69 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # NaiveDate 1.x
3 |
4 | ## NaiveDate 1.1
5 |
6 | *Sep 12, 2024*
7 |
8 | - Add FormatStyle & Mutability & Sendability & Inlinable by @lvalenta in https://github.com/CreateAPI/NaiveDate/pull/6
9 | - Add support for Swift 6
10 | - Increase minimimum supported platforms to iOS 13, tvOS 13, watchOS 6, and macOS 10.15
11 |
12 | ## NaiveDate 1.0
13 |
14 | *Dec 18, 2021*
15 |
16 | - Bump minimum required versions
17 | - Fix SPM support (tags were not formatted correctly)
18 | - Remove CocoaPods and Carthage support
19 |
20 | # NaiveDate 0.x
21 |
22 | ## NaiveDate 0.4
23 |
24 | - Add Swift 5.0 support
25 | - Add SwiftPM 5.0 support
26 | - Remove Swift 4.0 and Swift 4.1 support
27 | - Add a single `NaiveDate` target which can build the framework for any platform
28 |
29 | ## NaiveDate 0.3
30 |
31 | - `Date` conversion now supports optional timezone parameters
32 |
33 | ## NaiveDate 0.2.1
34 |
35 | - Improve Hashing implementation (reduce number of collisions)
36 | - Use [tuple comparison operators](https://github.com/apple/swift-evolution/blob/master/proposals/0015-tuple-comparison-operators.md) to simplify Comparable implementation
37 |
38 | ## NaiveDate 0.2
39 |
40 | - Get rid of time zones in `NaiveDate`, `NaiveTime`, `NaiveDateTime` APIs
41 | - Add convenience initializer for `NaiveDateTime` with individual date components
42 |
43 | ## NaiveDate 0.1
44 |
45 | Initial release which implements `NaiveDate`, `NaiveTime`, `NaiveDateTime` types, as well as two naive date formatters.
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Alexander Grebenyuk
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.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "NaiveDate",
7 | platforms: [
8 | .macOS(.v10_15),
9 | .iOS(.v13),
10 | .tvOS(.v13),
11 | .watchOS(.v6)
12 | ],
13 | products: [
14 | .library(name: "NaiveDate", targets: ["NaiveDate"]),
15 | ],
16 | targets: [
17 | .target(name: "NaiveDate", path: "Sources"),
18 | .testTarget(name: "NaiveDateTests", dependencies: ["NaiveDate"], path: "Tests")
19 | ]
20 | )
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NaiveDate
2 |
3 |
4 |
5 |
6 |
7 | The standard `Date` type is excellent for working with timestamps and time zones (e.g. `2024-09-29T15:00:00+0300`), but there are scenarios where you don't know or care about the time zone. These types of dates are often referred to as **naive**.
8 |
9 |
10 | ## Usage
11 |
12 | The `NaiveDate` library implements three types:
13 |
14 | - `NaiveDate` (e.g. `2024-09-29`)
15 | - `NaiveTime` (e.g. `15:30:00`)
16 | - `NaiveDateTime` (e.g. `2024-09-29T15:30:00` - no time zone and no offset).
17 |
18 | They all implement `Equatable`, `Comparable`, `LosslessStringConvertible`, and `Codable` protocols. Naive date types can also be converted to `Date`, and `DateComponents`.
19 |
20 | ### Create
21 |
22 | Naive dates and times can be created from a string (using a predefined format – [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6), using `Decodable`, or with a memberwise initializer:
23 |
24 | ```swift
25 | NaiveDate("2024-10-01")
26 | NaiveDate(year: 2024, month: 10, day: 1)
27 |
28 | NaiveTime("15:30:00")
29 | NaiveTime(hour: 15, minute: 30, second: 0)
30 |
31 | NaiveDateTime("2024-10-01T15:30")
32 | NaiveDateTime(
33 | date: NaiveDate(year: 2024, month: 10, day: 1),
34 | time: NaiveTime(hour: 15, minute: 30, second: 0)
35 | )
36 | ```
37 |
38 | ### Format
39 |
40 | `NaiveDate` supports `Foundation.FormatStyle`:
41 |
42 | ```swift
43 | let dateTime = NaiveDateTime("2024-11-01T15:30:00")!
44 | dateTime.formatted(date: .numeric, time: .standard)
45 | // prints "11/1/2024, 3:30:00 PM"
46 | ```
47 |
48 | In addition to the format style, you can use built-in `NaiveDateFormatter` directly.
49 |
50 | ```swift
51 | let date = NaiveDate("2024-11-01")!
52 | NaiveDateFormatter(dateStyle: .short).string(from: date)
53 | // prints "Nov 1, 2024"
54 |
55 | let time = NaiveTime("15:00")!
56 | NaiveDateFormatter(timeStyle: .short).string(from: time)
57 | // prints "3:00 PM"
58 |
59 | let dateTime = NaiveDateTime("2024-11-01T15:30:00")!
60 | NaiveDateFormatter(dateStyle: .short, timeStyle: .short).string(from: dateTime)
61 | // prints "Nov 1, 2024 at 3:30 PM"
62 | ```
63 |
64 | ### Convert
65 |
66 | When you do need to work with time zones, simply convert `NaiveDate` to `Date`:
67 |
68 | ```swift
69 | let date = NaiveDate(year: 2024, month: 10, day: 1)
70 |
71 | // Creates `Date` in a calendar's time zone
72 | // "2024-10-01T00:00:00+0300" if user is in MSK
73 | Calendar.current.date(from: date)
74 | ```
75 |
76 | ```swift
77 | let dateTime = NaiveDateTime(
78 | date: NaiveDate(year: 2024, month: 10, day: 1),
79 | time: NaiveTime(hour: 15, minute: 30, second: 0)
80 | )
81 |
82 | // Creates `Date` in a calendar's time zone
83 | // "2024-10-01T15:30:00+0300" if user is in MSK
84 | Calendar.current.date(from: dateTime)
85 | ```
86 |
87 | **Important!** The naive types are called this way because they don’t have a time zone associated with them. This means the date may not actually exist in some areas in the world, even though they are “valid”. For example, when daylight saving changes are applied the clock typically moves forward or backward by one hour. This means certain dates never occur or may occur more than once. If you need to do any precise manipulations with time, always use native `Date` and `Calendar`.
88 |
89 | ## Minimum Requirements
90 |
91 | | NaiveDate | Swift | Platforms |
92 | |----------------------|------------------|--------------------------------------------|
93 | | NaiveDate 1.1 | Swift 5.9 | iOS 13, tvOS 13, watchOS 6, macOS 10.15 |
94 | | NaiveDate 1.0 | Swift 5.3 | iOS 11, tvOS 11, watchOS 4, macOS 10.13, |
95 |
96 | ## License
97 |
98 | NaiveDate is available under the MIT license. See the LICENSE file for more info.
99 |
--------------------------------------------------------------------------------
/Sources/NaiveDate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // MARK: - NaiveDate
4 |
5 | /// Calendar date without a timezone.
6 | public struct NaiveDate: Sendable, Equatable, Hashable, Comparable, LosslessStringConvertible, Codable, _DateComponentsConvertible {
7 | public var year: Int, month: Int, day: Int
8 |
9 | /// Initializes the naive date with a given date components.
10 | /// - important: The naive types don't validate input components. For any
11 | /// precise manipulations with time use native `Date` and `Calendar` types.
12 | @inlinable
13 | public init(year: Int, month: Int, day: Int) {
14 | self.year = year; self.month = month; self.day = day
15 | }
16 |
17 | // MARK: Comparable
18 |
19 | @inlinable
20 | public static func <(lhs: NaiveDate, rhs: NaiveDate) -> Bool {
21 | return (lhs.year, lhs.month, lhs.day) < (rhs.year, rhs.month, rhs.day)
22 | }
23 |
24 | // MARK: LosslessStringConvertible
25 |
26 | /// Creates a naive date from a given string (e.g. "2017-12-30").
27 | @inlinable
28 | public init?(_ string: String) {
29 | // Not using `ISO8601DateFormatter` because it only works with `Date`
30 | guard let cmps = _components(from: string, separator: "-"), cmps.count == 3 else { return nil }
31 | self = NaiveDate(year: cmps[0], month: cmps[1], day: cmps[2])
32 | }
33 |
34 | /// Returns a string representation of a naive date (e.g. "2017-12-30").
35 | @inlinable
36 | public var description: String {
37 | return String(format: "%i-%.2i-%.2i", year, month, day)
38 | }
39 |
40 | // MARK: Codable
41 |
42 | @inlinable
43 | public init(from decoder: Decoder) throws {
44 | self = try _decode(from: decoder)
45 | }
46 |
47 | @inlinable
48 | public func encode(to encoder: Encoder) throws {
49 | try _encode(self, to: encoder)
50 | }
51 |
52 | // MARK: _DateComponentsConvertible
53 |
54 | @inlinable
55 | public var dateComponents: DateComponents {
56 | return DateComponents(year: year, month: month, day: day)
57 | }
58 | }
59 |
60 | // MARK: - NaiveTime
61 |
62 | /// Time without a timezone. Allows for second precision.
63 | public struct NaiveTime: Sendable, Equatable, Hashable, Comparable, LosslessStringConvertible, Codable, _DateComponentsConvertible {
64 | public var hour: Int, minute: Int, second: Int
65 |
66 | /// Initializes the naive time with a given date components.
67 | /// - important: The naive types don't validate input components. For any
68 | /// precise manipulations with time use native `Date` and `Calendar` types.
69 | public init(hour: Int = 0, minute: Int = 0, second: Int = 0) {
70 | self.hour = hour; self.minute = minute; self.second = second
71 | }
72 |
73 | /// Initializes a naive time with a given time interval. E.g.
74 | /// `NaiveTime(timeInterval: 3610)` returns `NaiveTime(hour: 1, second: 10)`.
75 | public init(timeInterval ti: Int) {
76 | self = NaiveTime(hour: ti / 3600, minute: (ti / 60) % 60, second: ti % 60)
77 | }
78 |
79 | /// Returns a total number of seconds. E.g.
80 | /// `NaiveTime(hour: 1, second: 10).timeInterval` returns `3610`.
81 | public var timeInterval: Int {
82 | return hour * 3600 + minute * 60 + second
83 | }
84 |
85 | // MARK: Comparable
86 |
87 | public static func <(lhs: NaiveTime, rhs: NaiveTime) -> Bool {
88 | return (lhs.hour, lhs.minute, lhs.second) < (rhs.hour, rhs.minute, rhs.second)
89 | }
90 |
91 | // MARK: LosslessStringConvertible
92 |
93 | /// Creates a naive time from a given string (e.g. "23:59", or "23:59:59").
94 | public init?(_ string: String) {
95 | guard let cmps = _components(from: string, separator: ":"),
96 | (2...3).contains(cmps.count) else { return nil }
97 | self.init(hour: cmps[0], minute: cmps[1], second: (cmps.count > 2 ? cmps[2] : 0))
98 | }
99 |
100 | /// Returns a string representation of a naive time (e.g. "23:59:59").
101 | public var description: String {
102 | return String(format: "%.2i:%.2i:%.2i", hour, minute, second)
103 | }
104 |
105 | // MARK: Codable
106 |
107 | public init(from decoder: Decoder) throws {
108 | self = try _decode(from: decoder)
109 | }
110 |
111 | public func encode(to encoder: Encoder) throws {
112 | try _encode(self, to: encoder)
113 | }
114 |
115 | // MARK: _DateComponentsConvertible
116 |
117 | public var dateComponents: DateComponents {
118 | return DateComponents(hour: hour, minute: minute, second: second)
119 | }
120 | }
121 |
122 |
123 | // MARK: - NaiveDateTime
124 |
125 | /// Combined date and time without timezone.
126 | public struct NaiveDateTime: Sendable, Equatable, Hashable, Comparable, LosslessStringConvertible, Codable, _DateComponentsConvertible {
127 | public var date: NaiveDate
128 | public var time: NaiveTime
129 |
130 | /// Initializes the naive datetime with a given date components.
131 | /// - important: The naive types don't validate input components. For any
132 | /// precise manipulations with time use native `Date` and `Calendar` types.
133 | public init(date: NaiveDate, time: NaiveTime) {
134 | self.date = date; self.time = time
135 | }
136 |
137 | /// Initializes the naive datetime with a given date components.
138 | /// - important: The naive types don't validate input components. For any
139 | /// precise manipulations with time use native `Date` and `Calendar` types.
140 | public init(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0, second: Int = 0) {
141 | self.date = NaiveDate(year: year, month: month, day: day)
142 | self.time = NaiveTime(hour: hour, minute: minute, second: second)
143 | }
144 |
145 | // MARK: Comparable
146 |
147 | public static func <(lhs: NaiveDateTime, rhs: NaiveDateTime) -> Bool {
148 | if lhs.date != rhs.date { return lhs.date < rhs.date }
149 | return lhs.time < rhs.time
150 | }
151 |
152 | // MARK: LosslessStringConvertible
153 |
154 | /// Creates a naive datetime from a given string (e.g. "2017-12-30T23:59:59").
155 | public init?(_ string: String) {
156 | let components = string.components(separatedBy: "T")
157 | guard components.count == 2 else { return nil } // must have both date & time
158 | guard let date = NaiveDate(components[0]), let time = NaiveTime(components[1]) else { return nil }
159 | self = NaiveDateTime(date: date, time: time)
160 | }
161 |
162 | /// Returns a string representation of a naive datetime (e.g. "2017-12-30T23:59:59").
163 | public var description: String {
164 | return "\(date)T\(time)"
165 | }
166 |
167 | // MARK: Codable
168 |
169 | public init(from decoder: Decoder) throws {
170 | self = try _decode(from: decoder)
171 | }
172 |
173 | public func encode(to encoder: Encoder) throws {
174 | try _encode(self, to: encoder)
175 | }
176 |
177 | // MARK: _DateComponentsConvertible
178 |
179 | public var dateComponents: DateComponents {
180 | return DateComponents(year: date.year, month: date.month, day: date.day, hour: time.hour, minute: time.minute, second: time.second)
181 | }
182 | }
183 |
184 |
185 |
186 | // MARK: - Calendar Extensions
187 |
188 | public extension Calendar {
189 | // MARK: Naive* -> Date
190 |
191 | /// Returns a date in calendar's time zone created from the naive date.
192 | @inlinable
193 | func date(from date: NaiveDate, in timeZone: TimeZone? = nil) -> Date? {
194 | return _date(from: date, in: timeZone)
195 | }
196 |
197 | /// Returns a date in calendar's time zone created from the naive time.
198 | @inlinable
199 | func date(from time: NaiveTime, in timeZone: TimeZone? = nil) -> Date? {
200 | return _date(from: time, in: timeZone)
201 | }
202 |
203 | /// Returns a date in calendar's time zone created from the naive datetime.
204 | @inlinable
205 | func date(from dateTime: NaiveDateTime, in timeZone: TimeZone? = nil) -> Date? {
206 | return _date(from: dateTime, in: timeZone)
207 | }
208 |
209 | @usableFromInline
210 | internal func _date(from value: T, in timeZone: TimeZone? = nil) -> Date? {
211 | var components = value.dateComponents
212 | components.timeZone = timeZone
213 | return self.date(from: components)
214 | }
215 |
216 | // MARK: Date -> Naive*
217 |
218 | /// Returns naive date from a date, as if in a given time zone. User calendar's time zone.
219 | /// - parameter timeZone: By default uses calendar's time zone.
220 | @inlinable
221 | func naiveDate(from date: Date, in timeZone: TimeZone? = nil) -> NaiveDate {
222 | let components = self.dateComponents(in: timeZone ?? self.timeZone, from: date)
223 | return NaiveDate(year: components.year!, month: components.month!, day: components.day!)
224 | }
225 |
226 | /// Returns naive time from a date, as if in a given time zone. User calendar's time zone.
227 | /// - parameter timeZone: By default uses calendar's time zone.
228 | @inlinable
229 | func naiveTime(from date: Date, in timeZone: TimeZone? = nil) -> NaiveTime {
230 | let components = self.dateComponents(in: timeZone ?? self.timeZone, from: date)
231 | return NaiveTime(hour: components.hour!, minute: components.minute!, second: components.second!)
232 | }
233 |
234 | /// Returns naive time from a date, as if in a given time zone. User calendar's time zone.
235 | /// - parameter timeZone: By default uses calendar's time zone.
236 | @inlinable
237 | func naiveDateTime(from date: Date, in timeZone: TimeZone? = nil) -> NaiveDateTime {
238 | let components = self.dateComponents(in: timeZone ?? self.timeZone, from: date)
239 | return NaiveDateTime(
240 | date: NaiveDate(year: components.year!, month: components.month!, day: components.day!),
241 | time: NaiveTime(hour: components.hour!, minute: components.minute!, second: components.second!)
242 | )
243 | }
244 | }
245 |
246 | // MARK: - Private
247 |
248 | /// A type that can be converted to DateComponents (and in turn to Date).
249 | @usableFromInline
250 | protocol _DateComponentsConvertible {
251 | var dateComponents: DateComponents { get }
252 | }
253 |
254 | @usableFromInline
255 | func _decode(from decoder: Decoder) throws -> T {
256 | let container = try decoder.singleValueContainer()
257 | let string = try container.decode(String.self)
258 | guard let value = T(string) else {
259 | throw Swift.DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid string format: \(string)")
260 | }
261 | return value
262 | }
263 |
264 | @usableFromInline
265 | func _encode(_ value: T, to encoder: Encoder) throws {
266 | var container = encoder.singleValueContainer()
267 | try container.encode(value.description)
268 | }
269 |
270 | @usableFromInline
271 | func _components(from string: String, separator: String) -> [Int]? {
272 | let substrings = string.components(separatedBy: separator)
273 | let components = substrings.compactMap(Int.init)
274 | return components.count == substrings.count ? components : nil
275 | }
276 |
--------------------------------------------------------------------------------
/Sources/NaiveDateFormatStyle.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
4 | public extension NaiveDate {
5 | struct FormatStyle: Foundation.FormatStyle {
6 | var date: Date.FormatStyle.DateStyle?
7 | var time: Date.FormatStyle.TimeStyle?
8 | var locale: Locale
9 | var calendar: Calendar
10 | var timeZone: TimeZone
11 | var capitalizationContext: FormatStyleCapitalizationContext
12 |
13 | /// Creates a format style for a NaiveDate.
14 | ///
15 | /// - Parameters:
16 | /// - date: The style to use for formatting the date component. Defaults to `nil`.
17 | /// - time: The style to use for formatting the time component. Defaults to `nil`.
18 | /// - locale: The locale to use for formatting. Defaults to `autoupdatingCurrent`.
19 | /// - calendar: The calendar to use for formatting. Defaults to `autoupdatingCurrent`.
20 | /// - timeZone: The time zone to use for formatting. Defaults to `autoupdatingCurrent`.
21 | /// - capitalizationContext: The context for capitalization. Defaults to `unknown`.
22 | public init(date: Date.FormatStyle.DateStyle? = nil,
23 | time: Date.FormatStyle.TimeStyle? = nil,
24 | locale: Locale = .autoupdatingCurrent,
25 | calendar: Calendar = .autoupdatingCurrent,
26 | timeZone: TimeZone = .autoupdatingCurrent,
27 | capitalizationContext: FormatStyleCapitalizationContext = .unknown) {
28 | self.date = date
29 | self.time = time
30 | self.locale = locale
31 | self.calendar = calendar
32 | self.timeZone = timeZone
33 | self.capitalizationContext = capitalizationContext
34 | }
35 |
36 | /// Formats the given NaiveDate value.
37 | ///
38 | /// - Parameter value: The NaiveDate value to format.
39 | /// - Returns: A formatted string representing the NaiveDate.
40 | public func format(_ value: NaiveDate) -> String {
41 | calendar.date(from: value).map { date in
42 | let dateStyle = Date.FormatStyle(
43 | date: self.date,
44 | time: self.time,
45 | locale: locale,
46 | calendar: calendar,
47 | timeZone: timeZone,
48 | capitalizationContext: capitalizationContext
49 | )
50 | return date.formatted(dateStyle)
51 | } ?? ""
52 | }
53 |
54 | /// Returns a new format style with the specified locale.
55 | ///
56 | /// - Parameter locale: The locale to apply to the format style.
57 | /// - Returns: A new `NaiveDate.FormatStyle` with the given locale.
58 | public func locale(_ locale: Locale) -> NaiveDate.FormatStyle {
59 | .init(
60 | date: date,
61 | time: time,
62 | locale: locale,
63 | calendar: calendar,
64 | timeZone: timeZone,
65 | capitalizationContext: capitalizationContext
66 | )
67 | }
68 | }
69 |
70 | /// Formats the NaiveDate using the provided format style.
71 | ///
72 | /// - Parameter format: The format style to apply.
73 | /// - Returns: The formatted string output.
74 | func formatted(_ format: F) -> F.FormatOutput where F.FormatInput == NaiveDate {
75 | format.format(self)
76 | }
77 |
78 | /// Formats the NaiveDate using the default format style.
79 | ///
80 | /// - Returns: A formatted string representation of the NaiveDate.
81 | func formatted() -> String {
82 | formatted(FormatStyle())
83 | }
84 |
85 | /// Formats the NaiveDate with specified date and time styles.
86 | ///
87 | /// - Parameters:
88 | /// - date: The style to use for formatting the date component.
89 | /// - time: The style to use for formatting the time component.
90 | /// - Returns: A formatted string representation of the NaiveDate.
91 | func formatted(date: Date.FormatStyle.DateStyle, time: Date.FormatStyle.TimeStyle = .omitted) -> String {
92 | formatted(FormatStyle(date: date, time: time))
93 | }
94 | }
95 |
96 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
97 | public extension NaiveTime {
98 | struct FormatStyle: Foundation.FormatStyle {
99 | var date: Date.FormatStyle.DateStyle?
100 | var time: Date.FormatStyle.TimeStyle?
101 | var locale: Locale
102 | var calendar: Calendar
103 | var timeZone: TimeZone
104 | var capitalizationContext: FormatStyleCapitalizationContext
105 |
106 | /// Creates a format style for a NaiveTime.
107 | ///
108 | /// - Parameters:
109 | /// - dateStyle: The style to use for formatting the date component. Defaults to `nil`.
110 | /// - timeStyle: The style to use for formatting the time component. Defaults to `nil`.
111 | /// - locale: The locale to use for formatting. Defaults to `autoupdatingCurrent`.
112 | /// - calendar: The calendar to use for formatting. Defaults to `autoupdatingCurrent`.
113 | /// - timeZone: The time zone to use for formatting. Defaults to `autoupdatingCurrent`.
114 | /// - capitalizationContext: The context for capitalization. Defaults to `unknown`.
115 | public init(date: Date.FormatStyle.DateStyle? = nil,
116 | time: Date.FormatStyle.TimeStyle? = nil,
117 | locale: Locale = .autoupdatingCurrent,
118 | calendar: Calendar = .autoupdatingCurrent,
119 | timeZone: TimeZone = .autoupdatingCurrent,
120 | capitalizationContext: FormatStyleCapitalizationContext = .unknown) {
121 | self.date = date
122 | self.time = time
123 | self.locale = locale
124 | self.calendar = calendar
125 | self.timeZone = timeZone
126 | self.capitalizationContext = capitalizationContext
127 | }
128 |
129 | /// Formats the given NaiveTime value.
130 | ///
131 | /// - Parameter value: The NaiveTime value to format.
132 | /// - Returns: A formatted string representing the NaiveTime.
133 | public func format(_ value: NaiveTime) -> String {
134 | calendar.date(from: value).map { date in
135 | let dateStyle = Date.FormatStyle(
136 | date: self.date,
137 | time: self.time,
138 | locale: locale,
139 | calendar: calendar,
140 | timeZone: timeZone,
141 | capitalizationContext: capitalizationContext
142 | )
143 | return date.formatted(dateStyle)
144 | } ?? ""
145 | }
146 |
147 | /// Returns a new format style with the specified locale.
148 | ///
149 | /// - Parameter locale: The locale to apply to the format style.
150 | /// - Returns: A new `NaiveTime.FormatStyle` with the given locale.
151 | public func locale(_ locale: Locale) -> NaiveTime.FormatStyle {
152 | .init(
153 | date: date,
154 | time: time,
155 | locale: locale,
156 | calendar: calendar,
157 | timeZone: timeZone,
158 | capitalizationContext: capitalizationContext
159 | )
160 | }
161 | }
162 |
163 | /// Formats the NaiveTime using the provided format style.
164 | ///
165 | /// - Parameter format: The format style to apply.
166 | /// - Returns: The formatted string output.
167 | func formatted(_ format: F) -> F.FormatOutput where F.FormatInput == NaiveTime {
168 | format.format(self)
169 | }
170 |
171 | /// Formats the NaiveTime using the default format style.
172 | ///
173 | /// - Returns: A formatted string representation of the NaiveTime.
174 | func formatted() -> String {
175 | formatted(FormatStyle())
176 | }
177 |
178 | /// Formats the NaiveTime with specified date and time styles.
179 | ///
180 | /// - Parameters:
181 | /// - date: The style to use for formatting the date component.
182 | /// - time: The style to use for formatting the time component.
183 | /// - Returns: A formatted string representation of the NaiveTime.
184 | func formatted(date: Date.FormatStyle.DateStyle = .omitted, time: Date.FormatStyle.TimeStyle) -> String {
185 | formatted(FormatStyle(date: date, time: time))
186 | }
187 | }
188 |
189 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
190 | public extension NaiveDateTime {
191 | struct FormatStyle: Foundation.FormatStyle {
192 | var date: Date.FormatStyle.DateStyle?
193 | var time: Date.FormatStyle.TimeStyle?
194 | var locale: Locale
195 | var calendar: Calendar
196 | var timeZone: TimeZone
197 | var capitalizationContext: FormatStyleCapitalizationContext
198 |
199 | /// Creates a format style for a NaiveDateTime.
200 | ///
201 | /// - Parameters:
202 | /// - dateStyle: The style to use for formatting the date component. Defaults to `nil`.
203 | /// - timeStyle: The style to use for formatting the time component. Defaults to `nil`.
204 | /// - locale: The locale to use for formatting. Defaults to `autoupdatingCurrent`.
205 | /// - calendar: The calendar to use for formatting. Defaults to `autoupdatingCurrent`.
206 | /// - timeZone: The time zone to use for formatting. Defaults to `autoupdatingCurrent`.
207 | /// - capitalizationContext: The context for capitalization. Defaults to `unknown`.
208 | public init(date: Date.FormatStyle.DateStyle? = nil,
209 | time: Date.FormatStyle.TimeStyle? = nil,
210 | locale: Locale = .autoupdatingCurrent,
211 | calendar: Calendar = .autoupdatingCurrent,
212 | timeZone: TimeZone = .autoupdatingCurrent,
213 | capitalizationContext: FormatStyleCapitalizationContext = .unknown) {
214 | self.date = date
215 | self.time = time
216 | self.locale = locale
217 | self.calendar = calendar
218 | self.timeZone = timeZone
219 | self.capitalizationContext = capitalizationContext
220 | }
221 |
222 | /// Formats the given NaiveDateTime value.
223 | ///
224 | /// - Parameter value: The NaiveDateTime value to format.
225 | /// - Returns: A formatted string representing the NaiveDateTime.
226 | public func format(_ value: NaiveDateTime) -> String {
227 | calendar.date(from: value).map { date in
228 | let dateStyle = Date.FormatStyle(
229 | date: self.date,
230 | time: time,
231 | locale: locale,
232 | calendar: calendar,
233 | timeZone: timeZone,
234 | capitalizationContext: capitalizationContext
235 | )
236 |
237 | return date.formatted(dateStyle)
238 | } ?? ""
239 | }
240 |
241 | /// Returns a new format style with the specified locale.
242 | ///
243 | /// - Parameter locale: The locale to apply to the format style.
244 | /// - Returns: A new `NaiveDateTime.FormatStyle` with the given locale.
245 | public func locale(_ locale: Locale) -> NaiveDate.FormatStyle {
246 | .init(
247 | date: date,
248 | locale: locale,
249 | calendar: calendar,
250 | timeZone: timeZone,
251 | capitalizationContext: capitalizationContext
252 | )
253 | }
254 | }
255 |
256 | /// Formats the NaiveDateTime using the provided format style.
257 | ///
258 | /// - Parameter format: The format style to apply.
259 | /// - Returns: The formatted string output.
260 | func formatted(_ format: F) -> F.FormatOutput where F.FormatInput == NaiveDateTime {
261 | format.format(self)
262 | }
263 |
264 | /// Formats the NaiveDateTime using the default format style.
265 | ///
266 | /// - Returns: A formatted string representation of the NaiveDateTime.
267 | func formatted() -> String {
268 | formatted(FormatStyle())
269 | }
270 |
271 | /// Formats the NaiveDateTime with specified date and time styles.
272 | ///
273 | /// - Parameters:
274 | /// - date: The style to use for formatting the date component.
275 | /// - time: The style to use for formatting the time component.
276 | /// - Returns: A formatted string representation of the NaiveDateTime.
277 | func formatted(date: Date.FormatStyle.DateStyle, time: Date.FormatStyle.TimeStyle) -> String {
278 | formatted(FormatStyle(date: date, time: time))
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/Sources/NaiveDateFormatter.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // MARK: - NaiveDateFormatter
4 |
5 | /// Formatting without time zones.
6 | public final class NaiveDateFormatter {
7 | @usableFromInline
8 | let formatter = DateFormatter()
9 |
10 | @inlinable
11 | public init(_ closure: (_ formatter: DateFormatter) -> Void) {
12 | closure(formatter)
13 | }
14 |
15 | @inlinable
16 | public convenience init(format: String) {
17 | self.init {
18 | $0.dateFormat = format
19 | }
20 | }
21 |
22 | @inlinable
23 | public convenience init(dateStyle: DateFormatter.Style = .none, timeStyle: DateFormatter.Style = .none) {
24 | self.init {
25 | $0.dateStyle = dateStyle
26 | $0.timeStyle = timeStyle
27 | }
28 | }
29 |
30 | public func string(from value: NaiveDate) -> String? {
31 | return formatter.calendar._date(from: value).map { formatter.string(from: $0) }
32 | }
33 |
34 | @inlinable
35 | public func string(from value: NaiveTime) -> String? {
36 | return formatter.calendar._date(from: value).map { formatter.string(from: $0) }
37 | }
38 |
39 | @inlinable
40 | public func string(from value: NaiveDateTime) -> String? {
41 | return formatter.calendar._date(from: value).map { formatter.string(from: $0) }
42 | }
43 | }
44 |
45 | // MARK: - NaiveDateRangeFormatter
46 |
47 | /// Formatting without time zones.
48 | public final class NaiveDateRangeFormatter {
49 | @usableFromInline
50 | let formatter = DateIntervalFormatter()
51 |
52 | @inlinable
53 | public init(_ closure: (_ formatter: DateIntervalFormatter) -> Void) {
54 | closure(formatter)
55 | }
56 |
57 | @inlinable
58 | public convenience init(format: String) {
59 | self.init {
60 | $0.dateTemplate = format
61 | }
62 | }
63 |
64 | @inlinable
65 | public convenience init(dateStyle: DateIntervalFormatter.Style = .none, timeStyle: DateIntervalFormatter.Style = .none) {
66 | self.init {
67 | $0.dateStyle = dateStyle
68 | $0.timeStyle = timeStyle
69 | }
70 | }
71 |
72 | @inlinable
73 | public func string(from start: NaiveDate, to end: NaiveDate) -> String? {
74 | return formatter.calendar._dateRange(from: start, to: end).map { formatter.string(from: $0, to: $1) }
75 | }
76 |
77 | @inlinable
78 | public func string(from start: NaiveTime, to end: NaiveTime) -> String? {
79 | return formatter.calendar._dateRange(from: start, to: end).map { formatter.string(from: $0, to: $1) }
80 | }
81 |
82 | @inlinable
83 | public func string(from start: NaiveDateTime, to end: NaiveDateTime) -> String? {
84 | return formatter.calendar._dateRange(from: start, to: end).map { formatter.string(from: $0, to: $1) }
85 | }
86 | }
87 |
88 | // MARK: - Private
89 |
90 | extension Calendar {
91 | @inlinable
92 | func _dateRange(from start: T, to end: T) -> (Date, Date)? {
93 | guard let start = _date(from: start), let end = _date(from: end) else { return nil }
94 | return (start, end)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Tests/NaiveDateFormatStyleTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | import NaiveDate
4 |
5 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
6 | final class NaiveDateFormatStyleTest: XCTestCase {
7 | func testFormattedNaiveDateAgainstDate_withoutParameters() throws {
8 | let naiveDate = NaiveDate(year: 2024, month: 8, day: 12)
9 | let foundationDate = try XCTUnwrap(Calendar.current.date(from: naiveDate))
10 |
11 | let formattedFoundationDate = foundationDate.formatted()
12 | let formattedNaiveDate = naiveDate.formatted()
13 |
14 | XCTAssertEqual(formattedNaiveDate, formattedFoundationDate)
15 | }
16 |
17 | func testFormattedNaiveDateAgainstDate_numeric() throws {
18 | let naiveDate = NaiveDate(year: 2024, month: 8, day: 12)
19 | let foundationDate = try XCTUnwrap(Calendar.current.date(from: naiveDate))
20 |
21 | let formattedFoundationDate = foundationDate.formatted(date: .numeric, time: .omitted)
22 | let formattedNaiveDate = naiveDate.formatted(date: .numeric)
23 |
24 | XCTAssertEqual(formattedNaiveDate, formattedFoundationDate)
25 | }
26 |
27 | func testFormattedNaiveDateTimeAgainstDate_withoutParameters() throws {
28 | let naiveDate = NaiveDateTime(date: .init(year: 2024, month: 8, day: 12), time: .init(hour: 5, minute: 3, second: 1))
29 | let foundationDate = try XCTUnwrap(Calendar.current.date(from: naiveDate))
30 |
31 | let formattedFoundationDate = foundationDate.formatted()
32 | let formattedNaiveDate = naiveDate.formatted()
33 |
34 | XCTAssertEqual(formattedNaiveDate, formattedFoundationDate)
35 | }
36 |
37 | func testFormattedNaiveDateTimeAgainstDate_numeric() throws {
38 | let naiveDate = NaiveDateTime(date: .init(year: 2024, month: 8, day: 12), time: .init(hour: 5, minute: 3, second: 1))
39 | let foundationDate = try XCTUnwrap(Calendar.current.date(from: naiveDate))
40 |
41 | let formattedFoundationDate = foundationDate.formatted(date: .numeric, time: .standard)
42 | let formattedNaiveDate = naiveDate.formatted(date: .numeric, time: .standard)
43 |
44 | XCTAssertEqual(formattedNaiveDate, formattedFoundationDate)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Tests/NaiveDateFormatterTest.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | import NaiveDate
4 |
5 | class NaiveDateFormatterTest: XCTestCase {
6 | func testNaiveTimeFormatter_enUS() {
7 | let formatter = NaiveDateFormatter {
8 | $0.locale = Locale(identifier: "en_US")
9 | $0.timeStyle = .short
10 | }
11 |
12 | XCTAssertEqual(formatter.string(from: NaiveTime("16:10")!), "4:10 PM")
13 | XCTAssertEqual(formatter.string(from: NaiveTime("16:10:15")!), "4:10 PM")
14 | }
15 |
16 | func testNaiveTimeFormatter_enGB() {
17 | let formatter = NaiveDateFormatter {
18 | $0.locale = Locale(identifier: "en_GB")
19 | $0.timeStyle = .short
20 | }
21 |
22 | XCTAssertEqual(formatter.string(from: NaiveTime("16:10")!), "16:10")
23 | XCTAssertEqual(formatter.string(from: NaiveTime("16:10:15")!), "16:10")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/NaiveDateTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | import NaiveDate
4 |
5 | // MARK: - NaiveDate -
6 |
7 | class NaiveDateTest: XCTestCase {
8 | // MARK: Equatable, Hashable, Comparable
9 |
10 | func testEquatable() {
11 | XCTAssertNotEqual(NaiveDate(year: 2017, month: 10, day: 1), NaiveDate(year: 2018, month: 10, day: 1))
12 | XCTAssertNotEqual(NaiveDate(year: 2017, month: 10, day: 1), NaiveDate(year: 2017, month: 11, day: 1))
13 | XCTAssertNotEqual(NaiveDate(year: 2017, month: 10, day: 1), NaiveDate(year: 2017, month: 10, day: 2))
14 | XCTAssertEqual(NaiveDate(year: 2017, month: 10, day: 1), NaiveDate(year: 2017, month: 10, day: 1))
15 | }
16 |
17 | func testHashable() {
18 | XCTAssertNotEqual(NaiveDate(year: 2017, month: 10, day: 1).hashValue, NaiveDate(year: 2018, month: 10, day: 1).hashValue)
19 | XCTAssertNotEqual(NaiveDate(year: 2017, month: 10, day: 1).hashValue, NaiveDate(year: 2017, month: 11, day: 1).hashValue)
20 | XCTAssertNotEqual(NaiveDate(year: 2017, month: 10, day: 1).hashValue, NaiveDate(year: 2017, month: 10, day: 2).hashValue)
21 | XCTAssertEqual(NaiveDate(year: 2017, month: 10, day: 1).hashValue, NaiveDate(year: 2017, month: 10, day: 1).hashValue)
22 | }
23 |
24 | func testComparable() {
25 | XCTAssertLessThan(NaiveDate(year: 2017, month: 10, day: 1), NaiveDate(year: 2018, month: 10, day: 1))
26 | XCTAssertLessThan(NaiveDate(year: 2017, month: 10, day: 1), NaiveDate(year: 2017, month: 11, day: 1))
27 | XCTAssertLessThan(NaiveDate(year: 2017, month: 10, day: 1), NaiveDate(year: 2017, month: 10, day: 2))
28 | XCTAssertEqual(NaiveDate(year: 2017, month: 10, day: 1), NaiveDate(year: 2017, month: 10, day: 1))
29 | }
30 |
31 | // MARK: LosslessStringConvertible
32 |
33 | func testFromString() {
34 | XCTAssertNil(NaiveDate("2017"))
35 | XCTAssertNil(NaiveDate("2017-10"))
36 | XCTAssertNil(NaiveDate("2017-AA-10-01"))
37 | XCTAssertNil(NaiveDate(" 2017-10-01"))
38 | XCTAssertNil(NaiveDate("2017- 10-01"))
39 | XCTAssertNil(NaiveDate("2017:10:01"))
40 |
41 | XCTAssertEqual(NaiveDate("2017-10-01"), NaiveDate(year: 2017, month: 10, day: 1))
42 | XCTAssertEqual(NaiveDate("2017-10-1"), NaiveDate(year: 2017, month: 10, day: 1))
43 | }
44 |
45 | func testToString() {
46 | XCTAssertEqual(NaiveDate(year: 2017, month: 10, day: 1).description, "2017-10-01")
47 | }
48 |
49 | // MARK: Codable
50 |
51 | private struct Wrapped: Codable {
52 | let date: NaiveDate
53 | }
54 |
55 | func testDecodable() {
56 | let date = try! JSONDecoder().decode(Wrapped.self, from: "{\"date\":\"2017-02-01\"}".data(using: .utf8)!)
57 | XCTAssertEqual(date.date, NaiveDate(year: 2017, month: 2, day: 1))
58 | }
59 |
60 | func testEncodable() {
61 | let data = try! JSONEncoder().encode(Wrapped(date: NaiveDate(year: 2017, month: 2, day: 1)))
62 | XCTAssertEqual(String(data: data, encoding: .utf8), "{\"date\":\"2017-02-01\"}")
63 | }
64 |
65 | // MARK: Date, DateComponents
66 |
67 | func testFromDate() {
68 | let date = ISO8601DateFormatter().date(from: "2017-12-01T12:00:00Z")!
69 | var calendar = Calendar.current
70 | calendar.timeZone = TimeZone(identifier: "GMT")!
71 | XCTAssertEqual(
72 | calendar.naiveDate(from: date),
73 | NaiveDate("2017-12-01")
74 | )
75 | }
76 |
77 | func testToDate() {
78 | test("converting to date in calendar's time zone") {
79 | let date = NaiveDate(year: 2017, month: 10, day: 1)
80 |
81 | var calendar = Calendar.current
82 | calendar.timeZone = TimeZone(secondsFromGMT: 3600)!
83 |
84 | XCTAssertEqual(
85 | calendar.date(from: date),
86 | _date(from: "2017-10-01T00:00:00+0100")
87 | )
88 | }
89 | }
90 |
91 | func testToDateComponents() {
92 | XCTAssertEqual(
93 | NaiveDate(year: 2017, month: 10, day: 1).dateComponents,
94 | DateComponents(year: 2017, month: 10, day: 1)
95 | )
96 | }
97 | }
98 |
99 |
100 | // MARK: - NaiveTime -
101 |
102 | class NaiveTimeTest: XCTestCase {
103 | // MARK: Equatable, Hashable, Comparable
104 |
105 | func testEquatable() {
106 | XCTAssertNotEqual(NaiveTime(hour: 21, minute: 15, second: 10), NaiveTime(hour: 22, minute: 15, second: 10))
107 | XCTAssertNotEqual(NaiveTime(hour: 21, minute: 15, second: 10), NaiveTime(hour: 21, minute: 16, second: 10))
108 | XCTAssertNotEqual(NaiveTime(hour: 21, minute: 15, second: 10), NaiveTime(hour: 21, minute: 15, second: 11))
109 | XCTAssertEqual(NaiveTime(hour: 21, minute: 15, second: 10), NaiveTime(hour: 21, minute: 15, second: 10))
110 | }
111 |
112 | func testHashable() {
113 | XCTAssertNotEqual(NaiveTime(hour: 21, minute: 15, second: 10).hashValue, NaiveTime(hour: 22, minute: 15, second: 10).hashValue)
114 | XCTAssertNotEqual(NaiveTime(hour: 21, minute: 15, second: 10).hashValue, NaiveTime(hour: 21, minute: 16, second: 10).hashValue)
115 | XCTAssertNotEqual(NaiveTime(hour: 21, minute: 15, second: 10).hashValue, NaiveTime(hour: 21, minute: 15, second: 11).hashValue)
116 | XCTAssertEqual(NaiveTime(hour: 21, minute: 15, second: 10).hashValue, NaiveTime(hour: 21, minute: 15, second: 10).hashValue)
117 | }
118 |
119 | func testComparable() {
120 | XCTAssertLessThan(NaiveTime(hour: 21, minute: 15, second: 10), NaiveTime(hour: 22, minute: 15, second: 10))
121 | XCTAssertLessThan(NaiveTime(hour: 21, minute: 15, second: 10), NaiveTime(hour: 21, minute: 16, second: 10))
122 | XCTAssertLessThan(NaiveTime(hour: 21, minute: 15, second: 10), NaiveTime(hour: 21, minute: 15, second: 11))
123 | XCTAssertEqual(NaiveTime(hour: 21, minute: 15, second: 10), NaiveTime(hour: 21, minute: 15, second: 10))
124 | }
125 |
126 | // MARK: Time Interval
127 |
128 | func funcInitWithTotalSeconds() {
129 | XCTAssertEqual(NaiveTime(timeInterval: 605), NaiveTime(minute: 10, second: 5))
130 | XCTAssertEqual(NaiveTime(timeInterval: 3600), NaiveTime(hour: 1))
131 | XCTAssertEqual(NaiveTime(timeInterval: 3610), NaiveTime(hour: 1, second: 10))
132 | }
133 |
134 | func testTotalSeconds() {
135 | XCTAssertEqual(NaiveTime(minute: 10, second: 5).timeInterval, 605)
136 | XCTAssertEqual(NaiveTime(hour: 1).timeInterval, 3600)
137 | XCTAssertEqual(NaiveTime(hour: 1, second: 10).timeInterval, 3610)
138 | }
139 |
140 | // MARK: LosslessStringConvertible
141 |
142 | func testFromString() {
143 | XCTAssertNil(NaiveTime("AA"))
144 | XCTAssertNil(NaiveTime(""))
145 | XCTAssertNil(NaiveTime("23:AA"))
146 | XCTAssertNil(NaiveTime("23-59"))
147 | XCTAssertNil(NaiveTime("23"))
148 | XCTAssertNil(NaiveTime("23:AA:59:59"))
149 |
150 | XCTAssertNil(NaiveTime(" 23:59:59"))
151 | XCTAssertNil(NaiveTime("23:59:59 "))
152 | XCTAssertNil(NaiveTime("23:59 :59"))
153 |
154 | XCTAssertEqual(NaiveTime("23:59:59"), NaiveTime(hour: 23, minute: 59, second: 59))
155 | XCTAssertEqual(NaiveTime("23:59"), NaiveTime(hour: 23, minute: 59, second: 0))
156 | }
157 |
158 | func testToString() {
159 | XCTAssertEqual(NaiveTime(hour: 23, minute: 59, second: 59).description, "23:59:59")
160 | XCTAssertEqual(NaiveTime(hour: 23, minute: 59, second: 0).description, "23:59:00")
161 | XCTAssertEqual(NaiveTime(hour: 23, minute: 0, second: 0).description, "23:00:00")
162 | }
163 |
164 | // MARK: Codable
165 |
166 | private struct Wrapped: Codable {
167 | let time: NaiveTime
168 | }
169 |
170 | func testDecodable() {
171 | let time = try! JSONDecoder().decode(Wrapped.self, from: "{\"time\":\"22:15:10\"}".data(using: .utf8)!)
172 | XCTAssertEqual(time.time, NaiveTime(hour: 22, minute: 15, second: 10))
173 | }
174 |
175 | func testEncodable() {
176 | let data = try! JSONEncoder().encode(Wrapped(time: NaiveTime(hour: 22, minute: 15, second: 10)))
177 | XCTAssertEqual(String(data: data, encoding: .utf8), "{\"time\":\"22:15:10\"}")
178 | }
179 | }
180 |
181 |
182 | // MARK: - NaiveDateTime -
183 |
184 | class NaiveDateTimeTest: XCTestCase {
185 | // MARK: Equatable, Hashable, Comparable
186 |
187 | func testEquatable() {
188 | XCTAssertEqual(
189 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
190 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10))
191 | )
192 |
193 | // Same time
194 |
195 | XCTAssertNotEqual(
196 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
197 | NaiveDateTime(date: NaiveDate(year: 2017, month: 11, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10))
198 | )
199 | XCTAssertNotEqual(
200 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
201 | NaiveDateTime(date: NaiveDate(year: 2017, month: 11, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10))
202 | )
203 | XCTAssertNotEqual(
204 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
205 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 2), time: NaiveTime(hour: 22, minute: 15, second: 10))
206 | )
207 |
208 | // Same date
209 |
210 | XCTAssertNotEqual(
211 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 21, minute: 15, second: 10)),
212 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10))
213 | )
214 | XCTAssertNotEqual(
215 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
216 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 16, second: 10))
217 | )
218 | XCTAssertNotEqual(
219 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
220 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 11))
221 | )
222 | }
223 |
224 | func testHashable() {
225 | XCTAssertEqual(
226 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue,
227 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue
228 | )
229 |
230 | // Same time
231 |
232 | XCTAssertNotEqual(
233 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue,
234 | NaiveDateTime(date: NaiveDate(year: 2017, month: 11, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue
235 | )
236 | XCTAssertNotEqual(
237 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue,
238 | NaiveDateTime(date: NaiveDate(year: 2017, month: 11, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue
239 | )
240 | XCTAssertNotEqual(
241 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue,
242 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 2), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue
243 | )
244 |
245 | // Same date
246 |
247 | XCTAssertNotEqual(
248 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 21, minute: 15, second: 10)).hashValue,
249 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue
250 | )
251 | XCTAssertNotEqual(
252 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue,
253 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 16, second: 10)).hashValue
254 | )
255 | XCTAssertNotEqual(
256 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)).hashValue,
257 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 11)).hashValue
258 | )
259 | }
260 |
261 | func testComparable() {
262 | XCTAssertEqual(
263 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
264 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10))
265 | )
266 |
267 | // Same time
268 |
269 | XCTAssertLessThan(
270 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
271 | NaiveDateTime(date: NaiveDate(year: 2017, month: 11, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10))
272 | )
273 | XCTAssertLessThan(
274 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
275 | NaiveDateTime(date: NaiveDate(year: 2017, month: 11, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10))
276 | )
277 | XCTAssertLessThan(
278 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
279 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 2), time: NaiveTime(hour: 22, minute: 15, second: 10))
280 | )
281 |
282 | // Same date
283 |
284 | XCTAssertLessThan(
285 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 21, minute: 15, second: 10)),
286 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10))
287 | )
288 | XCTAssertLessThan(
289 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
290 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 16, second: 10))
291 | )
292 | XCTAssertLessThan(
293 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 10)),
294 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 22, minute: 15, second: 11))
295 | )
296 | }
297 |
298 | // MARK: LosslessStringConvertible
299 |
300 | func testFromString() {
301 | XCTAssertNil(NaiveDateTime("2017"))
302 |
303 | XCTAssertNil(NaiveDateTime("2017-10T23:10:15"))
304 | XCTAssertNil(NaiveDateTime("2017-AT23:10:15"))
305 | XCTAssertNil(NaiveDateTime("SADT23:10:15"))
306 |
307 | XCTAssertNil(NaiveDateTime("2017-10-01T23:10:AA"))
308 | XCTAssertNil(NaiveDateTime("2017-10-01TBB"))
309 | XCTAssertNil(NaiveDateTime("2017-10-01")) // FIXME: add support for this case?
310 |
311 | XCTAssertEqual(
312 | NaiveDateTime("2017-10-01T23:59:59"),
313 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 23, minute: 59, second: 59))
314 | )
315 | XCTAssertEqual(
316 | NaiveDateTime("2017-10-1T23:59:59"),
317 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 23, minute: 59, second: 59))
318 | )
319 | XCTAssertEqual(
320 | NaiveDateTime("2017-10-01T23:59"),
321 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 23, minute: 59, second: 0))
322 | )
323 | }
324 |
325 | func testToString() {
326 | XCTAssertEqual(
327 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 23, minute: 59, second: 59)).description,
328 | "2017-10-01T23:59:59"
329 | )
330 | XCTAssertEqual(
331 | NaiveDateTime(date: NaiveDate(year: 2017, month: 10, day: 1), time: NaiveTime(hour: 23, minute: 59, second: 0)).description,
332 | "2017-10-01T23:59:00"
333 | )
334 | }
335 |
336 | // MARK: Codable
337 |
338 | private struct Wrapped: Codable {
339 | let dateTime: NaiveDateTime
340 | }
341 |
342 | func testDecodable() {
343 | let dateTime = try! JSONDecoder().decode(Wrapped.self, from: "{\"dateTime\":\"2017-02-01T10:09:08\"}".data(using: .utf8)!)
344 | XCTAssertEqual(
345 | dateTime.dateTime,
346 | NaiveDateTime(date: NaiveDate(year: 2017, month: 2, day: 1), time: NaiveTime(hour: 10, minute: 9, second: 8))
347 | )
348 | }
349 |
350 | func testEncodable() {
351 | let dateTime = NaiveDateTime(date: NaiveDate(year: 2017, month: 2, day: 1), time: NaiveTime(hour: 10, minute: 9, second: 8))
352 | let data = try! JSONEncoder().encode(Wrapped(dateTime: dateTime))
353 | XCTAssertEqual(String(data: data, encoding: .utf8), "{\"dateTime\":\"2017-02-01T10:09:08\"}")
354 | }
355 |
356 | // MARK: Date <-> NaiveDateTime
357 |
358 | func testFromDate() {
359 | let date = ISO8601DateFormatter().date(from: "2017-12-01T12:00:00Z")!
360 | var calendar = Calendar.current
361 | calendar.timeZone = TimeZone(secondsFromGMT: 3600)!
362 | XCTAssertEqual(
363 | calendar.naiveDateTime(from: date),
364 | NaiveDateTime("2017-12-01T13:00:00")!
365 | )
366 | }
367 |
368 | func testToDate() {
369 | let dateTime = NaiveDateTime(
370 | date: NaiveDate(year: 2017, month: 10, day: 1),
371 | time: NaiveTime(hour: 15, minute: 30, second: 0)
372 | )
373 | var calendar = Calendar.current
374 | calendar.timeZone = TimeZone(secondsFromGMT: 3600)!
375 |
376 | XCTAssertEqual(
377 | calendar.date(from: dateTime),
378 | _date(from: "2017-10-01T15:30:00+0100")
379 | )
380 | }
381 |
382 | func testToDateComponents() {
383 | XCTAssertEqual(
384 | NaiveDateTime(
385 | date: NaiveDate(year: 2017, month: 10, day: 1),
386 | time: NaiveTime(hour: 15, minute: 30, second: 0)
387 | ).dateComponents,
388 | DateComponents(
389 | year: 2017, month: 10, day: 1,
390 | hour: 15, minute: 30, second: 0
391 | )
392 | )
393 | }
394 | }
395 |
396 | // MARK: - Helpers -
397 |
398 | /// ISO-8601 formatter "2017-10-01T15:30:00Z"
399 | private func _date(from string: String) -> Date {
400 | return _iso8601Formatter.date(from: string)!
401 | }
402 |
403 | private let _iso8601Formatter = ISO8601DateFormatter()
404 |
405 | /// For code organization.
406 | private func test(_ title: String? = nil, _ closure: () -> Void) {
407 | closure()
408 | }
409 |
410 | private func test(_ title: String? = nil, with element: T, _ closure: (T) -> Void) {
411 | closure(element)
412 | }
413 |
--------------------------------------------------------------------------------