Do you mean look into the AttributedString
, find the AM/PM range, if it's "PM", change the hour range to twenty four hour value. Finally remove the AM/PM range? (but what about any space in from of AM/PM? How to get at that and also remove it?)
I dump out all the runs, the separator and space is Foundation.AttributedString._InternalRun
, so just remove the one preceding AM/PM.
Edit: here is what I come up with:
// VerbatimHour option is Foundation private, so do this get at its option settings for all twentyFourHour options
extension Date.FormatStyle.Symbol.VerbatimHour {
var isTwentyFourHour: Bool {
[Date.FormatStyle.Symbol.VerbatimHour.twoDigits(clock: .twentyFourHour, hourCycle: .zeroBased),
.twoDigits(clock: .twentyFourHour, hourCycle: .oneBased),
.defaultDigits(clock: .twentyFourHour, hourCycle: .zeroBased),
.defaultDigits(clock: .twentyFourHour, hourCycle: .oneBased)
].contains(self)
}
}
extension Date {
/// Format this Date in twenty four hour format
/// - Parameters:
/// - formatStyle: FormatStyle to format this Date. Should have hour part. If in AM/PM style, the hour is changed to 24-hour
/// - hour: how the hour should be formatted in 24-hour format
/// - Returns: the result in an AttributedString
func twentyFourHourFormatted(_ formatStyle: Date.FormatStyle, hour: Date.FormatStyle.Symbol.VerbatimHour) -> AttributedString {
func scan(_ s: AttributedString) -> (Range<AttributedString.Index>?, Range<AttributedString.Index>?, Range<AttributedString.Index>?) {
var hourRange: Range<AttributedString.Index>?
// remember the range preceding AM/PM to remove only if it's a space
var precedingAMPMRange: Range<AttributedString.Index>?
var amPMRange: Range<AttributedString.Index>?
var previousRun: AttributedString.Runs.Run?
for r in s.runs {
if s[r.range].dateField == .hour {
hourRange = r.range
} else if s[r.range].dateField == .amPM {
amPMRange = r.range
// only remove if preceding run is not a `dateField`, (only remove space which is `Foundation.AttributedString._InternalRun`)
if previousRun?.dateField == nil {
precedingAMPMRange = previousRun?.range
}
// hour field always precede amPM?, so all done?
break
}
previousRun = r
}
return (hourRange, precedingAMPMRange, amPMRange)
}
assert(hour.isTwentyFourHour)
var result = formatted(formatStyle.attributed)
let (hourRange, precedingAMPMRange, amPMRange) = scan(result)
let twentyFourHour = formatted(Date.VerbatimFormatStyle(format: "\(hour: hour)",
timeZone: formatStyle.timeZone,
calendar: formatStyle.calendar).attributed
)
if let hourRange = hourRange {
// if there is AM/PM, remove it and its preceding delimiter
// must remove in the order from end to start, otherwise index out-of-range!
if let amPMRange = amPMRange {
result.removeSubrange(amPMRange)
}
if let precedingAMPMRange = precedingAMPMRange {
result.removeSubrange(precedingAMPMRange)
}
// fix up the hour field to twenty four hour format:
result.replaceSubrange(hourRange, with: twentyFourHour)
}
return result
}
}
Usage:
date.twentyFourHourFormatted(.dateTime.hour(.defaultDigits(amPM: .narrow)).minute().second(),
hour: .defaultDigits(clock: .twentyFourHour, hourCycle: .zeroBased))