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