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

  最近闲来无事,突然想了解下中国农历与中国阳历之间的关系,经过一番调研发现这里面的水还比较深,涉及天文学、历史、宗教等一些知识,发现挺有意思的就准备做一系列的总结,主要是防止自己忘记了,而且搜索了一下简书上的文章也么没有相关文章进行描述,所以借此机会跟大家分享同大家讨论,这篇文章是这个系列的第一篇,主要讲是如何推算指定日期的星期问题

根据已知日期进行推算

该方法主要是通过计算已知日期和指定日期之间的天数来推算,因为星期是一个非常固定的时间周期,7天一循环,因此通过计算出相差天数后,再对7进行取余操作就能推算出指定的日期的星期

根据公历平闰年计算

最常见的计算两个日期之间的天数就是根据年数进行推算,但是由于有闰年的影像,因此需要考虑闰年的情况,闰年的判断规则如下:

  1. 公历年数是4的倍数,且不是100倍数
  2. 公历年数是400倍数

满足上述两个条件中的一个既是闰年,该描述可以参考李永乐老师的视频讲解: 闰年是怎么回事

依次遍历年份计算天数

经过分析我们发现两个日期之间的天数可分为三部分:

  • 起始日期所在年中剩余的天数
  • 结束日期所在年中已经度过的天数
  • 中间年份的整年的天数

上述是一个通用描述,对于起止时间正好在一个年的开始和结尾,该方案也能描述。
因为中间是一个有可能存在闰年的,所需要根据闰年规则对每个年份进行判断,代码如下:

/**
     * @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;
    }

因为需要计算截止年月已经度过的天数,所以我复用了这个计算逻辑,以为剩余的天数 = 年内总天数 - 已经过的天数基于这个公式就能算出其实年份剩余的天数

 /**
     * 计算一年中到指定日期时已经度过的天数
     * @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日就是星期二。

非遍历计算天数

上述方法需要依次遍历起止年份之间的所有年份进行平闰年的判断,比较复杂,判断起来比较麻烦,其实我们只需要知道两个年份之间的闰年个数就可以解决我们的问题了,闰年的个数的计算公式:(分数结构部分表示向下取整)

获取闰年个数公式
  • 公式中的y表示的当前年份,该公式结果是从公元元年到y年中所有闰年的个数,如果y年是闰年的话,y年也算在内。
  • 这个公式中临界值确定需要注意,就是在起止年份,如果起止年份是闰年的话,要看起止年份是过了2月,这个直接关系到是+1天还是不加1天的问题。

这样就不需要遍历每个年份是否为闰年了,而直接计算天数即可,只需要将之前小节中计算整年天数代码进行调整,如下:

 /**
     * @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小时,依次顺推,每一天都被赋予一个唯一的数字。使用儒略日可以把不同历法的年表统一起来,很方便地在各种历法中追溯日期。如果计算两个日期之间的天数,利用儒略日计算也很方便,先计算出两个日期的儒略日数,然后直接相减就可以得到两个日期相隔的天数。

儒略日公式

儒略日计算公式

y表示年份
m表示月份,如果m≤2则m修正为m+12,同时y修正为y-1
d表示日期
c的求值公式

儒略日c值计算公式

实现代码

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日为周一,那么前一天就是下面介绍一个直接定位日期所在星期日,基于这个想法退出下面的公式。

计算公式

儒略日简化公式

上述公式适用1582年10月15日及之后的日期,对于1582年10月4及之前的公式:


儒略日简化公式1582年之前

实现代码

/***
     * 计算指定日其所在的星期
     * 兼容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/article/details/7749723
https://blog.csdn.net/orbit/article/details/7825004
http://www.365yg.com/i6550819663779987981/#mid=4234740937

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341