资讯专栏INFORMATION COLUMN

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

AbnerMing / 2048人阅读

摘要:儒略日就是指从公元前年月日开始所经过的天数,就被指定为公元前年月日到公元前年月日之间的小时,依次顺推,每一天都被赋予一个唯一的数字。

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

根据已知日期进行推算

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

根据公历平闰年计算

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

公历年数是4的倍数,且不是100倍数

公历年数是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日就是星期二。

实现代码二

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

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

儒略日公式

$$ julian = leftlfloor 1461 imes frac{y + 4716}{4} ight floor + leftlfloor 153 imes frac{m + 1}{4} ight floor + d + c - 1524.5 $$
y表示年份
m表示月份,如果m≤2则m修正为m+12,同时y修正为y-1
d表示日期
c的求值公式
$$ c = 2 - leftlfloorfrac{y}{100} ight floor + leftlfloorfrac{y}{400} ight floor $$

实现代码
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 + leftlfloorfrac{y}{4} ight floor + leftlfloorfrac{c}{4} ight floor -2c + leftlfloor 13 imes frac{m+1}{5} ight floor + d - 1 ) \% 7 $$
上述公式适用1582年10月15日及之后的日期,对于1582年10月4及之前的公式:
$$ weekday = ( y + leftlfloorfrac{y}{4} ight floor + leftlfloorfrac{c}{4} ight floor -2c + leftlfloor 13 imes frac{m+1}{5} ight floor + 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...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71394.html

相关文章

  • 带你开发一个日历控件

    摘要:直接使用事件代理机制,将事件绑定在整个日历的上即可,这样事件只用在创建时初始化一次即可,简单高效省内存。 首发我的博客 - https://blog.cdswyda.com/post/2017121010 日历控件多的不胜枚举,为什么我们还要再造一个轮子呢? 因为大多数日历控件都是用于选择日期的,有种需求是要在日历上展示各种各样的内容,这样的日历控件较少,而且试用下来并不满意。 因此就...

    shiina 评论0 收藏0
  • Calendar -『为移动端而生』自定义日历

    摘要:,欢迎使用中文文档在后面自我介绍是为了满足移动端对各种场景的需求而生的,兼容性强,灵活度高。如空数组默认设置成当月日数组的每一位分别是年月日。 Calendar - A Flexible Calendar for Mobile Intro Calendar was born for several product requirements in the mobile. It’s f...

    MycLambert 评论0 收藏0
  • Calendar -『为移动端而生』自定义日历

    摘要:,欢迎使用中文文档在后面自我介绍是为了满足移动端对各种场景的需求而生的,兼容性强,灵活度高。如空数组默认设置成当月日数组的每一位分别是年月日。 Calendar - A Flexible Calendar for Mobile Intro Calendar was born for several product requirements in the mobile. It’s f...

    Gu_Yan 评论0 收藏0
  • 简介:CII最佳实践徽章 - CNCF毕业标准要求之一

    摘要:已经实现并维护了核心基础结构计划的最佳实践徽章。年中国开源峰会提案征集现已开放在中国开源峰会上,与会者将共同合作及共享信息,了解最新和最有趣的开源技术,包括容器云技术网络微服务等并获得如何在开源社区中导向和引领的信息。 从沙箱或孵化状态毕业,或者作为一个新项目加入作为一个毕业项目,项目必须符合孵化阶段标准以及: 有来自至少两个机构的提交者。 已经实现并维护了核心基础结构计划(CII)...

    jzman 评论0 收藏0

发表评论

0条评论

AbnerMing

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<