0

I need a way to format a time in the shortest way possible.

Unforunately using a timeStyle of .short doesn't achieve what I want because times with zero minutes always unnecessarily show :00 in 12-hour clock contexts.

Is there a better way to achieve this than below?

let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .short
dateFormatter.string(from: date)
   .replacingOccurrences(of: ":00am", with: "am")
   .replacingOccurrences(of: ":00pm", with: "pm")
   .replacingOccurrences(of: ":00 am", with: " am")
   .replacingOccurrences(of: ":00 pm", with: " pm")
   .replacingOccurrences(of: ":00AM", with: "AM")
   .replacingOccurrences(of: ":00PM", with: "PM")
   .replacingOccurrences(of: ":00 AM", with: " AM")
   .replacingOccurrences(of: ":00 PM", with: " PM")
// etc
4
  • What's the end goal? To only delete all minutes if 0? Or also delete all hours if 0 only displaying the day? What about the seconds? Commented Apr 1, 2024 at 12:11
  • Always display hour. Only show minutes if non-zero. Don't ever display seconds. Commented Apr 1, 2024 at 12:53
  • Are you looking for something like this .replacingOccurrences(of: ":00(\\s)(\([Calendar.current.amSymbol, Calendar.current.pmSymbol].joined(separator: "|")))", with: "$1$2", options: .regularExpression)? But I don't think there is a guarantee that the minutes will be put there... Commented Apr 1, 2024 at 13:06
  • I think you'll be better off with DateComponentsFormatter for this task. Commented Apr 1, 2024 at 15:17

2 Answers 2

0

How about creating the formatter yourself?

First you retrieve the hour and the minute from your date. Then you check if minute is 0.

extension Date {
    private static var componentFlags: Set<Calendar.Component> { [.year, .month, .day, .hour, .minute, .second] }
    
    private func components() -> DateComponents { Calendar.current.dateComponents(Date.componentFlags, from: self) }
    
    func hour() -> Int { return self.components().hour ?? 1 }
    func minute() -> Int { return self.components().minute ?? 1 }
}

let now = Date()
let hour = now.hour()
let strHour = String(format: "%02d", hour)
let minute = now.minute()

let output: String
if minute == 0 {
    output = strHour
} else {
    let strMin = String(format: "%02d", minute)
    output = "\(strHour):\(strMin)"
}

print("For date \(now), output is \(output)")
// OUTPUT:
// For date 2024-04-01 16:00:00 +0000, output is 16
// For date 2024-04-01 16:40:07 +0000, output is 16:40

Note that when you retrieve the hour with this method, the timezone is automatically applied. So if the date is 16:00 UTC+0 and you are in a -6 timezone, you will get 10 from hour(). This is usually the desired behavior.

Sign up to request clarification or add additional context in comments.

2 Comments

I don't think this respects either the users chosen locale/region nor their 12hr/24hr clock preference. Also not sure a time reading "16" makes any sense, if you're always formatting as a 24hr clock you need to show the minutes even if they're zero.
A simpler implementation of hour could be: var hour: Int { Calendar.current.component(.hour, from: self) }. Similar for minute.
0

It appears that your goal is to format a date into a time string showing hours and minutes. But if at the top of the hour (0 minutes), just show the hour.

The basic approach would be to use DateComponents to look at the minute value of the date. Then based on whether it is zero or not, format the date accordingly.

Ideally you could use DateFormatter setLocalizedDateFormatFromTemplate passing in either h:mm or just h or maybe either H:mm or just H. But running some tests with different locales resulted in the wrong localized time format in many cases.

The working solution I found was to set timeStyle to .short and then look at the resulting dateFormat value. When minutes is 0, remove the :mm (or similar) from dateFormat.

Here's some working code:

func shortFormat(date: Date, locale: Locale = Locale.current) -> String {
    // Setup the formatter for localized short time style
    let formatter = DateFormatter()
    formatter.locale = locale
    formatter.dateStyle = .none
    formatter.timeStyle = .short

    // If the date's time is at 0 minutes, remove the minutes from the format
    let mins = Calendar.current.component(.minute, from: date)
    if mins == 0 {
        // Some locales use mm and some use m
        // Locales use a colon, period, or other character
        // The RE '\Wm{1,2}' means a non-word character followed by 1 or 2 m characters.
        if let format = formatter.dateFormat, format.range(of: "\\Wm{1,2}", options: .regularExpression) != nil {
            formatter.dateFormat = format.replacingOccurrences(of: "\\Wm{1,2}", with: "", options: .regularExpression)
        }
    }

    return formatter.string(from: date)
}

Here's some test code:

print(shortFormat(date: date))
print(shortFormat(date: date, locale: Locale(identifier: "de_DE")))
print(shortFormat(date: date, locale: Locale(identifier: "fr_FR")))
print(shortFormat(date: date, locale: Locale(identifier: "es_CL")))

Sample output:

3:54 PM
15:54
15:54
3:54 p. m.

And here's some that ensures 0 minutes:

let now = Date()
var comps = Calendar.current.dateComponents([.calendar, .era, .year, .month, .day, .hour, .second], from: now)
let date = Calendar.current.date(from: comps)!
print(shortFormat(date: date))
print(shortFormat(date: date, locale: Locale(identifier: "de_DE")))
print(shortFormat(date: date, locale: Locale(identifier: "fr_FR")))
print(shortFormat(date: date, locale: Locale(identifier: "es_CL")))

Sample output:

3 PM
15
15
3 p. m.


Note that the output in locales that use 24-hour time may be confusing when minutes is 0. You may wish to consider your approach.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.