[Swift] Duration の extension と FormatStyle の作成

     
⌛️ 3 min.
あまり使われていない(?) Duration を使いやすいように拡張し、さらに FormatStyle を拡張します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS14.2 RC
  • Xcode 15.1 RC
  • iOS 17.2
  • Swift 5.9

Duration

Duration は、Swift5.7 から導入された 時間を表現する型の1つです。

Apple のドキュメントは、こちら

Duration は、時間の長さを表現するために使用されます。以前から使われている TimeInterval と同じ様な目的で使用されます。

TimerInterval は、秒を単位として保持していましたが、Duration は、単位を指定して 初期化することができます。
例えば、10 秒は、以下のように 定義します。

import Foundation
let du = Duration.seconds(10)
print(du)   // print-out:  10.0 seconds

.seconds だけでなく、.milliseconds という ミリ秒単位で使いやすい initializer も用意されています。

Duration と TimeInterval

Swift5.7 以前は、TimeInterval という型が、時間の長さを表現するために使用されていました。

Duration と TimeInterval の関係は以下の記事で説明しています。

[Swift] Duration と TimeInterval の変換

extend Duration

時間長を表現する時に使う Duration は、初期化時に seconds/ milliseconds/ microseconds/ nanoseconds を使って初期化することができます。つまり、秒/ ミリ秒/ マイクロ秒/ ナノ秒 で初期化できます。

なお、Duration を使って保持できる 最小単位は、アト秒 (10 ^ (-18) 秒)です。

Duration を使うケースでは、もう少し大きい単位でも指定できたら便利そうです。

ということで、minutes/ hours/ days を追加してみます。
その名の通り、分/ 時間/ 日 です。 Double を受け取ることで、例えば、1.5日 = 36 時間 という使い方をできる様にしてみます。

Duration の extension として static func を定義してみました。

//
//  Duration+Extension.swift
//
//  Created by : Tomoaki Yagishita on 2023/12/09
//  © 2023  SmallDeskSoftware
//

import Foundation

extension Duration {
    static public func minutes(_ value: Double) -> Duration {
        return Duration.seconds(value * 60)
    }
    static public func hours(_ value: Double) -> Duration {
        return Duration.seconds(value * 60 * 60)
    }
    static public func days(_ value: Double) -> Duration {
        return Duration.seconds(value * 60 * 60 * 24)
    }
}

以下のテストコードで 動作を確認しました。

final class DurationExampleTests: XCTestCase {
    func test_Inits() async throws {
        let seconds90FromSeconds = Duration.seconds(90)
        let hours2_5FromSeconds = Duration.seconds(2.5 * 60 * 60)
        let days1_5FromSeconds = Duration.seconds(1.5 * 24 * 60 * 60)

        // Duration.minutes
        let seconds90FromMinutes = Duration.minutes(1.5)
        XCTAssertEqual(seconds90FromSeconds, seconds90FromMinutes)
        
        // Duration.hours
        let hours2_5FromHours = Duration.hours(2.5)
        XCTAssertEqual(hours2_5FromSeconds, hours2_5FromHours)

        // Duration.days
        let days1_5FromDays = Duration.days(1.5)
        XCTAssertEqual(days1_5FromSeconds, days1_5FromDays)
    }
}

# 上記は、XCTAssertEqual でチェックしていますが、一般的に 計算結果を Equal でチェックするのは危険です。

FormatStyle

FormatStyle は、iOS15/ macOS12 から導入された データを特定の書式に変換するための 仕組みです。

FormatStyle 以前は、DateFormatter 等の Formatter を使うのが通常でしたが、新しく導入された FormatStyle は、Formatter に比べて直感的に記述できるようになっています。

データを特定の書式に変換するための FormatStyle、特定の書式からデータに変換する ParseableFormatStyle は、以下の記事で説明しています。
[Swift] FormatStyle の使い方 [Swift] ParseableFormatStyle の使い方 [Swift] FormatStyle の作り方 SwiftUI2021 [Swift] ParseableFormatStyle の作り方

上記では、電話番号の書式を例に 書式への変換方法から、書式からデータへの変換方法を説明しています。

Duration 向け FormatStyle

Duration に対して FormatStyle も用意されています。

以下の記事で説明しました。
[Swift] Duration を FormatStyle 指定で表示する

デフォルトでは、hourMinute, hourMinuteSecond, minuteSecond が用意されています。
それぞれ、時分、時分秒、分秒 という表示で、最も大きい単位が 必要に応じて(繰り上がらずに)大きくなります。

Duration 向け FormatStyle 拡張

Duration 向けに用意されている FormatStyle では 最大の単位は時間でした。
以降では、最大単位を 日まで拡大してみます。

つまり、26時間30分を表す Duration については、日数+hourMinuteSecond 表記 をするということです。
具体的には 最初に日数を表示し、その後に hourMinuteSecond 相当を表示する “01:02:30:00” という表示にしたいということです。

# 月まで拡大すると、1ヶ月の日数をどうするかという問題を考えなければいけないので、日までにしています。

DayHourMinuteSecondStyle という FormatStyle を以下のように作りました。

struct DayHourMinuteSecondStyle: FormatStyle, Codable {
    typealias FormatInput = Duration
    typealias FormatOutput = String
    
    enum Pattern: Codable {
        case dayHour
        case dayHourMinute
        case dayHourMinuteSecond
    }
    let pattern: Pattern
    let padding: Int
    
    init(pattern: Pattern = .dayHour, padding: Int = 2) {
        self.pattern = pattern
        self.padding = padding
    }

    func format(_ value: Duration) -> String {
        let (second, atto) = value.components
        let secDay = second / (24*60*60)
        let restDuration = Duration(secondsComponent: second - secDay*24*60*60, attosecondsComponent: atto)
        
        switch pattern {
        case .dayHour:
            let restHour = (second - secDay*24*60*60)/(60*60)
            return paddedInt(secDay, minLength: padding) + ":" + paddedInt(restHour, minLength: 2)
        case .dayHourMinute:
            return paddedInt(secDay, minLength: padding) + ":" + restDuration.formatted(.time(pattern: .hourMinute(padHourToLength: 2)))
        case .dayHourMinuteSecond:
            return paddedInt(secDay, minLength: padding) + ":" + restDuration.formatted(.time(pattern: .hourMinuteSecond(padHourToLength: 2)))
        }
    }
    
    func paddedInt(_ value: Int64, minLength: Int) -> String {
        let valueString = value.formatted(.number)
        if valueString.count >= minLength { return valueString }
        let padding = String(repeating: "0", count: minLength - valueString.count)
        return padding + valueString
    }
}

確認に使用した テストコードは以下です。

func test_FormatStyle() async throws {
  let hours25 = Duration.hours(25)
  
  XCTAssertEqual(hours25.formatted(.time(pattern: .hourMinute)), "25:00")

  XCTAssertEqual(hours25.formatted(DayHourMinuteSecondStyle(pattern: .dayHour, padding: 2)), "01:01")
  XCTAssertEqual(hours25.formatted(.dayHour), "01:01")
  XCTAssertEqual(hours25.formatted(.dayHourMinute), "01:01:00")
  XCTAssertEqual(hours25.formatted(.dayHourMinuteSecond), "01:01:00:00")

  XCTAssertEqual(hours25.formatted(DayHourMinuteSecondStyle(pattern: .dayHour, padding: 5)), "00001:01")
}

.formatted ごとに、DayHourMinuteSecondStyle… と書くのは大変なので、以下のように、FormatStyle に対しての extension を定義すると使いやすくなります。(テストコードでも使用しています。)


extension FormatStyle where Self == DayHourMinuteSecondStyle {
    static var dayHour            : DayHourMinuteSecondStyle { .init(pattern: .dayHour) }
    static var dayHourMinute      : DayHourMinuteSecondStyle { .init(pattern: .dayHourMinute) }
    static var dayHourMinuteSecond: DayHourMinuteSecondStyle { .init(pattern: .dayHourMinuteSecond) }
}

まとめ

Duration の拡張と カスタムFormatStyleの作成 をしてみました。

Duration の拡張と カスタムFormatStyleの作成
  • FormatStyle は、Duration.TimeFormatStyle.time
  • おおきく3パターン( hourMinuteSecond, hourMinute, minuteSecond ) が用意されている
  • 最上位の単位については、0 padding できる
  • 最下位の単位については、処理方法を指定できる
  • 独自 FormatStyle を作るのは、独自型を作るのと同様
  • let (second, attoSec) = duration.components とすると Duration から 秒とアト秒が取得できる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です