Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 39 additions & 30 deletions Packages/Formatters/Sources/RelativeDateFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,51 @@
import Foundation

public struct RelativeDateFormatter: Sendable {
private static let relativeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.dateStyle = .medium
formatter.doesRelativeDateFormatting = true
return formatter
}()
let relativeFormatter: DateFormatter
let time: DateFormatter
let dateAndTime: DateFormatter
let calendar: Calendar

private static let time: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .short
formatter.dateStyle = .none
return formatter
}()
public init(locale: Locale = .current, timeZone: TimeZone = .current) {
var calendar = Calendar.current
calendar.locale = locale
calendar.timeZone = timeZone
self.calendar = calendar

private static let dateAndTime: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .short
formatter.dateStyle = .long
return formatter
}()
relativeFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.dateStyle = .medium
formatter.doesRelativeDateFormatting = true
formatter.locale = locale
formatter.timeZone = timeZone
return formatter
}()

private static let dateOnly: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.dateStyle = .long
return formatter
}()
time = {
let formatter = DateFormatter()
formatter.timeStyle = .short
formatter.dateStyle = .none
formatter.locale = locale
formatter.timeZone = timeZone
return formatter
}()

public init() {}
dateAndTime = {
let formatter = DateFormatter()
formatter.timeStyle = .short
formatter.dateStyle = .long
formatter.locale = locale
formatter.timeZone = timeZone
return formatter
}()
}

public func string(from date: Date) -> String {
if Calendar.current.isDateInToday(date) || Calendar.current.isDateInYesterday(date) {
let relativeDate = Self.relativeFormatter.string(from: date)
return String(format: "%@, %@", relativeDate, Self.time.string(from: date))
if calendar.isDateInToday(date) || calendar.isDateInYesterday(date) {
let relativeDate = relativeFormatter.string(from: date)
return String(format: "%@, %@", relativeDate, time.string(from: date))
}
return Self.dateAndTime.string(from: date)
return dateAndTime.string(from: date)
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,45 @@
// Copyright (c). Gem Wallet. All rights reserved.

import Testing
import Foundation
@testable import Formatters
import Foundation
import Testing

struct RelativeDateFormatterTests {
private let formatter = RelativeDateFormatter()
private let locale = Locale.US
private let timeZone = TimeZone.NewYork!
private let formatter: RelativeDateFormatter
private let calendar: Calendar

init() {
formatter = RelativeDateFormatter(locale: locale, timeZone: timeZone)
calendar = formatter.calendar
}

@Test
func today() {
let calendar = Calendar.current
let todayWithTime = calendar.date(bySettingHour: 14, minute: 30, second: 0, of: Date())!
func today() throws {
let todayWithTime = try #require(calendar.date(bySettingHour: 14, minute: 30, second: 0, of: Date()))

#expect(formatter.string(from: todayWithTime) == "Today, 2:30 PM")
}

@Test
func yesterday() {
let calendar = Calendar.current
let yesterdayDate = calendar.date(byAdding: .day, value: -1, to: Date())!
func yesterday() throws {
let yesterdayDate = try #require(calendar.date(byAdding: .day, value: -1, to: Date()))
let yesterdayWithTime = try #require(calendar.date(bySettingHour: 9, minute: 15, second: 0, of: yesterdayDate))

let yesterdayWithTime = calendar.date(bySettingHour: 9, minute: 15, second: 0, of: yesterdayDate)!
#expect(formatter.string(from: yesterdayWithTime) == "Yesterday, 9:15 AM")
}

@Test
func oneMonthOld() {
func oneMonthOld() throws {
var components = DateComponents()
components.year = 2025
components.month = 2
components.day = 2
components.hour = 10
components.minute = 25
let date = Calendar.current.date(from: components)!
components.timeZone = timeZone
let date = try #require(calendar.date(from: components))

#expect(formatter.string(from: date) == "February 2, 2025 at 10:25 AM")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

import Foundation

extension TimeZone {
public static let NewYork = TimeZone(identifier: "America/New_York")
}