我想将以下格式的字符串转换为 java.util.Date 范围、开始和结束日期:

January 16th – 21st, 2025
January 16th – February 21st, 2025
January 16th – February 21st, 2025
December 16th 2024 – January 2nd 2025
November 16th – 19th

您有不同月份和不同年份的示例,也有没有年份但应该使用当前年份的示例

你知道我应该如何处理所有这些格式吗?

17

  • 2
    java.util.Date表示时间戳而不是日期。是的,您可能会想:这太荒谬了,为什么他们会将一个明确不代表日期的东西命名为Date?但这是事实。检查 apidocs。它的每个与日期相关的方面(例如.getYear(),当然是 Date 对象会具有的东西)都被弃用是有原因的,并附上一条注释,这是因为它无法正常工作。您想要的是java.time.LocalDate


    – 

  • java.time.Period如果范围部分很重要,则应使用LocalDates,否则应使用s


    – 


  • 2
    如果不知道用例的详细情况,就很难知道。Period::between需要两个LocalDate。必须为这些输入字符串构建某种解析器


    – 


  • 1
    MonthDay或者如果不需要年份也可以使用…但是混合两者(有和没有年份)可能会很混乱


    – 


  • 1
    理想的解决方案是使用标准 ISO 8601 格式而不是本地化文本来以文本方式交换日期时间值:2025-02-16/2025-02-21。请参阅ThreeTen-ExtraLocalDateRange中的类。


    – 



最佳答案
4

java.time 和 ThreeTen Extra

像其他人一样,我建议您使用 java.time(现代 Java 日期和时间 API)进行日期工作。在这种情况下,您可以使用ThreeTen ExtraLocalDateRange中的类进行补充,这是一个与 java.time 一起开发并基于它构建的库(链接位于底部)。

此答案的特点是:

  • 对于没有年份的范围,确保它们是在现在还是将来解释的:日期被视为今年的剩余部分或下一个日历年。
  • 精确解析序数日期(1号、2号、16号等等),以便获得良好的验证。
  • 使用ThreeTen ExtraLocalDateRange指定日期范围。
  • 无需正则表达式(除了用于字符串的初始拆分),也无需显式数字解析。所有解析工作都通过对象String.split()进行。DateTimeFormatter
  • 模式匹配(自 Java 14 起)用于将解析的优雅转换TemporalAccessorLocalDate
// For parsing 1st, 2nd, 16th, 21st, etc.
private static final Map<Long, String> ORDINAL_DAYS = createOrdinalDays();

private static Map<Long, String> createOrdinalDays() {
    Map<Long, String> dayMap = new HashMap<>();
    putSuffix(dayMap, "st", 1, 21, 31);
    putSuffix(dayMap, "nd", 2, 22);
    putSuffix(dayMap, "rd", 3, 23);
    for (long dayNumber = 1; dayNumber <= 31; dayNumber++) {
        dayMap.computeIfAbsent(dayNumber, d -> String.valueOf(d) + "th");
    }
    return dayMap;
}

private static void putSuffix(
        Map<Long, String> dayMap, String suffix, long... dayNumbers) {
    for (long dayNumber : dayNumbers) {
        dayMap.put(dayNumber, String.valueOf(dayNumber) + suffix);
    }
}

private static final DateTimeFormatter START_DATE_PARSER
        = new DateTimeFormatterBuilder()
                .appendPattern("MMMM ")
                .appendText(ChronoField.DAY_OF_MONTH, ORDINAL_DAYS)
                .appendPattern("[[,] u]")
                .toFormatter(Locale.ENGLISH);

private static final DateTimeFormatter BASE_END_DATE_PARSER
        = new DateTimeFormatterBuilder()
                .appendPattern("[MMMM ]")
                .appendText(ChronoField.DAY_OF_MONTH, ORDINAL_DAYS)
                .appendPattern("[[,] u]")
                .toFormatter(Locale.ENGLISH);

public static LocalDateRange parse(String input) {
    // Break into start and end strings
    String[] strings = input.split(" – ");
    if (strings.length != 2) {
        throw new IllegalArgumentException("Invalid input, must be two dates separated by a dash: " + input);
    }

    TemporalAccessor parsedStart = START_DATE_PARSER.parseBest(
            strings[0], LocalDate::from, MonthDay::from);

    // For end date use start month as default
    DateTimeFormatter endDateParser = new DateTimeFormatterBuilder()
            .append(BASE_END_DATE_PARSER)
            .parseDefaulting(ChronoField.MONTH_OF_YEAR,
                    parsedStart.get(ChronoField.MONTH_OF_YEAR))
            .toFormatter(Locale.ENGLISH);
    TemporalAccessor parsedEnd = endDateParser.parseBest(
            strings[1], LocalDate::from, MonthDay::from);

    LocalDate start;
    LocalDate endInclusive;
    // Have we got a year at all? If so, we should at least have end year
    if (parsedEnd instanceof LocalDate endLocalDate) { // Yes, we have end year
        endInclusive = endLocalDate;
        if (parsedStart instanceof LocalDate startLocalDate) {
            start = startLocalDate;
        } else {
            start = ((MonthDay) parsedStart).atYear(endLocalDate.getYear());
        }
    } else { // No, no years.
        if (parsedStart instanceof LocalDate) {
            throw new IllegalArgumentException("Invalid input; start year requires end year: " + input);
        }

        // We need to calculate a year.
        // Find the next instance of the parsed start month and day.
        LocalDate today = LocalDate.now(ZoneId.systemDefault());
        MonthDay startMonthDay = (MonthDay) parsedStart;
        int year = today.getYear();
        if (startMonthDay.isBefore(MonthDay.from(today))) { // Over for this year; use next year
            year++;
        }
        start = startMonthDay.atYear(year);
        endInclusive = ((MonthDay) parsedEnd).atYear(year);
    }

    return LocalDateRange.ofClosed(start, endInclusive);
}

让我们用您的示例字符串尝试一下:

    String[] testInputs = {
            "January 16th – 21st, 2025",
            "January 16th – February 21st, 2025",
            "December 16th 2024 – January 2nd 2025",
            "November 16th – 19th"
    };

    for (String testInput : testInputs) {
        LocalDateRange dateRange = parse(testInput);
        System.out.format(Locale.ROOT, "7s -> %s - %s%n",
                testInput, dateRange.getStart(), dateRange.getEndInclusive());
    }

今天运行时输出为:

            January 16th – 21st, 2025 -> 2025-01-16 - 2025-01-21
   January 16th – February 21st, 2025 -> 2025-01-16 - 2025-02-21
December 16th 2024 – January 2nd 2025 -> 2024-12-16 - 2025-01-02
                 November 16th – 19th -> 2024-11-16 - 2024-11-19

我使用DateTimeFormatter.parseBest()来解析可能包含或不包含年份的字符串,LocalDate如果年份存在,则解析为 ,MonthDay如果年份不存在,则解析为 。后者用于没有年份的月份和日期。当我稍后确定年份时,我会将其转换为LocalDate

LocalDateRange迫使我们选择日期范围是半开放(从开始到结束)还是封闭(包括开始和结束)。我认为您的范围是封闭的,因为用户可读的日期间隔通常是封闭的,而计算机在半开放间隔下工作得更好、更自然。

我无法判断我的代码是否完全满足您在特殊情况下的要求;但您可以随时根据您的确切需求进行修改。

避免使用 java.util.Date

你写道:

我想将以下格式的字符串转换为 java.util.Date 范围,…

这个java.uil.Date类存在严重的设计问题,这就是为什么它和其他几个日期和时间类在 10 多年前被 Java 8 中的 java.time 取代的原因。你没有理由要使用 java.time 以外的任何东西java.utiljava.text

提示:ISO 8601 时间间隔

如果可以,不要交换和解析人类可读的日期范围,而是使用更适合机器阅读的 ISO 8601 时间间隔(例如2025-01-16/2025-01-21用于数据交换)。它们仍然是人类可读的,但解析起来容易得多。仅在向用户展示时才使用良好的人类可读格式。

LocalDateRange我使用的类有一个方法parse,可以直接解析 ISO 8601 日期间隔,例如2025-01-16/2025-01-21,它无需任何编码就可以解决你的整个问题。

链接

  • 维基百科上的

1

  • 回答得真好!谢谢!


    – 

看起来你的格式基本上是这样的:

month day[[,] year] – [month] day[[,] year]

我可能会使用正则表达式:

import java.time.LocalDate;
import java.time.Month;
import java.time.Year;

import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

import java.text.NumberFormat;
import java.text.ParseException;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class DateRangeParser {
    
    public record DateRange(LocalDate start,
                            LocalDate end) {
    }

    private final Pattern format = Pattern.compile(
        "(\\p{L}+)"                         // start month
        + "\\s*(\\d+)(?i:st|nd|rd|th)?"     // start day
        + "(?:,?\\s*(\\d+))?"               // start year (optional)
        + "\\s*\\p{Pd}"                     // dash separating start and end
        + "\\s*(\\p{L}+)?"                  // end month (optional)
        + "\\s*(\\d+)(?i:st|nd|rd|th)?"     // end day
        + "(?:,?\\s*(\\d+))?");             // end year (optional)

    private final NumberFormat numberFormat =
        NumberFormat.getIntegerInstance();

    private final DateTimeFormatter monthFormat =
        DateTimeFormatter.ofPattern("MMMM").withLocale(Locale.ENGLISH);

    public DateRange parse(String s)
    throws ParseException {
        Matcher matcher = format.matcher(s);
        if (!matcher.matches()) {
            throw new ParseException(s, 0);
        }

        String startMonthStr = matcher.group(1);
        String startDayStr = matcher.group(2);
        String startYearStr = matcher.group(3);
        String endMonthStr = matcher.group(4);
        String endDayStr = matcher.group(5);
        String endYearStr = matcher.group(6);

        try {
            Month startMonth = Month.from(monthFormat.parse(startMonthStr));
            int startDay = numberFormat.parse(startDayStr).intValue();

            Integer startYear;
            if (startYearStr != null) {
                startYear = numberFormat.parse(startYearStr).intValue();
            } else {
                startYear = null;
            }

            Month endMonth;
            if (endMonthStr != null) {
                endMonth = Month.from(monthFormat.parse(endMonthStr));
            } else {
                endMonth = startMonth;
            }

            int endDay = numberFormat.parse(endDayStr).intValue();

            Integer endYear;
            if (endYearStr != null) {
                endYear = numberFormat.parse(endYearStr).intValue();
            } else {
                endYear = null;
            }

            if (startYear == null) {
                if (endYear == null) {
                    endYear = Year.now().getValue();
                }
                startYear = endYear;
            } else if (endYear == null) {
                endYear = startYear;
            }

            LocalDate start = LocalDate.of(startYear, startMonth, startDay);
            LocalDate end = LocalDate.of(endYear, endMonth, endDay);

            return new DateRange(start, end);
        } catch (DateTimeParseException e) {
            ParseException pe = new ParseException(s, e.getErrorIndex());
            pe.initCause(e);
            throw pe;
        }
    }

    public static void main(String[] args)
    throws ParseException {
        String[] testInputs = {
            "January 16th – 21st, 2025",
            "January 16th – February 21st, 2025",
            "January 16th – February 21st, 2025",
            "December 16th 2024 – January 2nd 2025",
            "November 16th – 19th",
        };

        DateRangeParser parser = new DateRangeParser();
        for (String input : testInputs) {
            DateRange range = parser.parse(input);
            System.out.println(range);
        }
    }
}

关于正则表达式的一些注释:

  • \p{L}+匹配 1 个或多个字母。
  • \d+匹配 1 位或多位数字。
  • \p{Pd}匹配任何破折号。在您的示例中,您使用的是短破折号字符 ( '\u2013'),而不是 ASCII 连字符。这将匹配两者。

\p{L}\p{Pd}参考

有关更多信息,请参阅

使用 Month.valueOf(s.toUpperCase()) 而不是仅使用Month startMonth = Month.from(monthFormat.parse(string))Month.valueOf(s.toUpperCase()) 可以实现月份名称的本地化匹配。正如 Turo 指出的那样,如果计算机未设置(或可能未设置)为英语区域设置,但您想要解析英语月份名称,则必须在月份格式中指定英语:

private final DateTimeFormatter monthFormat =
    DateTimeFormatter.ofPattern("MMMM").withLocale(Locale.ENGLISH);

16

  • 此代码不起作用


    – 

  • 您尝试过运行它吗?


    – 

  • 2
    我也遇到了异常,并且添加 withLocale 使它消失,正如我在评论中提到的那样


    – 

  • 1
    代码正在运行,但输出错误 2025 年 1 月 16 日至 21 日应为 DateRange[start=2025-01-16, end=2025-01-21] 第一个日期给出的是 2024 年,因为第二部分提到了年份,这是否意味着第一个日期也是 2025 年?


    – 

  • 1
    @khalifaR。因此,如果没有起始年份,您将永远不会有重叠范围。例如August 16th – February 21st, 2025。在这种情况下,2025没有意义。


    – 

您可以使用以下代码:

public class Test {

   public Test() {
      String[] allDates = {
         "January 16th – 21st, 2025",
         "January 16th – February 21st, 2025",
         "January 16th – February 21st, 2025",
         "December 16th 2024 – January 2nd 2025",
         "November 16th – 19th" };
      for( int i = 0; i < allDates.length; i ++ ) {
         String[] twoDates = allDates[ i ].split( " – " );  //  "January 16th", "21st, 2025"
         LocalDate[] dates = translate( twoDates );
      }
   }

   LocalDate[] translate( String[] twoDates ) {
      LocalDate out[] = new LocalDate[ 2 ];
      String inheritedMonth = twoDates[ 0 ].split( " " )[ 0 ];  //  January
      out[ 0 ] = getDate( twoDates[ 0 ].replace( ",", "" ).split( " " ), inheritedMonth );
      out[ 1 ] = getDate( twoDates[ 1 ].replace( ",", "" ).split( " " ), inheritedMonth );
      return out;
   }

   public LocalDate getDate( String[] date, String inheritedMonth ) {
      int day = -1, year = -1, month = -1;           
      switch( date.length ) {
         case 3:
            year = Integer.parseInt( date[ 2 ] );
            month = getMonth( date[ 0 ] );
            day = getDay( date[ 1 ] );
            break;
         case 2:
            if( date[ 0 ].substring( 0, 1 ).matches( "[a-zA-Z]" ) ) {
               year = LocalDate.now().getYear();
               month = getMonth( date[ 0 ] );
               day = getDay( date[ 1 ] );
            }
            else {
               year = Integer.parseInt( date[ 1 ] );
               month = getMonth( inheritedMonth );
               day = getDay( date[ 0 ] );
            }  break;
         case 1:
            year = LocalDate.now().getYear();
            month = getMonth( inheritedMonth );
            day = getDay( date[ 0 ] );
            break;
         default:
            break;
      }
      return LocalDate.of( year, month, day );
   }

   int getMonth( String month ) {
      return Month.valueOf( month.toUpperCase() ).getValue();
   }

   int getDay( String day ) {
      return Integer.parseInt( day.replaceAll( "[a-zA-Z]", "" ) );
   }

   public static void main( String[] args ) {
      Test test = new Test();
   }
}

注意:当我测试代码时,还是同样的失败,我检查了所有内容,并且没有任何错误,经过长时间的“挣扎”,我发现 String [] twoDates = allDates[ i ].split( “ – ” );这一行没有执行split,这似乎没有任何意义,直到我发现问题所在,才明白过来。

我无法准确描述它,只是指出它在哪里以及如何修复它。


不知何故,我不明白,我作为参数传递的 “ – ” 与您作为示例传递的那个不一样(例如:’January 16th – 21st, 2025” ),解决方案是从您的示例中复制模式并将其粘贴到 split 调用

根据问题和评论做出的假设。

  • 所有可能的范围格式均已呈现。
  • 每个范围格式都有唯一数量的字段需要处理。
  • 所有字段都以开始开头monthday因此这些字段可以从 switch 块之外提取。

流程

  • 首先,使用正则表达式解析每个范围字段并存储LinkedList已处理字段的跟踪。
  • 然后,对于每种类型的日期,创建StringBuilders开始和结束日期并获取适当的标记并将其附加到 StringBuilder
  • 然后可以根据需要格式化每个创建的开始和结束日期。
import java.time.LocalDate;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ParsingRanges {

    public record Range(LocalDate start, LocalDate end, DateTimeFormatter dtf) {
        @Override
        public String toString() {
            return "%s - %s".formatted(start.format(dtf), end.format(dtf) );
        }
    }

    public static void main(String[] args) {

        String regex = "(?:(\\w+)[\\s\\-–,]*)";

        List<Range> processed = new ArrayList<>();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("MMMM d[,] yyyy");

        String[] ranges = {"January 16th – 21st, 2025", // 4 fields
                "January 1st – February 21st, 2025", // 5 fields
                "January 16th  – August 21st, 2025",
                "December 16th 2024 – January 2nd 2025", // 6 fields
                "November 16th – 19th"}; // 3 fields
        Pattern p = Pattern.compile(regex);
        for (String t : ranges) {
            LinkedList<String> tokens = new LinkedList<>();
            int fields = 0;
            // remove day ordinal designations and trailing comma
            t = t.replaceAll("(?<=\\d)(th|nd|st|rd|,)", "");
            Matcher m = p.matcher(t);
            while (m.find()) {
                tokens.add(m.group(1));
                fields++;
            }

            StringBuilder start = new StringBuilder(tokens.get(0)).append(" ")
                    .append(tokens.get(1)).append(" ");
            StringBuilder end = new StringBuilder();
            switch (fields) {
                case 3: { // three fields
                    start.append(Year.now());
                    end.append(tokens.get(0)).append(" ").append(tokens.get(2))
                            .append(" ").append(Year.now());
                    break;
                }
                case 4: { // four fields
                    start.append(tokens.getLast()); // year
                    end.append(tokens.getFirst()).append(" ")
                            .append(tokens.get(2)).append(" ")
                            .append(tokens.get(3));
                    break;
                }
                case 5: { // five fields
                    start.append(tokens.getLast()); // year
                    end.append(tokens.get(2)).append(" ").append(tokens.get(3))
                            .append(" ").append(tokens.get(4));
                    break;
                }
                case 6: { // six fields
                    
                    start.append(tokens.get(2)); // year
                    end.append(tokens.get(3)).append(" ").append(tokens.get(4))
                            .append(" ").append(tokens.get(5));
                    break;
                }
            };

            LocalDate s = LocalDate.parse(start, dtf);
            LocalDate e = LocalDate.parse(end, dtf);
            
            processed.add(new Range(s, e, dtf));
        }
        for (Range range : processed) {
            System.out.println(range);
        }
    }

}

印刷

January 16, 2025 - January 21, 2025
January 16, 2025 - February 16, 2025
January 16, 2025 - August 21, 2025
December 16, 2024 - January 02, 2025
November 16, 2024 - November 19, 2024

请注意,DateTimeFormaterLocale可能需要更改以满足您的最终要求。

我选择将开始日期和结束日期以及 LocalDate 格式化程序存储在 中record

4

  • 在文本中首先列出你的假设是很好的。我建议读者也考虑在他们的代码中检查这些假设。


    – 

  • @Anonymous 感谢您的评论。我已经检查了月份是否存在序数冲突,并且知道了stAugust然后我很快就忘记修复它了。我更改了正则表达式以确保序数前面有一个数字。关于您的另一条评论,我不清楚建议是什么。那是针对其他人还是针对我?再次感谢!


    – 

  • 主要针对 OP 和其他人。假设很危险,所以在我看来,如果你提醒每个人检查假设,或者你自己在代码中检查假设,那么你会改进答案。我仍然不确定输入变化在多大程度上可能是正交的,因此 4 个字段输入也可能是November 16th – December 3rd


    – 

  • 1
    @Anonymous 随着字段和范围格式的变化不断发展,这变得越来越复杂。几乎到了需要 FSM 进行额外检查或使用类似 的格式的地步ANTLR。这就是为什么我说了 OP 所说的内容,即那些是唯一的格式变化。我考虑过的一种这样的变化是当存在隐含的年份重叠时。例如(August 23rd - March 4th 2025)。在这种情况下,必须将起始年份指定为 的前一年2024。否则无法处理。所以我的 5 个字段数需要处理两个变化。


    –