算法——指定日期的星期推算

  最近閒來無事,忽然想了解下中國農曆與中國陽曆之間的關係,通過一番調研發現這裏面的水還比較深,涉及天文學、歷史、宗教等一些知識,發現挺有意思的就準備作一系列的總結,主要是防止本身忘記了,並且搜索了一下簡書上的文章也麼沒有相關文章進行描述,因此藉此機會跟你們分享同你們討論,這篇文章是這個系列的第一篇,主要講是如何推算指定日期的星期問題java

根據已知日期進行推算

該方法主要是經過計算已知日期和指定日期之間的天數來推算,由於星期是一個很是固定的時間週期,7天一循環,所以經過計算出相差天數後,再對7進行取餘操做就能推算出指定的日期的星期git

根據公曆平閏年計算

最多見的計算兩個日期之間的天數就是根據年數進行推算,可是因爲有閏年的影像,所以須要考慮閏年的狀況,閏年的判斷規則以下:算法

  1. 公曆年數是4的倍數,且不是100倍數
  2. 公曆年數是400倍數

知足上述兩個條件中的一個既是閏年,該描述能夠參考李永樂老師的視頻講解:閏年是怎麼回事ui

依次遍歷年份計算天數

通過分析咱們發現兩個日期之間的天數可分爲三部分:spa

  • 起始日期所在年中剩餘的天數
  • 結束日期所在年中已經度過的天數
  • 中間年份的全年的天數

實現代碼一

上述是一個通用描述,對於起止時間正好在一個年的開始和結尾,該方案也能描述。
由於中間是一個有可能存在閏年的,所須要根據閏年規則對每一個年份進行判斷,代碼以下:.net

/**
     * @param startYear 起始年份
     * @param endYear 截止年份
     * @return 從起止年份到截止年份前一年的天數
     * 常量 YearDays.LEAP_YEAR_DAYS = 366
     * 常量 YearDays.NONLEAP_YEAR_DAYS = 365
     * @throws CalendarException
     */
public static int calculateDaysOfYears(int startYear, int endYear) throws CalendarException {
        if(startYear > endYear) {
            throw new CalendarException(String.format("illegal parameter year, startYear=%s endYear=%s", startYear, endYear));
        }

        int difference = endYear - startYear;

        int totalDays = 0;
        //從起始年的1月1號開始加和,一直到截止年的前一年全部的天數,這樣的計算須要減去起始年已通過過去的天數
        for(int i= startYear; i < endYear; i++) {
            boolean isLeap = CalendarTool.isLeapyear(i);
            if (isLeap) {
                totalDays += YearDays.LEAP_YEAR_DAYS;
            } else {
                totalDays += YearDays.NONLEAP_YEAR_DAYS;
            }
        }

        return totalDays;
    }

由於須要計算截止年月已經度過的天數,因此我複用了這個計算邏輯,覺得剩餘的天數 = 年內總天數 - 已通過的天數基於這個公式就能算出其實年份剩餘的天數code

/**
     * 計算一年中到指定日期時已經度過的天數
     * @param year 年
     * @param month 枚舉類型,定義了sort月數和 days天數兩個屬性
     * @param day 所在月的日期
     * @return
     */
    public static int calculatePassedDays(int year, CalendarMonth month, int day) throws CalendarException {
        CalendarTool.checkParam(year, month, day);

        int monthValue = month.getSort();

        int passedTotal = 0;
        for(int i = 1; i < monthValue; i++) {
            CalendarMonth passedMonth = CalendarMonth.getMonthBySort(i);
            if(passedMonth.equals(CalendarMonth.FEBRUARY)) {
                boolean isLeap = CalendarTool.isLeapyear(year);
                if(isLeap) {
                    passedTotal += 29;
                    continue;
                }
            }
            passedTotal += passedMonth.getDays();
        }

        return passedTotal + day;
    }

舉個例子驗證一下:已知1977年3月27日是星期天,計算2005年5月31日是星期幾,按照上述的算法進行計算獲得天數是10292,而後用10292 % 7 = 2獲得餘數爲2,那麼2005年5月31日就是星期二。orm

實現代碼二

上述方法須要依次遍歷起止年份之間的全部年份進行平閏年的判斷,比較複雜,判斷起來比較麻煩,其實咱們只須要知道兩個年份之間的閏年個數就能夠解決咱們的問題了,閏年的個數的計算公式:(分數結構部分表示向下取整)
$$ leap = \left\lfloor\frac y4\right\rfloor - \left\lfloor\frac y{100}\right\rfloor + \left\lfloor\frac y{400}\right\rfloor$$視頻

  • 公式中的y表示的當前年份,該公式結果是從公元元年到y年中全部閏年的個數,若是y年是閏年的話,y年也算在內。
  • 這個公式中臨界值肯定須要注意,就是在起止年份,若是起止年份是閏年的話,要看起止年份是過了2月,這個直接關係到是+1天仍是不加1天的問題。

這樣就不須要遍歷每一個年份是否爲閏年了,而直接計算天數便可,只須要將以前小節中計算全年天數代碼進行調整,以下:blog

/**
     * @param startYear 起始年份
     * @param endYear 截止年份
     * @return 從起止年份到截止年份前一年的天數
     * 常量 YearDays.LEAP_YEAR_DAYS = 366
     * 常量 YearDays.NONLEAP_YEAR_DAYS = 365
     * @throws CalendarException
     */
    public static int calculateDays(int startYear, int endYear) throws CalendarException {

        startYear -= 1;
        endYear -= 1;
        int startLeaps = Integer.valueOf(startYear / 4) - Integer.valueOf(startYear / 100) + Integer.valueOf(startYear / 400);
        int endLeaps = Integer.valueOf(endYear / 4) - Integer.valueOf(endYear / 100) + Integer.valueOf(endYear / 400);

        int totalDays = endLeaps * YearDays.LEAP_YEAR_DAYS + (endYear - endLeaps) * YearDays.NONLEAP_YEAR_DAYS -(startLeaps * YearDays.LEAP_YEAR_DAYS + (startYear - startLeaps) * YearDays.NONLEAP_YEAR_DAYS);

        return totalDays;
    }

年數-1是由於起止年份並非完全年,計算以前的一年就不用考慮起止年份是否爲閏年和月份是否過了2月份了,簡化了邏輯判斷。這樣代碼比以前的方法看起來精簡了很多。

代碼出處

上述代碼片斷可能不能知足你們的須要,因此附上代碼出處
閏年輔助推算代碼出處

根據儒略日進行推算

上面的方法是根據平閏年進行推算的,須要進行復雜的平閏年判斷,就算第二種方法也是簡化了起止年份之間的年份的閏年判斷,開始和結束年份的閏年判斷再計算度過了多少天的時候仍是要進行判斷的,經過儒略日能夠規避平閏年的判斷。

儒略日

儒略日是一種不記年,不記月,只記日的歷法,是由法國學者Joseph Justus Scaliger(1540-1609)在1583年提出來的一種以天數爲計量單位的流水日曆。儒略日就是指從公元前4713年1月1日UTC 12:00開始所通過的天數,JD0就被指定爲公元前4713年1月1日 12:00到公元前4713年1月2日12:00之間的24小時,依次順推,每一天都被賦予一個惟一的數字。使用儒略日能夠把不一樣曆法的年表統一塊兒來,很方便地在各類曆法中追溯日期。若是計算兩個日期之間的天數,利用儒略日計算也很方便,先計算出兩個日期的儒略日數,而後直接相減就能夠獲得兩個日期相隔的天數。

儒略日公式

$$ julian = \left\lfloor 1461 \times \frac{y + 4716}{4} \right\rfloor + \left\lfloor 153 \times \frac{m + 1}{4}\right\rfloor + d + c - 1524.5 $$
y表示年份
m表示月份,若是m≤2則m修正爲m+12,同時y修正爲y-1
d表示日期
c的求值公式
$$ c = 2 - \left\lfloor\frac{y}{100} \right\rfloor + \left\lfloor\frac{y}{400}\right\rfloor $$

實現代碼

public static int calculateDaysWithJulian(int year, CalendarMonth month, int day) throws CalendarException {
        CalendarTool.checkParam(year, month, day);

        int monthVal = month.getSort();
        if(month.compare(CalendarMonth.FEBRUARY) <= 0) {
            monthVal = month.getSort() + 12;
            year = year - 1;
        }

        //計算 c 值
        int c = 2 - Integer.valueOf(year/100) + Integer.valueOf(year/400);

        //由於儒略日的開始是從中午12點開始的,因此須要加0.5天, 0.0000115740天=1秒
        day += 0.5000115740;
        return (int) (Integer.valueOf(1461 * (year + 4716) / 4) + Integer.valueOf(153 * (monthVal + 1)/5) + day + c - 1514.5);
    }

這樣經過儒略日公式進行推算,代碼較以前代碼更加精簡

代碼出處

儒略日計算代碼出處儒略日計算代碼出處

直接定位日期所在星期

前面所述的推送方法都是基於已知日期的星期,而後計算與所指定的日期之間的天數來進行推算,在已知日期以後向後推算,在已知日期以前的向前推算,不是很方便。基於公元元年1月1日爲週一,那麼前一天就是下面介紹一個直接定位日期所在星期日,基於這個想法退出下面的公式。

計算公式

$$ weekday = ( y + \left\lfloor\frac{y}{4} \right\rfloor + \left\lfloor\frac{c}{4}\right\rfloor -2c + \left\lfloor\ 13 \times frac{m+1}{5} \right\rfloor + d - 1 ) \% 7 $$
上述公式適用1582年10月15日及以後的日期,對於1582年10月4及以前的公式:
$$ weekday = ( y + \left\lfloor\frac{y}{4} \right\rfloor + \left\lfloor\frac{c}{4}\right\rfloor -2c + \left\lfloor\ 13 \times frac{m+1}{5} \right\rfloor + d + 3 ) \% 7 $$

實現代碼

/***
     * 計算指定日其所在的星期
     * 兼容1582年先後10月4日以前和1582年10月15以後的全部時間
     * @param year
     * @param month
     * @param day
     * @return
     * @throws CalendarException
     */
    public static int calculateweek(int year, CalendarMonth month, int day) throws CalendarException {

        int m = month.getSort();
        if(month.compare(CalendarMonth.MARCH) < 0) {
            m = month.getSort() + 12;
            year = year - 1;
        }

        int c = year / 100;
        int y = year % 100;

        int week = 0;
        if(year == 1582 && month.equals(CalendarMonth.OCTOBER) && day > 4 && day < 15) {
            throw new CalendarException(String.format("illegal date exception, date=%s-%s-%s not exist", year, month.getSort(), day));
        } else {
            if(year < 1582 || (year == 1582 && month.compare(CalendarMonth.OCTOBER) < 0) || (year == 1582 && month.equals(CalendarMonth.OCTOBER) && day <= 4)) {
                week = Integer.valueOf(y + Integer.valueOf(y / 4) + Integer.valueOf(c / 4) - 2*c + Integer.valueOf(13 * (m + 1) / 5) + day + 3) % 7;
            } else {
                week = Integer.valueOf(y + Integer.valueOf(y / 4) + Integer.valueOf(c / 4) - 2*c + Integer.valueOf(13 * (m + 1) / 5) + day - 1) % 7;
            }
        }

        // 公式會產生負數,修正便可
        if(week < 0)
            week = (week + 7) % 7;

        return week;

    }

代碼出處

儒略日定位計算代碼出處儒略公式高級版

這篇文章主要是從網上搜集來的知識並非本身讀書的來,如下是主要文檔的出處
相關文檔:
https://blog.csdn.net/orbit/a...
https://blog.csdn.net/orbit/a...
http://www.365yg.com/i6550819...

相關文章
相關標籤/搜索