├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── DateHelper │ └── DateHelper.swift ├── Tests └── DateHelperTests │ ├── DateAdjustingTests.swift │ ├── DateComparisonTests.swift │ ├── DateFromStringTests.swift │ ├── MiscelaniousTests.swift │ ├── StringFromDateTests.swift │ └── TimeSinceTests.swift ├── logo.png └── logo.sketch /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build/ 3 | .swiftpm/ 4 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - statement_position 3 | - valid_ibinspectable 4 | - fallthrough 5 | - explicit_enum_raw_value 6 | - nesting 7 | - closure_parameter_position 8 | opt_in_rules: 9 | - private_action 10 | - private_outlet 11 | - redundant_nil_coalescing 12 | - attributes 13 | - closure_end_indentation 14 | - closure_spacing 15 | - empty_count 16 | - explicit_enum_raw_value 17 | - force_unwrapping 18 | - implicit_return 19 | - literal_expression_end_indentation 20 | - unneeded_parentheses_in_closure_argument 21 | - explicit_init 22 | - redundant_type_annotation 23 | - strong_iboutlet 24 | - unused_import 25 | 26 | identifier_name: 27 | min_length: 1 28 | 29 | line_length: 180 30 | 31 | custom_rules: 32 | no_bylines: 33 | name: "Bylines" # rule name. optional. 34 | regex: "([Cc]reated by)" # matching pattern 35 | match_kinds: # SyntaxKinds to match. optional. 36 | - comment 37 | message: "No bylines allowed in headers" # violation message. optional. 38 | severity: warning # violation severity. optional. 39 | 40 | excluded: # paths to ignore during linting. Takes precedence over `included`. 41 | - Carthage 42 | - NewsTests 43 | - Pods 44 | - News/cpc-dependencies 45 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Melvin Rivera 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DateHelper", 8 | platforms: [ 9 | .iOS(.v11), 10 | .macOS(.v10_13), 11 | .tvOS(.v12), 12 | .watchOS(.v4) 13 | ], 14 | products: [ 15 | .library( 16 | name: "DateHelper", 17 | targets: ["DateHelper"]), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "DateHelper", 24 | dependencies: []), 25 | .testTarget( 26 | name: "DateHelperTests", 27 | dependencies: ["DateHelper"]), 28 | ], 29 | swiftLanguageVersions: [.v5] 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DateHelper 5.0.1 2 | 3 | [![License](https://img.shields.io/badge/License-MIT-lightgrey)](https://github.com/melvitax/DateHelper/blob/master/LICENSE) 4 | [![Platform](https://img.shields.io/badge/Platform-iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20macOS-lightgrey)](https://github.com/melvitax/DateHelper) 5 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-Compatible-green)](https://github.com/Carthage/Carthage) 6 | [![Swift Package Manager Compatible](https://img.shields.io/badge/Swift%20Package%20Manager-Compatible-green)](https://swift.org/package-manager/) 7 | 8 | ![Sample Project Screenshot](https://raw.githubusercontent.com/melvitax/DateHelper/master/logo.png "Date Helper") 9 | 10 | A high performant Swift Date Extension for creating, comparing, or modifying dates. 11 | 12 | 13 | ## Capabilities 14 | - **Date from String** 15 | - Using date detection i.e. `"Tomorrow at 5:30 PM"` 16 | - With predefined format: i.e. `.isoDateTime` 17 | - With custom format: i.e. `"dd MMM yyyy HH:mm:ss"` 18 | - **String from Date** 19 | - With predefined format: i.e. `.rss` 20 | - With custom format: i.e. `""MMM d, yyyy""` 21 | - With combined date and time style: i.e. `.medium` 22 | - With individual date and time style: i.e. `.medium, .short` 23 | - **Modify Date** 24 | - Offset date component: i.e. `.offset(.second, value: 110)` 25 | - Adjust date component: i.e. `.adjust(hour: 12, minute: 0, second: 0)` 26 | - Adjust date to a predefined time: i.e. `.adjust(for: .startOfDay)` 27 | - **Compare Date** 28 | - Compare against relative date in predefined format: i.e. `.isToday, .isThisWeek` 29 | - Compare againnst target date: i.e. `firstDate.compare(.isSameMonth(as: secondDate))` 30 | - **Time Since** 31 | - Time since target date in component: i.e. `Date().since(secondDate, in: .second)` 32 | - **Extras** 33 | - Extract date and time components: i.e. `.hour, .minute, .day` 34 | - Conveniance methods: i.e. `numberOfDaysInMonth(), firstDayOfWeek(), .lastDayOfWeek()` 35 | 36 | ## Date From String 37 | Provides initializers to create a Date from a String 38 | 39 | 40 | ### Detect a Date from natural language in a String 41 | 42 | ```swift 43 | Date(detectFromString: "Tomorrow at 5:30 PM") 44 | ``` 45 | Uses NSDataDetector to detect a date from natural language in a string. It works similar to how Apple Mail detects dates. This initializer is not as efficient as **fromString:format:** and should not be used in collections or lists. 46 | 47 | 48 | ### Date from a string with predefined format 49 | 50 | ```swift 51 | Date(fromString: "2009", format: .isoYear) 52 | Date(fromString: "2009-08", format: .isoYearMonth) 53 | Date(fromString: "2009-08-11", format: .isoDate) 54 | Date(fromString: "2009-08-11T06:00:00-07:00", format: .isoDateTime) 55 | Date(fromString: "2009-08-11T06:00:00.000-07:00", format: .isoDateTimeFull) 56 | Date(fromString: "/Date(1260123281843)/", format: .dotNet) 57 | Date(fromString: "Fri, 09 Sep 2011 15:26:08 +0200", format: .rss) 58 | Date(fromString: "09 Sep 2011 15:26:08 +0200", format: .altRSS) 59 | Date(fromString: "Wed, 01 03 2017 06:43:19 -0500", format: .httpHeader) 60 | ``` 61 | Highly performant, cached and thread safe. Can optionally receive timeZone, locale or isLenient flag. 62 | 63 | ### Date from a string with custom format 64 | 65 | ```swift 66 | Date(fromString: "16 July 1972 6:12:00", format: .custom("dd MMM yyyy HH:mm:ss")) 67 | ``` 68 | Highly performant, cached and thread safe. Can optionally receive timeZone, locale or isLenient flag. 69 | 70 | ## String From Date 71 | Provides three ways to convert a Date object to a String 72 | 73 | 74 | ### Convert Date to String using predefined format 75 | 76 | ```swift 77 | Date().toString(format: .isoYear) 78 | "2017" 79 | Date().toString(format: .isoYearMonth) 80 | "2017-03" 81 | Date().toString(format: .isoDate) 82 | "2017-03-01" 83 | Date().toString(format: .isoDateTime) 84 | "2017-03-01T06:43:19-05:00" 85 | Date().toString(format: .isoDateTimeFull) 86 | "2017-03-01T06:43:19.000-05:00" 87 | Date().toString(format: .dotNet) 88 | "/Date(-51488368599000.000000)/" 89 | Date().toString(format: .rss) 90 | "Wed, 1 Mar 2017 06:43:19 -0500" 91 | Date().toString(format: .altRSS) 92 | "1 Mar 2017 06:43:19 -0500" 93 | Date().toString(format: .httpHeader) 94 | "Wed, 01 03 2017 06:43:19 -0500" 95 | ``` 96 | Highly performant, cached and thread safe. Can optionally receive timeZone and locale. 97 | 98 | ### Convert Date to String using custom format 99 | 100 | ```swift 101 | Date().toString(format: .custom("MMM d, yyyy")) 102 | "Mar 1, 2017" 103 | Date().toString(format: .custom("h:mm a")) 104 | "6:43 AM" 105 | Date().toString(format: .custom("MMM d")) 106 | "Wed Mar 1" 107 | ``` 108 | Highly performant, cached and thread safe. Can optionally receive timeZone and locale. 109 | 110 | ### Convert Date to String using predefined combined date and time styles 111 | ```swift 112 | Date().toString(style: .short) 113 | "3/1/17, 6:43 AM" 114 | Date().toString(style: .medium) 115 | "Mar 1, 2017 at 6:43:19 AM" 116 | Date().toString(style: .long) 117 | "March 1, 2017 at 6:43:19 AM EST" 118 | Date().toString(style: .full) 119 | "Wednesday, March 1, 2017 at 6:43:19 AM Eastern Standard Time" 120 | Date().toString(style: .ordinalDay) 121 | "1st" 122 | Date().toString(style: .weekday) 123 | "Wednesday" 124 | Date().toString(style: .shortWeekday) 125 | "Wed" 126 | Date().toString(style: .veryShortWeekday) 127 | "W" 128 | Date().toString(style: .month) 129 | "April" 130 | Date().toString(style: .shortMonth) 131 | "Apr" 132 | Date().toString(style: .veryShortMonth) 133 | "A" 134 | ``` 135 | 136 | ### Convert Date to String using predefined individual date and time styles 137 | 138 | ```swift 139 | Date().toString(dateStyle: .none, timeStyle: .short) 140 | "6:43 AM" 141 | Date().toString(dateStyle: .short, timeStyle: .none) 142 | "3/1/17" 143 | Date().toString(dateStyle: .short, timeStyle: .short) 144 | "3/1/17, 6:43 AM" 145 | Date().toString(dateStyle: .medium, timeStyle: .medium) 146 | "Mar 1, 2017 at 6:43:19 AM" 147 | Date().toString(dateStyle: .long, timeStyle: .long) 148 | "March 1, 2017 at 6:43:19 AM EST" 149 | Date().toString(dateStyle: .full, timeStyle: .full) 150 | "Wednesday, March 1, 2017 at 6:43:19 AM Eastern Standard Time" 151 | ``` 152 | 153 | ## Modifying dates 154 | Provides functions for adjusting or shifting dates 155 | 156 | ### Offset components 157 | 158 | ```swift 159 | Date().offset(.second, value: 10) 160 | "18:14:41" -> "18:14:51" 161 | Date().offset(.minute, value: 10) 162 | "18:14:41" -> "18:24:41" 163 | Date().offset(.hour, value: 2) 164 | "18:14:41" -> "20:14:41" 165 | Date().offset(.day, value: 1) 166 | "2009-12-06" -> "2009-12-07" 167 | Date().offset(.weekday, value: 2) 168 | "2009-12-06" -> "2009-16-06" 169 | Date().offset(.weekdayOrdinal, value: 1) 170 | "2009-12-06" -> "2009-12-20" 171 | Date().offset(.week, value: -2) 172 | "2009-12-06" -> "2009-11-22" 173 | Date().offset(.month, value: 2) 174 | "2009-12-06" -> "2010-02-06" 175 | Date().offset(.year, value: -2) 176 | "2009-12-06" -> "2007-12-06" 177 | ``` 178 | 179 | ### Adjust components 180 | Modifies date with the specified date component 181 | 182 | ```swift 183 | Date().adjust(hour: 1, minute: 10, second: 30, day: 15, month: 1) 184 | "2009-12-06 18:14:41" -> "2009-01-15 06:10:30" 185 | Date().adjust(minute: 59) 186 | "2009-12-06 18:14:41" -> "2009-12-06 18:59:30" 187 | ``` 188 | 189 | ### Adjust date 190 | Modifies date with predefined times like endOfDay, startOfDay startOfWeek etc. 191 | 192 | ```swift 193 | Date().adjust(for: .startOfDay) 194 | "2009-12-06 18:14:41" -> "2009-12-06 00:00:00" 195 | Date().adjust(for: .endOfDay) 196 | "2009-12-06 18:14:41" -> "2009-12-06 23-59-59" 197 | Date().adjust(for: .startOfWeek) 198 | "2009-12-08 18:14:41" -> "2009-12-06 18:14:41" 199 | Date().adjust(for: .endOfWeek) 200 | "2009-12-06 18:14:41" -> "2009-12-12 18:14:41" 201 | Date().adjust(for: .startOfMonth) 202 | "2009-12-06 18:14:41" -> "2009-12-01 18:14:41" 203 | Date().adjust(for: .endOfMonth) 204 | "2009-12-06 18:14:41" -> "2009-12-31 18:14:41" 205 | Date().adjust(for: .tomorrow) 206 | "2009-12-06 18:14:41" -> "tomorrow at 18:14:41" 207 | Date().adjust(for: .yesterday) 208 | "2009-12-06 18:14:41" -> "yesterday at 18:14:41" 209 | Date().adjust(for: .nearestMinute(minute:30)) 210 | "2009-12-07 18-14-00" -> "2009-12-07 18-00-00" 211 | "2009-12-07 18-40-00" -> "2009-12-07 18-30-00" 212 | "2009-12-07 18-50-00" -> "2009-12-07 19-00-00" 213 | Date().adjust(for: .nearestHour(hour:2)) 214 | "2009-12-07 18-00-00" -> "2009-12-08 00-00-00" 215 | "2009-12-07 07-00-00" -> "2009-12-07 12-00-00" 216 | "2009-12-07 03-00-00" -> "2009-12-07 00-00-00" 217 | Date().adjust(for: .startOfYear) 218 | "2009-12-06 18:14:41" -> "2009-01-01 00-00-00" 219 | Date().adjust(for: .endOfYear) 220 | "2009-12-06 18:14:41" -> "2009-12-31 23-59-59" 221 | ``` 222 | 223 | ## Compare Dates 224 | Compares dates using predefined times like today, tomorrow, this year, next year etc. Returns true if it matches. 225 | 226 | ### Compare against relative date 227 | ```swift 228 | Date().compare(.isToday) 229 | Date().compare(.isTomorrow) 230 | Date().compare(.isYesterday) 231 | Date().compare(.isThisWeek) 232 | Date().compare(.isNextWeek) 233 | Date().compare(.isLastWeek) 234 | Date().compare(.isThisYear) 235 | Date().compare(.isNextYear) 236 | Date().compare(.isLastYear) 237 | Date().compare(.isInTheFuture) 238 | Date().compare(.isInThePast) 239 | Date().compare(.isWeekend) 240 | "2021-12-15" != weekend 241 | "2021-12-18" == weekend 242 | ``` 243 | 244 | ### Compare against another date 245 | ```swift 246 | firstDate.compare(.isSameDay(as: secondDate)) 247 | "2022-01-08" != "2022-01-07" 248 | "2022-01-06" != "2022-01-07" 249 | "2022-01-07" == "2022-01-07" 250 | firstDate.compare(.isSameWeek(as: secondDate)) 251 | "2022-01-14" != "2022-01-07" 252 | "2021-12-31" != "2022-01-07" 253 | "2022-01-07" == "2022-01-07" 254 | firstDate.compare(.isSameMonth(as: secondDate)) 255 | "2022-02-07" != "2022-01-07" 256 | "2021-12-07" != "2022-01-07" 257 | "2022-01-07" == "2022-01-07" 258 | firstDate.compare(.isSameYear(as: secondDate)) 259 | "2023-01-07" != "2022-01-07" 260 | "2021-01-07" != "2022-01-07" 261 | "2022-01-07" == "2022-01-07" 262 | firstDate.compare(.isEarlier(than: secondDate)) 263 | "2022-01-07 19:26:53" != "2022-01-07 19:25:53" 264 | "2022-01-07 19:24:53" == "2022-01-07 19:25:53" 265 | firstDate.compare(.isLater(than: secondDate)) 266 | "2022-01-07 19:28:49" == "2022-01-07 19:27:49" 267 | "2022-01-07 19:26:49" != "2022-01-07 19:27:49" 268 | ``` 269 | 270 | ## Time since... 271 | Returns a number in the specified unit of measure since the secondary date. 272 | 273 | ```swift 274 | Date().since(secondDate, in: .second) 275 | "2009-12-06 06-14-11" since "2009-12-06 06-13-41" in .second == 30 276 | Date().since(secondDate, in: .minute) 277 | "2009-12-06 06-14-11" since "2009-12-06 04-14-11" in .minute == 120 278 | Date().since(secondDate, in: .hour) 279 | "2009-12-06 06-14-11" since "2009-12-06 04-14-11" in .hour == 2 280 | Date().since(secondDate, in: .day) 281 | "2009-12-06" since "2009-12-05" in .day == 1 282 | Date().since(secondDate, in: .week) 283 | "2009-12-06" since "2009-11-29" in .week == 1 284 | "2009-12-06" since "2009-12-13" in .week == -1 285 | Date().since(secondDate, in: .weekdayOrdinal) 286 | "2009-12-06" since "2009-11-22" in .weekdayOrdinal == 2 287 | Date().since(secondDate, in: .month) 288 | "2009-12-06" since "2009-11-06" in .month == 2 289 | Date().since(secondDate, in: .year) 290 | "2009-12-06" since "2008-12-06" in .year == 1 291 | ``` 292 | 293 | ## Miscellaneous 294 | 295 | ### Extracting components from a date 296 | 297 | ```swift 298 | Date().component(.second) 299 | "2009-12-06 18:14:11" .second == "11" 300 | Date().component(.minute) 301 | "2009-12-06 18:14:11" .minute == "14" 302 | Date().component(.hour) 303 | "2009-12-06 18:14:11" .hour == "18" 304 | Date().component(.day) 305 | "2009-12-06 18:14:11" .day == "6" 306 | Date().component(.weekday) 307 | "2009-12-06 18:14:11" .weekday == "1" 308 | Date().component(.weekdayOrdinal) 309 | "2009-12-06 18:14:11" .weekdayOrdinal == "1" 310 | Date().component(.month) 311 | "2009-12-06 18:14:11" .month == "12" 312 | Date().component(.year) 313 | "2009-12-06 18:14:11" .year == "2009" 314 | ``` 315 | 316 | ### Conveneience methods 317 | 318 | ```swift 319 | Date().numberOfDaysInMonth() 320 | "2021-12-17" numberOfDaysInMonth() == 31 321 | Date().firstDayOfWeek() 322 | "2021-12-17" firstDayOfWeek() == 12 323 | Date().lastDayOfWeek() 324 | "2021-12-17" lastDayOfWeek() == 19 325 | ``` 326 | 327 | ### Custom start day of the week 328 | 329 | ```swift 330 | var calendar = Calendar(identifier: .gregorian) 331 | calendar.firstWeekday = 2 // sets the week to start on Monday 332 | Date().dateFor(.startOfWeek, calendar: calendar) 333 | ``` 334 | 335 | ## Custom Component guide 336 | 337 | **Unicode Date Field Symbol Guide** 338 | 339 | | Format | Description | Example | 340 | | ------------- | ------------- | ------------- | 341 | | "y" | 1 digit min year | 1, 42, 2017 | 342 | | "yy" | 2 digit year | 01, 42, 17 | 343 | | "yyy" | 3 digit min year | 001, 042, 2017 | 344 | | "yyyy" | 4 digit min year | 0001, 0042, 2017 | 345 | | "M" | 1 digit min month | 7, 12 | 346 | | "MM" | 2 digit month | 07, 12 | 347 | | "MMM" | 3 letter month abbr. | Jul, Dec | 348 | | "MMMM" | Full month | July, December | 349 | | "MMMMM" | 1 letter month abbr. | J, D | 350 | | "d" | 1 digit min day | 4, 25 | 351 | | "dd" | 2 digit day | 04, 25 | 352 | | "E", "EE", "EEE" | 3 letter day name abbr. | Wed, Thu | 353 | | "EEEE" | full day name | Wednesday, Thursday | 354 | | "EEEEE" | 1 letter day name abbr. | W, T | 355 | | "EEEEEE" | 2 letter day name abbr. | We, Th | 356 | | "a" | Period of day | AM, PM | 357 | | "h" | AM/PM 1 digit min hour | 5, 7 | 358 | | "hh" | AM/PM 2 digit hour | 05, 07 | 359 | | "H" | 24 hr 1 digit min hour | 17, 7 | 360 | | "HH" | 24 hr 2 digit hour | 17, 07 | 361 | | "m" | 1 digit min minute | 1, 40 | 362 | | "mm" | 2 digit minute | 01, 40 | 363 | | "s" | 1 digit min second | 1, 40 | 364 | | "ss" | 2 digit second | 01, 40 | 365 | | "S" | 10th's place of fractional second | 123ms -> 1, 7ms -> 0 | 366 | | "SS" | 10th's & 100th's place of fractional second | 123ms -> 12, 7ms -> 00 | 367 | | "SSS" | 10th's & 100th's & 1,000's place of fractional second | 123ms -> 123, 7ms -> 007 | 368 | 369 | ## Requirements 370 | 371 | Language: Swift 5.0 372 | Supports: iOS, tvOS, watchOS, macOS 373 | 374 | 375 | ## Installation 376 | 377 | **Swift Package Manager** https://github.com/melvitax/DateHelper.git 378 | **Carthage** github "melvitax/DateHelper" 379 | **Manually** Include DateHelper.swift in your project 380 | 381 | 382 | ## Author 383 | 384 | Melvin Rivera 385 | 386 | ## License 387 | 388 | DateHelper is available under the MIT license. See the LICENSE file for more info. 389 | -------------------------------------------------------------------------------- /Sources/DateHelper/DateHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateHelper.swift 3 | // Version: 5.0.1 4 | // https://github.com/melvitax/DateHelper 5 | // 6 | 7 | import Foundation 8 | 9 | // swiftlint:disable file_length 10 | public extension Date { 11 | 12 | // MARK: - Date from String 13 | 14 | /// Creates a new Date from on a string with predefined or custom string format. Supports optional timezone and locale. 15 | init?(fromString string: String, format: DateFormatType, timeZone: TimeZoneType = .local, locale: Locale = Foundation.Locale.current, isLenient: Bool = true) { 16 | 17 | guard !string.isEmpty else { return nil } 18 | 19 | var string = string 20 | 21 | switch format { 22 | case .dotNet: 23 | let pattern = "\\\\?/Date\\((\\d+)(([+-]\\d{2})(\\d{2}))?\\)\\\\?/" 24 | let regex = try? NSRegularExpression(pattern: pattern) 25 | let range = NSRange(location: 0, length: string.utf16.count) 26 | if let match = regex?.firstMatch(in: string, range: range) { 27 | let dateString = (string as NSString).substring(with: match.range(at: 1)) 28 | if let dateNumber = Double(dateString) { 29 | let interval = dateNumber / 1000.0 30 | self.init(timeIntervalSince1970: interval) 31 | return 32 | } 33 | } 34 | return nil 35 | case .rss, .altRSS: 36 | if string.hasSuffix("Z") { 37 | string = string[.. String? { 76 | switch style { 77 | case .short, .medium, .long, .full: 78 | return self.toString(dateStyle: style.formatterStyle, timeStyle: style.formatterStyle, isRelative: false) 79 | case .ordinalDay: 80 | let formatter = Date.cachedDateFormatters.cachedNumberFormatter() 81 | formatter?.numberStyle = .ordinal 82 | if let day = component(.day) as NSNumber? { 83 | return formatter?.string(from: day) 84 | } 85 | default: 86 | if let symbols = Date.cachedDateFormatters.cachedFormatter()?.symbols(for: style), 87 | let weekday = component(.weekday) { 88 | return symbols[weekday-1] as String 89 | } 90 | } 91 | return nil 92 | } 93 | 94 | /// Converts the date to string based on a date format, optional timezone and optional locale. 95 | func toString(format: DateFormatType, timeZone: TimeZoneType = .local, locale: Locale = Locale.current) -> String? { 96 | switch format { 97 | case .dotNet: 98 | let offset = Foundation.NSTimeZone.default.secondsFromGMT() / 3600 99 | let nowMillis = 1000 * self.timeIntervalSince1970 100 | return String(format: format.stringFormat, nowMillis, offset) 101 | case .isoDateTimeFull, .isoDateTime, .isoYear, .isoDate, .isoYearMonth: 102 | let formatter = Date.cachedDateFormatters.cachedISOFormatter(format, timeZone: timeZone, locale: locale) 103 | return formatter?.string(from: self) 104 | default: 105 | break 106 | } 107 | if let zone = timeZone.timeZone { 108 | let formatter = Date.cachedDateFormatters.cachedFormatter(format.stringFormat, timeZone: zone, locale: locale) 109 | return formatter?.string(from: self) 110 | } 111 | return nil 112 | } 113 | 114 | /// Converts the date to string based on a date style and time style with optional relative date formatting, optional time zone and optional locale. 115 | // swiftlint:disable:next line_length 116 | func toString(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style, isRelative: Bool = false, timeZone: Foundation.TimeZone = Foundation.NSTimeZone.local, locale: Locale = Locale.current) -> String? { 117 | let formatter = Date.cachedDateFormatters.cachedFormatter(dateStyle, timeStyle: timeStyle, doesRelativeDateFormatting: isRelative, timeZone: timeZone, locale: locale) 118 | return formatter?.string(from: self) 119 | } 120 | 121 | /// Converts date to string based on a relative time language. i.e. just now, 1 minute ago etc... 122 | // swiftlint:disable:next cyclomatic_complexity 123 | func toStringWithRelativeTime(customStrings: [RelativeTimeType: String]? = nil) -> String? { 124 | let time = self.timeIntervalSince1970 125 | let now = Date().timeIntervalSince1970 126 | let seconds: Double = abs(now - time) 127 | let minutes: Double = round(seconds/60) 128 | let hours: Double = round(minutes/60) 129 | let days: Double = round(hours/24) 130 | let relativeTimeType = toRelativeTime() 131 | let format = customStrings?[relativeTimeType] ?? relativeTimeType.defaultFormat 132 | switch relativeTimeType { 133 | case .secondsPast, .secondsFuture: 134 | return String(format: format, seconds) 135 | case .minutesPast, .minutesFuture: 136 | return String(format: format, minutes) 137 | case .hoursPast, .hoursFuture: 138 | return String(format: format, hours) 139 | case .daysPast, .daysFuture: 140 | return String(format: format, days) 141 | case .weeksPast, .weeksFuture: 142 | if let since = since(Date(), in: .week) { 143 | let w = Double(abs(since)) 144 | return String(format: format, w) 145 | } 146 | case .monthsPast, .monthsFuture: 147 | if let since = since(Date(), in: .month) { 148 | let m = Double(abs(since)) 149 | return String(format: format, m) 150 | } 151 | case .yearsPast, .yearsFuture: 152 | if let since = since(Date(), in: .year) { 153 | let y = Double(abs(since)) 154 | return String(format: format, y) 155 | } 156 | default: 157 | return format 158 | } 159 | return nil 160 | } 161 | 162 | /// Converts the date to a relative time language. i.e. .nowPast, .minutesFuture 163 | // swiftlint:disable:next cyclomatic_complexity 164 | private func toRelativeTime() -> RelativeTimeType { 165 | let time = self.timeIntervalSince1970 166 | let now = Date().timeIntervalSince1970 167 | let isPast = now - time > 0 168 | let seconds: Double = abs(now - time) 169 | 170 | switch seconds { 171 | case let s where s <= DateComponentType.second.inSeconds*10: 172 | return isPast ? .nowPast : .nowFuture 173 | case let s where s < DateComponentType.minute.inSeconds: 174 | return isPast ? .secondsPast : .secondsFuture 175 | case let s where s < DateComponentType.minute.inSeconds*2: 176 | return isPast ? .oneMinutePast : .oneMinuteFuture 177 | case let s where s < DateComponentType.hour.inSeconds: 178 | return isPast ? .minutesPast : .minutesFuture 179 | case let s where s < DateComponentType.hour.inSeconds*2: 180 | return isPast ? .oneHourPast : .oneHourFuture 181 | case let s where s < DateComponentType.day.inSeconds: 182 | return isPast ? .hoursPast : .hoursFuture 183 | case let s where s < DateComponentType.day.inSeconds*2: 184 | return isPast ? .oneDayPast : .oneDayFuture 185 | case let s where s < DateComponentType.day.inSeconds*7: 186 | return isPast ? .daysPast : .daysFuture 187 | case let s where s < DateComponentType.day.inSeconds*14: 188 | return isPast ? .oneWeekPast : .oneWeekFuture 189 | case let s where s < DateComponentType.month.inSeconds: 190 | if isPast { 191 | return compare(.isLastWeek) ? .oneWeekPast : .weeksPast 192 | } else { 193 | return compare(.isNextWeek) ? .oneWeekFuture : .weeksFuture 194 | } 195 | default: 196 | if compare(.isThisYear) { 197 | if isPast { 198 | return compare(.isLastMonth) ? .oneMonthPast : .monthsPast 199 | } else { 200 | return compare(.isNextMonth) ? .oneMonthFuture : .monthsFuture 201 | } 202 | } else { 203 | if isPast { 204 | return compare(.isLastYear) ? .oneYearPast : .yearsPast 205 | } else { 206 | return compare(.isNextYear) ? .oneYearFuture : .yearsFuture 207 | } 208 | } 209 | } 210 | } 211 | 212 | // MARK: - Compare Dates 213 | 214 | /// Compares dates to see if they are equal while ignoring time. 215 | // swiftlint:disable:next cyclomatic_complexity function_body_length 216 | func compare(_ comparison: DateComparisonType) -> Bool { 217 | switch comparison { 218 | case .isToday: 219 | return compare(.isSameDay(as: Date())) 220 | case .isYesterday: 221 | if let comparison = Date().offset(.day, value: -1) { 222 | return compare(.isSameDay(as: comparison)) 223 | } 224 | case .isTomorrow: 225 | if let comparison = Date().offset(.day, value: 1) { 226 | return compare(.isSameDay(as: comparison)) 227 | } 228 | case .isSameDay(let date): 229 | return component(.year) == date.component(.year) && component(.month) == date.component(.month) && component(.day) == date.component(.day) 230 | case .isThisWeek: 231 | return self.compare(.isSameWeek(as: Date())) 232 | case .isLastWeek: 233 | if let comparison = Date().offset(.week, value: -1) { 234 | return compare(.isSameWeek(as: comparison)) 235 | } 236 | case .isNextWeek: 237 | if let comparison = Date().offset(.week, value: 1) { 238 | return compare(.isSameWeek(as: comparison)) 239 | } 240 | case .isSameWeek(let date): 241 | if component(.week) != date.component(.week) { 242 | return false 243 | } 244 | // Ensure time interval is under 1 week 245 | return abs(self.timeIntervalSince(date)) < DateComponentType.week.inSeconds 246 | case .isThisMonth: 247 | return self.compare(.isSameMonth(as: Date())) 248 | case .isLastMonth: 249 | if let comparison = Date().offset(.month, value: -1) { 250 | return compare(.isSameMonth(as: comparison)) 251 | } 252 | case .isNextMonth: 253 | if let comparison = Date().offset(.month, value: 1) { 254 | return compare(.isSameMonth(as: comparison)) 255 | } 256 | case .isSameMonth(let date): 257 | return component(.year) == date.component(.year) && component(.month) == date.component(.month) 258 | case .isThisYear: 259 | return self.compare(.isSameYear(as: Date())) 260 | case .isLastYear: 261 | if let comparison = Date().offset(.year, value: -1) { 262 | return compare(.isSameYear(as: comparison)) 263 | } 264 | case .isNextYear: 265 | if let comparison = Date().offset(.year, value: 1) { 266 | return compare(.isSameYear(as: comparison)) 267 | } 268 | case .isSameYear(let date): 269 | return component(.year) == date.component(.year) 270 | case .isInTheFuture: 271 | return self.compare(.isLater(than: Date())) 272 | case .isInThePast: 273 | return self.compare(.isEarlier(than: Date())) 274 | case .isEarlier(let date): 275 | return timeIntervalSince1970 < date.timeIntervalSince1970 276 | case .isLater(let date): 277 | return timeIntervalSince1970 > date.timeIntervalSince1970 278 | case .isWeekday: 279 | return !compare(.isWeekend) 280 | case .isWeekend: 281 | if let range = Calendar.current.maximumRange(of: Calendar.Component.weekday) { 282 | return (component(.weekday) == range.lowerBound || component(.weekday) == range.upperBound - range.lowerBound) 283 | } 284 | } 285 | return false 286 | } 287 | 288 | // MARK: - Adjust dates 289 | 290 | /// Offsets a date component with a +/- value 291 | func offset(_ component: DateComponentType, value: Int) -> Date? { 292 | var dateComp = DateComponents() 293 | switch component { 294 | case .second: 295 | dateComp.second = value 296 | case .minute: 297 | dateComp.minute = value 298 | case .hour: 299 | dateComp.hour = value 300 | case .day: 301 | dateComp.day = value 302 | case .weekday: 303 | dateComp.weekday = value 304 | case .weekdayOrdinal: 305 | dateComp.weekdayOrdinal = value 306 | case .week: 307 | dateComp.weekOfYear = value 308 | case .month: 309 | dateComp.month = value 310 | case .year: 311 | dateComp.year = value 312 | } 313 | return Calendar.current.date(byAdding: dateComp, to: self) 314 | } 315 | 316 | /// Sets a new value to the specified component and returns as a new date 317 | func adjust(hour: Int? = nil, minute: Int? = nil, second: Int? = nil, nanosecond: Int? = nil, day: Int? = nil, month: Int? = nil) -> Date? { 318 | var comp = Date.components(self) 319 | comp.month = month ?? comp.month 320 | comp.day = day ?? comp.day 321 | comp.hour = hour ?? comp.hour 322 | comp.minute = minute ?? comp.minute 323 | comp.second = second ?? comp.second 324 | comp.nanosecond = nanosecond ?? comp.nanosecond 325 | return Calendar.current.date(from: comp) 326 | } 327 | 328 | // MARK: - Date for... 329 | 330 | // swiftlint:disable:next cyclomatic_complexity 331 | func adjust(for type: DateAdjustmentType, calendar: Calendar = Calendar.current) -> Date? { 332 | switch type { 333 | case .startOfDay: 334 | return adjust(hour: 0, minute: 0, second: 0) 335 | case .endOfDay: 336 | return adjust(hour: 23, minute: 59, second: 59, nanosecond: 999_000_000) // 23:59:59:999 337 | case .startOfWeek: 338 | return calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear, .hour, .minute, .second, .nanosecond], from: self)) 339 | case .endOfWeek: 340 | let weekStart = self.adjust(for: .startOfWeek, calendar: calendar) 341 | return weekStart?.offset(.day, value: 6) 342 | case .startOfMonth: 343 | return adjust(day: 1) 344 | case .endOfMonth: 345 | return adjust(day: numberOfDaysInMonth()) 346 | case .tomorrow: 347 | let tomorrow = Date().offset(.day, value: 1) 348 | var components = Date.components(self) 349 | components.year = tomorrow?.component(.year) 350 | components.month = tomorrow?.component(.month) 351 | components.day = tomorrow?.component(.day) 352 | return Calendar.current.date(from: components) 353 | case .yesterday: 354 | let tomorrow = Date().offset(.day, value: -1) 355 | var components = Date.components(self) 356 | components.year = tomorrow?.component(.year) 357 | components.month = tomorrow?.component(.month) 358 | components.day = tomorrow?.component(.day) 359 | return Calendar.current.date(from: components) 360 | case .nearestMinute(let minute): 361 | if let component = component(.minute) { 362 | let value = max(min(minute, 60), 0) 363 | let minutes = (component + value/2) / value * value 364 | return adjust(hour: nil, minute: minutes, second: nil) 365 | } 366 | case .nearestHour(let hour): 367 | if let component = component(.hour) { 368 | let value = max(min(hour, 24), 0) 369 | let hours = (component + value/2) / value * value 370 | return adjust(hour: hours, minute: 0, second: nil) 371 | } 372 | case .startOfYear: 373 | if let component = component(.year) { 374 | let date = Date(fromString: "\(component)-01-01", format: .isoDate) 375 | return date?.adjust(hour: 0, minute: 0, second: 0) 376 | } 377 | case .endOfYear: 378 | if let component = component(.year) { 379 | let date = Date(fromString: "\(component)-12-31", format: .isoDate) 380 | return date?.adjust(hour: 23, minute: 59, second: 59) 381 | } 382 | } 383 | return nil 384 | } 385 | 386 | // MARK: - Time since... 387 | 388 | func since(_ date: Date, in component: DateComponentType) -> Int64? { 389 | let calendar = Calendar.current 390 | switch component { 391 | case .second, .minute, .hour: 392 | let interval = timeIntervalSince(date) 393 | return Int64(interval / component.inSeconds) 394 | case .day, .weekday, .weekdayOrdinal, .week, .month, .year: 395 | if let end = calendar.ordinality(of: component.calendarComponent, in: .era, for: self), 396 | let start = calendar.ordinality(of: component.calendarComponent, in: .era, for: date) { 397 | return Int64(end - start) 398 | } 399 | } 400 | return nil 401 | } 402 | 403 | // MARK: - Extracting components 404 | 405 | func component(_ component: DateComponentType) -> Int? { 406 | let components = Date.components(self) 407 | switch component { 408 | case .second: 409 | return components.second 410 | case .minute: 411 | return components.minute 412 | case .hour: 413 | return components.hour 414 | case .day: 415 | return components.day 416 | case .weekday: 417 | return components.weekday 418 | case .weekdayOrdinal: 419 | return components.weekdayOrdinal 420 | case .week: 421 | return components.weekOfYear 422 | case .month: 423 | return components.month 424 | case .year: 425 | return components.year 426 | } 427 | } 428 | 429 | func numberOfDaysInMonth() -> Int? { 430 | let day = Calendar.Component.day 431 | let month = Calendar.Component.month 432 | if let range = Calendar.current.range(of: day, in: month, for: self) { 433 | return range.upperBound - range.lowerBound 434 | } 435 | return nil 436 | } 437 | 438 | func firstDayOfWeek() -> Int? { 439 | guard let component = component(.weekday) else { return nil } 440 | let aDay = DateComponentType.day.inSeconds 441 | let startOfWeekOffset = aDay * Double(component - 1) 442 | let interval: TimeInterval = timeIntervalSinceReferenceDate - startOfWeekOffset 443 | return Date(timeIntervalSinceReferenceDate: interval).component(.day) 444 | } 445 | 446 | func lastDayOfWeek() -> Int? { 447 | guard let component = component(.weekday) else { return nil } 448 | let aDay = DateComponentType.day.inSeconds 449 | let tartOfWeekOffset = aDay * Double(component - 1) 450 | let endOfWeekOffset = aDay * Double(7) 451 | let interval: TimeInterval = timeIntervalSinceReferenceDate - tartOfWeekOffset + endOfWeekOffset 452 | return Date(timeIntervalSinceReferenceDate: interval).component(.day) 453 | } 454 | 455 | // MARK: - Internal Components 456 | 457 | internal static func componentFlags() -> Set { 458 | [.year, .month, .day, .weekOfYear, .hour, .minute, .second, .weekday, .weekdayOrdinal, .weekOfYear] 459 | } 460 | internal static func components(_ fromDate: Date) -> DateComponents { 461 | Calendar.current.dateComponents(Date.componentFlags(), from: fromDate) 462 | } 463 | 464 | // MARK: Formatter 465 | 466 | private class ConcurrentFormatterCache { 467 | 468 | private static let cachedISODateFormattersQueue = DispatchQueue( 469 | label: "iso-date-formatter-queue", 470 | attributes: .concurrent 471 | ) 472 | private static let cachedDateFormattersQueue = DispatchQueue( 473 | label: "date-formatter-queue", 474 | attributes: .concurrent 475 | ) 476 | 477 | private static let cachedNumberFormatterQueue = DispatchQueue( 478 | label: "number-formatter-queue", 479 | attributes: .concurrent 480 | ) 481 | 482 | private static var cachedISODateFormatters = [String: ISO8601DateFormatter]() 483 | private static var cachedDateFormatters = [String: DateFormatter]() 484 | private static var cachedNumberFormatter = NumberFormatter() 485 | 486 | private func register(hashKey: String, formatter: ISO8601DateFormatter) { 487 | ConcurrentFormatterCache.cachedISODateFormattersQueue.async(flags: .barrier) { 488 | ConcurrentFormatterCache.cachedISODateFormatters.updateValue(formatter, forKey: hashKey) 489 | } 490 | } 491 | 492 | private func register(hashKey: String, formatter: DateFormatter) { 493 | ConcurrentFormatterCache.cachedDateFormattersQueue.async(flags: .barrier) { 494 | ConcurrentFormatterCache.cachedDateFormatters.updateValue(formatter, forKey: hashKey) 495 | } 496 | } 497 | 498 | private func retrieve(hashKeyForISO hashKey: String) -> ISO8601DateFormatter? { 499 | let dateFormatter = ConcurrentFormatterCache.cachedISODateFormattersQueue.sync { () -> ISO8601DateFormatter? in 500 | guard let result = ConcurrentFormatterCache.cachedISODateFormatters[hashKey] else { return nil } 501 | return result.copy() as? ISO8601DateFormatter 502 | } 503 | return dateFormatter 504 | } 505 | 506 | private func retrieve(hashKey: String) -> DateFormatter? { 507 | let dateFormatter = ConcurrentFormatterCache.cachedDateFormattersQueue.sync { () -> DateFormatter? in 508 | guard let result = ConcurrentFormatterCache.cachedDateFormatters[hashKey] else { return nil } 509 | return result.copy() as? DateFormatter 510 | } 511 | return dateFormatter 512 | } 513 | 514 | private func retrieve() -> NumberFormatter? { 515 | let numberFormatter = ConcurrentFormatterCache.cachedNumberFormatterQueue.sync { () -> NumberFormatter? in 516 | if let formatter = ConcurrentFormatterCache.cachedNumberFormatter.copy() as? NumberFormatter { 517 | return formatter 518 | } 519 | return nil 520 | } 521 | return numberFormatter 522 | } 523 | 524 | public func cachedISOFormatter(_ format: DateFormatType, timeZone: TimeZoneType, locale: Locale) -> ISO8601DateFormatter? { 525 | 526 | let hashKey = "\(format.stringFormat.hashValue)\(timeZone.timeZone.hashValue)\(locale.hashValue)" 527 | 528 | if Date.cachedDateFormatters.retrieve(hashKeyForISO: hashKey) == nil { 529 | let formatter = ISO8601DateFormatter() 530 | formatter.timeZone = timeZone.timeZone 531 | 532 | var options: ISO8601DateFormatter.Options = [] 533 | switch format { 534 | case .isoYear: 535 | options = [.withYear, .withFractionalSeconds] 536 | case .isoYearMonth: 537 | options = [.withYear, .withMonth, .withDashSeparatorInDate] 538 | case .isoDate: 539 | options = [.withFullDate] 540 | case .isoDateTime: 541 | options = [.withInternetDateTime] 542 | case .isoDateTimeFull: 543 | options = [.withInternetDateTime, .withFractionalSeconds] 544 | default: 545 | fatalError("Unimplemented format \(format)") 546 | } 547 | formatter.formatOptions = options 548 | Date.cachedDateFormatters.register(hashKey: hashKey, formatter: formatter) 549 | } 550 | return Date.cachedDateFormatters.retrieve(hashKeyForISO: hashKey) 551 | } 552 | 553 | public func cachedFormatter(_ format: String = DateFormatType.standard.stringFormat, 554 | timeZone: Foundation.TimeZone = Foundation.TimeZone.current, 555 | locale: Locale = Locale.current, isLenient: Bool = true) -> DateFormatter? { 556 | 557 | let hashKey = "\(format.hashValue)\(timeZone.hashValue)\(locale.hashValue)" 558 | 559 | if Date.cachedDateFormatters.retrieve(hashKey: hashKey) == nil { 560 | let formatter = DateFormatter() 561 | formatter.dateFormat = format 562 | formatter.timeZone = timeZone 563 | formatter.locale = locale 564 | formatter.isLenient = isLenient 565 | Date.cachedDateFormatters.register(hashKey: hashKey, formatter: formatter) 566 | } 567 | return Date.cachedDateFormatters.retrieve(hashKey: hashKey) 568 | } 569 | 570 | /// Generates a cached formatter based on the provided date style, time style and relative date. 571 | /// Formatters are cached in a singleton array using hashkeys. 572 | // swiftlint:disable:next line_length 573 | public func cachedFormatter(_ dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style, doesRelativeDateFormatting: Bool, timeZone: Foundation.TimeZone = Foundation.NSTimeZone.local, locale: Locale = Locale.current, isLenient: Bool = true) -> DateFormatter? { 574 | let hashKey = "\(dateStyle.hashValue)\(timeStyle.hashValue)\(doesRelativeDateFormatting.hashValue)\(timeZone.hashValue)\(locale.hashValue)" 575 | if Date.cachedDateFormatters.retrieve(hashKey: hashKey) == nil { 576 | let formatter = DateFormatter() 577 | formatter.dateStyle = dateStyle 578 | formatter.timeStyle = timeStyle 579 | formatter.doesRelativeDateFormatting = doesRelativeDateFormatting 580 | formatter.timeZone = timeZone 581 | formatter.locale = locale 582 | formatter.isLenient = isLenient 583 | Date.cachedDateFormatters.register(hashKey: hashKey, formatter: formatter) 584 | } 585 | return Date.cachedDateFormatters.retrieve(hashKey: hashKey) 586 | } 587 | 588 | public func cachedNumberFormatter() -> NumberFormatter? { 589 | Date.cachedDateFormatters.retrieve() 590 | } 591 | } 592 | 593 | /// A cached static array of DateFormatters so that thy are only created once. 594 | private static var cachedDateFormatters = Self.ConcurrentFormatterCache() 595 | 596 | // MARK: - Enums 597 | 598 | /// The date format type used for string conversion. 599 | enum DateFormatType { 600 | /// The ISO8601 formatted year "yyyy" i.e. 1997 601 | case isoYear 602 | /// The ISO8601 formatted year and month "yyyy-MM" i.e. 1997-07 603 | case isoYearMonth 604 | /// The ISO8601 formatted date "yyyy-MM-dd" i.e. 1997-07-16 605 | case isoDate 606 | /// The ISO8601 formatted date, time and sec "yyyy-MM-dd'T'HH:mm:ssZ" i.e. 1997-07-16T19:20:30+01:00 607 | case isoDateTime 608 | /// The ISO8601 formatted date, time and millisec "yyyy-MM-dd'T'HH:mm:ss.SSSZ" i.e. 1997-07-16T19:20:30.45+01:00 609 | case isoDateTimeFull 610 | /// The dotNet formatted date "/Date(%d%d)/" i.e. "/Date(1268123281843)/" 611 | case dotNet 612 | /// The RSS formatted date "EEE, d MMM yyyy HH:mm:ss ZZZ" i.e. "Fri, 09 Sep 2011 15:26:08 +0200" 613 | case rss 614 | /// The Alternative RSS formatted date "d MMM yyyy HH:mm:ss ZZZ" i.e. "09 Sep 2011 15:26:08 +0200" 615 | case altRSS 616 | /// The http header formatted date "EEE, dd MM yyyy HH:mm:ss ZZZ" i.e. "Tue, 15 Nov 1994 12:45:26 GMT" 617 | case httpHeader 618 | /// A generic standard format date i.e. "EEE MMM dd HH:mm:ss Z yyyy" 619 | case standard 620 | /// A custom date format string 621 | case custom(String) 622 | var stringFormat: String { 623 | switch self { 624 | case .isoYear: return "yyyy" 625 | case .isoYearMonth: return "yyyy-MM" 626 | case .isoDate: return "yyyy-MM-dd" 627 | case .isoDateTime: return "yyyy-MM-dd'T'HH:mm:ssZ" 628 | case .isoDateTimeFull: return "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 629 | case .dotNet: return "/Date(%d%f)/" 630 | case .rss: return "EEE, d MMM yyyy HH:mm:ss ZZZ" 631 | case .altRSS: return "d MMM yyyy HH:mm:ss ZZZ" 632 | case .httpHeader: return "EEE, dd MM yyyy HH:mm:ss ZZZ" 633 | case .standard: return "EEE MMM dd HH:mm:ss Z yyyy" 634 | case .custom(let customFormat): return customFormat 635 | } 636 | } 637 | } 638 | 639 | /// The time zone to be used for date conversion 640 | enum TimeZoneType { 641 | case local, `default`, utc, custom(Int) 642 | var timeZone: TimeZone? { 643 | switch self { 644 | case .local: return NSTimeZone.local 645 | case .default: return NSTimeZone.default 646 | case .utc: return TimeZone(secondsFromGMT: 0) 647 | case let .custom(gmt): return TimeZone(secondsFromGMT: gmt) 648 | } 649 | } 650 | } 651 | 652 | /// The string keys to modify the strings in relative format 653 | enum RelativeTimeType { 654 | // swiftlint:disable:next line_length 655 | /// A string representing the past tense of something recent in less than 10 seconds: "just now" 656 | case nowPast 657 | /// A string representing the future tense of something soon in less than 10 seconds: "in a few seconds" 658 | case nowFuture 659 | /// A formatted string representing the past tense of something less than a minute in seconds: "%.f seconds ago" 660 | case secondsPast 661 | /// A formatted string representing the future tense of something less than a minute in seconds: "in %.f seconds" 662 | case secondsFuture 663 | /// A string representing the past tense of less than 2 minutes: "one minute ago" 664 | case oneMinutePast 665 | /// A string representing the future tense of less than 2 minutes: "in one minute" 666 | case oneMinuteFuture 667 | /// A formatted string representing the past tense of something less than an hour: "%.f minutes ago" 668 | case minutesPast 669 | /// A formatted string representing the future tense of something in less than an hour: "in %.f minutes" 670 | case minutesFuture 671 | /// A string representing the past tense of less than 2 hours: "one hour ago" 672 | case oneHourPast 673 | /// A string representing the future tense of less than 2 hours: "in one hour" 674 | case oneHourFuture 675 | /// A formatted string representing the past tense of something less than a day: "%.f hours ago" 676 | case hoursPast 677 | /// A formatted string representing the future tense of something in less than a day: "in %.f hours" 678 | case hoursFuture 679 | /// A string representing the past tense of less than 2 days: "yesterday" 680 | case oneDayPast 681 | /// A string representing the future tense of less than 2 days: "tomorrow" 682 | case oneDayFuture 683 | /// A formatted string representing the past tense of something less than a week: "%.f days ago" 684 | case daysPast 685 | /// A formatted string representing the future tense of something in less than a week: "in %.f days" 686 | case daysFuture 687 | /// A string representing the past tense of less than 2 weeks: "last week" 688 | case oneWeekPast 689 | /// A string representing the future tense of less than 2 weeks: "next week" 690 | case oneWeekFuture 691 | /// A formatted string representing the past tense of something less than a month: "%.f weeks ago" 692 | case weeksPast 693 | /// A formatted string representing the future tense of something in less than a month: "in %.f weeks" 694 | case weeksFuture 695 | /// A string representing the past tense of less than 2 months: "last month" 696 | case oneMonthPast 697 | /// A string representing the future tense of less than 2 months: "next month" 698 | case oneMonthFuture 699 | /// A formatted string representing the past tense of something less than a year: "%.f months ago" 700 | case monthsPast 701 | /// A formatted string representing the future tense of something in less than a year: "in %.f months" 702 | case monthsFuture 703 | /// A string representing the past tense of less than 2 years: "last year" 704 | case oneYearPast 705 | /// A string representing the future tense of less than 2 years: "next year" 706 | case oneYearFuture 707 | /// A formatted string representing past tense of years: "%.f years ago" 708 | case yearsPast 709 | /// A formatted string representing the future tense of years: "in %.f years" 710 | case yearsFuture 711 | var defaultFormat: String { 712 | switch self { 713 | case .nowPast: 714 | return NSLocalizedString("just now", comment: "Date format") 715 | case .nowFuture: 716 | return NSLocalizedString("in a few seconds", comment: "Date format") 717 | case .secondsPast: 718 | return NSLocalizedString("%.f seconds ago", comment: "Date format") 719 | case .secondsFuture: 720 | return NSLocalizedString("in %.f seconds", comment: "Date format") 721 | case .oneMinutePast: 722 | return NSLocalizedString("1 minute ago", comment: "Date format") 723 | case .oneMinuteFuture: 724 | return NSLocalizedString("in 1 minute", comment: "Date format") 725 | case .minutesPast: 726 | return NSLocalizedString("%.f minutes ago", comment: "Date format") 727 | case .minutesFuture: 728 | return NSLocalizedString("in %.f minutes", comment: "Date format") 729 | case .oneHourPast: 730 | return NSLocalizedString("last hour", comment: "Date format") 731 | case .oneHourFuture: 732 | return NSLocalizedString("next hour", comment: "Date format") 733 | case .hoursPast: 734 | return NSLocalizedString("%.f hours ago", comment: "Date format") 735 | case .hoursFuture: 736 | return NSLocalizedString("in %.f hours", comment: "Date format") 737 | case .oneDayPast: 738 | return NSLocalizedString("yesterday", comment: "Date format") 739 | case .oneDayFuture: 740 | return NSLocalizedString("tomorrow", comment: "Date format") 741 | case .daysPast: 742 | return NSLocalizedString("%.f days ago", comment: "Date format") 743 | case .daysFuture: 744 | return NSLocalizedString("in %.f days", comment: "Date format") 745 | case .oneWeekPast: 746 | return NSLocalizedString("last week", comment: "Date format") 747 | case .oneWeekFuture: 748 | return NSLocalizedString("next week", comment: "Date format") 749 | case .weeksPast: 750 | return NSLocalizedString("%.f weeks ago", comment: "Date format") 751 | case .weeksFuture: 752 | return NSLocalizedString("in %.f weeks", comment: "Date format") 753 | case .oneMonthPast: 754 | return NSLocalizedString("last month", comment: "Date format") 755 | case .oneMonthFuture: 756 | return NSLocalizedString("next month", comment: "Date format") 757 | case .monthsPast: 758 | return NSLocalizedString("%.f months ago", comment: "Date format") 759 | case .monthsFuture: 760 | return NSLocalizedString("in %.f months", comment: "Date format") 761 | case .oneYearPast: 762 | return NSLocalizedString("last year", comment: "Date format") 763 | case .oneYearFuture: 764 | return NSLocalizedString("next year", comment: "Date format") 765 | case .yearsPast: 766 | return NSLocalizedString("%.f years ago", comment: "Date format") 767 | case .yearsFuture: 768 | return NSLocalizedString("in %.f years", comment: "Date format") 769 | } 770 | } 771 | } 772 | 773 | /// The type of comparison to do against today's date or with the suplied date. 774 | enum DateComparisonType { 775 | // Days 776 | /// Checks if date today. 777 | case isToday 778 | /// Checks if date is tomorrow. 779 | case isTomorrow 780 | /// Checks if date is yesterday. 781 | case isYesterday 782 | /// Compares date days 783 | case isSameDay(as: Date) 784 | // Weeks 785 | /// Checks if date is in this week. 786 | case isThisWeek 787 | /// Checks if date is in next week. 788 | case isNextWeek 789 | /// Checks if date is in last week. 790 | case isLastWeek 791 | /// Compares date weeks 792 | case isSameWeek(as: Date) 793 | // Months 794 | /// Checks if date is in this month. 795 | case isThisMonth 796 | /// Checks if date is in next month. 797 | case isNextMonth 798 | /// Checks if date is in last month. 799 | case isLastMonth 800 | /// Compares date months 801 | case isSameMonth(as: Date) 802 | // Years 803 | /// Checks if date is in this year. 804 | case isThisYear 805 | /// Checks if date is in next year. 806 | case isNextYear 807 | /// Checks if date is in last year. 808 | case isLastYear 809 | /// Compare date years 810 | case isSameYear(as: Date) 811 | // Relative Time 812 | /// Checks if it's a future date 813 | case isInTheFuture 814 | /// Checks if the date has passed 815 | case isInThePast 816 | /// Checks if earlier than date 817 | case isEarlier(than: Date) 818 | /// Checks if later than date 819 | case isLater(than: Date) 820 | /// Checks if it's a weekday 821 | case isWeekday 822 | /// Checks if it's a weekend 823 | case isWeekend 824 | } 825 | 826 | /// The date components available to be retrieved or modifed 827 | enum DateComponentType { 828 | case second, minute, hour, day, weekday, weekdayOrdinal, week, month, year 829 | var inSeconds: Double { 830 | switch self { 831 | case .second: return 1 832 | case .minute: return 60 833 | case .hour: return 3600 834 | case .day: return 86400 835 | case .week: return 604800 836 | case .month: return 2419200 // 28 days 837 | case .year: return 31556926 838 | default: return 0 839 | } 840 | } 841 | var calendarComponent: Calendar.Component { 842 | switch self { 843 | case .second: return .second 844 | case .minute: return .minute 845 | case .hour: return .hour 846 | case .day: return .day 847 | case .weekday: return .weekday 848 | case .weekdayOrdinal: return .weekdayOrdinal 849 | case .week: return .weekOfYear 850 | case .month: return .month 851 | case .year: return .year 852 | } 853 | } 854 | } 855 | 856 | /// The type of date that can be used for the dateFor function 857 | enum DateAdjustmentType { 858 | // Adjusts the date to the begining of the day at 00:00:01 859 | case startOfDay 860 | // Adjusts the date to the end of the day at 11:59:59 861 | case endOfDay 862 | // Adjusts the day to the start of the week 863 | case startOfWeek 864 | // Adjusts the day to the end of the week 865 | case endOfWeek 866 | // Adjusts the day to the start of the month 867 | case startOfMonth 868 | // Adjusts the day to the end of the month 869 | case endOfMonth 870 | // Adjusts the day to tomorrow 871 | case tomorrow 872 | // Adjusts the day to yestersay 873 | case yesterday 874 | // Adjusts the date to the nearest minute. ie: with nearestMinute(minute: 30) 10:17 becomes 10:30 and 10:05 becomes 10:00 875 | case nearestMinute(minute: Int) 876 | // Adjusts the date to the nearest hours. ie: with nearestHour(hour: 6) 10:17 becomes 12:17 and 07:017 becomes 06:17 877 | case nearestHour(hour: Int) 878 | // Adjusts the day to the start of the year 879 | case startOfYear 880 | // Adjusts the day to the end of the year 881 | case endOfYear 882 | } 883 | 884 | /// Convenience types for date to string conversion 885 | enum DateStyleType: String { 886 | /// Short style: "2/27/17, 2:22 PM" 887 | case short 888 | /// Medium style: "Feb 27, 2017, 2:22:06 PM" 889 | case medium 890 | /// Long style: "February 27, 2017 at 2:22:06 PM EST" 891 | case long 892 | /// Full style: "Monday, February 27, 2017 at 2:22:06 PM Eastern Standard Time" 893 | case full 894 | /// Ordinal day: "27th" 895 | case ordinalDay 896 | /// Weekday: "Monday" 897 | case weekday 898 | /// Short week day: "Mon" 899 | case shortWeekday 900 | /// Very short weekday: "M" 901 | case veryShortWeekday 902 | /// Month: "February" 903 | case month 904 | /// Short month: "Feb" 905 | case shortMonth 906 | /// Very short month: "F" 907 | case veryShortMonth 908 | var formatterStyle: DateFormatter.Style { 909 | switch self { 910 | case .short: return .short 911 | case .medium: return .medium 912 | case .long: return .long 913 | case .full: return .full 914 | default: return .none 915 | } 916 | } 917 | } 918 | } 919 | 920 | extension Date.DateFormatType: Equatable { 921 | // swiftlint:disable:next operator_whitespace 922 | public static func ==(lhs: Date.DateFormatType, rhs: Date.DateFormatType) -> Bool { 923 | switch (lhs, rhs) { 924 | case (.custom(let lhsString), .custom(let rhsString)): 925 | return lhsString == rhsString 926 | default: 927 | return lhs == rhs 928 | } 929 | } 930 | } 931 | 932 | extension DateFormatter { 933 | fileprivate func symbols(for dateStyleType: Date.DateStyleType) -> [String] { 934 | switch dateStyleType { 935 | case .weekday: return weekdaySymbols 936 | case .shortWeekday: return shortWeekdaySymbols 937 | case .veryShortWeekday: return veryShortWeekdaySymbols 938 | case .month: return monthSymbols 939 | case .shortMonth: return shortMonthSymbols 940 | case .veryShortMonth: return veryShortMonthSymbols 941 | default: return [String]() 942 | } 943 | } 944 | } 945 | -------------------------------------------------------------------------------- /Tests/DateHelperTests/DateAdjustingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DateHelper 3 | 4 | final class DateHelperAdjustingTests: XCTestCase { 5 | // adjust date and time 6 | func testDateAdjustDateAndTime() throws { 7 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 8 | let adjusted = original.adjust(hour: 1, minute: 10, second: 30, day: 15, month: 1), 9 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 01, day: 15, hour: 1, minute: 10, second: 30)) else { 10 | return 11 | } 12 | XCTAssertEqual(adjusted, comparison) 13 | } 14 | // offset second 15 | func testDateOffsetSecond() throws { 16 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 17 | let adjusted = original.offset(.second, value: 10), 18 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 51)) else { 19 | return 20 | } 21 | XCTAssertEqual(adjusted, comparison) 22 | } 23 | // offset minute 24 | func testDateOffsetMinute() throws { 25 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14)), 26 | let adjusted = original.offset(.minute, value: 10), 27 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 24)) else { 28 | return 29 | } 30 | XCTAssertEqual(adjusted, comparison) 31 | } 32 | // offset hour 33 | func testDateOffsetHour() throws { 34 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18)), 35 | let adjusted = original.offset(.hour, value: 2), 36 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 20)) else { 37 | return 38 | } 39 | XCTAssertEqual(adjusted, comparison) 40 | } 41 | // offset day 42 | func testDateOffsetDay() throws { 43 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06)), 44 | let adjusted = original.offset(.day, value: 10), 45 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 16)) else { 46 | return 47 | } 48 | XCTAssertEqual(adjusted, comparison) 49 | } 50 | // offset weekday 51 | func testDateOffsetWeekday() throws { 52 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06)), 53 | let adjusted = original.offset(.weekday, value: 10), 54 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 16)) else { 55 | return 56 | } 57 | XCTAssertEqual(adjusted, comparison) 58 | } 59 | // offset ordinal weekday 60 | func testDateOffsetWeekdayOrdinal() throws { 61 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06)), 62 | let adjusted = original.offset(.weekdayOrdinal, value: 2), 63 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 20)) else { 64 | return 65 | } 66 | XCTAssertEqual(adjusted, comparison) 67 | } 68 | // offset week 69 | func testDateOffsetWeek() throws { 70 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06)), 71 | let adjusted = original.offset(.week, value: -2), 72 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 11, day: 22)) else { 73 | return 74 | } 75 | XCTAssertEqual(adjusted, comparison) 76 | } 77 | // offset month 78 | func testDateOffsetMonth() throws { 79 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06)), 80 | let adjusted = original.offset(.month, value: 2), 81 | let comparison = Calendar.current.date(from: DateComponents(year: 2010, month: 02, day: 06)) else { 82 | return 83 | } 84 | XCTAssertEqual(adjusted, comparison) 85 | } 86 | // offset week 87 | func testDateOffsetYear() throws { 88 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06)), 89 | let adjusted = original.offset(.year, value: -2), 90 | let comparison = Calendar.current.date(from: DateComponents(year: 2007, month: 12, day: 06)) else { 91 | return 92 | } 93 | XCTAssertEqual(adjusted, comparison) 94 | } 95 | // adjust .startOfDay 96 | func testDateAdjustStartOfDay() throws { 97 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 98 | let adjusted = original.adjust(for: .startOfDay), 99 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 0, minute: 0, second: 0)) else { 100 | return 101 | } 102 | XCTAssertEqual(adjusted, comparison) 103 | } 104 | // adjust .endOfDay 105 | func testDateAdjustEndOfDay() throws { 106 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 107 | let adjusted = original.adjust(for: .endOfDay), 108 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000)) else { 109 | return 110 | } 111 | XCTAssertEqual(adjusted, comparison) 112 | } 113 | // adjust .startOfWeek 114 | func testDateAdjustStartOfWeek() throws { 115 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 08, hour: 18, minute: 14, second: 41)), 116 | let adjusted = original.adjust(for: .startOfWeek), 117 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)) else { 118 | return 119 | } 120 | XCTAssertEqual(adjusted, comparison) 121 | } 122 | // adjust .endOfWeek 123 | func testDateAdjustEndOfWeek() throws { 124 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 125 | let adjusted = original.adjust(for: .endOfWeek), 126 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 12, hour: 18, minute: 14, second: 41)) else { 127 | return 128 | } 129 | XCTAssertEqual(adjusted, comparison) 130 | } 131 | // adjust .startOfMonth 132 | func testDateAdjustStartOfMonth() throws { 133 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 134 | let adjusted = original.adjust(for: .startOfMonth), 135 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 01, hour: 18, minute: 14, second: 41)) else { 136 | return 137 | } 138 | XCTAssertEqual(adjusted, comparison) 139 | } 140 | // adjust .endOfMonth 141 | func testDateAdjustEndOfMonth() throws { 142 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 143 | let adjusted = original.adjust(for: .endOfMonth), 144 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 31, hour: 18, minute: 14, second: 41)) else { 145 | return 146 | } 147 | XCTAssertEqual(adjusted, comparison) 148 | } 149 | // adjust .tomorrow 150 | func testDateAdjustTomorrow() throws { 151 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 152 | let adjusted = original.adjust(for: .tomorrow), 153 | let tomorrow = Date().offset(.day, value: 1) else { 154 | return 155 | } 156 | let components = Date.components(tomorrow) 157 | guard let comparison = Calendar.current.date(from: DateComponents(year: components.year, month: components.month, day: components.day, hour: 18, minute: 14, second: 41)) else { 158 | return 159 | } 160 | XCTAssertEqual(adjusted, comparison) 161 | } 162 | // adjust .yesterday 163 | func testDateAdjustYesterday() throws { 164 | guard let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 165 | let adjusted = original.adjust(for: .yesterday), 166 | let yesterday = Date().offset(.day, value: -1) else { 167 | return 168 | } 169 | let components = Date.components(yesterday) 170 | guard let comparison = Calendar.current.date(from: DateComponents(year: components.year, month: components.month, day: components.day, hour: 18, minute: 14, second: 41)) else { 171 | return 172 | } 173 | XCTAssertEqual(adjusted, comparison) 174 | } 175 | // adjust .nearestMinute 176 | func testDateAdjustNearestMinute() throws { 177 | guard 178 | let original1 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 18, minute: 14)), 179 | let adjusted1 = original1.adjust(for: .nearestMinute(minute: 30)), 180 | let comparison1 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 18, minute: 00)), 181 | let original2 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 18, minute: 40)), 182 | let adjusted2 = original2.adjust(for: .nearestMinute(minute: 30)), 183 | let comparison2 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 18, minute: 30)), 184 | let original3 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 18, minute: 50)), 185 | let adjusted3 = original3.adjust(for: .nearestMinute(minute: 30)), 186 | let comparison3 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 19, minute: 00)) else { 187 | return 188 | } 189 | XCTAssertEqual(adjusted1, comparison1) 190 | XCTAssertEqual(adjusted2, comparison2) 191 | XCTAssertEqual(adjusted3, comparison3) 192 | } 193 | // adjust .nearestHour 194 | func testDateAdjustNearestHour() throws { 195 | guard 196 | let original1 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 18)), 197 | let adjusted1 = original1.adjust(for: .nearestHour(hour: 12)), 198 | let comparison1 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 08, hour: 00)), 199 | let original2 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 07)), 200 | let adjusted2 = original2.adjust(for: .nearestHour(hour: 12)), 201 | let comparison2 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 12)), 202 | let original3 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 03)), 203 | let adjusted3 = original3.adjust(for: .nearestHour(hour: 12)), 204 | let comparison3 = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 07, hour: 00)) else { 205 | return 206 | } 207 | XCTAssertEqual(adjusted1, comparison1) 208 | XCTAssertEqual(adjusted2, comparison2) 209 | XCTAssertEqual(adjusted3, comparison3) 210 | } 211 | // adjust .startOfYear 212 | func testDateAdjustStartOfYear() throws { 213 | guard 214 | let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41)), 215 | let adjusted = original.adjust(for: .startOfYear), 216 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 01, day: 01, hour: 0)) else { 217 | return 218 | } 219 | XCTAssertEqual(adjusted, comparison) 220 | } 221 | // adjust .endOfYear 222 | func testDateAdjustEndOfYear() throws { 223 | guard 224 | let original = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 00)), 225 | let adjusted = original.adjust(for: .endOfYear), 226 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 31, hour: 23, minute: 59, second: 59)) else { 227 | return 228 | } 229 | XCTAssertEqual(adjusted, comparison) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Tests/DateHelperTests/DateComparisonTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DateHelper 3 | 4 | final class DateHelperComparisonTests: XCTestCase { 5 | let now = Date() 6 | // .isToday 7 | func testDateTodayComparison() throws { 8 | guard let comparison = now.offset(.day, value: 1) else { 9 | return 10 | } 11 | XCTAssertTrue(!comparison.compare(.isToday)) 12 | XCTAssertTrue(now.compare(.isToday)) 13 | } 14 | // .isTomorrow 15 | func testDateTomorrowComparison() throws { 16 | guard let comparison = now.offset(.day, value: 1) else { 17 | return 18 | } 19 | XCTAssertTrue(comparison.compare(.isTomorrow)) 20 | XCTAssertTrue(!now.compare(.isTomorrow)) 21 | } 22 | // .isYesterday 23 | func testDateYesterdayComparison() throws { 24 | guard let comparison = now.offset(.day, value: -1) else { 25 | return 26 | } 27 | XCTAssertTrue(comparison.compare(.isYesterday)) 28 | XCTAssertTrue(!now.compare(.isYesterday)) 29 | } 30 | // .isThisWeek 31 | func testDateThisWeekComparison() throws { 32 | guard let comparison = now.offset(.week, value: -1) else { 33 | return 34 | } 35 | XCTAssertTrue(!comparison.compare(.isThisWeek)) 36 | XCTAssertTrue(now.compare(.isThisWeek)) 37 | } 38 | // .isNextWeek 39 | func testDateNextWeekComparison() throws { 40 | guard let comparison = now.offset(.week, value: 1) else { 41 | return 42 | } 43 | XCTAssertTrue(comparison.compare(.isNextWeek)) 44 | XCTAssertTrue(!now.compare(.isNextWeek)) 45 | } 46 | // .isLastWeek 47 | func testDateLastWeekComparison() throws { 48 | guard let comparison = now.offset(.week, value: -1) else { 49 | return 50 | } 51 | XCTAssertTrue(comparison.compare(.isLastWeek)) 52 | XCTAssertTrue(!now.compare(.isLastWeek)) 53 | } 54 | // .isThisYear 55 | func testDateThisYearComparison() throws { 56 | guard let comparison = now.offset(.year, value: -1) else { 57 | return 58 | } 59 | XCTAssertTrue(!comparison.compare(.isThisYear)) 60 | XCTAssertTrue(now.compare(.isThisYear)) 61 | } 62 | // .isNextYear 63 | func testDateNextYearComparison() throws { 64 | guard let comparison = now.offset(.year, value: 1) else { 65 | return 66 | } 67 | XCTAssertTrue(comparison.compare(.isNextYear)) 68 | XCTAssertTrue(!now.compare(.isNextYear)) 69 | } 70 | // .isLastYear 71 | func testDateLastYearComparison() throws { 72 | guard let comparison = now.offset(.year, value: -1) else { 73 | return 74 | } 75 | XCTAssertTrue(comparison.compare(.isLastYear)) 76 | XCTAssertTrue(!now.compare(.isLastYear)) 77 | } 78 | // .isInTheFuture 79 | func testDateFutureComparison() throws { 80 | guard let future = now.offset(.week, value: 1), 81 | let past = now.offset(.week, value: -1) else { 82 | return 83 | } 84 | XCTAssertTrue(future.compare(.isInTheFuture)) 85 | XCTAssertTrue(!past.compare(.isInTheFuture)) 86 | } 87 | // .isInThePast 88 | func testDatePastComparison() throws { 89 | guard let future = now.offset(.week, value: 1), 90 | let past = now.offset(.week, value: -1) else { 91 | return 92 | } 93 | XCTAssertTrue(!future.compare(.isInThePast)) 94 | XCTAssertTrue(past.compare(.isInThePast)) 95 | } 96 | // .isSameDay 97 | func testDateSameDayComparison() throws { 98 | guard let future = now.offset(.day, value: 1), 99 | let past = now.offset(.day, value: -1), 100 | let today = now.adjust(for: .startOfDay) else { 101 | return 102 | } 103 | XCTAssertTrue(!future.compare(.isSameDay(as: now))) 104 | XCTAssertTrue(!past.compare(.isSameDay(as: now))) 105 | XCTAssertTrue(today.compare(.isSameDay(as: now))) 106 | } 107 | // .isSameWeek 108 | func testDateSameWeekComparison() throws { 109 | guard let future = now.offset(.week, value: 1), 110 | let past = now.offset(.week, value: -1), 111 | let today = now.adjust(for: .startOfDay) else { 112 | return 113 | } 114 | XCTAssertTrue(!future.compare(.isSameWeek(as: now))) 115 | XCTAssertTrue(!past.compare(.isSameWeek(as: now))) 116 | XCTAssertTrue(today.compare(.isSameWeek(as: now))) 117 | } 118 | // .isSameMonth 119 | func testDateSameMonthComparison() throws { 120 | guard let future = now.offset(.month, value: 1), 121 | let past = now.offset(.month, value: -1), 122 | let today = now.adjust(for: .startOfDay) else { 123 | return 124 | } 125 | XCTAssertTrue(!future.compare(.isSameMonth(as: now))) 126 | XCTAssertTrue(!past.compare(.isSameMonth(as: now))) 127 | XCTAssertTrue(today.compare(.isSameMonth(as: now))) 128 | 129 | } 130 | // .isSameYear 131 | func testDateSameYearComparison() throws { 132 | guard let future = now.offset(.year, value: 1), 133 | let past = now.offset(.year, value: -1), 134 | let today = now.adjust(for: .startOfDay) else { 135 | return 136 | } 137 | XCTAssertTrue(!future.compare(.isSameYear(as: now))) 138 | XCTAssertTrue(!past.compare(.isSameYear(as: now))) 139 | XCTAssertTrue(today.compare(.isSameYear(as: now))) 140 | } 141 | // .isEarlierThan 142 | func testDateEarlierThanComparison() throws { 143 | guard let future = now.offset(.minute, value: 1), 144 | let past = now.offset(.minute, value: -1) else { 145 | return 146 | } 147 | XCTAssertTrue(!future.compare(.isEarlier(than: now))) 148 | XCTAssertTrue(past.compare(.isEarlier(than: now))) 149 | } 150 | // .isLaterThan 151 | func testDateLaterThanComparison() throws { 152 | guard let future = now.offset(.minute, value: 1), 153 | let past = now.offset(.minute, value: -1) else { 154 | return 155 | } 156 | XCTAssertTrue(future.compare(.isLater(than: now))) 157 | XCTAssertTrue(!past.compare(.isLater(than: now))) 158 | } 159 | // .isWeekend 160 | func testDateIsWeekendComparison() throws { 161 | guard let weekday = Calendar.current.date(from: DateComponents(year: 2021, month: 12, day: 15)), 162 | let weekend = Calendar.current.date(from: DateComponents(year: 2021, month: 12, day: 18)) else { 163 | return 164 | } 165 | XCTAssertTrue(!weekday.compare(.isWeekend)) 166 | XCTAssertTrue(weekend.compare(.isWeekend)) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Tests/DateHelperTests/DateFromStringTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DateHelper 3 | 4 | final class DateHelperFromStringTests: XCTestCase { 5 | 6 | // date from isoYear string 7 | func testDateFromIsoYearString() throws { 8 | let date = Date(fromString: "2009", format: .isoYear) 9 | let comparison = Calendar.current.date(from: DateComponents(year: 2009)) 10 | XCTAssertEqual(date, comparison) 11 | } 12 | 13 | // date from isoYearMonth string 14 | func testDateFromIsoYearMonthString() throws { 15 | let date = Date(fromString: "2009-08", format: .isoYearMonth) 16 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 08)) 17 | XCTAssertEqual(date, comparison) 18 | } 19 | 20 | // date from isoYearMonth string 21 | func testDateFromIsoDateString() throws { 22 | let date = Date(fromString: "2009-08-11", format: .isoDate) 23 | let comparison = Calendar.current.date(from: DateComponents(year: 2009, month: 08, day: 11)) 24 | XCTAssertEqual(date, comparison) 25 | } 26 | 27 | // date from isoDateTime string 28 | func testDateFromIsoDateTimeString() throws { 29 | let date = Date(fromString: "2009-08-11T06:30:03-05:00", format: .isoDateTime) 30 | let offset = -5 31 | let timeZone = TimeZone(secondsFromGMT: 60 * 60 * offset) 32 | let comparison = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2009, month: 08, day: 11, hour: 06, minute: 30, second: 03)) 33 | XCTAssertEqual(date, comparison) 34 | } 35 | 36 | // date from isoDateTimeFull string 37 | func testDateFromIsoDateTimeFullString() throws { 38 | let date = Date(fromString: "2009-08-11T06:30:03.161-05:00", format: .isoDateTimeFull) 39 | let offset = -5 40 | let nanoseconds = 161*1000000 41 | let timeZone = TimeZone(secondsFromGMT: 60 * 60 * offset) 42 | let comparison = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2009, month: 08, day: 11, hour: 06, minute: 30, second: 03, nanosecond: nanoseconds)) 43 | XCTAssertEqual(date, comparison) 44 | } 45 | 46 | // date from dotNet string 47 | func testDateFromDotNetString() throws { 48 | let date = Date(fromString: "/Date(1260123281843)/", format: .dotNet) 49 | let timeZone = TimeZone(secondsFromGMT: 0) 50 | let nanoseconds = 843*1000000 51 | let comparison = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 41, nanosecond: nanoseconds)) 52 | XCTAssertEqual(date?.timeIntervalSince1970, comparison?.timeIntervalSince1970) 53 | } 54 | 55 | // date from rss string 56 | func testDateFromRSSString() throws { 57 | guard let date = Date(fromString: "Fri, 09 Sep 2011 15:26:08 +0200", format: .rss), 58 | let timeZone = TimeZone(secondsFromGMT: 60 * 60 * 2), 59 | let comparison = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2011, month: 09, day: 09, hour: 15, minute: 26, second: 08)) else { 60 | return 61 | } 62 | XCTAssertEqual(date, comparison) 63 | } 64 | 65 | // date from altRSS string 66 | func testDateFromAltRSSString() throws { 67 | guard let date = Date(fromString: "09 Sep 2011 15:26:08 +0200", format: .altRSS), 68 | let timeZone = TimeZone(secondsFromGMT: 60 * 60 * 2), 69 | let comparison = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2011, month: 09, day: 09, hour: 15, minute: 26, second: 08)) else { 70 | return 71 | } 72 | XCTAssertEqual(date, comparison) 73 | } 74 | 75 | // date from httpHeader string 76 | func testDateFromHttpHeaderString() throws { 77 | guard let date = Date(fromString: "Wed, 01 03 2017 06:43:19 -0500", format: .httpHeader), 78 | let timeZone = TimeZone(secondsFromGMT: 60 * 60 * (-5)), 79 | let comparison = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2017, month: 03, day: 01, hour: 06, minute: 43, second: 19)) else { 80 | return 81 | } 82 | XCTAssertEqual(date, comparison) 83 | } 84 | 85 | // date from custom format string 86 | func testDateFromCustomFormatString() throws { 87 | guard let date = Date(fromString: "16 July 1972 6:12:00", format: .custom("dd MMM yyyy HH:mm:ss")), 88 | let comparison = Calendar.current.date(from: DateComponents(year: 1972, month: 07, day: 16, hour: 06, minute: 12, second: 00)) else { 89 | return 90 | } 91 | XCTAssertEqual(date, comparison) 92 | } 93 | 94 | // date from string using date detector 95 | func testDateFromDetectorString() throws { 96 | let date = Date(detectFromString: "Tomorrow at 5:30 PM")! 97 | let calendar = Calendar.current 98 | let today = Date() 99 | let tomorrow = calendar.date(byAdding: .day, value: 1, to: today)! 100 | let components = calendar.dateComponents(Date.componentFlags(), from: tomorrow) 101 | let comparison = calendar.date(from: DateComponents(timeZone: TimeZone.current, year: components.year, month: components.month, day: components.day, hour: 17, minute: 30, second: 0)) 102 | XCTAssertEqual(date, comparison) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/DateHelperTests/MiscelaniousTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DateHelper 3 | 4 | final class DateHelperMiscelaniousTests: XCTestCase { 5 | 6 | // extract date components 7 | func testDateComponents() throws { 8 | guard 9 | let timeZone = TimeZone(secondsFromGMT: 60 * 60 * (-5)), 10 | let date = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 11)), 11 | let second = date.component(.second), 12 | let minute = date.component(.minute), 13 | let hour = date.component(.hour), 14 | let day = date.component(.day), 15 | let weekday = date.component(.weekday), 16 | let weekdayOrdinal = date.component(.weekdayOrdinal), 17 | let month = date.component(.month), 18 | let year = date.component(.year) else { return } 19 | XCTAssertEqual(second, 11) 20 | XCTAssertEqual(minute, 14) 21 | XCTAssertEqual(hour, 18) 22 | XCTAssertEqual(day, 06) 23 | XCTAssertEqual(weekday, 1) 24 | XCTAssertEqual(weekdayOrdinal, 1) 25 | XCTAssertEqual(month, 12) 26 | XCTAssertEqual(year, 2009) 27 | print("date: \(date)") 28 | print("second: \(second)") 29 | print("minute: \(minute)") 30 | print("hour: \(hour)") 31 | print("day: \(day)") 32 | print("weekday: \(weekday)") 33 | print("weekdayOrdinal: \(weekdayOrdinal)") 34 | print("month: \(month)") 35 | print("year: \(year)") 36 | } 37 | // extract date items 38 | func testDateItemExtraction() throws { 39 | guard 40 | let date = Calendar.current.date(from: DateComponents(year: 2021, month: 12, day: 17)), 41 | let numberOfDaysInMonth = date.numberOfDaysInMonth(), 42 | let firstDayOfWeek = date.firstDayOfWeek(), 43 | let lastDayOfWeek = date.lastDayOfWeek() else { return } 44 | XCTAssertEqual(numberOfDaysInMonth, 31) 45 | XCTAssertEqual(firstDayOfWeek, 12) 46 | XCTAssertEqual(lastDayOfWeek, 19) 47 | print("date: \(date)") 48 | print("numberOfDaysInMonth: \(numberOfDaysInMonth)") 49 | print("firstDayOfWeek: \(firstDayOfWeek)") 50 | print("lastDayOfWeek: \(lastDayOfWeek)") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/DateHelperTests/StringFromDateTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DateHelper 3 | 4 | final class DateHelperToStringTests: XCTestCase { 5 | 6 | // date to string with style 7 | func testToStringWithStyle() throws { 8 | guard let timeZone = TimeZone(secondsFromGMT: 60 * 60 * (-5)), 9 | let date = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2017, month: 03, day: 01, hour: 06, minute: 43, second: 19)) else { 10 | return 11 | } 12 | // short 13 | if let string = date.toString(style: .short) { 14 | XCTAssertEqual(string, "3/1/17, 6:43 AM") 15 | } 16 | // medium 17 | if let string = date.toString(style: .medium) { 18 | XCTAssertEqual(string, "Mar 1, 2017 at 6:43:19 AM") 19 | } 20 | // long 21 | if let string = date.toString(style: .long) { 22 | XCTAssertEqual(string, "March 1, 2017 at 6:43:19 AM EST") 23 | } 24 | // full 25 | if let string = date.toString(style: .full) { 26 | XCTAssertEqual(string, "Wednesday, March 1, 2017 at 6:43:19 AM Eastern Standard Time") 27 | } 28 | // ordinalDay 29 | if let string = date.toString(style: .ordinalDay) { 30 | XCTAssertEqual(string, "1st") 31 | } 32 | // weekday 33 | if let string = date.toString(style: .weekday) { 34 | XCTAssertEqual(string, "Wednesday") 35 | } 36 | // shortWeekday 37 | if let string = date.toString(style: .shortWeekday) { 38 | XCTAssertEqual(string, "Wed") 39 | } 40 | // veryShortWeekday 41 | if let string = date.toString(style: .veryShortWeekday) { 42 | XCTAssertEqual(string, "W") 43 | } 44 | // month 45 | if let string = date.toString(style: .month) { 46 | XCTAssertEqual(string, "April") 47 | } 48 | // shortMonth 49 | if let string = date.toString(style: .shortMonth) { 50 | XCTAssertEqual(string, "Apr") 51 | } 52 | // veryShortMonth 53 | if let string = date.toString(style: .veryShortMonth) { 54 | XCTAssertEqual(string, "A") 55 | } 56 | } 57 | // date to string with format 58 | func testToStringWithFormat() throws { 59 | guard let timeZone = TimeZone(secondsFromGMT: 60 * 60 * (-5)), 60 | let date = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2017, month: 03, day: 01, hour: 06, minute: 43, second: 19)), 61 | let isoYear = date.toString(format: .isoYear), 62 | let isoYearMonth = date.toString(format: .isoYearMonth), 63 | let isoDate = date.toString(format: .isoDate), 64 | let isoDateTime = date.toString(format: .isoDateTime), 65 | let isoDateTimeFull = date.toString(format: .isoDateTimeFull), 66 | let dotNet = date.toString(format: .dotNet), 67 | let rss = date.toString(format: .rss), 68 | let altRSS = date.toString(format: .altRSS), 69 | let httpHeader = date.toString(format: .httpHeader), 70 | let custom1 = date.toString(format: .custom("MMM d, yyyy")), 71 | let custom2 = date.toString(format: .custom("h:mm a")), 72 | let custom3 = date.toString(format: .custom("E MMM d")) 73 | else { 74 | return 75 | } 76 | XCTAssertEqual(isoYear, "2017") 77 | XCTAssertEqual(isoYearMonth, "2017-03") 78 | XCTAssertEqual(isoDate, "2017-03-01") 79 | XCTAssertEqual(isoDateTime, "2017-03-01T06:43:19-05:00") 80 | XCTAssertEqual(isoDateTimeFull, "2017-03-01T06:43:19.000-05:00") 81 | XCTAssertEqual(dotNet, "/Date(-51488368599000.000000)/") 82 | XCTAssertEqual(rss, "Wed, 1 Mar 2017 06:43:19 -0500") 83 | XCTAssertEqual(altRSS, "1 Mar 2017 06:43:19 -0500") 84 | XCTAssertEqual(httpHeader, "Wed, 01 03 2017 06:43:19 -0500") 85 | XCTAssertEqual(custom1, "Mar 1, 2017") 86 | XCTAssertEqual(custom2, "6:43 AM") 87 | XCTAssertEqual(custom3, "Wed Mar 1") 88 | 89 | } 90 | // date to string with date and time style 91 | func testToStringWithDateAndTimeStyle() throws { 92 | guard let timeZone = TimeZone(secondsFromGMT: 60 * 60 * (-5)), 93 | let date = Calendar.current.date(from: DateComponents(timeZone: timeZone, year: 2017, month: 03, day: 01, hour: 06, minute: 43, second: 19)) else { 94 | return 95 | } 96 | // dateStyle: .none, timeStyle: .short 97 | if let string = date.toString(dateStyle: .none, timeStyle: .short) { 98 | XCTAssertEqual(string, "6:43 AM") 99 | } 100 | // dateStyle: .short, timeStyle: .none 101 | if let string = date.toString(dateStyle: .short, timeStyle: .none) { 102 | XCTAssertEqual(string, "3/1/17") 103 | } 104 | // dateStyle: dateStyle: .short, timeStyle: .short 105 | if let string = date.toString(dateStyle: .short, timeStyle: .short) { 106 | XCTAssertEqual(string, "3/1/17, 6:43 AM") 107 | } 108 | // dateStyle: .medium, timeStyle: .medium 109 | if let string = date.toString(dateStyle: .medium, timeStyle: .medium) { 110 | XCTAssertEqual(string, "Mar 1, 2017 at 6:43:19 AM") 111 | } 112 | // dateStyle: .long, timeStyle: .long 113 | if let string = date.toString(dateStyle: .long, timeStyle: .long) { 114 | XCTAssertEqual(string, "March 1, 2017 at 6:43:19 AM EST") 115 | } 116 | // dateStyle: .full, timeStyle: .full 117 | if let string = date.toString(dateStyle: .full, timeStyle: .full) { 118 | XCTAssertEqual(string, "Wednesday, March 1, 2017 at 6:43:19 AM Eastern Standard Time") 119 | } 120 | } 121 | 122 | // date to string with date and time style 123 | func testToStringWithRelativeTime() throws { 124 | guard let date4years = Date().offset(.year, value: -4), 125 | let justNow = Date().offset(.second, value: -8) else { 126 | return 127 | } 128 | // toStringWithRelativeTime default behaviour 129 | if let string = date4years.toStringWithRelativeTime() { 130 | XCTAssertEqual(string, "4 years ago") 131 | } 132 | // toStringWithRelativeTime with custom year string 133 | if let string = date4years.toStringWithRelativeTime(customStrings: [.yearsPast: "%.f ages ago in a galaxy far away"]) { 134 | XCTAssertEqual(string, "4 ages ago in a galaxy far away") 135 | } 136 | // toStringWithRelativeTime with custom just now string 137 | if let string = justNow.toStringWithRelativeTime(customStrings: [.nowPast: "just happened"]) { 138 | XCTAssertEqual(string, "just happened") 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Tests/DateHelperTests/TimeSinceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DateHelper 3 | 4 | final class DateHelperTimeSinceTests: XCTestCase { 5 | 6 | // time since in second 7 | func testDateTimeSinceInSecond() throws { 8 | guard 9 | let date = Calendar.current.date(from: DateComponents(year: 2009, month: 12, day: 06, hour: 18, minute: 14, second: 11)), 10 | let minus30Sec = date.offset(.second, value: -30), 11 | let minus30SecSince = date.since(minus30Sec, in: .second), 12 | let minus2Hr = date.offset(.hour, value: -2), 13 | let minus2HrSinceMin = date.since(minus2Hr, in: .minute), 14 | let minus2HrSinceHr = date.since(minus2Hr, in: .hour), 15 | let minus1Day = date.offset(.day, value: -1), 16 | let minus1DaySince = date.since(minus1Day, in: .day), 17 | let minus1Week = date.offset(.week, value: -1), 18 | let minus1WeekSince = date.since(minus1Week, in: .week), 19 | let plus1Week = date.offset(.week, value: 1), 20 | let plus1WeekSince = date.since(plus1Week, in: .week), 21 | let minus2Week = date.offset(.week, value: -2), 22 | let minus2WeekSince = date.since(minus2Week, in: .weekdayOrdinal), 23 | let minus1Month = date.offset(.month, value: -1), 24 | let minus1MonthSince = date.since(minus1Month, in: .month), 25 | let minus1Year = date.offset(.year, value: -1), 26 | let minus1YearSince = date.since(minus1Year, in: .year) 27 | else { 28 | return 29 | } 30 | print("date: \(date.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 31 | print("minus30SecSince: \(minus30Sec.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 32 | print("minus2Hr: \(minus2Hr.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 33 | print("minus1Day: \(minus1Day.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 34 | print("minus1Week: \(minus1Week.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 35 | print("plus1Week: \(plus1Week.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 36 | print("minus2Week: \(minus2Week.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 37 | print("minus1Month: \(minus1Month.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 38 | print("minus1Year: \(minus1Year.toString(format: .custom("yyyy-MM-dd hh-mm-ss"))!)") 39 | XCTAssertEqual(minus30SecSince, 30) 40 | XCTAssertEqual(minus2HrSinceMin, 120) 41 | XCTAssertEqual(minus2HrSinceHr, 2) 42 | XCTAssertEqual(minus1DaySince, 1) 43 | XCTAssertEqual(minus1WeekSince, 1) 44 | XCTAssertEqual(plus1WeekSince, -1) 45 | XCTAssertEqual(minus2WeekSince, 2) 46 | XCTAssertEqual(minus1MonthSince, 1) 47 | XCTAssertEqual(minus1YearSince, 1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvitax/DateHelper/573c18a6a8a1394da599ff90da966c7afe92dd71/logo.png -------------------------------------------------------------------------------- /logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melvitax/DateHelper/573c18a6a8a1394da599ff90da966c7afe92dd71/logo.sketch --------------------------------------------------------------------------------