PCがあれば何でもできる!

へっぽこアラサープログラマーが、覚えたての知識を得意げにお届けします

【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,
  }
};