解析日期时间字符串中日期的序号指示符 (st, nd, rd, th)

Parsing a date’s ordinal indicator ( st, nd, rd, th ) in a date-time string

我检查了 SimpleDateFormat javadoc, but I am not able to find a way to parse the ordinal indicator 日期格式如下:

 Feb 13th 2015 9:00AM

我试过了"MMM dd yyyy hh:mma",但是天数必须是数字才能正确吗?

是否可以使用 SimpleDateFormat 解析“第 13 个”日期而不必截断字符串?

Java 的 SimpleDateFormat 不支持序数后缀,但序数后缀只是花哨的东西——它是多余的,可以很容易地删除以允许直接解析:

Date date = new SimpleDateFormat("MMM dd yyyy hh:mma")
    .parse(str.replaceAll("(?<=\d)(st|nd|rd|th)", ""));

替换正则表达式非常简单,因为这些序列不会出现在有效日期的任何其他地方。


要处理将来自任何语言的任意长度的序号指示字符附加为后缀的任何语言:

Date date = new SimpleDateFormat("MMM dd yyyy hh:mma")
    .parse(str.replaceAll("(?<=\d)(?=\D* \d+ )\p{L}+", ""));

一些语言,例如普通话,在它们的序号指示符之前,但这也可以使用交替来处理 - 留作 reader :)

的练习

Java 8个答案(以及Java 6和7)(因为2015年问这个问题的时候,SimpleDateFormat的替换已经出来了):

    DateTimeFormatter parseFormatter = DateTimeFormatter
            .ofPattern("MMM d['st']['nd']['rd']['th'] uuuu h:mma", Locale.ENGLISH);
    LocalDateTime dateTime = LocalDateTime.parse(dateTimeString, parseFormatter);

根据问题的样本日期得出:

2015-02-13T09:00

在格式模式中,[] 表示可选部分,'' 表示文字部分。所以该模式表示数字后面可能跟有 stndrdth.

要在 Java 6 或 7 中使用它,您需要 ThreeTen Backport. Or for Android ThreeTenABP.

因为这些后缀是英文特有的,而其他的languages/locales写日期和时间的用法完全不同(而且他们不使用AM/PM),我相信除非你有其他的要求,您应该尝试仅针对英语日期和时间实施此操作。此外,您还应该明确提供一个说英语的语言环境,这样它就可以独立于您的计算机或 JVM 的语言环境设置工作。

我已经尝试通过 Hugo and myself to a duplicate question. Under that duplicate question 将答案的最佳部分结合起来,还有更多 java 8 个答案。上述代码的一个局限性是它没有非常严格的验证:你将摆脱 Feb 13rd 甚至 Feb 13stndrdth.

编辑:在我对序数指标的回答中,我最喜欢的是 this one。它是关于格式化的,但我在那里展示的格式化程序也可以很好地解析。

如果有人觉得它有用:DateTimeFormatter 生成器。此格式化程序允许您格式化和解析带有序数后缀的英国日期(例如“2017 年 1 月 1 日”):

public class UkDateFormatterBuilder
{
    /**
     * The UK date formatter that formats a date without an offset, such as '14th September 2020' or '1st January 2017'.
     * @return an immutable formatter which uses the {@link ResolverStyle#SMART SMART} resolver style. It has no override chronology or zone.
     */
    public DateTimeFormatter build()
    {
        return new DateTimeFormatterBuilder()
                .parseCaseInsensitive()
                .parseLenient()
                .appendText(DAY_OF_MONTH, dayOfMonthMapping())
                .appendLiteral(' ')
                .appendText(MONTH_OF_YEAR, monthOfYearMapping())
                .appendLiteral(' ')
                .appendValue(YEAR, 4)
                .toFormatter(Locale.UK);
    }

    private Map<Long, String> monthOfYearMapping()
    {
        Map<Long, String> monthOfYearMapping = new HashMap<>();
        monthOfYearMapping.put(1L, "January");
        monthOfYearMapping.put(2L, "February");
        monthOfYearMapping.put(3L, "March");
        monthOfYearMapping.put(4L, "April");
        monthOfYearMapping.put(5L, "May");
        monthOfYearMapping.put(6L, "June");
        monthOfYearMapping.put(7L, "July");
        monthOfYearMapping.put(8L, "August");
        monthOfYearMapping.put(9L, "September");
        monthOfYearMapping.put(10L, "October");
        monthOfYearMapping.put(11L, "November");
        monthOfYearMapping.put(12L, "December");
        return monthOfYearMapping;
    }

    private Map<Long, String> dayOfMonthMapping()
    {
        Map<Long, String> suffixes = new HashMap<>();
        for (int day=1; day<=31; day++)
        {
            suffixes.put((long)day, String.format("%s%s", (long) day, dayOfMonthSuffix(day)));
        }
        return suffixes;
    }

    private String dayOfMonthSuffix(final int day)
    {
        Preconditions.checkArgument(day >= 1 && day <= 31, "Illegal day of month: " + day);
        if (day >= 11 && day <= 13)
        {
            return "th";
        }
        switch (day % 10)
        {
            case 1:  return "st";
            case 2:  return "nd";
            case 3:  return "rd";
            default: return "th";
        }
    }
}

加上测试片段class:

public class UkDateFormatterBuilderTest
{
    DateTimeFormatter formatter = new UkDateFormatterBuilder().build();

    @Test
    public void shouldFormat1stJanuaryDate()
    {
        final LocalDate date = LocalDate.of(2017, 1, 1);

        final String formattedDate = date.format(formatter);

        Assert.assertEquals("1st January 2017", formattedDate);
    }

    @Test
    public void shouldParse1stJanuaryDate()
    {
        final String formattedDate = "1st January 2017";

        final LocalDate parsedDate = LocalDate.parse(formattedDate, formatter);

        Assert.assertEquals(LocalDate.of(2017, 1, 1), parsedDate);
    }
}

PS。我从这里使用 Greg Mattes 的序数后缀解决方案: How do you format the day of the month to say "11th", "21st" or "23rd" in Java? (ordinal indicator)

您应该使用 RuleBasedNumberFormat。它工作完美并且尊重语言环境。