【问题标题】:Why does parsing "0000:00:00 00:00:00" into a Date return -0001-11-28T00:00:00Z?为什么将“0000:00:00 00:00:00”解析为日期返回-0001-11-28T00:00:00Z?
【发布时间】:2021-01-31 20:48:53
【问题描述】:

为什么下面的代码输出的是-0001-11-28T00:00:00Z而不是0000-00-00T00:00:00Z

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.TimeZone;

class Main
{
    public static void main (String[] args) throws ParseException
    {
        DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
        parser.setTimeZone(TimeZone.getTimeZone("GMT"));
        Date date = parser.parse("0000:00:00 00:00:00");
        System.out.println(date.toInstant());
    }
}

我的第一个想法是这是一个时区问题,但输出比预期日期早了 34 天。

这是一个第 3 方库,因此我实际上无法修改代码,但如果我能理解它为什么返回此值,那么也许我可以调整输入以获得所需的输出。

如果您想知道,0000:00:00 00:00:00 来自图像或视频的EXIF metadata

【问题讨论】:

  • 可以在ideone上转载:ideone.com/QUc6P8
  • 首先,没有零编号的年份(零的概念不存在)。日期从公元前 1 年到公元 1 年,这说明年份是-0001。至于额外的 34 天……可能有一个很好的解释。这种转变中至少有 10 天可能与公历调整有关。这可能java.util.Date 已被弃用的事实有关。您应该尝试使用 java.time.* 类。
  • 即使是第 0 个月和第 0 天也是非法的......月份是 1-12,天是 1-31(或更少,取决于月份和年份)
  • 0000 编号年份问题不谈,历史上有一些有趣的点是日历被更改/调整,例如当 1582 年和 1883 年 11 月 18 日在美国标准化时采用公历时丢失了 10 天采用了时区 - 如果您在此之前/之后操纵日期,您会发现您损失了 7 分 2 秒。
  • 跟进@KevinHooke 关于公历的评论:如果您使用01 表示日期和月份,那么您将获得使用从1583 开始的年份的预期输出(例如:1583-01-01T00:00:00Z)。在 1582 年之前(当时有 10 天的轮班),每个世纪之交都会倒退 1 年,时间越往后倒退。而且,如果这个世纪是闰年,看起来这抵消了这种倒退的影响。

标签: java datetime java-time


【解决方案1】:

请注意,在旧版 API 中,year-of-erayear 之间没有区别。年份,0 实际上是1 BC。月份 0 和日期 0 是无效值,但 SimpleDateFormat 不会引发异常,而是错误地解析它们。

月份转换为11的原因:

SimpleDateFormat 将文本中的月份数字减少了1,因为java.util.Date 是基于0。换句话说,月份1SimpleDateFormat 解析为0,即月份Jan 对应java.util.Date。同样,月份,0SimpleDateFormat 解析为-1。现在,java.util.Date 处理消极月份如下:

month = CalendarUtils.mod(month, 12);

CalendarUtils#mod 的定义如下:

public static final int mod(int x, int y) {
    return (x - y * floorDivide(x, y));
}
public static final int floorDivide(int n, int d) {
    return ((n >= 0) ?
            (n / d) : (((n + 1) / d) - 1));
}

因此,CalendarUtils.mod(-1, 12) 返回11

java.util.DateSimpleDateFormat充满了这样的惊喜。建议完全停止使用,转用modern date-time API

现代日期时间 API:

现代日期时间 API 分别使用 yu 区分 year-of-erayear

y 指定时代(时代指定为ADBC)并且始终为正数,而u 指定年份 这是一个带符号 (+/-) 的数字。

通常,我们不使用+ 符号来写入正数,但我们总是使用- 符号指定负数。同一规则适用于 。只要您打算使用时代的一年,ADyu 都会给您相同的数字。但是,当您使用时代的年份时,您会得到不同的数字,BC e.g. 时代1 BC被指定为0时代2 BC被指定为-1等等。

您可以通过以下演示更好地理解它:

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class Testing {
    public static void main(String[] args) {
        System.out.println(LocalDate.of(-1, 1, 1).format(DateTimeFormatter.ofPattern("u M d")));
        System.out.println(LocalDate.of(-1, 1, 1).format(DateTimeFormatter.ofPattern("y M d")));
        System.out.println(LocalDate.of(-1, 1, 1).format(DateTimeFormatter.ofPattern("yG M d")));

        System.out.println();

        System.out.println(LocalDate.of(0, 1, 1).format(DateTimeFormatter.ofPattern("u M d")));
        System.out.println(LocalDate.of(0, 1, 1).format(DateTimeFormatter.ofPattern("y M d")));
        System.out.println(LocalDate.of(0, 1, 1).format(DateTimeFormatter.ofPattern("yG M d")));

        System.out.println();

        System.out.println(LocalDate.of(1, 1, 1).format(DateTimeFormatter.ofPattern("u M d")));
        System.out.println(LocalDate.of(1, 1, 1).format(DateTimeFormatter.ofPattern("y M d")));
        System.out.println(LocalDate.of(1, 1, 1).format(DateTimeFormatter.ofPattern("yG M d")));
    }
}

输出:

-1 1 1
2 1 1
2BC 1 1

0 1 1
1 1 1
1BC 1 1

1 1 1
1 1 1
1AD 1 1

现代日期时间 API 如何处理 0000:00:00 00:00:00

import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

class Main {
    public static void main(String[] args) {
        DateTimeFormatter parser = DateTimeFormatter.ofPattern("uuuu:MM:dd HH:mm:ss")
                                                    .withZone(ZoneOffset.UTC)
                                                    .withLocale(Locale.ENGLISH);
        
        ZonedDateTime zdt = ZonedDateTime.parse("0000:00:00 00:00:00", parser);
    }
}

输出:

Exception in thread "main" java.time.format.DateTimeParseException: Text '0000:00:00 00:00:00' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 0
....

DateTimeFormatter#withResolverStyle(ResolverStyle.LENIENT)

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss", Locale.ENGLISH)
                .withResolverStyle(ResolverStyle.LENIENT);
        String str = "0000-00-00 00:00:00";

        LocalDateTime ldt = LocalDateTime.parse(str, dtf);
        System.out.println(ldt);
    }
}

输出:

-0001-11-30T00:00

【讨论】:

  • -0001-11-28T00:00:00Z 与输入 0000-00-00T00:00:00Z 有什么关系?
  • @MartinSmith - SimpleDateFormat 充满了这样的惊喜。目前,我正在编写一些代码来找出差异的原因。我会用我的发现更新我的答案。
  • 能否请您改写difference occurs when you use a year of the era 部分?我不明白你想在那里说什么。这听起来像是一个连续的句子。
  • @Gili - 我已经改写了它。我希望现在很清楚。我想说的是AD 的数字将相同,而BC 的数字将不同。我在同一个句子中给出了几个例子。
【解决方案2】:

正如其他答案所解释的,这是使用未进行正确验证的旧类 (SimpleDateFormat) 处理无效时间戳(无效的年、月和日值)的结果。

简而言之......垃圾进,垃圾出1

解决方案:

  1. 重写使用SimpleDateFormat 的代码以使用Java 8 中引入的新日期/时间类。(如果您必须使用Java 7 及更早版本,则使用反向移植。)

  2. 在尝试将字符串作为日期处理之前,通过测试此特定情况来解决此问题。

    从上下文看来,“0000:00:00 00:00:00”是 EXIF 表示“没有这样的日期时间”的方式。如果是这种情况,那么试图将其视为日期时间似乎适得其反。而是将其视为特殊情况。

  3. 如果您无法重写代码或解决问题,请针对(第 3 方)库提交错误报告和/或补丁,并希望获得最好的结果...


1 - 为什么差异正好是 1 年 34 天有点神秘,但我相信您可以通过深入研究源代码找出原因。 IMO,这不值得努力。但是,我无法想象为什么格里高利转变会牵涉到这...

【讨论】:

  • 你说得对,有问题的字符串来自 EXIF 元数据。
【解决方案3】:

这是因为第 0 年是无效的,它不会 存在。 https://en.m.wikipedia.org/wiki/Year_zero

月、日也为0无效。

【讨论】:

  • 这并不能解释额外的 34 天
  • 没有 34 天的差异,因为您无法将其与无效输入进行比较。仅当输入为 0001-01-01 且结果仍为 -0001-11-28 时,才会存在 34 天差异。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-09
  • 2015-07-19
  • 1970-01-01
  • 2014-02-19
  • 1970-01-01
相关资源
最近更新 更多