Pocket

こんにちは。kikuchiです。

~例えばこんなシチュエーション~

今年の来場者数の推移を分析しています。
やはり休日、特に祝日含めた連休は来場者数が多いです。
さて、じゃあ去年の同じ時期の休日と比べてどうだろうか?

~~~~~~~~~~~~~~~~

ここで必要になるのは、「去年の同じ時期の休日」の日付です。
人間であれば、カレンダーをめくりながら、あぁこの日だねーと目で見て確認することが出来ますが、プログラムだとそうはいきません。
ロジックで算出する必要があります。
このようなロジックを考える機会がちょっとあったので、今回はその紹介をしたいと思います。
※Javascript で作成します

動作要件をまとめるとこんな感じです
・ある特定の日付を指定する(例:2015年11月21日(土))
・その日付に対し、過去の同じ時期、同じ曜日の日付を取得する
(例:2015年11月21日(土)に対し、2014年11月22日(土)を取得)

1.去年の同じ曜日の日付を取ってみる

同じ日付であれば、単に年を引けば良いだけですが、「同じ曜日」となるとそうはいきません。
1年は365日で、1週間は7日です。
なので、1年が何週間か?というのは、
365 ÷ 7 = 52週 余り 1日(うるう年の場合は、366日となるので余り2日)
となります。
つまり、今日(対象)の日付から52周間分(364日)を引くと、去年の同じ曜日の日付になります。
コードで書くとこんな感じでしょうか。

  • 去年の同じ曜日の日を計算する関数
    // 減算する日数
    var SUBTRACT_DAYS_WEEK = 7;
    var SUBTRACT_DAYS_YEAR = SUBTRACT_DAYS_WEEK * 52;
    
    function getDateOfSameDayOfWeek(inputDate) {
        
        // 基準日 日付取得
        var date = new Date(inputDate);

        // 現在の日から、52週間分引く(364日、前年の同じ曜日となる)
        var dayOfMonth = date.getDate();
        date.setDate(dayOfMonth - SUBTRACT_DAYS_YEAR);
        
        return date;
    }

動作確認用のHTMLはこちら
getDateOfSameDayOfWeek

2.n年前の同じ曜日の日付を取りたい

去年の日付は上記の通り取れましたが、指定した年数分さかのぼった同じ曜日の日付はどうでしょう?
※例えば、過去5年分をまとめて表示して比較したい
上記関数をただ単にループして計算してしまうと、そのうち週がズレてしまいます。
また、たとえ1年前であったとしても、指定した日付によっては1日~2日ずれるだけで週がずれる場合もあります。
(例)2013年1月14日(第2週月曜)
   →上記計算を行うと、2012年1月16日(第3週月曜)になる
   →仕様的には、2012年1月9日(第2週月曜)となって欲しい

そこで、対象日が年初から数えて第何週であるか計算しておきます。
昨年の同じ曜日の日付を計算後、同様にその年の第何週であるか計算し、ずれている場合に1週分戻します。
  • 週数を計算する関数

こちらの記事を参考にさせて頂きました。
Javascriptで年初からの日数/週数を取得する
    /**
     * 年初からの日数を取得
     * 1月1日を1日目とする
     */
    function getDayOfYear(source) {
        var date = new Date(source);
        date.setHours(9); // 繰り上げ用

        // 対象年の1月1日
        var january_1 = new Date(date.getFullYear(), 0, 1);

        // 対象の年月日から同年1月1日を引いた値 の日表記(86400000 = 24h * 60m * 60s * 1000ms) 切り上げ
        return Math.ceil((date - january_1) / 86400000);
    };

    /**
     * 年初からの週数を取得
     */
    function getWeekOfYear(source) {
        var date = new Date(source);

        // 対象年の1月1日を取得
        var january_1 = new Date(date.getFullYear(), 0, 1);

        // 年初からの日数
        var dayOfYear = getDayOfYear(source);

        // 1月1日の週を0週目、翌日曜の週を1週目となる計算にするため、日にちをずらす
        // Date.getDay() → 日:0、月:1、火:2、水:3、木:4、金:5、土:6
        var offset = january_1.getDay() - 1;

        // 週数を計算
        var weeks = Math.floor((dayOfYear + offset) / 7);

        // 1月1日の週を第1週と数える場合は加算する
        if (isFirstWeek(january_1)) {
            weeks += 1;
        }
        return weeks;
    };

    /**
     * 1月1日が1週目であるか判定する
     * 週の始まりは日曜とする
     */
    function isFirstWeek(january_1) {

        // 月曜を含む週が1週目(1月のハッピーマンデーを考慮するため)※これで全ての月を対応出来る訳では無い
        return (january_1.getDay() <= 1)

        // 【以下参考までに】
        // 日曜を含む週が1週目
        //return (january_1.getDay() <= 0)
        // 本年の4日以上を含む週が1週目
        //return (january_1.getDay() <= 3)
    }

内容はコード内コメントを参照してください。
Line:30のoffsetの目的ですが、文章での説明が難しかったので図にしてみました。
6778_1

つまり、その週のどの日にちで週数を計算しても同じ結果となるように、日付の補正をしています。
1月1日を含む週を第1週と数えるかは、考え方により変わるので別関数として定義し、加算することにしてます。
参考:MySQLのWEEK関数

  • 上記週数を使い、過去の同じ曜日の日を計算する関数

    // 減算する日数
    var SUBTRACT_DAYS_WEEK = 7;
    var SUBTRACT_DAYS_YEAR = SUBTRACT_DAYS_WEEK * 52;

    /**
     * 同じ曜日の過去日付を取得
     */
    function getDateOfPastOfSameDayOfWeek(inputDate, inputYears) {

        // 基準日 日付取得
        var date = new Date(inputDate);

        // 1年ずつ計算する
        for (var i = 0; i < inputYears; i++) {

            // 現在の日の、年初からの週数を算出しておく
            var weekOfYear = getWeekOfYear(date);

            // 現在の日から、52週間分引く(364日、前年の同じ曜日となる)
            var dayOfMonth = date.getDate();
            date.setDate(dayOfMonth - SUBTRACT_DAYS_YEAR);

            // 前年の、年初からの週数を算出
            var lastWeekOfYear = getWeekOfYear(date);

            // 前年の週数が繰り上がっている場合、1週間分引く
            if (weekOfYear < lastWeekOfYear) {
                date.setDate(date.getDate() - SUBTRACT_DAYS_WEEK);
            }
            // 算出した日を基準として、指定年数分繰り返す
        }
        return date;
    }

関数名にやたらOfが付いて長くなってしまいました。

動作確認用のHTMLはこちら
getDateOfPastOfSameDayOfWeek1

上記は1年ずつ繰り下げていく場合で作成しましたが、年初からの週数と曜日をもとに、過去年の1月1日から加算して計算していく方法も考えられます。

	/**
	 * 	同じ曜日の過去日付を取得
	 */
	function getDateOfPastOfSameDayOfWeek(inputDate, inputYears) {

		// 基準日 日付取得
		var date = new Date(inputDate);
		// 基準日の、年初からの週数を算出しておく
		var weekOfYear = getWeekOfYear(date);
		// 現在の曜日を取得しておく
		var day = date.getDay();

		// 指定年数前の1月1日を取得
		var pastDate = new Date(date.getFullYear() - inputYears, 0, 1);
		// 1月1日が1週目の場合、計算後に1週間分引く
		var sub = 0;
		if (isFirstWeek(pastDate)) {
			sub = 7;
		}
		// 1月1日を日曜にする(元々日曜であっても、1週間ずれる)
		pastDate.setDate(pastDate.getDate() + (7 - pastDate.getDay()))

		// 基準日と同じ週数、曜日になるよう加算する
		pastDate.setDate(pastDate.getDate() + ((weekOfYear - 1)  * 7) + day - sub);

		return pastDate;
	}

動作確認用のHTMLはこちら
getDateOfPastOfSameDayOfWeek2

年初からの週数で判定しているため、個々の月での第何週かは考慮していません。
なので、冒頭で触れたような2015年9月21日(敬老の日)では、
2015年9月21日:38週目、9月第3月曜
2014年9月22日:38週目、9月第4月曜
となり、時期としては合ってますが、祝日としてはズレてしまいます。
こういった要件で対応したい場合は、月単位で対応する範囲を固定した上で、ロジックを組む必要があるでしょう。
※9月の第3月曜を、過去何年分算出する…みたいな

* オマケ
Javaで何か便利なものないかなーと思って調べてみた時に、見つけて試したものです。

package jo.co.opentone.tk;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.WeekFields;

public class Main {

    public static void main(String[] args) {

        WeekFields week = WeekFields.of(DayOfWeek.SUNDAY, 1);
        DateTimeFormatter dtf = new DateTimeFormatterBuilder()
            .appendPattern("yyyy/MM/dd(E) ")
            .appendValue(week.weekOfYear())
            .appendLiteral("週目")
            .toFormatter();

        LocalDate date = LocalDate.of(2015, 11, 1);
        System.out.println(date.format(dtf));
    }
}

出力
2015/11/01(日) 45週目

参考:
Java8日時APIのちょっと特殊なクラスたち
WeekFields