New Date.FormatStyle: Anyway to do 24 hour?

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))