DateFormatter.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. import Foundation
  21. /// Format and convert dates and time
  22. ///
  23. /// All methods are `static`, no initalization needed.
  24. ///
  25. /// Formatters are cached to improve performance. Call `forceReinitialize()` to reset them.
  26. ///
  27. /// - Note: All examples of the formatted strings reflect **February 1, 2020 at 1:14:15 PM GMT+1**
  28. public class DateFormatter: NSObject {
  29. // MARK: - Formats provided by the system
  30. /// Localized short date and time string
  31. ///
  32. /// Examples in multiple locales:
  33. /// - 2/1/20, 1:14 PM (en_US)
  34. /// - 01.02.20, 13:14 (de_DE)
  35. /// - 01.02.20 13:14 (fr_CH)
  36. ///
  37. /// - Parameter date: Date to format
  38. /// - Returns: Localized short date and time string or empty string if `date` is nil
  39. @objc
  40. public static func shortStyleDateTime(_ date: Date?) -> String {
  41. guard let date = date else {
  42. return ""
  43. }
  44. if shortDateTimeDateFormatter == nil {
  45. shortDateTimeDateFormatter = dateFormatterWith(date: .short, andTime: .short)
  46. }
  47. return shortDateTimeDateFormatter!.string(from: date)
  48. }
  49. /// Localized short date and medium time (with seconds) string
  50. ///
  51. /// Examples in multiple locales:
  52. /// - 2/1/20, 1:14:15 PM (en_US)
  53. /// - 01.02.20, 13:14:15 (de_DE)
  54. /// - 01.02.20 13:14:15 (fr_CH)
  55. ///
  56. /// - Parameter date: Date to format
  57. /// - Returns: Localized short date and medium time (with seconds) string or empty string if `date` is nil
  58. @objc
  59. public static func shortStyleDateTimeSeconds(_ date: Date?) -> String {
  60. guard let date = date else {
  61. return ""
  62. }
  63. if shortDateMediumTimeDateFormatter == nil {
  64. shortDateMediumTimeDateFormatter = dateFormatterWith(date: .short, andTime: .medium)
  65. }
  66. return shortDateMediumTimeDateFormatter!.string(from: date)
  67. }
  68. /// Localized medium date and time (with seconds) string
  69. ///
  70. /// Examples in multiple locales:
  71. /// - Feb 1, 2020 at 1:14:15 PM (en_US)
  72. /// - 01.02.2020, 13:14:15 (de_DE)
  73. /// - 1 févr. 2020 à 13:14:15 (fr_CH)
  74. ///
  75. /// - Parameter date: Date to format
  76. /// - Returns: Localized medium date and time (with seconds) string or empty string if `date` is nil
  77. @objc
  78. public static func mediumStyleDateTime(_ date: Date?) -> String {
  79. guard let date = date else {
  80. return ""
  81. }
  82. if mediumDateTimeDateFormatter == nil {
  83. mediumDateTimeDateFormatter = dateFormatterWith(date: .medium, andTime: .medium)
  84. }
  85. return mediumDateTimeDateFormatter!.string(from: date)
  86. }
  87. /// Localized medium date and short time (no seconds) string
  88. ///
  89. /// Examples in multiple locales:
  90. /// - Feb 1, 2020 at 1:14 PM (en_US)
  91. /// - 01.02.2020, 13:14 (de_DE)
  92. /// - 1 févr. 2020 à 13:14 (fr_CH)
  93. ///
  94. /// - Parameter date: Date to format
  95. /// - Returns: Localized medium date and short time (no seconds) string
  96. public static func mediumStyleDateShortStyleTime(_ date: Date) -> String {
  97. if mediumDateShortTimeDateFormatter == nil {
  98. mediumDateShortTimeDateFormatter = dateFormatterWith(date: .medium, andTime: .short)
  99. }
  100. return mediumDateShortTimeDateFormatter!.string(from: date)
  101. }
  102. /// Localized long date and time (with time zone) string
  103. ///
  104. /// Examples in multiple locales:
  105. /// - February 1, 2020 at 1:14:15 PM GMT+1 (en_US)
  106. /// - 1. Februar 2020 um 13:14:15 MEZ (de_DE)
  107. /// - 1 février 2020 à 13:14:15 UTC+1 (fr_CH)
  108. ///
  109. /// - Parameter date: Date to format
  110. /// - Returns: Localized long date and time (with time zone) string or empty string if `date` is nil
  111. @objc
  112. public static func longStyleDateTime(_ date: Date?) -> String {
  113. guard let date = date else {
  114. return ""
  115. }
  116. if longDateTimeDateFormatter == nil {
  117. longDateTimeDateFormatter = dateFormatterWith(date: .long, andTime: .long)
  118. }
  119. return longDateTimeDateFormatter!.string(from: date)
  120. }
  121. /// Localized short time (no seconds) string
  122. ///
  123. /// e.g. 1:14 PM or 13:14
  124. ///
  125. /// - Parameter date: Date to format
  126. /// - Returns: Localized short time (no seconds) string or empty string if `date` is nil
  127. @objc
  128. public static func shortStyleTimeNoDate(_ date: Date?) -> String {
  129. guard let date = date else {
  130. return ""
  131. }
  132. if shortTimeDateFormatter == nil {
  133. shortTimeDateFormatter = dateFormatterWith(date: .none, andTime: .short)
  134. }
  135. return shortTimeDateFormatter!.string(from: date)
  136. }
  137. /// Localized relative medium date string
  138. ///
  139. /// - Note: Marked as private, because it's only used internally
  140. ///
  141. /// Examples in multiple locales:
  142. /// - Today, Yesterday, .., Feb 1, 2020 at 1:14 PM (en_US)
  143. /// - Heute, Gestern, Vorgestern, ..., 01.02.2020, 13:14 (de_DE)
  144. /// - aujourd’hui, hier, avant-hier, ..., 1 févr. 2020 à 13:14 (fr_CH)
  145. ///
  146. /// - Parameter date: Date to format
  147. /// - Returns: Localized relative medium date string
  148. private static func realtiveMediumStyleDate(_ date: Date) -> String {
  149. if relativeMediumDateDateForamtter == nil {
  150. relativeMediumDateDateForamtter = dateFormatterWith(date: .medium, andTime: .none)
  151. relativeMediumDateDateForamtter?.doesRelativeDateFormatting = true
  152. }
  153. return relativeMediumDateDateForamtter!.string(from: date)
  154. }
  155. // MARK: - Custom formats
  156. /// Localized short day, month and year string
  157. ///
  158. /// Examples in multiple locales:
  159. /// - 2/1/2020 (en_US)
  160. /// - 1.2.2020 (de_DE)
  161. /// - 01.02.2020 (fr_CH)
  162. ///
  163. /// - Parameter date: Date to format
  164. /// - Returns: Localized short day, month and year string or empty string if `date` is nil
  165. @objc
  166. public static func getShortDate(_ date: Date?) -> String {
  167. guard let date = date else {
  168. return ""
  169. }
  170. if shortDayMonthAndYearDateFormatter == nil {
  171. shortDayMonthAndYearDateFormatter = dateFormatter(for: "d M y")
  172. }
  173. return shortDayMonthAndYearDateFormatter!.string(from: date)
  174. }
  175. /// Localized short weekday, medium day, medium month and long year string
  176. ///
  177. /// Examples in multiple locales:
  178. /// - Sat, Feb 01, 2020 (en_US)
  179. /// - Sa. 01. Feb. 2020 (de_DE)
  180. /// - sam. 01 févr. 2020 (fr_CH)
  181. ///
  182. /// - Parameter date: Date to format
  183. /// - Returns: Localized short weekday, medium day, medium month and full year string or empty string if `date` is nil
  184. @objc
  185. public static func getDayMonthAndYear(_ date: Date?) -> String {
  186. guard let date = date else {
  187. return ""
  188. }
  189. if mediumWeekdayDayMonthAndYearDateFormatter == nil {
  190. mediumWeekdayDayMonthAndYearDateFormatter = dateFormatter(for: "EE dd MMM yyyy")
  191. }
  192. return mediumWeekdayDayMonthAndYearDateFormatter!.string(from: date)
  193. }
  194. /// Localized short weekday, medium day and medium month
  195. ///
  196. /// - Note: Marked as private, because it's only used internally
  197. ///
  198. /// Examples in multiple locales:
  199. /// - Sat, Feb 01 (en_US)
  200. /// - Sa. 01. Feb. (de_DE)
  201. /// - sam. 01 févr. (fr_CH)
  202. ///
  203. /// - Parameter date: Date to format
  204. /// - Returns: Localized short weekday, medium day and medium month
  205. @objc
  206. private static func mediumWeekdayDayAndMonth(_ date: Date) -> String {
  207. if mediumWeekdayDayAndMonthDateFormatter == nil {
  208. mediumWeekdayDayAndMonthDateFormatter = dateFormatter(for: "EE dd MMM")
  209. }
  210. return mediumWeekdayDayAndMonthDateFormatter!.string(from: date)
  211. }
  212. /// Localized short weekday, medium day, medium month and long year string including short time
  213. ///
  214. /// Examples in multiple locales:
  215. /// - Sat, Feb 01, 2020, 1:14 PM (en_US)
  216. /// - Sa. 01. Feb. 2020, 13:14 (de_DE)
  217. /// - sam. 01 févr. 2020 à 13:14 (fr_CH)
  218. ///
  219. /// - Parameter date: Date to format
  220. /// - Returns: Localized short weekday, medium day, medium month and long year string including short time or empty string if `date` is nil
  221. @objc
  222. public static func getFullDate(for date: Date?) -> String {
  223. guard let date = date else {
  224. return ""
  225. }
  226. if mediumWeekdayDayMonthYearAndTimeDateFormatter == nil {
  227. mediumWeekdayDayMonthYearAndTimeDateFormatter = dateFormatter(for: "j:mm EE dd MMM yyyy")
  228. }
  229. return mediumWeekdayDayMonthYearAndTimeDateFormatter!.string(from: date)
  230. }
  231. // MARK: - To `Date` converter
  232. /// Convert localized date string into into `Date`
  233. ///
  234. /// This is the reverse function of `getDayMonthAndYear(_:)`
  235. ///
  236. /// Examples for localized inputs:
  237. /// - Sat, Feb 01, 2020 (en_US)
  238. /// - Sa. 01. Feb. 2020 (de_DE)
  239. /// - sam. 01 févr. 2020 (fr_CH)
  240. ///
  241. /// - Parameter dateString: Date string in current locale
  242. /// - Returns: Parsed date or nil if parsing failed
  243. @objc
  244. public static func getDateFromDayMonthAndYearDateString(_ dateString: String) -> Date? {
  245. let setMediumWeekdayDayMonthAndYearDateFormatter = { mediumWeekdayDayMonthAndYearDateFormatter = dateFormatter(for: "EE dd MMM yyyy") }
  246. if mediumWeekdayDayMonthAndYearDateFormatter == nil {
  247. setMediumWeekdayDayMonthAndYearDateFormatter()
  248. }
  249. if let date = mediumWeekdayDayMonthAndYearDateFormatter!.date(from: dateString) {
  250. return date
  251. }
  252. // Try to recover from locale change by resetting the formatter
  253. locale = Locale.current
  254. setMediumWeekdayDayMonthAndYearDateFormatter()
  255. if let date = mediumWeekdayDayMonthAndYearDateFormatter!.date(from: dateString) {
  256. return date
  257. }
  258. return nil
  259. }
  260. /// Convert localized date and time string into `Date`
  261. ///
  262. /// This is the reverse function of `getFullDate(for:)`
  263. ///
  264. /// Examples for localized inputs:
  265. /// - Sat, Feb 01, 2020, 1:14 PM (en_US)
  266. /// - Sa. 01. Feb. 2020, 13:14 (de_DE)
  267. /// - sam. 01 févr. 2020 à 13:14 (fr_CH)
  268. ///
  269. /// - Parameter dateString: Date string with time in current locale
  270. /// - Returns: Parsed date or nil if parsing failed
  271. @objc
  272. public static func getDateFromFullDateString(_ dateString: String) -> Date? {
  273. let setMediumWeekdayDayMonthYearAndTimeDateFormatter = { mediumWeekdayDayMonthYearAndTimeDateFormatter = dateFormatter(for: "j:mm EE dd MMM yyyy") }
  274. if mediumWeekdayDayMonthYearAndTimeDateFormatter == nil {
  275. setMediumWeekdayDayMonthYearAndTimeDateFormatter()
  276. }
  277. if let date = mediumWeekdayDayMonthYearAndTimeDateFormatter!.date(from: dateString) {
  278. return date
  279. }
  280. // Try to recover from locale change by resetting the formatter
  281. locale = Locale.current
  282. setMediumWeekdayDayMonthYearAndTimeDateFormatter()
  283. if let date = mediumWeekdayDayMonthYearAndTimeDateFormatter!.date(from: dateString) {
  284. return date
  285. }
  286. return nil
  287. }
  288. // MARK: - Relative custom formats
  289. /// Localized realtive date
  290. ///
  291. /// Localized text for today and yesterday, weekday, day and month for the rest of this calendar year. For previous years it also shows the year.
  292. ///
  293. /// Examples in multiple locales:
  294. /// - Today, Yesterday, Sat, Feb 01, ..., Tue, Dec 31, 2019, Sat, Feb 01 2019 (en_US)
  295. /// - Heute, Gestern, Sa. 01. Feb., ..., Di. 31. Dez. 2019, Sa. 01. Feb. 2019 (de_DE)
  296. /// - aujourd’hui, hier, sam. 01 févr., ..., mar. 31 déc. 2019, sam. 01 févr. 2019 (fr_CH)
  297. ///
  298. /// - Parameter date: Date to format
  299. /// - Returns: Localized realtive date or empty string if `date` is nil
  300. @objc
  301. public static func relativeMediumDate(for date: Date?) -> String {
  302. guard let date = date else {
  303. return ""
  304. }
  305. if isDateInTodayOrYesterday(date) {
  306. return realtiveMediumStyleDate(date)
  307. } else if isDateInThisCalendarYear(date) {
  308. return mediumWeekdayDayAndMonth(date)
  309. } else {
  310. return getDayMonthAndYear(date)
  311. }
  312. }
  313. // MARK: - Accessibility formats
  314. /// Localized date and time for accessibility
  315. ///
  316. /// Examples in multiple locales:
  317. /// - February 1, 2020, 1:14 PM (en_US)
  318. /// - 1. Februar 2020, 13:14 (de_DE)
  319. /// - 1 février 2020 à 13:14 (fr_CH)
  320. ///
  321. /// - Parameter date: Date to format
  322. /// - Returns: Localized date and time for accessibility or empty string if `date` is nil
  323. @objc
  324. public static func accessibilityDateTime(_ date: Date?) -> String {
  325. guard let date = date else {
  326. return ""
  327. }
  328. if accessibilityDateTimeDateFormatter == nil {
  329. accessibilityDateTimeDateFormatter = dateFormatter(for: "j:mm d MMMM yyyy")
  330. }
  331. return accessibilityDateTimeDateFormatter!.string(from: date)
  332. }
  333. /// Localized date and time for accessibility using relative dates for recent days (e.g. today, yesterday)
  334. ///
  335. /// Examples in multiple locales:
  336. /// - February 1, 2020 at 1:14 PM (en_US)
  337. /// - 1. Februar 2020 um 13:14 (de_DE)
  338. /// - 1 février 2020 à 13:14 (fr_CH)
  339. ///
  340. /// - Parameter date: Date to format
  341. /// - Returns: Localized date and time for accessibility using relative dates for recent days (e.g. today, yesterday) or empty string if `date` is nil
  342. @objc
  343. public static func accessibilityRelativeDayTime(_ date: Date?) -> String {
  344. guard let date = date else {
  345. return ""
  346. }
  347. if accessibilityRelativeDateTimeDateFormatter == nil {
  348. accessibilityRelativeDateTimeDateFormatter = dateFormatterWith(date: .long, andTime: .short)
  349. accessibilityRelativeDateTimeDateFormatter?.doesRelativeDateFormatting = true
  350. }
  351. return accessibilityRelativeDateTimeDateFormatter!.string(from: date)
  352. }
  353. // MARK: - Locale independent time formatter
  354. /// Date independent of locale
  355. ///
  356. /// Example: 20200102-131415
  357. ///
  358. /// - Parameter date: Date to format
  359. /// - Returns: Formated date or empty string if `date` is nil
  360. @objc
  361. public static func getDateForWeb(_ date: Date?) -> String {
  362. guard let date = date else {
  363. return ""
  364. }
  365. if webDateFormatter == nil {
  366. webDateFormatter = Foundation.DateFormatter()
  367. // Always ue this locale for locale independent formats (see https://nsdateformatter.com)
  368. webDateFormatter?.locale = Locale(identifier: "en_US_POSIX")
  369. webDateFormatter?.dateFormat = "yyyyddMM-HHmmss"
  370. }
  371. return webDateFormatter!.string(from: date)
  372. }
  373. @objc
  374. public static func getNowDateString() -> String {
  375. if nowDateFormatter == nil {
  376. nowDateFormatter = Foundation.DateFormatter()
  377. webDateFormatter?.locale = Locale(identifier: "en_US_POSIX")
  378. webDateFormatter?.dateFormat = "yyyyMMddHHmm"
  379. }
  380. return nowDateFormatter!.string(from: Date())
  381. }
  382. // MARK: - Time conversion
  383. /// Format seconds into time string
  384. ///
  385. /// This might be replaced by `DateComponentsFormatter` in the future for better localization. It requires that
  386. /// `totalSeconds` is not required as an inverse function.
  387. ///
  388. /// - Parameter totalSeconds: Seconds to transform
  389. /// - Returns: String of format "01:02:03" with hour obmitted if it's zero
  390. @objc
  391. public static func timeFormatted(_ totalSeconds: Int) -> String {
  392. let seconds = totalSeconds % 60
  393. let minutes = (totalSeconds / 60) % 60
  394. let hours = totalSeconds / 60 / 60
  395. if hours == 0 {
  396. return String(format: "%02d:%02d", minutes, seconds)
  397. } else {
  398. return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
  399. }
  400. }
  401. /// Converts time string into seconds
  402. ///
  403. /// - Parameter timeFormatted: Time string with format "01:02:03" where the hour can be obmitted
  404. /// - Returns: Number of seconds
  405. public static func totalSeconds(_ timeFormatted: String) -> Int {
  406. // Convert components to `Int` or set to 0 otherwise
  407. let components: [Int] = timeFormatted.split(separator: ":").map { Int($0) ?? 0 }
  408. var seconds = 0
  409. var minutes = 0
  410. var hours = 0
  411. switch components.count {
  412. case 1:
  413. seconds = components.first!
  414. case 2:
  415. seconds = components[components.endIndex - 1]
  416. minutes = components[components.endIndex - 2]
  417. case 3:
  418. seconds = components[components.endIndex - 1]
  419. minutes = components[components.endIndex - 2]
  420. hours = components[components.endIndex - 3]
  421. default:
  422. return 0
  423. }
  424. return seconds + (minutes * 60) + (hours * 60 * 60)
  425. }
  426. // MARK: - Caching
  427. // Does it still make sense to cache formatters in 2020?
  428. // Probably, yes (http://jordansmith.io/performant-date-parsing/).
  429. /// Reset all cached date formatters and reset to current locale
  430. public static func forceReinitialize() {
  431. shortDateTimeDateFormatter = nil
  432. shortDateMediumTimeDateFormatter = nil
  433. mediumDateTimeDateFormatter = nil
  434. mediumDateShortTimeDateFormatter = nil
  435. longDateTimeDateFormatter = nil
  436. shortTimeDateFormatter = nil
  437. relativeMediumDateDateForamtter = nil
  438. shortDayMonthAndYearDateFormatter = nil
  439. mediumWeekdayDayMonthAndYearDateFormatter = nil
  440. mediumWeekdayDayAndMonthDateFormatter = nil
  441. longWeekdayDayMonthAndYearDateFormatter = nil
  442. mediumWeekdayDayMonthYearAndTimeDateFormatter = nil
  443. accessibilityDateTimeDateFormatter = nil
  444. accessibilityRelativeDateTimeDateFormatter = nil
  445. webDateFormatter = nil
  446. locale = Locale.current
  447. }
  448. // Note: If you add a new property reset it in `foreReinitalize()`.
  449. private static var shortDateTimeDateFormatter: Foundation.DateFormatter?
  450. private static var shortDateMediumTimeDateFormatter: Foundation.DateFormatter?
  451. private static var mediumDateTimeDateFormatter: Foundation.DateFormatter?
  452. private static var mediumDateShortTimeDateFormatter: Foundation.DateFormatter?
  453. private static var longDateTimeDateFormatter: Foundation.DateFormatter?
  454. private static var shortTimeDateFormatter: Foundation.DateFormatter?
  455. private static var relativeMediumDateDateForamtter: Foundation.DateFormatter?
  456. private static var shortDayMonthAndYearDateFormatter: Foundation.DateFormatter?
  457. private static var mediumWeekdayDayMonthAndYearDateFormatter: Foundation.DateFormatter?
  458. private static var mediumWeekdayDayAndMonthDateFormatter: Foundation.DateFormatter?
  459. private static var longWeekdayDayMonthAndYearDateFormatter: Foundation.DateFormatter?
  460. private static var mediumWeekdayDayMonthYearAndTimeDateFormatter: Foundation.DateFormatter?
  461. private static var accessibilityDateTimeDateFormatter: Foundation.DateFormatter?
  462. private static var accessibilityRelativeDateTimeDateFormatter: Foundation.DateFormatter?
  463. private static var webDateFormatter: Foundation.DateFormatter?
  464. private static var nowDateFormatter: Foundation.DateFormatter?
  465. // MARK: - Private helper functions
  466. private static func dateFormatterWith(date dateStyle: Foundation.DateFormatter.Style, andTime timeStyle: Foundation.DateFormatter.Style) -> Foundation.DateFormatter {
  467. let dateFormatter = Foundation.DateFormatter()
  468. dateFormatter.locale = DateFormatter.locale
  469. dateFormatter.dateStyle = dateStyle
  470. dateFormatter.timeStyle = timeStyle
  471. return dateFormatter
  472. }
  473. private static func dateFormatter(for format: String) -> Foundation.DateFormatter {
  474. let dateFormatter = Foundation.DateFormatter()
  475. dateFormatter.locale = DateFormatter.locale
  476. dateFormatter.setLocalizedDateFormatFromTemplate(format)
  477. return dateFormatter
  478. }
  479. // MARK: - Private realtive date helper
  480. /// Checks if `date` is in today or yesterday
  481. ///
  482. /// - Parameter date: Date to check
  483. /// - Returns: `True` if the date is in today or yesterday, `False` otherwise
  484. private static func isDateInTodayOrYesterday(_ date: Date) -> Bool {
  485. Calendar.current.isDateInToday(date) || Calendar.current.isDateInYesterday(date)
  486. }
  487. /// Checks if `date` is in this calendar year
  488. ///
  489. /// - Parameter date: Date to check
  490. /// - Returns: `True` if the date is in this calenadar year, `False` otherwise
  491. private static func isDateInThisCalendarYear(_ date: Date) -> Bool {
  492. var dateComponents = Calendar.current.dateComponents([.year], from: Date())
  493. dateComponents.second = -1
  494. guard let lastNewYearsEveJustBeforeMidnight = Calendar.current.date(from: dateComponents) else {
  495. return false
  496. }
  497. return date > lastNewYearsEveJustBeforeMidnight
  498. }
  499. /// Checks if `date` is younger than one year
  500. ///
  501. /// - Parameter date: Date to check
  502. /// - Returns: `False` if the date is a year ago or older, or not determable in the current calendar, otherwise `True`
  503. private static func isDateInLastYear(_ date: Date) -> Bool {
  504. var dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: Date())
  505. guard let yearComponent = dateComponents.year else {
  506. return false
  507. }
  508. dateComponents.year = yearComponent - 1
  509. guard let dayComponent = dateComponents.day else {
  510. return false
  511. }
  512. dateComponents.day = dayComponent + 1
  513. guard let aYearAgoMidnight = Calendar.current.date(from: dateComponents) else {
  514. return false
  515. }
  516. return date > aYearAgoMidnight
  517. }
  518. /// Checks if `date` is in last six days
  519. ///
  520. /// i.e. if today is _Wednesday_ this function returns `true` for all dates up to and including last _Thursday_
  521. ///
  522. /// - Parameter date: Date to check
  523. /// - Returns: `False` if the date is in last 6 days, or not determable in the current calendar, otherwise `True`
  524. private static func isDateInLastSixDays(_ date: Date) -> Bool {
  525. var dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: Date())
  526. guard let dayComponent = dateComponents.day else {
  527. return false
  528. }
  529. dateComponents.day = dayComponent - 6
  530. guard let aSevenDaysAgoMidnight = Calendar.current.date(from: dateComponents) else {
  531. return false
  532. }
  533. return date > aSevenDaysAgoMidnight
  534. }
  535. // MARK: - Helper for testing
  536. /// Only reassign this value for testing
  537. static var locale = Locale.current
  538. }