JavaScript の日付処理で、「第n指定曜日」を求める関数

思わぬ所でハマってしまった。徐々に進化(退化?)。

  function WKNO (month, no, week) {
    var length = (no - 1) * 7;
    month--;
    return function (year) {
      var date = new Date (year, month, 0);
       date.setDate (date.getDate () + ((week + 6 - date.getDay ()) % 7) + length + 1);
      return date;
    };
  }

  function WKNO (month, no, week) {
    var length = no * 7 - 6;
    month--;
    return function (year) {
      var date = new Date (year, month, 0);
       date.setDate (date.getDate () + ((week + 6 - date.getDay ()) % 7) + length);
      return date;
    };
  }

  function WKNO (month, no, week) {
    var length = no * 7 - 6;
    month--;
    return function (year) {
      var date = new Date (year, month, 1);
       date.setDate (((week + 7 - date.getDay ()) % 7) + length);
      return date;
    };
  }

  function WKNO (month, no, week) {
    var length = no * 7 - 6;
    month --;
    week += 7;
    return function (year) {
      var date = new Date (year, month, 1);
       date.setDate ((week - date.getDay ()) % 7 + length);
      return date;
    };
  }
  
  
  alert (WKNO(1,5,4)(2013));// 1-31
  alert (WKNO(2,1,0)(2013));// 2-03
  alert (WKNO(3,3,6)(2013));// 3-16
  alert (WKNO(4,5,2)(2013));// 4-30

//2014-06-26
function getNoWeek (date, no, week) {
 var d = new Date (date.getFullYear (), date.getMonth (), 1);
 return (d.setDate (((week + 7) - d.getDay ()) % 7 + (no * 7 - 6)), d);
}

振替休日が・・・

以下、書きかけ中。

<!DOCTYPE html>
<title>Calendar</title>
<meta charset="UTF-8"/>
<style>
table.calendar {
  border-collapse: collapse;
  float: left;
  font-size : small;
  margin : 1em 2em;
  border : none;
  width :30em;
  height :17em;
}

table.calendar caption {
  text-align: left;
  padding: .5ex 1em;
  color : green;
  font-size: large;

}

table.calendar tr > * {
  color : #666;
  border : none;
  text-align : center;
  padding : .5ex;
}

table.calendar tr > *:nth-of-type(1) {
  color : #f22;
}
table.calendar tr > *:nth-of-type(7) {
  color : #22f;
}

table.calendar th {
  background : #eee;
  height : 1em;
}

table.calendar td {
  border-bottom : 1px #ddd solid;
}

</style>

<table class="calendar">
  <caption>2013-1</caption>
</table>

<table class="calendar" border="1">
  <caption>2013-2</caption>
</table>

<table class="calendar" border="1">
  <caption>2013-3</caption>
</table>

<table class="calendar" border="1">
  <caption>2013-4</caption>
</table>

<table class="calendar" border="1">
  <caption>2013-5</caption>
</table>

<table class="calendar" border="1">
  <caption>2013-6</caption>
</table>


<script>
(function HolidayJPN () {

  var YEAR_BEGIN  = 1948; // 制定1948-7-20 なので以前はエラーにする
  var YAER_END    = 2100; // MEMORY に影響するので、制限を設ける
  var YEAR_MEMORY = [ ];  // 問い合わせのあった西暦年の祝日の配列を、バッファーに溜め込む
  var QUICK_FLAG  = 0;    // 0:年が設定される度に記憶, 1:最初に全て計算し記憶
  
  var floor = Math.floor;
  
  var WEEK_STR = [
    ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
    ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
    ['日曜', '月曜', '火曜', '水曜', '木曜', '金曜', '土曜'],
    ['日', '月', '火', '水', '木', '金', '土']
  ];

  var MONTH_STR = [
    ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
    ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'June', 'July', 'Aug.','Sept.', 'Oct.', 'Nov.', 'Dec.'],
    ['睦月', '如月', '弥生', '卯月', '皐月', '水無月', '文月', '葉月', '長月', '神無月', '霜月', '師走'],
    ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
  ] 
  
  var SHUKUJITU_NAME = [
    '元日',     '成人の日',   '建国記念の日',
    '春分の日', '天皇誕生日', 'みどりの日',
    '昭和の日', '憲法記念日', 'こどもの日',
    '海の日',   '敬老の日',   '秋分の日',
    '体育の日', '文化の日',   '勤労感謝の日',
    '振替休日', '国民の休日'
  ];
  
  var SHUKUJITU_NOTE = [
    '年のはじめを祝う', '大人として青年を励ます', '建国をしのび、国を愛する心を養う',
    '自然をたたえ、生物をいつくしむ', '天皇の誕生日を祝う', '自然をたたえ、生物をいつくしむ',
    '昭和天皇の誕生日', '日本国憲法の施行を記念し、国の成長を期する', 'こどもの幸福をはかるとともに、母に感謝する',
    '海の恩恵に感謝し、海洋国日本の繁栄を願う', '老人を敬愛し、長寿を祝う', '祖先をうやまい、なくなった人々をしのぶ',
    'スポーツにしたしみ、健康な心身をつちかう', '自由と平和を愛し、文化をすすめる', '生産を祝い、国民たがいに感謝しあう',
    '祝日が日曜日にあたるときは、その翌日', '前日及び翌日が「国民の祝日」である平日は、休日'
  ];

  var SHUKUJITU_LIST = [
    new HD ('元日',         MMDD (1, 1)),
    new HD ('成人の日',     MMDD (1, 15),  1949, 1999),
    new HD ('成人の日',     WKNO (1, 2, 1),2000),
    new HD ('建国記念の日', MMDD (2, 11),  1967),
    new HD ('春分の日',     SHUNBUN),
    new HD ('天皇誕生日',   MMDD (4, 29),     0,  1988),
    new HD ('みどりの日',   MMDD (4, 29),  1989, 2006),
    new HD ('昭和の日',     MMDD (4, 29),  2007),
    new HD ('憲法記念日',   MMDD (5, 3)),
    new HD ('みどりの日',   MMDD (5, 4),   2007),
    new HD ('こどもの日',   MMDD (5, 5)),
    new HD ('海の日',       MMDD (7, 20),  1966, 2002),
    new HD ('海の日',       WKNO (7, 3, 1),2003),
    new HD ('敬老の日',     MMDD (9, 15),  1966, 2002),
    new HD ('敬老の日',     WKNO (9, 3, 1),2003),
    new HD ('秋分の日',     SHUUBUN),
    new HD ('体育の日',     MMDD (10,10),  1966, 1999),
    new HD ('体育の日',     WKNO (10, 2, 1),2000),
    new HD ('文化の日',     MMDD (11, 3)),
    new HD ('勤労感謝の日', MMDD (11, 23)),
    new HD ('天皇誕生日',   MMDD (12, 23), 1989),
//    new HD ('振替休日',     FURIKAE,       1973),
//    new HD ('国民の休日',   KOKUMIN,       1988),
    new HD ('皇太子・明仁親王の結婚の儀',       MMDD (4, 10), 1959, 1959),
    new HD ('昭和天皇の大喪の礼',              MMDD (2, 24), 1989, 1989), //祝日と扱うのは如何なものか。
    new HD ('即位の礼正殿の儀',                MMDD (11, 12), 1990, 1900),
    new HD ('皇太子・皇太子徳仁親王の結婚の儀', MMDD (6, 9), 1993, 1993)
  ];
  
  //____________________________________________
  function MMDD (month, day) {
    month--;
    return function (year) { return new Date (year, month, day); };
  }


  function WKNO (month, no, week) {
    var length = no * 7 - 6;
    month --;
    week += 7;
    return function (year) {
      var date = new Date (year, month, 1);
       date.setDate ((week - date.getDay ()) % 7 + length);
      return date;
    };
  }
  
  
  function SHUNBUN (year) { // 1900-2099
    var day;
    switch (year % 4) {
    case 0 : day = year <= 1956 ? 21: year <= 2088 ? 20: 19; break;
    case 1 : day = year <= 1989 ? 21: 20; break;
    case 2 : day = year <= 2022 ? 21: 20; break;
    case 3 : day = year <= 1923 ? 22: year <= 2055 ? 21: 20; break;
    }
    return new Date (year, 2, day);
  }

 
  function SHUUBUN (year) { // 1900-2099
    var day;
    switch (year % 4) {
    case 0 : day = year <= 2008 ? 23: 22; break;
    case 1 : day = year <= 1917 ? 24: year <= 2041 ? 23: 22; break;
    case 2 : day = year <= 1946 ? 24: year <= 2074 ? 23: 22; break;
    case 3 : day = year <= 1979 ? 24: 23; break;
    }
    return new Date (year, 8, day);
  }
  
  function FURIKAE () {
    new Date;
  }
  
  function KOKUMIN () {
    new Date;
  }
  
 
  //____________________________________________
  
  // 'HD' object
  function HD (name, checkerFunc, sYear, eYear) {
    this.name   = name;
    this.create = checkerFunc;
    this.sYear  = isNaN (sYear) ? YEAR_BEGIN: sYear;
    this.eYear  = isNaN (eYear) ? YAER_END: eYear;
  }
  
  // Holiday Object
  function Holiday (name, dateObj) {
    this.name = name;
    this.date = dateObj;
  }

  //____________________________________________
  
  // 指定された年の祝日を配列で取得する
  function getHolidaysByYear (year) {
    if (arguments.length < 1)
      year = (new Date).getFullYear ();

    var result =
      (YEAR_MEMORY[year]) ||
      (SHUKUJITU_LIST
        .filter (isTargetYear, year)
        .map (newHoliday, year)
        .sort (daysSort));

    return (result.length)
    ? (YEAR_MEMORY[yy] = result)
    : null;
  }
   
  // HD が年を対象としているか?
  function isTargetYear (hd) { return ! (this < hd.sYear || hd.eYear < this); }

  // holidayObject を生成
  function newHoliday (hd) { return new Holiday (hd.name, hd.create (this)); }

  // 日付順で並び替えをする
  function daysSort (a, b) { return a.date.getTime () > b.date.getTime (); }

  // その祝日の説明
  function getHolidayNote (name) {
    var no = SHUKUJITU_NAME.indexOf (name);
    return (0 <= no) ? SHUKUJITU_NOTE[no]: '';
  }
  
  
  // 与えられた日付が、祝日であれば、{name: holiday, date: dateObject } を返す。
  function isHoliday (date) {
    var yy, holidays, result;
    
    if (arguments.length < 1)
      date = new Date;

    date = theDate (date);
    yy = date.getFullYear ();
    holidays = YEAR_MEMORY[yy] || getHolidaysByYear (yy);
    result = holidays.filter (isSameDate, date);
    
    return (result.length) ? result[0]: null;
  }
  
  // {date: obj} == this を探す
  function isSameDate (holiday) { return holiday == this; } 

  // 時間部分を消す
  function theDate (dt) { return new Date (dt.getFullYear (), dt.getMonth ()+1, dt.getDate ()); }
 
  //____________________________________________

  // 与えられた年が範囲内か?
  function checkYear (yy) {
    if (isNaN (yy))
       throw new Error ('指定した年が、想定外の値です');
    if (yy < YEAR_BEGIN)
      throw new Error ('指定した年が、想定外の値です');
    if (YAER_END < yy)
      throw new Error ('指定した年が、想定外の値です');
   
    return true;
  }

  //____________________________________________

  // 初期化すべきこと
  
  switch (QUICK_FLAG) {
  case 1 : // 全て前処理で計算を済ませる
    for (var y = YEAR_BEGIN; y <= YAER_END ; y++)
      YEAR_MEMORY[y] = getHolidaysByYear (y);
    break;

  case 0 : default : // 今年だけ
    var y = (new Date).getFullYear ();
    YEAR_MEMORY[y] = getHolidaysByYear (y);
  }

  //____________________________________________
  
  var Holiday = new Function;
  
    Holiday.getHolidayNote    = getHolidayNote;     //祝日名の内容を返す
    Holiday.isHoliday         = isHoliday;          // dateObject が、祝日か?
    Holiday.getHolidaysByYear = getHolidaysByYear;  //その年にある祝日を配列にして返す
  
  this.Holiday = Holiday;

}) ();





(function () {
  var A = Array.prototype;
  var doc = document;
  var yyyymm_reg = /(\d{4}).*(\d{1,2})/;
  var num = '123456789abcdefghijklmnopqrstuv';
  var youbi = ['日','月', '火', '水', '木', '金', '土'];
 
  function Calendar () { ; }

  
  function insertTH (t) {
    var th = this.ownerDocument.createElement ('th');
    th.textContent = t;
    this.appendChild (th);
  }
  
  function insertTD (t) {
    this.insertCell (-1).textContent = t;
  }
  
  function insertTR (r) {
    r.forEach (insertTD, this.insertRow (-1));
  }
  
  function insertTRh (r) {
    r.forEach (insertTH, this.insertRow (-1));
  }
  
  function appendTbody (t, a) {
    a.forEach (insertTR, t);
  }

  
  function ArraySplit (ary, n) {
    return (n < ary.length)
      ? A.concat.apply ([ary.splice (0, n)], [ArraySplit (ary, n)]): [ary];
  }
  
  function makeDays (date) {
    var matubi = new Date (date.getFullYear (), date.getMonth () + 1, 0);
    var dayMax = matubi.getDate ();
    var week = matubi.getDay ();
    var begin = (7 + week - (dayMax % 7)) % 7 + 2;
    var map = [
          (new Array (begin)).join (' '),
          (num.substring (0, dayMax)),
          (new Array (7 - week)).join (' ')
       ].join('').split ('').map (function (c) { return ' ' === c ? c: parseInt (c, 32); });

    return ArraySplit (map, 7);
  }
  
  function dayFormat (date) {
    return [
      date.getFullYear (), '年',
      date.getMonth () + 1, '月'
    ].join (' ');
  }

 
  function theMonthObj (str) {
    var ym = yyyymm_reg.exec (str);
    return (ym) ? new Date (ym[1], ym[2]-1, 1): new Date;
  }

  
  function create (table) {
    var caption = table.querySelector ('caption');
    var tbody = doc.createElement ('tbody');
    var base = theMonthObj (caption ? caption.textContent: null);

    if (caption)
      caption.textContent = dayFormat (base);

    insertTRh.call (tbody, youbi);
    appendTbody (tbody, makeDays (base));
    table.appendChild (tbody);
   }
  
  Calendar.create = create;
  
  this.Calendar = Calendar;
}) ();



var table = document.querySelectorAll ('table.calendar');
Array.prototype.forEach.call (table, Calendar.create);

var h = Holiday.getHolidaysByYear (2013);
alert(h.length);

var s = h.map (function (h) { return [h.name, h.date.getMonth ()+1, h.date.getDate ()]; });
alert (s.join ('\n'));

</script>