我想将以下格式的字符串转换为 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
最佳答案
4
java.time 和 ThreeTen Extra
像其他人一样,我建议您使用 java.time(现代 Java 日期和时间 API)进行日期工作。在这种情况下,您可以使用ThreeTen ExtraLocalDateRange
中的类进行补充,这是一个与 java.time 一起开发并基于它构建的库(链接位于底部)。
此答案的特点是:
- 对于没有年份的范围,确保它们是在现在还是将来解释的:日期被视为今年的剩余部分或下一个日历年。
- 精确解析序数日期(1号、2号、16号等等),以便获得良好的验证。
- 使用ThreeTen Extra
LocalDateRange
来指定日期范围。 - 无需正则表达式(除了用于
–
字符串的初始拆分),也无需显式数字解析。所有解析工作都通过对象String.split()
进行。DateTimeFormatter
- 模式匹配(自 Java 14 起)用于将解析的优雅转换
TemporalAccessor
为LocalDate
。
// 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.util
。java.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 调用中。
|
根据问题和评论做出的假设。
- 所有可能的范围格式均已呈现。
- 每个范围格式都有唯一数量的字段需要处理。
- 所有字段都以开始开头
month
,day
因此这些字段可以从 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
请注意,DateTimeFormater
和Locale
可能需要更改以满足您的最终要求。
我选择将开始日期和结束日期以及 LocalDate 格式化程序存储在 中record
。
4
-
在文本中首先列出你的假设是很好的。我建议读者也考虑在他们的代码中检查这些假设。
– -
@Anonymous 感谢您的评论。我已经检查了月份是否存在序数冲突,并且知道了
st
。August
然后我很快就忘记修复它了。我更改了正则表达式以确保序数前面有一个数字。关于您的另一条评论,我不清楚建议是什么。那是针对其他人还是针对我?再次感谢!
– -
主要针对 OP 和其他人。假设很危险,所以在我看来,如果你提醒每个人检查假设,或者你自己在代码中检查假设,那么你会改进答案。我仍然不确定输入变化在多大程度上可能是正交的,因此 4 个字段输入也可能是
November 16th – December 3rd
。
– -
1@Anonymous 随着字段和范围格式的变化不断发展,这变得越来越复杂。几乎到了需要 FSM 进行额外检查或使用类似 的格式的地步
ANTLR
。这就是为什么我说了 OP 所说的内容,即那些是唯一的格式变化。我考虑过的一种这样的变化是当存在隐含的年份重叠时。例如(August 23rd - March 4th 2025
)。在这种情况下,必须将起始年份指定为 的前一年2024
。否则无法处理。所以我的 5 个字段数需要处理两个变化。
–
|
java.util.Date
表示时间戳而不是日期。是的,您可能会想:这太荒谬了,为什么他们会将一个明确不代表日期的东西命名为Date
?但这是事实。检查 apidocs。它的每个与日期相关的方面(例如.getYear()
,当然是 Date 对象会具有的东西)都被弃用是有原因的,并附上一条注释,这是因为它无法正常工作。您想要的是java.time.LocalDate
。–
java.time.Period
如果范围部分很重要,则应使用LocalDate
s,否则应使用s–
Period::between
需要两个LocalDate
。必须为这些输入字符串构建某种解析器–
MonthDay
或者如果不需要年份也可以使用…但是混合两者(有和没有年份)可能会很混乱–
2025-02-16/2025-02-21
。请参阅ThreeTen-Extra库LocalDateRange
中的类。–
|