iOS 15 中 Foundation
引入了一个新的协议:FormatStyle
,它定义了一个转化办法用来将给定的数据转化成别的一种表现办法,并供给了一些本地化的支撑,而与时刻格式化相关的 TimeFormatStyle
在 iOS 16 中才缓不济急(留意这儿并非日期格式化)。
在此之前咱们将一段时刻(比方:秒)格式化为字符串时一般的做法是:别离算出给定时刻对应的小时,分钟和秒,然后再将这三个数据进行格式化输出。
extension Int {
func timeFormated() -> String {
let hour = self / 3_600
let minute = (self - hour * 3_600) / 60
let second = self % 60
return String(format: "%02d:%02d:%02d", hour, minute, second)
}
}
print(1234.timeFormated()) // 00:20:34
但这种计划都的缺陷很明显:
- 无法知晓带格式化的数据为秒仍是毫秒,或许其他单位导致数据错误;
- 无法指定格式化的样式,如:时分秒仍是分秒;
- 数据进位不行灵活,如:
1:03:37.568
,格式化成分秒办法后,究竟是63:37
仍是63:38
; - API 可扩展性不行友爱,未来添加新的格式化类型可能会导致不必要的重复代码出现,并下降可读性。
FormatStyle
和 TimeFormatStyle
为咱们供给了新的思路去解决这些问题,但为了兼容 iOS 15 以下的系统版别,咱们能够仿写一个 FormatStyle
用于格式化一个时刻段(TimeInterval),将其命名为 FormatStyleBacked
,之后咱们一切的 FormatStyle 都基于该协议:
/// A type that can convert a given data type into a representation.
public protocol FormatStyleBacked : Decodable, Encodable, Hashable {
/// The type of data to format.
associatedtype FormatInput
/// The type of the formatted data.
associatedtype FormatOutput
/// Creates a `FormatOutput` instance from `value`.
func format(_ value: Self.FormatInput) -> Self.FormatOutput
/// If the format allows selecting a locale, returns a copy of this format with the new locale set. Default implementation returns an unmodified self.
func locale(_ locale: Locale) -> Self
}
extension FormatStyleBacked {
public func locale(_ locale: Locale) -> Self { self }
}
同样地,数据类型 DurationBacked
用来表示不同的时刻段类型。
DurationBacked
的品种并不太重要,后文中会对其进行扩展以支撑恣意时刻段间隔,比方几分钟,几个小时或许几天。咱们需求的仅仅肯定的时刻时长:秒和纳秒。其中秒用来简化格式化核算的过程,纳秒用来做对时刻做增减核算。
public enum DurationBacked: Equatable, Sendable {
case seconds(Int)
case milliseconds(Int)
case microseconds(Int)
case nanoseconds(Int)
public static func == (lhs: DurationBacked, rhs: DurationBacked) -> Bool {
lhs.nanoseconds == rhs.nanoseconds
}
fileprivate var nanoseconds: Int {
switch self {
case .nanoseconds(let value): return value
case .microseconds(let value): return value * 1_000
case .milliseconds(let value): return value * 1_000_000
case .seconds(let value): return value * 1_000_000_000
}
}
fileprivate var seconds: TimeInterval {
switch self {
case .nanoseconds(let value): return Double(value) / 1_000_000_000
case .microseconds(let value): return Double(value) / 1_000_000
case .milliseconds(let value): return Double(value) / 1_000
case .seconds(let value): return Double(value)
}
}
}
随后咱们开端进入正题,定义 TimeFormatStyle
结构体用于 DurationBacked
数据的转化。
extension DurationBacked {
public struct TimeFormatStyle: FormatStyleBacked, Sendable {
/// The units to display a Duration with and configurations for the units.
public struct Pattern : Hashable, Codable, Sendable {
fileprivate enum Style: Hashable, Codable, Sendable {
case hourMinute, hourMinuteSecond, minuteSecond
}
fileprivate let style: Style
fileprivate var padHourToLength: Int = 0
fileprivate var padMinuteToLength: Int = 0
fileprivate var fractionalSecondsLength: Int = 0
fileprivate var roundSeconds: FloatingPointRoundingRule = .toNearestOrEven
fileprivate var roundFractionalSeconds: FloatingPointRoundingRule = .toNearestOrEven
/// Displays a duration in hours and minutes.
public static var hourMinute: TimeFormatStyle.Pattern {
hourMinute(padHourToLength: 1)
}
/// Displays a duration in terms of hours and minutes with the specified configurations.
public static func hourMinute(padHourToLength: Int, roundSeconds: FloatingPointRoundingRule = .toNearestOrEven) -> TimeFormatStyle.Pattern {
Pattern(style: .hourMinute, padHourToLength: padHourToLength, roundSeconds: roundSeconds)
}
/// Displays a duration in hours, minutes, and seconds.
public static var hourMinuteSecond: TimeFormatStyle.Pattern {
hourMinuteSecond(padHourToLength: 1)
}
/// Displays a duration in terms of hours, minutes, and seconds with the specified configurations.
public static func hourMinuteSecond(padHourToLength: Int, fractionalSecondsLength: Int = 0, roundFractionalSeconds: FloatingPointRoundingRule = .toNearestOrEven) -> TimeFormatStyle.Pattern {
Pattern(style: .hourMinuteSecond,
padHourToLength: padHourToLength,
fractionalSecondsLength: fractionalSecondsLength,
roundFractionalSeconds: roundFractionalSeconds)
}
/// Displays a duration in minutes and seconds. For example, one hour is formatted as "60:00" in en_US locale.
public static var minuteSecond: TimeFormatStyle.Pattern {
minuteSecond(padMinuteToLength: 1)
}
/// Displays a duration in minutes and seconds with the specified configurations.
public static func minuteSecond(padMinuteToLength: Int, fractionalSecondsLength: Int = 0, roundFractionalSeconds: FloatingPointRoundingRule = .toNearestOrEven) -> TimeFormatStyle.Pattern {
Pattern(style: .minuteSecond,
padMinuteToLength: padMinuteToLength,
fractionalSecondsLength: fractionalSecondsLength,
roundFractionalSeconds: roundFractionalSeconds)
}
}
/// The locale to use when formatting the duration.
public var locale: Locale
/// The pattern to display a Duration with.
public var pattern: TimeFormatStyle.Pattern
/// The type of data to format.
public typealias FormatInput = DurationBacked
/// The type of the formatted data.
public typealias FormatOutput = String
/// Creates an instance using the provided pattern and locale.
public init(pattern: Pattern, locale: Locale = .autoupdatingCurrent) {
self.pattern = pattern
self.locale = locale
}
/// Creates a locale-aware string representation from a duration value.
public func format(_ value: DurationBacked) -> String {
// format code here.
}
/// Modifies the format style to use the specified locale.
public func locale(_ locale: Locale) -> TimeFormatStyle {
TimeFormatStyle(pattern: pattern, locale: locale)
}
}
}
为了尽可能的和系统结构保持一致的 API 风格,TimeFormatStyle
和 Pattern
的规划都学习了 Foundation
结构中的 Duration
相关的 API。Pattern
供给了 hourMinute
, hourMinuteSecond
和 minuteSecond
3 种格式化计划。每一种都可配置相关的格式化参数,padHourToLength
和 padMinuteToLength
用于在小时和分钟前补充占位的字符‘0’, fractionalSecondsLength
指定了秒后面小数点的精度范围,roundSeconds
和 roundFractionalSeconds
则用于决议运用何种规矩对分钟和秒履行进位操作。详细能够查阅 Duration.TimeFormatStyle.Pattern
接下来,只需求完结 format(_:)
办法即可。在格式化之前需求先将 DurationBacked
类型的参数转化成带小数点的秒,然后根据 fractionalSecondsLength
和 roundFractionalSeconds
对转化而来的秒进行预处理操作。
public func format(_ value: DurationBacked) -> String {
var seconds = value.seconds
guard seconds < .infinity else {
return "inf"
}
seconds *= pow(10, Double(pattern.fractionalSecondsLength))
seconds = seconds.rounded(pattern.roundFractionalSeconds)
seconds /= pow(10, Double(pattern.fractionalSecondsLength))
// format code here.
}
完结数据的预处理后,就能够针对不同的格式化计划履行别离格式化操作了。操作办法和上文中提到的计划相同,别离核算对应的小时,分钟和秒再格式化输出。这儿有两点需求留意:
- 当 pattern 为
hourMinute
时,需求将总秒再次换算成总分钟,并根据roundSeconds
进行进位操作。 - 留意小时或许分钟以及秒数字符串的补零规矩,详见代码。
public func format(_ value: DurationBacked) -> String {
var seconds = value.seconds
guard seconds < .infinity else {
return "inf"
}
seconds *= pow(10, Double(pattern.fractionalSecondsLength))
seconds = seconds.rounded(pattern.roundFractionalSeconds)
seconds /= pow(10, Double(pattern.fractionalSecondsLength))
switch pattern.style {
case .hourMinute:
let minutes = (seconds / 60).rounded(pattern.roundSeconds)
let hour = Int(minutes) / 60
let minute = minutes - Double(hour * 60)
return String(format: "%0\(pattern.padHourToLength)d:%02.f", hour, minute)
case .hourMinuteSecond:
let hour = Int(seconds / 3_600)
let minute = Int(seconds - Double(hour * 3_600)) / 60
let second = seconds - Double(hour * 3_600 + minute * 60)
let l1 = pattern.padHourToLength
let l2 = pattern.fractionalSecondsLength
let format = """
%0\(l1)d:%02d:%0\(l2 <= 0 ? 2 : l2 + 3).\(l2)f
"""
return String(format: format, hour, minute, second)
case .minuteSecond:
let minute = Int(seconds / 60)
let second = seconds - Double(minute * 60)
let l1 = pattern.padMinuteToLength
let l2 = pattern.fractionalSecondsLength
let format = """
%0\(l1)d:%0\(l2 <= 0 ? 2 : l2 + 3).\(l2)f
"""
return String(format: format, minute, second)
}
}
至此就能够运用 TimeFormatStyle
来格式化指定的时刻段,比方运用时分秒的办法格式化 12345 秒:
let duration = DurationBacked.seconds(3 * 3_600 + 25 * 60 + 45)
let style = DurationBacked.TimeFormatStyle(pattern: .hourMinuteSecond(padHourToLength: 2))
let string = style.format(duration)
print(string) // 03:25:45
你会发现代码还不行简洁,咱们能够为 Int
和 DurationBacked
添加扩展,让它更为友爱的被咱们运用。
extension Int {
public var nanoseconds: DurationBacked { .nanoseconds(self) }
public var microseconds: DurationBacked { .microseconds(self) }
public var milliseconds: DurationBacked { .milliseconds(self) }
public var seconds: DurationBacked { .seconds(self) }
public var minutes: DurationBacked { .seconds(self * 60) }
public var hours: DurationBacked { .seconds(self * 3_600) }
}
extension FormatStyleBacked where Self == DurationBacked.TimeFormatStyle {
/// A factory variable to create a time format style to format a duration.
public static func time(pattern: DurationBacked.TimeFormatStyle.Pattern) -> Self {
DurationBacked.TimeFormatStyle(pattern: pattern)
}
}
extension DurationBacked {
public func formated<S: FormatStyleBacked>(_ format: S) -> S.FormatOutput where Self == S.FormatInput {
format.format(self)
}
/// Formats `self` using the hour-minute-second time pattern
public func formated() -> String {
formated(.time(pattern: .hourMinuteSecond))
}
}
extension DurationBacked : AdditiveArithmetic {
public static func + (lhs: DurationBacked, rhs: DurationBacked) -> DurationBacked {
.nanoseconds(lhs.nanoseconds + rhs.nanoseconds)
}
public static func - (lhs: DurationBacked, rhs: DurationBacked) -> DurationBacked {
.nanoseconds(lhs.nanoseconds - rhs.nanoseconds)
}
public static var zero: DurationBacked { .nanoseconds(0) }
}
运用新的 API,上面的比如就能够变得非常简洁且可读性更好:
let duration = 3.hours + 25.minutes + 45.seconds
let string = duration.formated(.time(pattern: .hourMinute(padHourToLength: 2)))
print(string) // 03:25:45
结语
为了方便演示,同时也鉴于本地化的复杂性,在format(_:)
办法里并没有关于 local 的任何处理,各位感兴趣的读者能够自行完结,如果自定义的 format style 中不需求本地化相关的功用就不用处理 local。
对 TimeFormatStyle
的扩展也相对比较简单:定义一个新的 Pattern 或许任何其他新的办法。那么你能够为时刻格式化添加自定义分隔符的功用吗?