为什么将“0000:00:00 00:00:00”解析为 Date return -0001-11-28T00:00:00Z?

Why does parsing "0000:00:00 00:00:00" into a Date return -0001-11-28T00:00:00Z?

为什么下面的代码输出的是-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 个图像或视频。

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

月、日为0也是无效的

请注意,在遗留 API 中,年代年代 之间没有区别。那一年,0其实是1 BC。月 0 和日 0 是无效值,但 SimpleDateFormat 没有抛出异常而是错误地解析了它们。

月份转换为11的原因:

SimpleDateFormat 将文本中的月份数字减少 1,因为 java.util.Date 是基于 0 的。换句话说,月份 1SimpleDateFormat 解析为 0,即 java.util.Date 的月份 Jan。同样,月份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) returns 11.

java.util.DateSimpleDateFormat充满了这样的惊喜。建议完全停止使用它们并切换到 modern date-time API.

  • 出于任何原因,如果您必须坚持使用 Java 6 或 Java 7,您可以使用 ThreeTen-Backport,它向后移植了大部分 java.time Java 6 和 7 的功能。
  • 如果您正在为 Android 项目工作,并且您的 Android API 级别仍然不符合 Java-8,请检查 Java 8+ APIs available through desugaring and

现代日期时间 API:

现代日期时间 API 使用 y 区分 year-of-erayearu 分别。

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

通常,我们不使用+ 符号来写正数,但我们总是用- 符号指定负数。同样的规则适用于 。只要您要使用纪元的年份,ADyu 都会给您相同的数字。但是,当您使用时代的年份时,您会得到不同的数字,BC 例如年代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

正如其他答案所解释的,这是处理无效时间戳(无效年、月和日值)的结果,遗留 class(SimpleDateFormat)不正确验证。

简而言之...垃圾输入,垃圾输出1

解决方案:

  1. 重写使用 SimpleDateFormat 的代码以使用 Java 8 中引入的新日期/时间 classes。(或者如果你有使用 Java 7 及更早版本。)

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

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

  3. 如果您无法重写代码或解决问题,请针对(第 3 方)库提交错误报告 and/or 补丁并希望一切顺利...


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