【TypeScript】日本の祝日判定
日本の祝日を求めるのに、良いライブラリがないか探していた所、Osamu Takeuchi氏のjapanese-holidays-jsを発見。
GitHub - osamutake/japanese-holidays-js: Provides utilities to manipulate japanese holidays.
元ソースは、あまり自分に馴染みのないCoffeeScriptで書かれていて、TypeScriptで使うのに、型定義作ったりラップしたりでも良かったのですが、ソース理解や今後弄りたくなることも考慮して、TypeScriptに焼き変えてみました。
参考程度にどうぞ。
'use strict'; /** * japanese-holidays-js * 日本の休日を JavaScript で計算するためのライブラリです。 * https://github.com/osamutake/japanese-holidays-js * * ↑のライブラリを、coffeescriptからtypescriptに焼き変え */ // 元の時刻から指定時間だけずらした時刻を生成して返す const shiftDate = (date: Date, year: number, mon: number, day: number, hour?: number, min?: number, sec?: number, msec?: number) => { const res = new Date(2000,0,1); res.setTime(date.getTime() + ((((day || 0) * 24 + (hour || 0)) * 60 + (min || 0)) * 60 + (sec || 0)) * 1000 + (msec || 0)); res.setFullYear(res.getFullYear() + (year || 0) + Math.floor((res.getMonth() + (mon || 0)) / 12)); res.setMonth(((res.getMonth() + (mon || 0)) % 12 + 12) % 12); return res; }; const u2j = (d: Date) => { return shiftDate(d,0,0,0,+9); }; const j2u = (d: Date) => { return shiftDate(d,0,0,0,-9); }; const uDate = (y: number, m: number, d: number) => { return new Date(Date.UTC(y,m,d)); }; const jDate = (y: number, m: number, d: number) => { return j2u(uDate(y,m,d)); }; const getJDay = (d: Date) => { return (u2j(d)).getUTCDay(); }; const getJDate = (d: Date) => { return (u2j(d)).getUTCDate(); }; const getJMonth = (d: Date) => { return (u2j(d)).getUTCMonth(); }; const getJFullYear = (d: Date) => { return (u2j(d)).getUTCFullYear(); }; const getJHours = (d: Date) => { return (u2j(d)).getUTCHours(); }; const getJMinutes = (d: Date) => { return (u2j(d)).getUTCMinutes(); }; /** * ヘルパ関数 */ // 年を与えると指定の祝日を返す関数を作成 const simpleHoliday = (month: number, day: number) => { return (year: number) => jDate(year, month-1, day); }; // 年を与えると指定の月の nth 月曜を返す関数を作成 const happyMonday = (month: number, nth: number) => { return (year: number) => { const monday = 1 const first = jDate(year, month-1, 1); const day = (7 - (getJDay(first) - monday)) % 7 + (nth - 1) * 7; return shiftDate(first, 0, 0, day); }; }; // 年を与えると春分の日を返す const shunbunWithTime = (year: number) => { return new Date(-655866700000 + 31556940400 * (year-1949) ); }; const shunbun = (year: number) => { const date = shunbunWithTime(year); return jDate(year, getJMonth(date), getJDate(date)); } // 年を与えると秋分の日を返す const shubunWithTime = (year: number) => { const day = { 1603: 23, 2074: 23, 2355: 23, 2384: 22 }[year]; if (day) { return jDate(year, 9-1, day); } return new Date(-671316910000 + 31556910000 * (year-1948)); } const shubun = (year: number) => { const date = shubunWithTime(year); return jDate(year, getJMonth(date), getJDate(date)); } /** * 休日定義 * https://ja.wikipedia.org/wiki/%E5%9B%BD%E6%B0%91%E3%81%AE%E7%A5%9D%E6%97%A5 */ const definition = [ ['元日', simpleHoliday(1, 1), 1949], ['成人の日', simpleHoliday(1, 15), 1949, 1999], ['成人の日', happyMonday(1, 2), 2000], ['建国記念の日', simpleHoliday(2, 11), 1967], ['昭和天皇の大喪の礼', simpleHoliday(2, 24), 1989, 1989], ['春分の日', shunbun, 1949], ['皇太子明仁親王の結婚の儀', simpleHoliday(4, 10), 1959, 1959], ['天皇誕生日', simpleHoliday(4, 29), 1949, 1988], ['みどりの日', simpleHoliday(4, 29), 1989, 2006], ['昭和の日', simpleHoliday(4, 29), 2007], ['憲法記念日', simpleHoliday(5, 3), 1949], ['みどりの日', simpleHoliday(5, 4), 2007], ['こどもの日', simpleHoliday(5, 5), 1949], ['皇太子徳仁親王の結婚の儀', simpleHoliday(6, 9), 1993, 1993], ['海の日', simpleHoliday(7, 20), 1996, 2002], ['海の日', happyMonday(7, 3), 2003], ['山の日', simpleHoliday(8, 11), 2016], ['敬老の日', simpleHoliday(9, 15), 1966, 2002], ['敬老の日', happyMonday(9, 3), 2003], ['秋分の日', shubun, 1948], ['体育の日', simpleHoliday(10, 10), 1966, 1999], ['体育の日', happyMonday(10, 2), 2000], ['文化の日', simpleHoliday(11, 3), 1948], ['即位礼正殿の儀', simpleHoliday(11, 12), 1990, 1990], ['勤労感謝の日',simpleHoliday(11, 23), 1948], ['天皇誕生日', simpleHoliday(12, 23), 1989], ] /** * 休日を与えるとその振替休日を返す * 振り替え休日がなければ null を返す */ const furikaeHoliday = (holiday: Date) => { // 振替休日制度制定前 または 日曜日でない場合 振り替え無し const sunday = 0 if (holiday < jDate(1973, 4-1, 30-1) || getJDay(holiday) != sunday) { return null; } // 日曜日なので一日ずらす let furikae = shiftDate(holiday, 0, 0, 1); // ずらした月曜日が休日でなければ振替休日 if (!isHolidayAt(furikae, false)) { return furikae; } // 旧振り替え制度では1日以上ずらさない if (holiday < jDate(2007, 1-1, 1)) { return null; // たぶんこれに該当する日はないはず? } // 振り替えた結果が休日だったら1日ずつずらす while(true) { furikae = shiftDate(furikae, 0, 0, 1); if (!isHolidayAt(furikae, false)) { return furikae; } } } /** * 休日を与えると、翌日が国民の休日かどうかを判定して、 * 国民の休日であればその日を返す */ const kokuminHoliday = (holiday: Date) => { // 制定前 if (getJFullYear(holiday) < 1988) { return null; } // 2日後が振り替え以外の祝日か if (!isHolidayAt(shiftDate(holiday, 0, 0, 2), false)) { return null; } const sunday = 0 const monday = 1 const kokumin = shiftDate(holiday, 0, 0, 1); if (isHolidayAt(kokumin, false) || // 次の日が祝日 getJDay(kokumin)==sunday || // 次の日が日曜 getJDay(kokumin)==monday // 次の日が月曜(振替休日になる) ) { return null; } return kokumin; } /** * 計算結果をキャッシュする * * holidays[furikae] = { * 1999: * "1,1": "元旦" * "1,15": "成人の日" * ... * } */ interface Holidays { [is_furikae: string]: { [y: number]: ({[month_day: string]: string}) }; } const holidays: Holidays = { true: {}, false: {} } const getHolidaysOf = (y: number, is_furikae = true) => { // キャッシュされていればそれを返す const furikae = is_furikae ? 'true' : 'false'; const cache = holidays[furikae][y]; if (cache) { return cache; } /** * されてなければ計算してキャッシュ * 振替休日を計算するには振替休日以外の休日が計算されて * いないとダメなので、先に計算する */ const wo_furikae: {[month_day: string]: string} = {} definition.forEach((entry) => { const entry0 = <string>entry[0]; const entry1 = <(y: number) => Date>entry[1]; const entry2 = <number>entry[2]; const entry3 = <number>entry[3]; if (entry2 && y < entry2) { return; } // 制定年以前 if (entry3 && entry3 < y) { return; } // 廃止年以降 const holiday = entry1(y); // 休日を計算 if (!holiday) { return; } // 無効であれば無視 const m = getJMonth(holiday) + 1; // 結果を登録 const d = getJDate(holiday); wo_furikae[`${m},${d}`] = entry0; }); holidays['false'][y] = wo_furikae; // 国民の休日を追加する const kokuminHolidays = [] Object.keys(wo_furikae).forEach((month_day) => { const [month, day] = month_day.split(',').map((v) => { return parseInt(v); }); const holiday = kokuminHoliday(jDate(y, month-1, day)); if (!holiday) { return; } const m = getJMonth(holiday) + 1; //結果を登録 const d = getJDate(holiday); kokuminHolidays.push(`${m},${d}`); }); kokuminHolidays.forEach((holiday) => { wo_furikae[holiday] = '国民の休日'; }); // 振替休日を追加する const w_furikae: {[month_day: string]: string} = {}; Object.keys(wo_furikae).forEach((month_day) => { const name = wo_furikae[month_day]; w_furikae[month_day] = name; const [month, day] = month_day.split(',').map((v) => { return parseInt(v); }); const holiday = kokuminHoliday(jDate(y, month-1, day)); if (!holiday) { return; } const m = getJMonth(holiday) + 1; // 結果を登録 const d = getJDate(holiday); w_furikae[`${m},${d}`] = '振替休日'; }); holidays['true'][y] = w_furikae; // 結果を登録 return holidays[furikae][y]; } const isHoliday = (date: Date, is_furikae?: boolean) => { return getHolidaysOf(date.getFullYear(), is_furikae)[`${date.getMonth()+1},${date.getDate()}`]; } const isHolidayAt = (date: Date, is_furikae?: boolean) => { return getHolidaysOf(getJFullYear(date), is_furikae)[`${getJMonth(date)+1},${getJDate(date)}`]; } /** * クラス定義 */ export default { getHolidaysOf: (y: number, is_furikae?: boolean) => { // データを整形する const result: { month: number, day: number, name: string }[] = []; const holidays = getHolidaysOf(y, is_furikae); Object.keys(holidays).forEach((month_day) => { const name = holidays[month_day]; const [month, day] = month_day.split(',').map((v) => { return parseInt(v); }); result.push({month, day, name}); }); // 日付順に並べ直す return result.sort((a,b) => { return (a.month-b.month) || (a.day-b.day); }); }, isHoliday, isHolidayAt, shiftDate, u2j, j2u, jDate, uDate, getJDay, getJDate, getJMonth, getJFullYear, getJHours, getJMinutes, __forTest: { shunbunWithTime: shunbunWithTime, shubunWithTime: shubunWithTime, } };