├── .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 | --------------------------------------------------------------------------------