[macOS  Swift]  日付を扱うクラスの相互の関連について整理してみた

#### Dateクラス、Calendarクラス、DateFormatterクラス、DateComponentsクラスの相互の関連について整理してみた

<img src="http://mikomokaru.sakura.ne.jp/data/B20/DateClass1.png" width="600" alt="クラス関連図">

## Dateクラス
日付に関する基本情報を保持する（年、月、日、曜日、時、分、秒など）
日時は、GMT（グリニッジ標準時）と同じ値を持つ。

このクラスは、日時の情報のみ保持し、日付の属性であるタイムゾーン、カレンダーの種類、地域・言語による表示様式の差異などに関しては関与しない。これらの事柄は以下に説明するクラスが担当する。

## Calendarクラス
Calendarクラスは、年・月・日・曜日など暦/カレンダーに関する情報を保持する。システムは、規定値としてグレゴリオ歴（gregorian）のカレンダーを作成する。普通はグレゴリオ歴以外を使うことはないだろうが、年に元号を使用する場合は、和暦カレンダを作成する必要がある。（イスラム暦、ヘブライ歴などにも対応しているようだ）

Calendarオブジェクトを直接利用するのは和暦を使用したり、後述する DateComponentsクラスと連携し、Calendarクラスのメソッドを利用し日付の計算を行う場合である。

```
//カレンダオブジェクト
Calendar.current

//カレンダオブジェクトのメソッドによる日付の計算
let date = Calendar.current.date(byAdding: ....)

//和暦カレンダーの作成
let calendarJpn = Calendar.init(identifier: .japanese)
```

なお、曜日（Manday or 月曜）や月（January or １月）の名前は国・言語によって異なるが、この差はシステムロケールによる相違であり、カレンダの種類による違いではない。

## DateFormatterクラス
日付オブジェクトを文字列として表示する。日付の文字列から日付オブジェクトを作成する。タイムゾーン、言語・地域（ロケール）を管理する。

### 日付（Dateオブジェクト）を文字列に編集して出力する
DateFormatterオブジェクトのstring(format:)メソッドにより、日付オブジェクトを指定したフォーマット書式子に従って文字列に出力する。

年月日・時分秒に相当するキーワードは、[Unicode Technical Standard #35](https://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns) を参照のこと。区切り文字は例にも示した通り任意である。

曜日は、日付オブジェクトの内部では、日曜から始まる整数値（1〜7）で保持されている。日付書式子の "e" は整数値そのまま、 "E" は、ロケール（言語・地域）に応じた曜日名に変換した値が表示される。日本であれば、日, 月, 火, ...となる。

4種類の定型的な編集形式が DateFormatter.Style定数を指定することで利用できる。

#### 現在日時を表示する

```
let now = Date.init()
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.init(identifier: "ja_JP")

//現在日付・時刻 custom format
formatter.dateFormat = "yyyyねん MMがつ ddにち HHじ mmふん ssびょう E曜日"
print(formatter.string(from: now))

//現在日付・時刻　full format
formatter.dateStyle = .full
formatter.timeStyle = .full
print(formatter.string(from: now))

//現在日付・時刻 long format
formatter.dateStyle = .long
formatter.timeStyle = .long
print(formatter.string(from: now))

//現在日付・時刻 medium format
formatter.dateStyle = .medium
formatter.timeStyle = .medium
print(formatter.string(from: now))

//現在日付・時刻 short format
formatter.dateStyle = .short
formatter.timeStyle = .short
print(formatter.string(from: now))

//結果
2021ねん 02がつ 16にち 19じ 50ふん 24びょう 火曜日
2021年2月16日火曜日 19時50分24秒 日本標準時
2021年2月16日 19:50:24 JST
2021/02/16 19:50:24
2021/02/16 19:50
```

### タイムゾーン（時差の調整）について


<img  src="http://mikomokaru.sakura.ne.jp/data/B20/DateTimeZone.png" width="400" alt="タイムゾーン" >

日付オブジェクトの日時はグリニッッジ標準時（GMT）で保持されるので、英国以外の地域で日時を表示したり、日時を指定して日付オブジェクトを作成したりするとき、時差の調整が必要になる。調整は DateFormatterオブジェクトのプロパティに指定したタイムゾーンの基づいて行われる。システムのデフォルトでは、プロパティにはその地域のタイムゾーン（localTimeZone）が自動的に設定される。アプリケーションは、DateFormatterオブジェクトを通して時差を意識することなく日付を取り扱うことができる。

日本のタイムゾーンは、名称"Asia/Tokyo”、略称”JST"


タイムゾーンを変更する場合には、タイムゾーン名を指定してTimeZoneオブジェクトを作成し、DateFormatterオブジェクトのtimeZoneプロパティにセットする。次のコードはタイムゾーンをグリニッジ標準時に変更する。

```
let now = Date.init()
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.init(identifier: "ja_JP")
formatter.timeZone = TimeZone.init(secondsFromGMT: 0) //GMTからの時差(分)
//formatter.timeZone = TimeZone(abbreviation:"GMT")   //略称指定
formatter.dateStyle = .full
formatter.timeStyle = .full
print(formatter.string(from: now))

//結果
2021年10月23日 土曜日 3時54分42秒 グリニッジ標準時
```

### 日付の文字列から日付（Dateオブジェクト）を作成する
DateFormatterクラスのdate(from:)メソッドにより、指定したフォーマット指定子の文字列から日付オブジェクトを作成する。年月日・時分秒に相当するキーワードは、[Unicode Technical Standard #35](https://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns) を参照のこと。区切り文字は任意。戻り値はオプショナル型で作成に失敗すると nilが返る。

```
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.init(identifier: "ja_JP")
formatter.dateFormat = "yyyy_MM_dd HH-mm-ss"
let dateString1 = "2000_5_31 23-15-8"
if let date = formatter.date(from: dateString1) {
    print(formatter.string(from: date))
    formatter.dateStyle = .full
    formatter.timeStyle = .full
    print(formatter.string(from: date))
}

//結果
2000_05_31 23-15-08
2000年5月31日 水曜日 23時15分08秒 日本標準時

//極端な例だがこれでも良い
formatter.dateFormat = "yyyyMMddHHmmss"
let dateString1 = "20000531231508"
```

### 和暦を使う
DateFormatterクラスのカレンダー属性に和暦カレンダー（japanese）を設定しロケールに日本語（ja_JP）を設定すれば、和暦を操作することができる。元号を表すキーワードは"G"になる。

#### 現在日を和暦で表示する

```
let now = Date.init()
let formatter = DateFormatter()
formatter.dateFormat = "Gyy年MM月dd日 HH時mm分ss秒"
formatter.calendar = Calendar.init(identifier: .japanese)
formatter.locale = Locale.init(identifier: "ja_JP")
print(formatter.string(from: now))

//結果
令和3年10月23日 11時21分55秒
```

### 和暦の日付文字列から日付オブジェクトを作成する
```
let formatter: DateFormatter = DateFormatter()
formatter.dateFormat = "Gyy_MM_dd HH-mm-ss"
formatter.calendar = Calendar.init(identifier: .japanese)
formatter.locale = Locale.init(identifier: "ja_JP")
let dateString1 = "昭和39_10_1 0-15-59"
if let date = formatter.date(from: dateString1){
    print(formatter.string(from: date))

    formatter.dateStyle = .full
    formatter.timeStyle = .full
    print(formatter.string(from: date))

    formatter.calendar = Calendar.current //カレンダーをグレゴリオ暦に変更
    formatter.dateStyle = .full
    formatter.timeStyle = .full
    print(formatter.string(from: date))
}

//結果
昭和39_10_01 00-15-59
昭和39年10月1日木曜日 0時15分59秒 GMT+09:00
1964年10月1日木曜日 0時15分59秒 GMT+09:00

```

### 言語・地域（ロケール）について

システムのデフォルト設定でロケールは日本語・日本（ja_JP）になっているので、Xcodeでアプリケーションを作成するときはロケールの指定は必要ない。（ただしPlaygroundで実行するときは指定が必要）

他地域・他言語を使用する場合は、Localeオブジェクトを作成し、DateFormatterオブジェクトのlocale属性に設定する。次の例ではロケールを英語（言語）・米国（地域）に変更している。

```
let now = Date.init()
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.init(identifier: "en_US")
formatter.dateStyle = .full
formatter.timeStyle = .full
print(formatter.string(from: now))

//結果
Saturday, October 23, 2021 at 11:47:27 AM Japan Standard Time
```

## DateComponentsクラス

<img src="http://mikomokaru.sakura.ne.jp/data/B20/DateClass2.png" width="600" alt="クラス関連図2">

DateComponentsオブジェクトは日付・日時の構成要素（年、月、日、曜日、時、分、秒）を保持する。Calendarクラスと連携して日付の計算などを行う。

### DateComponentsオブジェクトの取得
Calendarクラスのcomponents:メソッドを使用し DateオブジェクトからDateComponentsオブジェクトを作成し、日付・時刻の要素を取得する

```
let now = Date.init()
let comps:Set<Calendar.Component> = [.year, .month, .day,
                                     .hour, .minute, .second]
let dComp = Calendar.current.dateComponents(comps, from: now)
print(String(format:"%ld年%ld月%ld日%ld時%ld分%ld秒",
             dComp.year ?? 0, dComp.month ?? 0, dComp.day ?? 0,
             dComp.hour ?? 0, dComp.minute ?? 0, dComp.second ?? 0))

//結果
2021年10月23日11時50分38秒
```

### 日付オブジェクトの作成
DateComponentsオブジェクトからDateオブジェクトを作成することができる。DateComponentsオブジェクトの各要素に日時を設定する。未設定の要素の値はゼロで初期化されている。次の例であれば「時分秒」の値がゼロ。

```
var dComp = DateComponents.init()
dComp.year = 1965
dComp.month = 2
dComp.day = 14
if let date =  Calendar.current.date(from: dComp){
    let formatter: DateFormatter = DateFormatter()
    formatter.locale = Locale.init(identifier: "ja_JP")
    formatter.dateStyle = .full
    formatter.timeStyle = .full
    print(formatter.string(from: date))
}

//結果
1965年2月14日日曜日 0時00分00秒 GMT+09:00
```

### 日付の計算を行う
日付の操作に関して色々な例を示す。
### (1) 月末日を求める
DataComponentsオブジェクトから当月末日の Dateオブジェクトを作成する。要素の年月には対象日の年月を、日は当月の末日（日数）を指定する。末日は Calendarクラスの rangeメソッドで求まる。これらから日付オブジェクトを作成すれば、当月末日の日付を得ることができる。

```
let now = Date.init()
let range = Calendar.current.range(of: .day, in: .month, for: now)
let lastday: Int = range?.count ?? 0 //月末日
var dComp = Calendar.current.dateComponents([.year, .month, .day],
                                            from: now)
dComp.day = lastday
//月末日の日付オブジェクト
if let lastDate = Calendar.current.date(from: dComp){
    let formatter: DateFormatter = DateFormatter()
    formatter.locale = Locale.init(identifier: "ja_JP")
    formatter.dateStyle = .full
    formatter.timeStyle = .full
    print(formatter.string(from: lastDate))
}
//結果
2021年10月31日 日曜日 0時00分00秒 日本標準時
```

### (2) 日付の計算（加算/減算）
日付の加算/減算は、計算対象のオペランドに DataComponentsオブジェクトの要素（年月日時分秒）を指定する。

次の例はある日付に対して、１年2ヶ月３日後または、前を求める方法である。計算には Calrndarクラスの date(byAdding: to:）メソッドを使用する

```
let now = Date.init()
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.init(identifier: "ja_JP")
formatter.dateStyle = .full
formatter.timeStyle = .full
print(formatter.string(from: now)) //現在日

//1年2ヶ月3日後
var dComp = DateComponents()
dComp.year = 1
dComp.month = 2
dComp.day = 3
if let futureDate = Calendar.current.date(byAdding: dComp, to: now){
    print(formatter.string(from: futureDate))
}

//1年2ヶ月3日前
dComp.year = -1
dComp.month = -2
dComp.day = -3
if let pastDate = Calendar.current.date(byAdding: dComp, to: now){
    print(formatter.string(from: pastDate))
}

//結果
2021年10月23日 土曜日 12時19分40秒 日本標準時
2022年12月26日 月曜日 12時19分40秒 日本標準時
2020年8月20日 木曜日 12時19分40秒 日本標準時
```
月末日を求めるには、日付の加算/減算によって行うこともできる。対象日の月の１日から１ヶ月を加算して翌月の1日の日付を求め、そこから1日を引けば月末日となる。
>2021/2/1 + 1ヶ月 = 2021/3/1
>2021/3/1 - 1日 = 2021/2/28

```
var dComp = Calendar.current.dateComponents([.year, .month, .day], from: Date.init())
dComp.day = 1
if let firstDate = Calendar.current.date(from: dComp){
    var dComp = DateComponents()
    dComp.month = 1
    dComp.day = -1
    if let lastDate = Calendar.current.date(byAdding: dComp, to: firstDate){
        let formatter: DateFormatter = DateFormatter()
        formatter.locale = Locale.init(identifier: "ja_JP")
        formatter.dateStyle = .full
        formatter.timeStyle = .full
        print(formatter.string(from: lastDate))
    }
}

//結果
2021年10月31日 日曜日 0時00分00秒 日本標準時
```

### (3) 日付の差（期間）を求める
Calendarクラスの dateComponents(from: to:) メソッドを使用すれば、二つの日付の差（期間）を DataComponentsオブジェクトとして得ることができる。

```
let now = Date.init()
var dComp = DateComponents()
dComp.year = 2025
dComp.month = 4
dComp.day = 13
if let toDate = Calendar.current.date(from: dComp){
    let period = Calendar.current.dateComponents(
        [.year, .month, .day], from: now, to: toDate)
    print(String(format:"大阪万博まであと %ld年%ld月%ld日",
                 period.year ?? 0, period.month ?? 0, period.day ?? 0))

}

//結果
大阪万博まであと 3年5月20日
```

もう一つの方法として、暦の通算日数の差から期間を求める方法がある。日付の表し方の一つとして、グレゴリオ暦の場合、西暦1年1月1日0時からの通算日時で表わすことができる。

Calendarクラスのordinality(of: in: for:)メソッドを使う。例題では、ある日付が暦の始まり（in:era）を起点としたとき何日目（of:day)であるかを返す。例えば、起点を yearにすれば年初からの通算日数が得られる。

日付を通算日数として表しておけば、二つの日付の差分は、そのまま日数の差（期間）となる。

```
let now = Date.init()
var dComp = DateComponents()
dComp.year = 2025
dComp.month = 4
dComp.day = 13
if let toDate = Calendar.current.date(from: dComp){
    
    let totalDaysFrom = Calendar.current.ordinality(of: .day,
                                                    in: .era, for: now)
    let totalDaysTo = Calendar.current.ordinality(of: .day,
                                                in: .era, for: toDate)
    print(String(format:"大阪万博まであと %ld日",
                 (totalDaysTo ?? 0) - (totalDaysFrom ?? 0)))

}
//結果
大阪万博まであと 1267日
```

### 曜日を求める
DateComponentsオブジェクトの曜日要素（weekDay）には、日曜から始まる整数値（1〜7）がセッットされる。整数値を曜日名に変換するには、DateFormatterクラスのシンボル名配列から求めることができる。整数値-1を添え字として配列を読み込めば対応する曜日の文字列を取得できる。曜日名は、ロケール（言語・地域）に応じた名前に変換される。

```
let now = Date.init()
let dComp = Calendar.current.dateComponents([.weekday], from: now)
let weekday: Int = dComp.weekday ?? 0
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.init(identifier: "ja_JP")
print(String(format:"%ld %@ %@",
             weekday,
             formatter.weekdaySymbols[weekday - 1],
             formatter.shortWeekdaySymbols[weekday - 1]))

//結果
7 土曜日 土
```

<br>
Swift/Objective-Cに関連する記事はホームページでも公開しています。
[http://mikomokaru.sakura.ne.jp/lib/index1.html](http://mikomokaru.sakura.ne.jp/lib/index1.html)