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 可扩展性不行友爱,未来添加新的格式化类型可能会导致不必要的重复代码出现,并下降可读性。

FormatStyleTimeFormatStyle 为咱们供给了新的思路去解决这些问题,但为了兼容 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 风格,TimeFormatStylePattern 的规划都学习了 Foundation 结构中的 Duration 相关的 API。Pattern 供给了 hourMinute, hourMinuteSecondminuteSecond 3 种格式化计划。每一种都可配置相关的格式化参数,padHourToLengthpadMinuteToLength 用于在小时和分钟前补充占位的字符‘0’, fractionalSecondsLength 指定了秒后面小数点的精度范围,roundSecondsroundFractionalSeconds 则用于决议运用何种规矩对分钟和秒履行进位操作。详细能够查阅 Duration​.Time​Format​Style​.Pattern

接下来,只需求完结 format(_:) 办法即可。在格式化之前需求先将 DurationBacked 类型的参数转化成带小数点的秒,然后根据 fractionalSecondsLengthroundFractionalSeconds对转化而来的秒进行预处理操作。

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

你会发现代码还不行简洁,咱们能够为 IntDurationBacked 添加扩展,让它更为友爱的被咱们运用。

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 或许任何其他新的办法。那么你能够为时刻格式化添加自定义分隔符的功用吗?