0
0

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

Posted at

DateComponents オブジェクトの拡張

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

Consoleアプリ例
print(DateComponents(year: 2024, month: 3, day: 20).jpHoliday!)
実行結果
春分の日

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 秋分の日を取得 -

注意:春分/秋分の日は大雑把な計算で求めています

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!)")
})
実行結果
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 拡張コード

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版

Python版

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0