68fed087-9049-446f-bfc5-3f8.../Sources/engine/Holidays.swift

107 lines
4.7 KiB
Swift
Raw Normal View History

//
// Holidays.swift
// plugin v0.3.0
//
// China State Council statutory holidays for 2026. Calibrated to the
// + + typical pattern; **the official
// announcement (usually released Nov of prior year)** is the
// ultimate source of truth. If a date here disagrees with
// notice when published, edit this file and re-release.
//
// Reference dates used ( yyyy-MM-dd ):
// 2026-02-17 ()
// 2026-04-05 ()
// 2026-06-19 ()
// 2026-09-25 ()
//
// In years where falls within 6 days of , the State Council
// typically merges them into a single 8-10 day window. 2026 has
// 09-25 (Fri) and 10-01 (Thu) window 09-25 ~ 10-04 is the
// most likely pattern (with 10-10 Saturday or earlier 09-20 Sunday
// for makeup work day).
//
// Data structure is a flat list rather than a JSON resource so the
// plugin bundle stays single-binary and dependency-free.
//
import Foundation
struct Holiday {
/// "" / "" / "" / etc.
let name: String
/// First day of the holiday window (yyyy-MM-dd, Asia/Shanghai).
let startDate: String
/// Total consecutive days off, including the trigger day.
let days: Int
}
/// Resolved upcoming holiday with the days remaining and start Date.
struct UpcomingHoliday {
let name: String
let startDate: Date
let days: Int
/// Days from now to startDate. 0 = today is day 1 of the holiday.
let daysUntil: Int
/// True when "now" falls inside [startDate, startDate+days).
let isOngoing: Bool
}
enum HolidayDatabase {
/// 2026 best estimate per typical State Council pattern.
/// VERIFY against the official when published.
///
/// starts on (the day before ) per 2024 policy update.
/// + merged because 2026 09-25 falls 6 days before
/// 10-01 same pattern as 2017 / 2020 when they were close.
static let entries2026: [Holiday] = [
// 2024 1
Holiday(name: "元旦", startDate: "2026-01-01", days: 1),
// 02-16 ~ 02-22 7 02-14 + 02-28
Holiday(name: "春节", startDate: "2026-02-16", days: 7),
// 04-05 04-04 ~ 04-06 3
Holiday(name: "清明", startDate: "2026-04-04", days: 3),
// 05-01 ~ 05-05 5 04-26
Holiday(name: "劳动节", startDate: "2026-05-01", days: 5),
// 06-19 ~ 06-21 3
Holiday(name: "端午", startDate: "2026-06-19", days: 3),
// + 09-25 ~ 10-04 10
// 09-19 / 10-10
Holiday(name: "中秋·国庆", startDate: "2026-09-25", days: 10),
// roll-over 2027 01-01 ~ 01-03 3
Holiday(name: "元旦", startDate: "2027-01-01", days: 3),
]
/// Returns the soonest upcoming or currently-ongoing holiday.
/// `now` is injectable for testability.
static func upcoming(from now: Date, timeZone: TimeZone) -> UpcomingHoliday? {
let fmt = DateFormatter()
fmt.locale = Locale(identifier: "en_US_POSIX")
fmt.timeZone = timeZone
fmt.dateFormat = "yyyy-MM-dd"
var cal = Calendar(identifier: .gregorian)
cal.timeZone = timeZone
for h in entries2026 {
guard let start = fmt.date(from: h.startDate) else { continue }
guard let end = cal.date(byAdding: .day, value: h.days, to: start) else { continue }
if now >= start && now < end {
// In the holiday window already.
return UpcomingHoliday(
name: h.name, startDate: start,
days: h.days, daysUntil: 0, isOngoing: true
)
}
if start > now {
// Floor-divide of seconds to days for an integer answer
// independent of locale calendar shenanigans.
let secs = start.timeIntervalSince(now)
let daysUntil = Int(ceil(secs / 86400.0))
return UpcomingHoliday(
name: h.name, startDate: start,
days: h.days, daysUntil: daysUntil, isOngoing: false
)
}
}
return nil
}
}