📅

指定年月の指定曜日の出現回数を調べる

2023/06/11に公開

諸事情で今月の月曜日が何日あるか調べる必要がでてきました。
今話題のChatGPT君に問い合わせた結果、ループを使ってカウントしない方法では正確にできませんと言われてしまいました。(Zellerの公式を利用したコードを紹介されたがでたらめな答えが返ってくるので使い物にならない。)

また検索してもそれっぽいのが見つからず、しょうがないので自分で考えてみることにします。

カレンダーのパターン

まずカレンダーを見てみます。

Su Mo Tu We Th Fr Sa
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30

これは2023年4月のカレンダーです。月曜日の数は4です。
ここでまずカレンダーの重要な特徴があります。

月は最小28日= 7 * 4 なので4週間は確実に存在します。つまり4は確定です。

で、上のカレンダーを見ると30日で終わりなので4月の月曜日は4つなのですが、もしこれが31日まであれば5つになるのがわかるかと思います。

ここで重要なのははみ出しものです。上下にいますね。これのパターンを考えてみます。

はみ出してるようではみ出してない
⬛⬛⬛🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦

これは28日しかない2月のパターンですね。とりあえず特別なのは 29 30 31 なので計算の対象は「閏年の2月」と「他の月すべて」であることがわかります。

では29~31を🟥で塗ったカレンダーを見てみます。

パターン1 月末がはみ出る
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟥🟥🟥

⬛⬛🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟥🟥🟥

⬛⬛⬛⬛🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟥🟥🟥

ここらへんは29日の曜日が開始曜日以降ならみたいな感じで判定できそうです。

パターン2 月末と月初がはみ出る
⬛⬛⬛⬛⬛🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟦🟦
🟦🟦🟦🟦🟦🟥🟥
🟥

こちらは月初と月末がはみ出るケースです。すごく面倒くさそうです。
前者は周の真ん中が4日で端が5日あるケースで、後者は真ん中に5日あるが端が4日のケースです。

月末の情報から月初の情報を計算する

まず月初めの曜日を取得します。最終日を7で割った余りがはみ出している量なので最終日の曜日番号からその日数を引き+1すれば曜日番号が出てきそうです。
ただし曜日番号は 0~6 の範囲なので引き算で負の数が発生しかねません。なので調整として曜日番号に7を足すといった工夫をします。計算式をまとめると以下です。

(7 + 最終日の曜日番号 - (最終日 % 7) + 1) % 7

例えば2023/6の最終日は30日で金曜日(曜日番号5)なので (7 + 5 - (30 % 7) + 1) % 7 を計算して 4 となるので6/1は木曜日です。
他にも2023/5は最終日が31日で水曜日(曜日番号3)なので (7 + 3 - (31 % 7) + 1) % 7 = 1 となり、月曜日になります。

後は月初の曜日と月末の曜日、そして数えたい曜日の番号の関係性をなんとか考えればいけそうです。

パターン1 月末がはみ出る

これはなんとかなりそうです。
というのも 月初の曜日 < 指定の曜日 < 月末の曜日 の場合は5日あり、そうでない場合は4日あると計算可能です。

firstWeekDay <= weekday && weekday <= lastWeekDay ? 5 : 4

パターン2 月末と月初がはみ出る

この場合は指定した曜日番号が月末の曜日番号以下、もしくは月初の曜日番号以降ならば5日あるという判定が可能です。

weekday <= lastWeekDay || firstWeekDay <= weekday ? 5 : 4

とりあえずif文で分岐すれば正しい値は出そうです。

実装

では実装してみます。

/**
 * @param weekday 0-6
 * @param year
 * @param month 1-12
 * @returns
 */
function countWeekdaysInMonth(weekday, year, month) {
	// 最終日=来月の0日(だたしDateの月は0-11指定で引数として与えられるのは1-12なのでそのまま渡せば来月となる)
	const last = new Date(year, month, 0);
	// 最終日を取得
	const lastDay = last.getDate();

	// 最終日が28なら確定で4
	if (lastDay === 28) {
		return 4;
	}

	// 月末の曜日を取得
	const lastWeekDay = last.getDay();
	// 月初の曜日を計算
	const firstWeekDay = (7 + lastWeekDay - (lastDay % 7) + 1) % 7;

	// 月初の曜日 <= 月末の曜日
	if (firstWeekDay <= lastWeekDay) {
		// 月初と月末に挟まれた曜日なら5日ある
		return firstWeekDay <= weekday && weekday <= lastWeekDay ? 5 : 4;
	}

	// 月末の曜日 < 月初の曜日
	// 月末の曜日以下か月初の曜日以上なら5日ある。
	return weekday <= lastWeekDay || firstWeekDay <= weekday ? 5 : 4;
}

実行結果です。

// パターン1の5日
countWeekdaysInMonth(2, 2023, 5);
5
// パターン1の4日
countWeekdaysInMonth(5, 2023, 5);
4
// パターン2の5日
countWeekdaysInMonth(4, 2023, 6);
5
// パターン2の4日
countWeekdaysInMonth(2, 2023, 6);
4

正しそうですね。

テスト

さて、これをテストしたいのですが、テストはどうすればよいでしょうか?いくつか年をピックアップして答え合わせするという方法もあるかもしれません。
しかし、年間カレンダーにはパターンがあります。まず1/1の開始曜日の違いで7種類、閏年の有無でもう7種類の14パターンです。

つまり、12ヶ月分入れても 14 * 12 = 168 のデータセットしかないです。この程度なら全部書き出してテストをしてしまいましょう。

まずはテストパターンを抽出します。各曜日から始まる年と、閏年かつ各曜日から始まる年の一覧を作り、その年の各月の曜日の数をカウントしてデータセットを作ります。

// 対象年の記録。キー名は [1/1の曜日]_[閏年の有無(0 or 1)]` とします。
const years: { [keys: string]: number } = {};

// 確認しやすいよう今年から年を減らしてパターンを調べる
for (let year = new Date().getFullYear(); 1900 <= year; --year) {
	// 対象年の1/1の曜日を取得
	const startWeekday = new Date(year, 0, 1).getDay();
	// 対象年の3月0日(2/29か28)を取得
	const isLeapYear = new Date(year, 2, 0).getDate() === 29;
	const key = `${startWeekday}_${isLeapYear ? 1 : 0}`;
	if (years[key]) {
		// すでに存在しているので次に進む
		continue;
	}
	// 存在していないので追加
	years[key] = year;
	// パターンが14になったら終了
	if (14 <= Object.keys(years).length) {
		break;
	}
}
console.log(years);

// monthは0-11
function countWeekday(year, month) {
	const weekdays = [0, 0, 0, 0, 0, 0, 0];
	const date = new Date(year, month, 1);
	let weekday = date.getDay();
	const lastDay = new Date(year, month + 1, 0).getDate();
	for (let day = 1; day <= lastDay; ++day) {
		++weekdays[weekday];
		weekday = (weekday + 1) % 7;
	}
	return weekdays;
}

// JSON出力用
const result: {
	isLeapYear: boolean;
	startWeekDay: number;
	year: number;
	1: number[];
	2: number[];
	3: number[];
	4: number[];
	5: number[];
	6: number[];
	7: number[];
	8: number[];
	9: number[];
	10: number[];
	11: number[];
	12: number[];
}[] = [];

console.log('Normal year:');
for (let weekday = 0 ;weekday < 7; ++weekday) {
	const year = years[`${weekday}_0`];
	const data = <any>{
		isLeapYear: false,
		startWeekDay: weekday,
		year: year,
	};
	result.push(data);
	console.log(`${year}:`);
	for (let month = 0 ;month < 12; ++month) {
		data[month + 1] = countWeekday(year, month);
		console.log(`  ${new String(month + 1).padStart(2, ' ')}: ${data[month + 1].join(' ')}`);
	}
}

console.log('Leap year:');
for (let weekday = 0 ;weekday < 7; ++weekday) {
	const year = years[`${weekday}_1`];
	const data = <any>{
		isLeapYear: true,
		startWeekDay: weekday,
		year: year,
	};
	result.push(data);
	console.log(`${year}:`);
	for (let month = 0 ;month < 12; ++month) {
		data[month + 1] = countWeekday(year, month);
		console.log(`  ${new String(month + 1).padStart(2, ' ')}: ${data[month + 1].join(' ')}`);
	}
}

Deno.writeTextFile('dataset.json', JSON.stringify(result));

実行結果は以下です。

Normal year:
2023:
   1: 5 5 5 4 4 4 4
   2: 4 4 4 4 4 4 4
   3: 4 4 4 5 5 5 4
   4: 5 4 4 4 4 4 5
   5: 4 5 5 5 4 4 4
   6: 4 4 4 4 5 5 4
   7: 5 5 4 4 4 4 5
   8: 4 4 5 5 5 4 4
   9: 4 4 4 4 4 5 5
  10: 5 5 5 4 4 4 4
  11: 4 4 4 5 5 4 4
  12: 5 4 4 4 4 5 5
2018:
   1: 4 5 5 5 4 4 4
   2: 4 4 4 4 4 4 4
   3: 4 4 4 4 5 5 5
   4: 5 5 4 4 4 4 4
   5: 4 4 5 5 5 4 4
   6: 4 4 4 4 4 5 5
   7: 5 5 5 4 4 4 4
   8: 4 4 4 5 5 5 4
   9: 5 4 4 4 4 4 5
  10: 4 5 5 5 4 4 4
  11: 4 4 4 4 5 5 4
  12: 5 5 4 4 4 4 5
2019:
   1: 4 4 5 5 5 4 4
   2: 4 4 4 4 4 4 4
   3: 5 4 4 4 4 5 5
   4: 4 5 5 4 4 4 4
   5: 4 4 4 5 5 5 4
   6: 5 4 4 4 4 4 5
   7: 4 5 5 5 4 4 4
   8: 4 4 4 4 5 5 5
   9: 5 5 4 4 4 4 4
  10: 4 4 5 5 5 4 4
  11: 4 4 4 4 4 5 5
  12: 5 5 5 4 4 4 4
2014:
   1: 4 4 4 5 5 5 4
   2: 4 4 4 4 4 4 4
   3: 5 5 4 4 4 4 5
   4: 4 4 5 5 4 4 4
   5: 4 4 4 4 5 5 5
   6: 5 5 4 4 4 4 4
   7: 4 4 5 5 5 4 4
   8: 5 4 4 4 4 5 5
   9: 4 5 5 4 4 4 4
  10: 4 4 4 5 5 5 4
  11: 5 4 4 4 4 4 5
  12: 4 5 5 5 4 4 4
2015:
   1: 4 4 4 4 5 5 5
   2: 4 4 4 4 4 4 4
   3: 5 5 5 4 4 4 4
   4: 4 4 4 5 5 4 4
   5: 5 4 4 4 4 5 5
   6: 4 5 5 4 4 4 4
   7: 4 4 4 5 5 5 4
   8: 5 5 4 4 4 4 5
   9: 4 4 5 5 4 4 4
  10: 4 4 4 4 5 5 5
  11: 5 5 4 4 4 4 4
  12: 4 4 5 5 5 4 4
2021:
   1: 5 4 4 4 4 5 5
   2: 4 4 4 4 4 4 4
   3: 4 5 5 5 4 4 4
   4: 4 4 4 4 5 5 4
   5: 5 5 4 4 4 4 5
   6: 4 4 5 5 4 4 4
   7: 4 4 4 4 5 5 5
   8: 5 5 5 4 4 4 4
   9: 4 4 4 5 5 4 4
  10: 5 4 4 4 4 5 5
  11: 4 5 5 4 4 4 4
  12: 4 4 4 5 5 5 4
2022:
   1: 5 5 4 4 4 4 5
   2: 4 4 4 4 4 4 4
   3: 4 4 5 5 5 4 4
   4: 4 4 4 4 4 5 5
   5: 5 5 5 4 4 4 4
   6: 4 4 4 5 5 4 4
   7: 5 4 4 4 4 5 5
   8: 4 5 5 5 4 4 4
   9: 4 4 4 4 5 5 4
  10: 5 5 4 4 4 4 5
  11: 4 4 5 5 4 4 4
  12: 4 4 4 4 5 5 5
Leap year:
2012:
   1: 5 5 5 4 4 4 4
   2: 4 4 4 5 4 4 4
   3: 4 4 4 4 5 5 5
   4: 5 5 4 4 4 4 4
   5: 4 4 5 5 5 4 4
   6: 4 4 4 4 4 5 5
   7: 5 5 5 4 4 4 4
   8: 4 4 4 5 5 5 4
   9: 5 4 4 4 4 4 5
  10: 4 5 5 5 4 4 4
  11: 4 4 4 4 5 5 4
  12: 5 5 4 4 4 4 5
1996:
   1: 4 5 5 5 4 4 4
   2: 4 4 4 4 5 4 4
   3: 5 4 4 4 4 5 5
   4: 4 5 5 4 4 4 4
   5: 4 4 4 5 5 5 4
   6: 5 4 4 4 4 4 5
   7: 4 5 5 5 4 4 4
   8: 4 4 4 4 5 5 5
   9: 5 5 4 4 4 4 4
  10: 4 4 5 5 5 4 4
  11: 4 4 4 4 4 5 5
  12: 5 5 5 4 4 4 4
2008:
   1: 4 4 5 5 5 4 4
   2: 4 4 4 4 4 5 4
   3: 5 5 4 4 4 4 5
   4: 4 4 5 5 4 4 4
   5: 4 4 4 4 5 5 5
   6: 5 5 4 4 4 4 4
   7: 4 4 5 5 5 4 4
   8: 5 4 4 4 4 5 5
   9: 4 5 5 4 4 4 4
  10: 4 4 4 5 5 5 4
  11: 5 4 4 4 4 4 5
  12: 4 5 5 5 4 4 4
2020:
   1: 4 4 4 5 5 5 4
   2: 4 4 4 4 4 4 5
   3: 5 5 5 4 4 4 4
   4: 4 4 4 5 5 4 4
   5: 5 4 4 4 4 5 5
   6: 4 5 5 4 4 4 4
   7: 4 4 4 5 5 5 4
   8: 5 5 4 4 4 4 5
   9: 4 4 5 5 4 4 4
  10: 4 4 4 4 5 5 5
  11: 5 5 4 4 4 4 4
  12: 4 4 5 5 5 4 4
2004:
   1: 4 4 4 4 5 5 5
   2: 5 4 4 4 4 4 4
   3: 4 5 5 5 4 4 4
   4: 4 4 4 4 5 5 4
   5: 5 5 4 4 4 4 5
   6: 4 4 5 5 4 4 4
   7: 4 4 4 4 5 5 5
   8: 5 5 5 4 4 4 4
   9: 4 4 4 5 5 4 4
  10: 5 4 4 4 4 5 5
  11: 4 5 5 4 4 4 4
  12: 4 4 4 5 5 5 4
2016:
   1: 5 4 4 4 4 5 5
   2: 4 5 4 4 4 4 4
   3: 4 4 5 5 5 4 4
   4: 4 4 4 4 4 5 5
   5: 5 5 5 4 4 4 4
   6: 4 4 4 5 5 4 4
   7: 5 4 4 4 4 5 5
   8: 4 5 5 5 4 4 4
   9: 4 4 4 4 5 5 4
  10: 5 5 4 4 4 4 5
  11: 4 4 5 5 4 4 4
  12: 4 4 4 4 5 5 5
2000:
   1: 5 5 4 4 4 4 5
   2: 4 4 5 4 4 4 4
   3: 4 4 4 5 5 5 4
   4: 5 4 4 4 4 4 5
   5: 4 5 5 5 4 4 4
   6: 4 4 4 4 5 5 4
   7: 5 5 4 4 4 4 5
   8: 4 4 5 5 5 4 4
   9: 4 4 4 4 4 5 5
  10: 5 5 5 4 4 4 4
  11: 4 4 4 5 5 4 4
  12: 5 4 4 4 4 5 5

とりあえず今年(2023)と閏年で一番近い2020を確認しますがカウントがあってるので大丈夫でしょう。後はこのデータセットを元にテストを実装します。

countWeekdaysInMonth.test.ts
/**
 * @param weekday 0-6
 * @param year
 * @param month 1-12
 * @returns
 */
function countWeekdaysInMonth(weekday: number, year: number, month: number) {
	// 最終日=来月の0日(だたしDateの月は0-11指定で引数として与えられるのは1-12なのでそのまま渡せば来月となる)
	const last = new Date(year, month, 0);
	// 最終日を取得
	const lastDay = last.getDate();

	// 最終日が28なら確定で4
	if (lastDay === 28) {
		return 4;
	}

	// 月末の曜日を取得
	const lastWeekDay = last.getDay();
	// 月初の曜日を計算
	const firstWeekDay = (7 + lastWeekDay - (lastDay % 7) + 1) % 7;

	// 月初の曜日 <= 月末の曜日
	if (firstWeekDay <= lastWeekDay) {
		// 月初と月末に挟まれた曜日なら5日ある
		return firstWeekDay <= weekday && weekday <= lastWeekDay ? 5 : 4;
	}

	// 月末の曜日 < 月初の曜日
	// 月末の曜日以下か月初の曜日以上なら5日ある。
	return weekday <= lastWeekDay || firstWeekDay <= weekday ? 5 : 4;
}

const dataset = [
	{
		isLeapYear: false,
		startWeekDay: 0,
		year: 2023,
		1: [5, 5, 5, 4, 4, 4, 4],
		2: [4, 4, 4, 4, 4, 4, 4],
		3: [4, 4, 4, 5, 5, 5, 4],
		4: [5, 4, 4, 4, 4, 4, 5],
		5: [4, 5, 5, 5, 4, 4, 4],
		6: [4, 4, 4, 4, 5, 5, 4],
		7: [5, 5, 4, 4, 4, 4, 5],
		8: [4, 4, 5, 5, 5, 4, 4],
		9: [4, 4, 4, 4, 4, 5, 5],
		10: [5, 5, 5, 4, 4, 4, 4],
		11: [4, 4, 4, 5, 5, 4, 4],
		12: [5, 4, 4, 4, 4, 5, 5],
	},
	// 先程出力したすべてのカレンダーデータ(長いので省略)
];

import * as asserts from 'https://deno.land/std@0.191.0/testing/asserts.ts';

const WEEKDAY_NAME = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
dataset.forEach((data) => {
	const year = data.year;
	for (let month = 1; month <= 12; ++month) {
		Deno.test(`${ data.isLeapYear ? 'Leap' : 'Normal' } year(${WEEKDAY_NAME[data.startWeekDay]} start): ${year} / ${month}`, () => {
			const weekdays = data[<1> month];
			const counts: number[] = [];
			for (let weekday = 0; weekday < 7; ++weekday) {
				counts.push(countWeekdaysInMonth(weekday, year, month));
			}
			asserts.assertEquals(counts.join(','), weekdays.join(','));
		});
	}
});

これですべての年月パターンをテストしたところすべてパスしました。問題なさそうですね。

まとめ

今回ありそうで見つからなかった処理を実装しました。日付系の処理は頑張って大半を式にできそうなのですがどこかの誰かがやってないですかね?

また一応条件式を統一する試作も行いました。

// 月末の曜日が月初の曜日より前にある場合のみ値を調整する
if (lastWeekDay < firstWeekDay) {
	// 月末の曜日番号を7足す
	lastWeekDay += 7;
	// 指定された曜日が月初の曜日より前の場合のみ7足す
	if (weekday < firstWeekDay) {
		weekday += 7;
	}
}
// 月初の曜日 <= 指定曜日 <= 月末の曜日を満たすように調整したのでこの条件式でOK
return firstWeekDay <= weekday && weekday <= lastWeekDay ? 5 : 4;

ただif文多いし前のままでいいかということになりました。スッキリした処理にならないかなぁ。

その他

以下今回使ったテストのデータです。

[{"1":[5,5,5,4,4,4,4],"2":[4,4,4,4,4,4,4],"3":[4,4,4,5,5,5,4],"4":[5,4,4,4,4,4,5],"5":[4,5,5,5,4,4,4],"6":[4,4,4,4,5,5,4],"7":[5,5,4,4,4,4,5],"8":[4,4,5,5,5,4,4],"9":[4,4,4,4,4,5,5],"10":[5,5,5,4,4,4,4],"11":[4,4,4,5,5,4,4],"12":[5,4,4,4,4,5,5],"isLeapYear":false,"startWeekDay":0,"year":2023},{"1":[4,5,5,5,4,4,4],"2":[4,4,4,4,4,4,4],"3":[4,4,4,4,5,5,5],"4":[5,5,4,4,4,4,4],"5":[4,4,5,5,5,4,4],"6":[4,4,4,4,4,5,5],"7":[5,5,5,4,4,4,4],"8":[4,4,4,5,5,5,4],"9":[5,4,4,4,4,4,5],"10":[4,5,5,5,4,4,4],"11":[4,4,4,4,5,5,4],"12":[5,5,4,4,4,4,5],"isLeapYear":false,"startWeekDay":1,"year":2018},{"1":[4,4,5,5,5,4,4],"2":[4,4,4,4,4,4,4],"3":[5,4,4,4,4,5,5],"4":[4,5,5,4,4,4,4],"5":[4,4,4,5,5,5,4],"6":[5,4,4,4,4,4,5],"7":[4,5,5,5,4,4,4],"8":[4,4,4,4,5,5,5],"9":[5,5,4,4,4,4,4],"10":[4,4,5,5,5,4,4],"11":[4,4,4,4,4,5,5],"12":[5,5,5,4,4,4,4],"isLeapYear":false,"startWeekDay":2,"year":2019},{"1":[4,4,4,5,5,5,4],"2":[4,4,4,4,4,4,4],"3":[5,5,4,4,4,4,5],"4":[4,4,5,5,4,4,4],"5":[4,4,4,4,5,5,5],"6":[5,5,4,4,4,4,4],"7":[4,4,5,5,5,4,4],"8":[5,4,4,4,4,5,5],"9":[4,5,5,4,4,4,4],"10":[4,4,4,5,5,5,4],"11":[5,4,4,4,4,4,5],"12":[4,5,5,5,4,4,4],"isLeapYear":false,"startWeekDay":3,"year":2014},{"1":[4,4,4,4,5,5,5],"2":[4,4,4,4,4,4,4],"3":[5,5,5,4,4,4,4],"4":[4,4,4,5,5,4,4],"5":[5,4,4,4,4,5,5],"6":[4,5,5,4,4,4,4],"7":[4,4,4,5,5,5,4],"8":[5,5,4,4,4,4,5],"9":[4,4,5,5,4,4,4],"10":[4,4,4,4,5,5,5],"11":[5,5,4,4,4,4,4],"12":[4,4,5,5,5,4,4],"isLeapYear":false,"startWeekDay":4,"year":2015},{"1":[5,4,4,4,4,5,5],"2":[4,4,4,4,4,4,4],"3":[4,5,5,5,4,4,4],"4":[4,4,4,4,5,5,4],"5":[5,5,4,4,4,4,5],"6":[4,4,5,5,4,4,4],"7":[4,4,4,4,5,5,5],"8":[5,5,5,4,4,4,4],"9":[4,4,4,5,5,4,4],"10":[5,4,4,4,4,5,5],"11":[4,5,5,4,4,4,4],"12":[4,4,4,5,5,5,4],"isLeapYear":false,"startWeekDay":5,"year":2021},{"1":[5,5,4,4,4,4,5],"2":[4,4,4,4,4,4,4],"3":[4,4,5,5,5,4,4],"4":[4,4,4,4,4,5,5],"5":[5,5,5,4,4,4,4],"6":[4,4,4,5,5,4,4],"7":[5,4,4,4,4,5,5],"8":[4,5,5,5,4,4,4],"9":[4,4,4,4,5,5,4],"10":[5,5,4,4,4,4,5],"11":[4,4,5,5,4,4,4],"12":[4,4,4,4,5,5,5],"isLeapYear":false,"startWeekDay":6,"year":2022},{"1":[5,5,5,4,4,4,4],"2":[4,4,4,5,4,4,4],"3":[4,4,4,4,5,5,5],"4":[5,5,4,4,4,4,4],"5":[4,4,5,5,5,4,4],"6":[4,4,4,4,4,5,5],"7":[5,5,5,4,4,4,4],"8":[4,4,4,5,5,5,4],"9":[5,4,4,4,4,4,5],"10":[4,5,5,5,4,4,4],"11":[4,4,4,4,5,5,4],"12":[5,5,4,4,4,4,5],"isLeapYear":true,"startWeekDay":0,"year":2012},{"1":[4,5,5,5,4,4,4],"2":[4,4,4,4,5,4,4],"3":[5,4,4,4,4,5,5],"4":[4,5,5,4,4,4,4],"5":[4,4,4,5,5,5,4],"6":[5,4,4,4,4,4,5],"7":[4,5,5,5,4,4,4],"8":[4,4,4,4,5,5,5],"9":[5,5,4,4,4,4,4],"10":[4,4,5,5,5,4,4],"11":[4,4,4,4,4,5,5],"12":[5,5,5,4,4,4,4],"isLeapYear":true,"startWeekDay":1,"year":1996},{"1":[4,4,5,5,5,4,4],"2":[4,4,4,4,4,5,4],"3":[5,5,4,4,4,4,5],"4":[4,4,5,5,4,4,4],"5":[4,4,4,4,5,5,5],"6":[5,5,4,4,4,4,4],"7":[4,4,5,5,5,4,4],"8":[5,4,4,4,4,5,5],"9":[4,5,5,4,4,4,4],"10":[4,4,4,5,5,5,4],"11":[5,4,4,4,4,4,5],"12":[4,5,5,5,4,4,4],"isLeapYear":true,"startWeekDay":2,"year":2008},{"1":[4,4,4,5,5,5,4],"2":[4,4,4,4,4,4,5],"3":[5,5,5,4,4,4,4],"4":[4,4,4,5,5,4,4],"5":[5,4,4,4,4,5,5],"6":[4,5,5,4,4,4,4],"7":[4,4,4,5,5,5,4],"8":[5,5,4,4,4,4,5],"9":[4,4,5,5,4,4,4],"10":[4,4,4,4,5,5,5],"11":[5,5,4,4,4,4,4],"12":[4,4,5,5,5,4,4],"isLeapYear":true,"startWeekDay":3,"year":2020},{"1":[4,4,4,4,5,5,5],"2":[5,4,4,4,4,4,4],"3":[4,5,5,5,4,4,4],"4":[4,4,4,4,5,5,4],"5":[5,5,4,4,4,4,5],"6":[4,4,5,5,4,4,4],"7":[4,4,4,4,5,5,5],"8":[5,5,5,4,4,4,4],"9":[4,4,4,5,5,4,4],"10":[5,4,4,4,4,5,5],"11":[4,5,5,4,4,4,4],"12":[4,4,4,5,5,5,4],"isLeapYear":true,"startWeekDay":4,"year":2004},{"1":[5,4,4,4,4,5,5],"2":[4,5,4,4,4,4,4],"3":[4,4,5,5,5,4,4],"4":[4,4,4,4,4,5,5],"5":[5,5,5,4,4,4,4],"6":[4,4,4,5,5,4,4],"7":[5,4,4,4,4,5,5],"8":[4,5,5,5,4,4,4],"9":[4,4,4,4,5,5,4],"10":[5,5,4,4,4,4,5],"11":[4,4,5,5,4,4,4],"12":[4,4,4,4,5,5,5],"isLeapYear":true,"startWeekDay":5,"year":2016},{"1":[5,5,4,4,4,4,5],"2":[4,4,5,4,4,4,4],"3":[4,4,4,5,5,5,4],"4":[5,4,4,4,4,4,5],"5":[4,5,5,5,4,4,4],"6":[4,4,4,4,5,5,4],"7":[5,5,4,4,4,4,5],"8":[4,4,5,5,5,4,4],"9":[4,4,4,4,4,5,5],"10":[5,5,5,4,4,4,4],"11":[4,4,4,5,5,4,4],"12":[5,4,4,4,4,5,5],"isLeapYear":true,"startWeekDay":6,"year":2000}]

Discussion