DateComponents を拡張して休日プロパティを追加する


# DateComponents オブジェクトの拡張

DateComponents オブジェクトのプロパティに String? 型の jpHoliday を追加します。代替休日も対象としています。休日でない場合は nil です。このプロパティは文字列を保持していないので、取得する度にプログラムが動作することに注意してください。

```swift:Consoleアプリ例
print(DateComponents(year: 2024, month: 3, day: 20).jpHoliday!)
```

```console:実行結果
春分の日
```


# JPHoliday クラス

JPHoliday クラスが実際の休日名を求める処理です。
JPHoliday オブジェクトを作成して使用することはありません。

**静的メソッド**

| メソッド | 内容 | 代替 |
|:--|:--|:-:|
| holidays(_ year: Int) -> [DateComponents] | 年間の休日一覧 | × |
| holidays(_ year: Int, _ month: Int) -> [DateComponents] | 月間の休日一覧 | × |
| holiday(_ cdate: DateComponents) -> String? | 休日名を取得 | ○ |
| holiday(_ year: Int, _ month: Int, _ day: Int) -> String? | 休日名を取得 | ○ |
| vernalEquinox(_ year: Int) -> DateComponents | 春分の日を取得 | - |
| autumnEquinox(_ year: Int) -> DateComponents | 秋分の日を取得 | - |

注意：春分/秋分の日は[大雑把な計算](https://qiita.com/ikiuo/items/565a0f2ee455ec6d653a)で求めています

```swift:Consoleアプリ例
JPHoliday.holidays(2024).forEach({
    let wd = ["", "日","月","火","水","木","金","土"]
    let m = $0.month! < 10 ? " \($0.month!)" : "\($0.month!)";
    let d = $0.day! < 10 ? " \($0.day!)" : "\($0.day!)";
    print("\($0.year!)年\(m)月\(d)日(\(wd[$0.weekday!])): \($0.jpHoliday!)")
})
```

```console:実行結果
2024年 1月 1日(月): 元日
2024年 1月 8日(月): 成人の日
2024年 2月11日(日): 建国記念の日
2024年 2月23日(金): 天皇誕生日
2024年 3月20日(水): 春分の日
2024年 4月29日(月): 昭和の日
2024年 5月 3日(金): 憲法記念日
2024年 5月 4日(土): みどりの日
2024年 5月 5日(日): こどもの日
2024年 7月15日(月): 海の日
2024年 8月11日(日): 山の日
2024年 9月16日(月): 敬老の日
2024年 9月22日(日): 秋分の日
2024年10月14日(月): スポーツの日
2024年11月 3日(日): 文化の日
2024年11月23日(土): 勤労感謝の日
```


# DateComponents 拡張コード

```swift:jpholiday.swift
import Foundation

/// JPHoliday 休日情報クラス
///
/// 休日名に対して年月日を指定する情報を保持
///
class JPHoliday: CustomStringConvertible {
    /// 休日名
    let name: String

    /// 月
    let month: Int

    /// 日、第N曜日、春分、秋分
    let day: Any

    /// 開始年
    let start: Int?

    /// 終了年
    let end: Int?

    private let dayCode: Int
    private let startYear: Int
    private let endYear: Int

    init(_ parameter: [Any?]) {
        let pday = parameter[2]!
        let sday = pday as? String
        let nday = pday as? Int ?? [
            "M1": 32 + 0,
            "M2": 32 + 1,
            "M3": 32 + 2,
            "M4": 32 + 3,
            "VE": 64 + 0,
            "AE": 64 + 1,
        ][sday]

        name  = parameter[0] as! String
        month = parameter[1] as! Int
        day   = pday;
        start = parameter[3] as? Int
        end   = parameter[4] as? Int

        dayCode = nday!;
        startYear = start ?? 1984
        endYear = end ?? 9999
    }

    /// 適用年調査
    /// - parameters:
    ///   - year : 調査年
    /// - returns: 有効なら true
    func valid(_ year: Int) -> Bool {
        return startYear <= year && year <= endYear
    }

    /// 日を取得（移動休日は年から算出）
    /// - parameters:
    ///   - year : 年
    /// - returns: 日
    func getDay(_ year: Int) -> Int {
        switch (dayCode >> 5) {
        case 0:
            return dayCode
        case 1:
            let mtw = JPHoliday.weekDay(JPHoliday.date(year, month, 1))
            let wpos = (dayCode & 31)
            return 1 + (8 - mtw) % 7 + wpos * 7
        case 2:
            switch (dayCode & 31) {
            case 0:
                return JPHoliday.vernalEquinox(year).day!
            case 1:
                return JPHoliday.autumnEquinox(year).day!
            default:
                break
            }
            break
        default:
            break
        }
        return -1
    }

    /// 年から日を特定して DateComponents 型を取得する
    /// - parameters:
    ///   - year : 年
    /// - returns: DateComponents 型
    func getDateComponents(_ year: Int)
    -> DateComponents {
        let mday = getDay(year)
        let dt = JPHoliday.date(year, month, mday)
        return JPHoliday.dateComponents(dt)
    }

    var description: String {
        get {
            var sday = "ERROR"
            switch (dayCode >> 5) {
            case 0:
                sday = "\(dayCode)日"
            case 1:
                sday = "(第\(1 + (dayCode & 15))月曜)"
            case 2:
                switch (dayCode & 31) {
                case 0: sday = "(春分)"
                case 1: sday = "(秋分)"
                default: break
                }
            default:
                break
            }
            return "\(name):\(month)月\(sday)"
        }
    }

    // MARK: - グレゴリオ暦とタイムゾーン

    private static let timeZone = TimeZone(identifier: "Asia/Tokyo")!
    private static func getCalendar() -> Calendar {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = JPHoliday.timeZone
        return calendar;
    }
    private static let gregorian = JPHoliday.getCalendar();

    private static func date(_ year: Int, _ month: Int, _ day: Int) -> Date {
        let cdate = DateComponents(
            calendar: gregorian, timeZone: timeZone, era: 1,
            year: year, month: month, day: day)
        return gregorian.date(from: cdate)!
    }

    private static func dateComponents(_ date: Date) -> DateComponents {
        return gregorian.dateComponents([
            .calendar, .timeZone, .era, .year, .month, .day, .weekday
        ], from: date)
    }

    private static func jpFixDateComponents(_ timeIntervalSince1970: UInt64)
    -> DateComponents {
        let tzjp = UInt64(9 * 60 * 60)
        let rgmt = timeIntervalSince1970 - tzjp
        let jpdate = tzjp + rgmt - (rgmt % (24 * 60 * 60))
        return dateComponents(Date(timeIntervalSince1970: Double(jpdate)))
    }

    private static func weekDay(_ components: DateComponents) -> Int {
        return (components.weekday! - 1) % 7
    }

    private static func weekDay(_ date: Date) -> Int {
        return weekDay(dateComponents(date))
    }

    // MARK: - 休日データ

    private static let holidayTable: [[Any?]] = [
        ["元日", 1, 1, nil, nil],
        ["成人の日", 1, 15, 0, 1999],
        ["成人の日", 1, "M2", 2000, nil],
        ["建国記念の日", 2, 11, nil, nil],
        ["春分の日", 3, "VE", nil, nil],
        ["憲法記念日", 5, 3, nil, nil],
        ["みどりの日", 5, 4, 2007, nil],
        ["こどもの日", 5, 5, nil, nil],
        ["海の日", 7, 20, 1996, 2002],
        ["海の日", 7, "M3", 2003, nil],
        ["山の日", 8, 11, 2016, nil],
        ["敬老の日", 9, 15, 1966, 2002],
        ["敬老の日", 9, "M3", 2003, nil],
        ["秋分の日", 9, "AE", nil, nil],
        ["体育の日", 10, 10, 0, 1999],
        ["体育の日", 10, "M2", 2000, 2019],
        ["スポーツの日", 10, "M2", 2020, nil],
        ["文化の日", 11, 3, nil, nil],
        ["勤労感謝の日", 11, 23, nil, nil],
        // 昭和
        ["天皇誕生日", 4, 29, 0, 1988],
        ["みどりの日", 4, 29, 1989, 2006],
        ["昭和の日", 4, 29, 2007, nil],
        // 平成
        ["天皇誕生日", 12, 23, 1989, 2018],
        // 令和
        ["天皇の即位の日", 5, 1, 2019, 2019],
        ["即位礼正殿の儀が行われる日", 10, 22, 2019, 2019],
        ["天皇誕生日", 2, 23, 2020, nil],
    ]

    private static let instances: [JPHoliday] = Array(
        unsafeUninitializedCapacity: JPHoliday.holidayTable.count
    ) {
        buffer, initializedCount in
        let tab = JPHoliday.holidayTable
        for n in 0..<tab.count {
            buffer[n] = JPHoliday(tab[n])
        }
        initializedCount = tab.count
    }

    private static let monthTable: [[Int:[JPHoliday]]] = 
    Array<[Int:[JPHoliday]]>(unsafeUninitializedCapacity: 13) {
        buffer, initializedCount in
        for n in 0...12 {
            buffer[n] = [:]
        }
        for h in JPHoliday.instances {
            var mtab = buffer[h.month]
            var mday: [JPHoliday]? = mtab[h.dayCode]
            if mday == nil {
                mday = [h]
            } else {
                mday!.append(h)
            }
            mtab[h.dayCode] = mday
            buffer[h.month] = mtab
        }
        initializedCount = 13
    }

    // MARK: - 代替休日を含む休日処理

    private static func basicHoliday(_ date: Date) -> String? {
        let comp = dateComponents(date);
        let year = comp.year!, month = comp.month!, day = comp.day!
        let check = ({(h: JPHoliday) -> Bool in h.valid(year)})
        let mtab = monthTable[month]
        let dtab = mtab[day]
        if dtab != nil {
            let days = dtab!.filter(check)
            if days.count != 0 {
                return days[0].name
            }
        }
        let wday = weekDay(comp)
        if wday == 1 {
            let mweek = (day - 1) / 7
            let monday = mtab[32 + mweek]
            if monday != nil {
                let t = monday!.filter(check)
                if t.count != 0 {
                    return t[0].name
                }
            }
        }
        let tve = mtab[64+0]
        if tve != nil && day == vernalEquinox(year).day {
            return tve![0].name
        }
        let tae = mtab[64+1]
        if tae != nil && day == autumnEquinox(year).day {
            return tae![0].name
        }
        return nil;
    }

    private static var weekCache: [Int:[String?]] = [:]
    private static func weekLine(_ year: Int, _ month: Int, _ day: Int)
    -> [String?] {
        if year < 1948 {
            return Array(repeating: nil, count: 8)
        }

        let wtop = weekDay(date(year, month, 1))
        let mweek = (wtop + day - 1) / 7
        let index = (year * 12 + month) * 6 + mweek
        let cache = weekCache[index]
        if cache != nil {
            return cache!
        }

        let iday = UInt64(24 * 60 * 60)
        let dtcur = date(year, month, day)
        let dtcmp = dateComponents(dtcur)
        let wday = UInt64(weekDay(dtcmp))
        let mdtop = UInt64(dtcur.timeIntervalSince1970) - wday * iday
        var line: [String?] = Array<String?>(repeating: nil, count: 8)
        for i in 0...7 {
            let sec = Double(mdtop + iday * UInt64(i))
            let wdate = Date(timeIntervalSince1970: sec)
            let h = basicHoliday(wdate)
            if h != nil {
                line[i] = h!
            }
        }
        let altname = "休日"
        if year >= 1973 && line[0] != nil {
            for i in 1...6 {
                if line[i] == nil {
                    line[i] = altname
                    break
                }
            }
        }
        if year >= 1985 {
            for i in 1...6 {
                if line[i] == nil && line[i-1] != nil && line[i+1] != nil {
                    line[i] = altname
                    break
                }
            }
        }
        weekCache[index] = line
        return line
    }

    // MARK: - 春分 / 秋分

    ///
    /// 年から春分の日を取得する
    /// - parameters:
    ///   - year : 年
    /// - returns: 春分の日の DateComponents 型
    static func vernalEquinox(_ year: Int) -> DateComponents {
        let ve = UInt64(315569255616 * (year - 1970) + 68619916800) / 10000
        return jpFixDateComponents(ve)
    }

    /// 年から秋分の日を取得する
    /// - parameters:
    ///   - year : 年
    /// - returns: 秋分の日の DateComponents 型
    static func autumnEquinox(_ year: Int) -> DateComponents {
        let ae = UInt64(315569255616 * (year - 1970) + 229674407040) / 10000
        return jpFixDateComponents(ae)
    }

    // MARK: - 休日の一覧を取得

    /// 月間の休日一覧を取得
    /// - parameters:
    ///   - year : 年
    ///   - month : 月
    /// - returns: DateComponents の一覧
    static func holidays(_ year: Int, _ month: Int) -> [DateComponents] {
        let mtab = monthTable[month]
        let info = mtab.values.map {
            $0.filter({(h: JPHoliday) -> Bool in h.valid(year)})
        }.joined()
        var rval = Array(info).map { $0.getDateComponents(year) }
        rval.sort {
            switch ($0, $1) {
            default:
                if $0.year! < $1.year! { return true }
                if $0.month! < $1.month! { return true }
                if $0.day! < $1.day! { return true }
                return false;
            }
        }
        return rval
    }

    /// 年間の休日一覧を取得
    /// - parameters:
    ///   - year : 年
    /// - returns: DateComponents の一覧
    static func holidays(_ year: Int) -> [DateComponents] {
        var rval: [DateComponents] = []
        for month in 1...12 {
            rval += JPHoliday.holidays(year, month)
        }
        return rval
    }

    // MARK: - 日付から休日名を取得

    /// DateComponents から休日名（代替休日も対象）を取得する
    /// - parameters:
    ///   - cdate: year, month, day， の入った DateComponents 型
    /// - returns: 休日名または nil
    static func holiday(_ cdate: DateComponents) -> String? {
        let calendar = cdate.calendar
        if calendar != nil &&
            calendar!.identifier != Calendar.Identifier.gregorian {
            return holiday(dateComponents(calendar!.date(from: cdate)!))
        }
        if cdate.era != nil && cdate.era! == 0 {
            return nil
        }
        let year = cdate.year
        let month = cdate.month
        let day = cdate.day
        if year == nil || month == nil || day == nil {
            return nil
        }
        let week = weekLine(year!, month!, day!)
        let wday = dateComponents(date(year!, month!, day!)).weekday!
        return week[wday - 1]
    }

    /// 年月日から休日名（代替休日も対象）を取得する
    /// - parameters:
    ///   - year: 年
    ///   - month: 月
    ///   - day: 日
    /// - returns: 休日名または nil
    static func holiday(_ year: Int, _ month: Int, _ day: Int) -> String? {
        return holiday(dateComponents(date(year, month, day)))
    }
}

extension DateComponents {
    /// 休日名を取得
    var jpHoliday: String? {
        get {
            return JPHoliday.holiday(self)
        }
    }
}
```

# 他言語プログラム

**JavaScript版**

https://qiita.com/ikiuo/items/4ab99731ecac076f3608

**Python版**

https://qiita.com/ikiuo/items/ad8b7d2f6a52bced0b32