java8日期时间前世今生

  • 说明
  • 日期
    • 年月日如何确定?
    • 我国现行公历,格里高历规则
    • java8默认的日期时间表示规则
    • java中格里高历计算规则体现示例
  • 时间
    • 1秒钟的时间长度是多少?
    • 时区
    • 夏令时
  • 星期
  • 其他参考
      • 微信关注公众号获取更多精彩内容

说明

  java8日期时间api,实现了jsr310, 新api的设计原则是清晰,流式,不可变对象(线程安全),可扩展。关于api的使用网上有很多,这里就不再赘述。本文说一日期时间相关的知识点,帮助大家更好地理解与使用java8日期时间api。

  如果仔细来看日期时间,实际上是一个很复杂的工程,涉及地理,天文,政治,经济,文化,宗教等各种因素。很多其他语言都遇到过java8之前日期时间api的尴尬,由于设计不合理,很难用,如python的日期时间使用。

以下我们围绕年月日,时分秒进行展开,聊一聊隐藏在日期时间背后的知识,尽量用通俗的语言表述概念与原理,过程有简化,涉及的数据不是很精确,但不影响理解。

日期

年月日如何确定?

  今天是2018年7月19日,第一个问题什么年,年是如何确定的,一年有多长?古人都是根据天象来确定年月日与时间的,最简化的日期时间是这么来的,地球绕太阳公转一圈是一年,月亮圆缺变化是一个月,也有机械地把一年分成n个月(如10个月,12个月)这种,地球自转一圈是一天,

我国现行公历,格里高历规则

  我国现行的公历,也就是格利高历,是一种太阳历,历法规则相对中国农历简单很多。历法规定一年是365天,回归年(地球绕太阳一周)大概是365.2422日,即365天5小时48分46秒余,核心要点是尽量保持历法年与回归年的一致性,历法规则也就是做各种调整以适应这两个年之间的差异。差异肯定是会存在,因为地球公转不是恒定的,回归年也是计算的一个平均数,这个数还没办法整除。历法年与回归年,一年相差0.2422天,4年相差0.9688天,大概是一天,所以每4年历法规定闰一日,也就是我们理解的闰年,2月份多一天,29天。我们看到0.9688并不等于1,每4年还是有0.0312天的差异,这个差异累积4*100=400年,3.12天,也就是如果每4年闰一天,400年会多出3天多点,所以每400年需要在每4年闰一天的基础上减去3天,怎么处理?年号尾数是00,必须能被400整除才是闰年,这样每400年循环中,如1700,1800,1900,2000,这样的尾数只有2000是闰年。经过这样的处理,实际上误差已经很小了,大概每3300年会有一天1天的误差。下面是java代码示例,说明上面的计算规则,double计算,不要求太精确。

    //365天5小时48分46秒
    //平均回归年 365.2422天   365天5小时48分46秒多
    double tropicalYear  = 365.2422;
    System.out.println("tropical one year is: " + tropicalYear +" days");
    //格里高历 常规历法年
    double gregorianYear = 365.0;
    System.out.println("gregorian one year is: " + gregorianYear +" days");

    double diff4year = tropicalYear * 4.0 - gregorianYear * 4.0;

    // 0.9688000000001011   每4年差多出一天,所以要闰1日
    System.out.println("every 4 year , diff in tropical year and gregorian year: " + diff4year);

    System.out.println("so we just add leap one day in February!!");

    // 每4年闰1天后,多了 0.031199999999898864 天
    System.out.println("every 4 year gregorian year redundant days: "+ (1.0 - diff4year));

    // 每 25*4=100年多出 0.7799999999974716  天
    double add100Y = (1.0 - diff4year) * 25;
    System.out.println("every 100 year redundant: "+ add100Y);

    // 每 25*4*4=400年多出 3.1199999999898864  天
    // 每400年多出来的3天,可以用规则 对于整百(00结尾)的年,要能被400整除,
    //     也就是这个每400年中本来要闰的 1700,1800,1900,2000 这4天,只有2000这1天闰一天了
    double add400Y = add100Y * 4;
    System.out.println("every 400 year redundant: "+ add400Y);

执行结果如下

tropical one year is: 365.2422 days
gregorian one year is: 365.0 days
every 4 year , diff in tropical year and gregorian year: 0.9688000000001011
so we just add leap one day in February!!
every 4 year gregorian year redundant days: 0.031199999999898864
every 100 year redundant: 0.7799999999974716
every 400 year redundant: 3.1199999999898864
so we just reduce redundant 3 days every 400 year!!
every 3333.333333614266 years has 1 day diff in tropical year and gregorian yea

java8默认的日期时间表示规则

java8使用ISO-8601定义的规则表示日期时间。

java中格里高历计算规则体现示例

  epoch days是指1970-01-01 00:00:00,DAYS_0000_TO_1970是指0000年到1970的天数,注释很好地说明了计算规则

    /**
     * The number of days in a 400 year cycle.
     * 365*400 + (400/4-3)
     */
    private static final int DAYS_PER_CYCLE = 146097;
    /**
     * The number of days from year zero to year 1970.
     * There are five 400 year cycles from year zero to 2000.
     * There are 7 leap years from 1970 to 2000.
     */
    static final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L);

  DAYS_PER_CYCLE表示每400年有146097天,从0000年到1970年过了5个400年,多了30年,这30年中有7个闰年。

时间

1秒钟的时间长度是多少?

  一般我们使用地球自转一圈的时间作为一天,地球自转的同时也在公转,自转也不是恒定的,目前观测到的是在变慢。太阳连续两次经过本初子午线,也就是0度经线的时间长度为1天(太阳连续两次经过任意相同经线就行,不一定是本初子午线),太阳经过0度经线的时间为正午12点,换句话说,规定了一天的长度与正午12点怎么产生。简单来说就是太阳最高点应该正午12点,两个12点之间是1天,1天24个小时,86400秒,这些都是人为规定的,1/86400就是一秒的长度,这就是所谓的世界时。世界时主要是观测天象来确定。下面再说下UTC,世界协调时。

  UTC,世界协调时,是使用原子钟来度量秒,其长度为0.107758TAI秒。由于世界时不稳定,为确保协调世界时与世界时相差不会超过0.9秒,有时会增加或减少闰秒,所以有可能会看到23:59:60秒这种情况。调整秒数的民用实际意义是尽量维持正午12点,太阳在最高点,不然N多年后,有可能会出现下午2点太阳是最高点。

时区

  太阳经过每条经线的时间是不一样,将全球每隔15度划分为一个时区,共24个时区,每个时间之间相差1小时。UTC+00:00 0时区为标准时间,我们通用Instant.now()获取相对于epoch(1970-01-01 00:00:00)和毫秒数,实际是纳秒精度。墙上时间,instance+zone时区,也就是标准时间再上时区偏移的秒数,就是当地时间。

  下面是一个java实例,从北京乘飞机到达巴黎时当地时间是多少?

上网查下机票预订
java8日期时间前世今生_第1张图片
法航AF381,凌晨01:00点出发,经过10小时55分到达巴黎,到达时间是05:50,怎么计算呢

    LocalDateTime leavingDateTime = LocalDateTime.of(2018, Month.JULY, 18, 01, 00);
    ZoneId leavingZone = ZoneId.of("Asia/Shanghai");
    ZonedDateTime departure = ZonedDateTime.of(leavingDateTime, leavingZone);
    System.out.println(String.format("departure: %s", departure));

    //法航AF381 Flight is 10 hours and 55 minutes, or 655 minutes
    ZoneId arrivingZone = ZoneId.of("Europe/Paris");
    ZonedDateTime arrival = departure.withZoneSameInstant(arrivingZone)
            .plusMinutes(10 * 60 + 55);
    System.out.println(String.format("arrival:   %s", arrival));

    if (arrivingZone.getRules().isDaylightSavings(arrival.toInstant())) {
        System.out.printf("  (%s daylight saving time will be in effect.)%n", arrivingZone);
    } else {
        System.out.printf("  (%s standard time will be in effect.)%n", arrivingZone);
    }

执行结果

departure: 2018-07-18T01:00+08:00[Asia/Shanghai]
arrival:   2018-07-18T05:55+02:00[Europe/Paris]
  (Europe/Paris daylight saving time will be in effect.)

实际上法国巴黎是东一区,为什么执行结果是东二区(+02:00)呢,这就是夏令时的影响。

夏令时

  DST夏令时是政府行为,jre里面有一个tzdate文件,包含时区信息,规则,历史数据等,dst是在标准的时区划分上,再加上人为调整,如上例法国本来是东一区,现在2018-07-18已经进入夏令时,时间拨快一个小时,实际的时区是东二区。

以中国为例,dst对日期计算影响实例

    //中国在 1991414日至915日 实行过夏令时
    ZonedDateTime zonedDateTime = ZonedDateTime.of(1991, 4, 13,
            23,0,0, 0, ZoneId.of("Asia/Shanghai"));
    ZonedDateTime twoHoursLater = zonedDateTime.plusHours(2);
    System.out.println(zonedDateTime+" two hours after is "+twoHoursLater);

执行结果

1991-04-13T23:00+08:00[Asia/Shanghai] two hours after is 1991-04-14T02:00+09:00[Asia/Shanghai]

中国东八区1991-04-13T23:00 两小时后是 1991-04-14T02:00东九区。

星期

  很多工作单位是按周来安排工作的,如2018年第29周,那么问题来了,每年第一周如何计算的?
  对于ISO-8601日历表示方式,每星期从周一开始,第一周最少天数为4天,如果在横跨上年底与本年初的星期,在本年的天数少于4天则属于上一年的最后一周。

以2008与2009年为例说明:

date week Week n of week-based-year y
2008-12-28 Sunday Week 52 of week-based-year 2008
2008-12-29 Monday Week 1 of week-based-year 2009
2008-12-31 Wednesday Week 1 of week-based-year 2009
2009-01-01 Thursday Week 1 of week-based-year 2009
2009-01-04 Sunday Week 1 of week-based-year 2009
2009-01-05 Monday Week 2 of week-based-year 2009

2008-12-29是2009年的第一周。

代码示例:

    LocalDate oneDay = LocalDate.of(2008, 12, 28);

    for(int i = 0; i< 9; i++){
        LocalDate nextDay = oneDay.plusDays(i);

        System.out.println(String.format("%10s  %9s  %2d  %2d/%4d", nextDay,nextDay.getDayOfWeek(), nextDay.get(ChronoField.ALIGNED_WEEK_OF_YEAR),
                nextDay.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR), nextDay.get(IsoFields.WEEK_BASED_YEAR)));

    }

执行结果:

    2008-12-28     SUNDAY  52  52/2008
    2008-12-29     MONDAY  52   1/2009
    2008-12-30    TUESDAY  53   1/2009
    2008-12-31  WEDNESDAY  53   1/2009
    2009-01-01   THURSDAY   1   1/2009
    2009-01-02     FRIDAY   1   1/2009
    2009-01-03   SATURDAY   1   1/2009
    2009-01-04     SUNDAY   1   1/2009
    2009-01-05     MONDAY   1   2/2009

对于 ChronoField.ALIGNED_WEEK_OF_YEAR 是严格按照每一年的第一天,作为第一周的第一天,循环往复计算。对于ISO-8601星期计算得保证第一周最少有4天在本年。

其他参考

  • 农历
  • 协调世界时
  • 夏令时
  • ISO-8601
  • 地球公转
  • 对时区、各种时间(gmt,utc,unix时间戳)的理解
  • java date time tutorials

微信关注公众号获取更多精彩内容

我就是程序员

你可能感兴趣的:(java)