add RegexDateParser

This commit is contained in:
Looly 2024-06-06 21:42:12 +08:00
parent 9a34a155c6
commit 47872b8380
19 changed files with 385 additions and 40 deletions

View File

@ -717,7 +717,7 @@ public class CalendarUtil {
* @throws DateException if none of the date patterns were suitable
* @since 5.3.11
*/
public static Calendar parseByPatterns(final String str, final String... parsePatterns) throws DateException {
public static Calendar parseByPatterns(final CharSequence str, final String... parsePatterns) throws DateException {
return parseByPatterns(str, null, parsePatterns);
}
@ -734,7 +734,7 @@ public class CalendarUtil {
* @throws DateException if none of the date patterns were suitable
* @since 5.3.11
*/
public static Calendar parseByPatterns(final String str, final Locale locale, final String... parsePatterns) throws DateException {
public static Calendar parseByPatterns(final CharSequence str, final Locale locale, final String... parsePatterns) throws DateException {
return parseByPatterns(str, locale, true, parsePatterns);
}
@ -753,7 +753,7 @@ public class CalendarUtil {
* @see java.util.Calendar#isLenient()
* @since 5.3.11
*/
public static Calendar parseByPatterns(final String str, final Locale locale, final boolean lenient, final String... parsePatterns) throws DateException {
public static Calendar parseByPatterns(final CharSequence str, final Locale locale, final boolean lenient, final String... parsePatterns) throws DateException {
if (str == null || parsePatterns == null) {
throw new IllegalArgumentException("Date and Patterns must not be null");
}

View File

@ -37,9 +37,9 @@ public final class DateBuilder {
// region ----- fields
// 年份
private int year;
// 月份
// 月份从1开始
private int month;
// 周数
// 周数ISO8601规范1代表Monday2代表Tuesday以此类推
private int week;
//
private int day;
@ -88,18 +88,18 @@ public final class DateBuilder {
}
/**
* 获取月份
* 获取月份从1开始
*
* @return 返回设置的月份
* @return 返回设置的月份从1开始
*/
public int getMonth() {
return month;
}
/**
* 设置月份
* 设置月份从1开始
*
* @param month 要设置的月份
* @param month 要设置的月份从1开始
* @return this
*/
public DateBuilder setMonth(final int month) {
@ -117,9 +117,9 @@ public final class DateBuilder {
}
/**
* 设置日期构建器的周数
* 设置日期构建器的周数ISO8601规范1代表Monday2代表Tuesday以此类推
*
* @param week 指定的周数通常用于构建具体的日期对象
* @param week 指定的周数通常用于构建具体的日期对象ISO8601规范1代表Monday2代表Tuesday以此类推
* @return this
*/
public DateBuilder setWeek(final int week) {

View File

@ -344,17 +344,17 @@ public class FastDateFormat extends Format implements PositionDateParser, DatePr
// ----------------------------------------------------------------------- Parsing
@Override
public Date parse(final String source) throws DateException {
public Date parse(final CharSequence source) throws DateException {
return parser.parse(source);
}
@Override
public Date parse(final String source, final ParsePosition pos) {
public Date parse(final CharSequence source, final ParsePosition pos) {
return parser.parse(source, pos);
}
@Override
public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
public boolean parse(final CharSequence source, final ParsePosition pos, final Calendar calendar) {
return parser.parse(source, pos, calendar);
}

View File

@ -31,5 +31,5 @@ public interface DateParser extends DateBasic{
* @return {@link Date}对象
* @throws DateException 转换异常被转换的字符串格式错误
*/
Date parse(String source) throws DateException;
Date parse(CharSequence source) throws DateException;
}

View File

@ -17,6 +17,7 @@ import org.dromara.hutool.core.date.format.FastDateFormat;
import org.dromara.hutool.core.date.format.FastDatePrinter;
import org.dromara.hutool.core.date.format.SimpleDateBasic;
import org.dromara.hutool.core.map.concurrent.SafeConcurrentHashMap;
import org.dromara.hutool.core.text.StrUtil;
import java.io.IOException;
import java.io.ObjectInputStream;
@ -224,7 +225,7 @@ public class FastDateParser extends SimpleDateBasic implements PositionDateParse
}
@Override
public Date parse(final String source) throws DateException {
public Date parse(final CharSequence source) throws DateException {
final ParsePosition pp = new ParsePosition(0);
final Date date = parse(source, pp);
if (date == null) {
@ -239,7 +240,7 @@ public class FastDateParser extends SimpleDateBasic implements PositionDateParse
}
@Override
public Date parse(final String source, final ParsePosition pos) {
public Date parse(final CharSequence source, final ParsePosition pos) {
// timing tests indicate getting new instance is 19% faster than cloning
final Calendar cal = Calendar.getInstance(timeZone, locale);
cal.clear();
@ -248,7 +249,7 @@ public class FastDateParser extends SimpleDateBasic implements PositionDateParse
}
@Override
public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
public boolean parse(final CharSequence source, final ParsePosition pos, final Calendar calendar) {
final ListIterator<StrategyAndWidth> lt = patterns.listIterator();
while (lt.hasNext()) {
final StrategyAndWidth strategyAndWidth = lt.next();
@ -337,7 +338,7 @@ public class FastDateParser extends SimpleDateBasic implements PositionDateParse
return false;
}
abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth);
abstract boolean parse(FastDateParser parser, Calendar calendar, CharSequence source, ParsePosition pos, int maxWidth);
}
/**
@ -356,8 +357,8 @@ public class FastDateParser extends SimpleDateBasic implements PositionDateParse
}
@Override
boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
final Matcher matcher = pattern.matcher(source.substring(pos.getIndex()));
boolean parse(final FastDateParser parser, final Calendar calendar, final CharSequence source, final ParsePosition pos, final int maxWidth) {
final Matcher matcher = pattern.matcher(source.subSequence(pos.getIndex(), source.length()));
if (!matcher.lookingAt()) {
pos.setErrorIndex(pos.getIndex());
return false;
@ -486,7 +487,7 @@ public class FastDateParser extends SimpleDateBasic implements PositionDateParse
}
@Override
boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
boolean parse(final FastDateParser parser, final Calendar calendar, final CharSequence source, final ParsePosition pos, final int maxWidth) {
for (int idx = 0; idx < formatField.length(); ++idx) {
final int sIdx = idx + pos.getIndex();
if (sIdx == source.length()) {
@ -558,7 +559,7 @@ public class FastDateParser extends SimpleDateBasic implements PositionDateParse
}
@Override
boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) {
boolean parse(final FastDateParser parser, final Calendar calendar, final CharSequence source, final ParsePosition pos, final int maxWidth) {
int idx = pos.getIndex();
int last = source.length();
@ -590,7 +591,7 @@ public class FastDateParser extends SimpleDateBasic implements PositionDateParse
return false;
}
final int value = Integer.parseInt(source.substring(pos.getIndex(), idx));
final int value = Integer.parseInt(StrUtil.sub(source, pos.getIndex(), idx));
pos.setIndex(idx);
calendar.set(field, modify(parser, value));

View File

@ -48,7 +48,7 @@ public class ISO8601DateParser extends DefaultDateBasic implements PredicateDate
}
@Override
public DateTime parse(String source) throws DateException{
public DateTime parse(CharSequence source) throws DateException{
final int length = source.length();
if (StrUtil.contains(source, 'Z')) {
if (length == DatePattern.UTC_PATTERN.length() - 4) {
@ -65,7 +65,7 @@ public class ISO8601DateParser extends DefaultDateBasic implements PredicateDate
}
} else if (StrUtil.contains(source, '+')) {
// 去除类似2019-06-01T19:45:43 +08:00加号前的空格
source = source.replace(" +", "+");
source = StrUtil.replace(source, " +", "+");
final String zoneOffset = StrUtil.subAfter(source, '+', true);
if (StrUtil.isBlank(zoneOffset)) {
throw new DateException("Invalid format: [{}]", source);
@ -88,9 +88,9 @@ public class ISO8601DateParser extends DefaultDateBasic implements PredicateDate
// Issue#2612类似 2022-09-14T23:59:00-08:00 或者 2022-09-14T23:59:00-0800
// 去除类似2019-06-01T19:45:43 -08:00加号前的空格
source = source.replace(" -", "-");
source = StrUtil.replace(source, " -", "-");
if(':' != source.charAt(source.length() - 3)){
source = source.substring(0, source.length() - 2) + ":00";
source = StrUtil.sub(source, 0, source.length() - 2) + ":00";
}
if (StrUtil.contains(source, CharUtil.DOT)) {
@ -128,7 +128,7 @@ public class ISO8601DateParser extends DefaultDateBasic implements PredicateDate
* @return 规范之后的毫秒部分
*/
@SuppressWarnings("SameParameterValue")
private static String normalizeMillSeconds(final String dateStr, final CharSequence before, final CharSequence after) {
private static String normalizeMillSeconds(final CharSequence dateStr, final CharSequence before, final CharSequence after) {
if (StrUtil.isBlank(after)) {
final String millOrNaco = StrUtil.subPre(StrUtil.subAfter(dateStr, before, true), 3);
return StrUtil.subBefore(dateStr, before, true) + before + millOrNaco;

View File

@ -51,7 +51,7 @@ public class NormalDateParser extends DefaultDateBasic implements PredicateDateP
}
@Override
public DateTime parse(String source) throws DateException{
public DateTime parse(CharSequence source) throws DateException{
final int colonCount = StrUtil.count(source, CharUtil.COLON);
switch (colonCount) {
case 0:

View File

@ -80,7 +80,7 @@ public class PatternsDateParser extends DefaultDateBasic implements DateParser {
}
@Override
public DateTime parse(final String source) {
public DateTime parse(final CharSequence source) {
return new DateTime(CalendarUtil.parseByPatterns(source, this.locale, this.parsePatterns));
}
}

View File

@ -32,7 +32,7 @@ public interface PositionDateParser extends DateParser {
* @param pos {@link ParsePosition}
* @return {@link Date}
*/
Date parse(String source, ParsePosition pos);
Date parse(CharSequence source, ParsePosition pos);
/**
* 根据给定格式更新{@link Calendar}
@ -46,5 +46,5 @@ public interface PositionDateParser extends DateParser {
* @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated)
* @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is out of range.
*/
boolean parse(String source, ParsePosition pos, Calendar calendar);
boolean parse(CharSequence source, ParsePosition pos, Calendar calendar);
}

View File

@ -17,6 +17,7 @@ import org.dromara.hutool.core.date.DatePattern;
import org.dromara.hutool.core.date.DateTime;
import org.dromara.hutool.core.date.format.DefaultDateBasic;
import org.dromara.hutool.core.math.NumberUtil;
import org.dromara.hutool.core.text.StrUtil;
/**
* 纯数字的日期字符串解析支持格式包括
@ -45,7 +46,7 @@ public class PureDateParser extends DefaultDateBasic implements PredicateDatePar
}
@Override
public DateTime parse(final String source) throws DateException {
public DateTime parse(final CharSequence source) throws DateException {
final int length = source.length();
// 纯数字形式
if (length == DatePattern.PURE_DATETIME_PATTERN.length()) {
@ -58,7 +59,7 @@ public class PureDateParser extends DefaultDateBasic implements PredicateDatePar
return new DateTime(source, DatePattern.PURE_TIME_FORMAT);
} else if(length >= 11 && length <= 13){
// 时间戳
return new DateTime(NumberUtil.parseLong(source));
return new DateTime(NumberUtil.parseLong(StrUtil.str(source)));
}
throw new DateException("No pure format fit for date String [{}] !", source);

View File

@ -57,7 +57,7 @@ public class RFC2822DateParser extends DefaultDateBasic implements PredicateDate
}
@Override
public DateTime parse(final String source) {
public DateTime parse(final CharSequence source) {
// issue#I9C2D4
if(StrUtil.contains(source, ',')){
if(StrUtil.contains(source, KEYWORDS_LOCALE_CHINA)){

View File

@ -0,0 +1,252 @@
/*
* Copyright (c) 2024. looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* https://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.date.format.parser;
import org.dromara.hutool.core.date.*;
import org.dromara.hutool.core.date.format.DefaultDateBasic;
import org.dromara.hutool.core.regex.ReUtil;
import org.dromara.hutool.core.text.StrUtil;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 正则日期解析器<br>
* 通过定义一个命名分组的正则匹配日期格式使用正则分组获取日期各部分的值命名分组使用{@code (?<xxx>子表达式) }表示<br>
* <pre>{@code
* ^(?<year>\d{4})(?<month>\d{2})$ 匹配 201909
* }</pre>
*
* @author Looly
* @since 6.0.0
*/
public class RegexDateParser extends DefaultDateBasic implements PredicateDateParser {
private static final long serialVersionUID = 1L;
private static final int[] NSS = {100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1};
/**
* 根据给定带名称的分组正则创建RegexDateParser
*
* @param regex 正则表达式
* @return RegexDateParser
*/
public static RegexDateParser of(final String regex) {
return new RegexDateParser(Pattern.compile(regex));
}
private final Pattern pattern;
/**
* 构造
*
* @param pattern 正则表达式
*/
public RegexDateParser(final Pattern pattern) {
this.pattern = pattern;
}
@Override
public boolean test(final CharSequence source) {
return ReUtil.isMatch(this.pattern, source);
}
@Override
public Date parse(final CharSequence source) throws DateException {
final Matcher matcher = this.pattern.matcher(source);
if(!matcher.matches()){
throw new DateException("Invalid date string: [{}]", source);
}
final DateBuilder dateBuilder = DateBuilder.of();
// year
final String year = ReUtil.group(matcher, "year");
if (StrUtil.isNotEmpty(year)) {
dateBuilder.setYear(parseYear(year));
}
// month
final String month = ReUtil.group(matcher, "month");
if (StrUtil.isNotEmpty(month)) {
dateBuilder.setMonth(parseMonth(month));
}
// week
final String week = ReUtil.group(matcher, "week");
if (StrUtil.isNotEmpty(week)) {
dateBuilder.setWeek(parseWeek(week));
}
// day
final String day = ReUtil.group(matcher, "day");
if (StrUtil.isNotEmpty(day)) {
dateBuilder.setDay(parseNumberLimit(day, 1, 31));
}
// hour
final String hour = ReUtil.group(matcher, "hour");
if (StrUtil.isNotEmpty(hour)) {
dateBuilder.setHour(parseNumberLimit(hour, 0, 23));
}
// minute
final String minute = ReUtil.group(matcher, "minute");
if (StrUtil.isNotEmpty(minute)) {
dateBuilder.setMinute(parseNumberLimit(minute, 0, 59));
}
// second
final String second = ReUtil.group(matcher, "second");
if (StrUtil.isNotEmpty(second)) {
dateBuilder.setSecond(parseNumberLimit(second, 0, 59));
}
// ns
final String ns = ReUtil.group(matcher, "ns");
if (StrUtil.isNotEmpty(ns)) {
dateBuilder.setNs(parseNano(ns));
}
// am or pm
final String m = ReUtil.group(matcher, "m");
if (StrUtil.isNotEmpty(m)) {
if ('p' == m.charAt(0)) {
dateBuilder.setPm(true);
} else {
dateBuilder.setAm(true);
}
}
// zero zone offset
final String zero = ReUtil.group(matcher, "zero");
if (StrUtil.isNotEmpty(zero)) {
dateBuilder.setZoneOffsetSetted(true);
dateBuilder.setZoneOffset(0);
}
// zone offset
final String zoneOffset = ReUtil.group(matcher, "zoneOffset");
if (StrUtil.isNotEmpty(zoneOffset)) {
dateBuilder.setZoneOffsetSetted(true);
dateBuilder.setZoneOffset(parseZoneOffset(zoneOffset));
}
// unix时间戳
final String unixsecond = ReUtil.group(matcher, "unixsecond");
if (StrUtil.isNotEmpty(unixsecond)) {
dateBuilder.setUnixsecond(parseLong(unixsecond));
}
// 毫秒时间戳
final String millisecond = ReUtil.group(matcher, "millisecond");
if (StrUtil.isNotEmpty(millisecond)) {
return DateUtil.date(parseLong(millisecond));
}
return dateBuilder.toDate();
}
private static int parseYear(final String year) {
final int length = year.length();
switch (length) {
case 4:
return Integer.parseInt(year);
case 2:
final int num = Integer.parseInt(year);
return (num > 50 ? 1900 : 2000) + num;
default:
throw new DateException("Invalid year: [{}]", year);
}
}
private static int parseMonth(final String month) {
try {
final int monthInt = Integer.parseInt(month);
if (monthInt > 0 && monthInt < 13) {
return monthInt;
}
} catch (final NumberFormatException e) {
return Month.of(month).getValueBaseOne();
}
throw new DateException("Invalid month: [{}]", month);
}
private static int parseWeek(final String week){
return Week.of(week).getIso8601Value();
}
private static int parseNumberLimit(final String numberStr, final int minInclude, final int maxInclude) {
try {
final int monthInt = Integer.parseInt(numberStr);
if (monthInt >= minInclude && monthInt <= maxInclude) {
return monthInt;
}
} catch (final NumberFormatException ignored) {
}
throw new DateException("Invalid number: [{}]", numberStr);
}
private static long parseLong(final String numberStr) {
try {
return Long.parseLong(numberStr);
} catch (final NumberFormatException ignored) {
}
throw new DateException("Invalid long: [{}]", numberStr);
}
private static int parseInt(final String numberStr, final int from, final int to) {
try {
return Integer.parseInt(numberStr.substring(from, to));
} catch (final NumberFormatException ignored) {
}
throw new DateException("Invalid int: [{}]", numberStr);
}
private static int parseNano(final String ns) {
return NSS[ns.length() - 1] * Integer.parseInt(ns);
}
/**
* 解析时区偏移类似于'+0800', '+08', '+8:00', '+08:00'
* @param zoneOffset 时区偏移
* @return 偏移量
*/
private int parseZoneOffset(final String zoneOffset) {
int from = 0;
final int to = zoneOffset.length();
final boolean neg = '-' == zoneOffset.charAt(from);
from++;
// parse hour
final int hour;
if (from + 2 <= to && Character.isDigit(zoneOffset.charAt(from + 1))) {
hour = parseInt(zoneOffset, from, from + 2);
from += 2;
} else {
hour = parseInt(zoneOffset, from, from + 1);
from += 1;
}
// skip ':' optionally
if (from + 3 <= to && zoneOffset.charAt(from) == ':') {
from++;
}
// parse minute optionally
int minute = 0;
if (from + 2 <= to) {
minute = parseInt(zoneOffset, from, from + 2);
}
return (hour * 60 + minute) * (neg ? -1 : 1);
}
}

View File

@ -46,7 +46,7 @@ public class RegisterDateParser extends DefaultDateBasic implements DateParser {
}
@Override
public Date parse(final String source) throws DateException {
public Date parse(final CharSequence source) throws DateException {
return parserList
.stream()
.filter(predicateDateParser -> predicateDateParser.test(source))

View File

@ -44,7 +44,7 @@ public class TimeParser extends DefaultDateBasic implements PredicateDateParser
}
@Override
public DateTime parse(String source) {
public DateTime parse(CharSequence source) {
// issue#I9C2D4 处理时分秒
//15时45分59秒 修正为 15:45:59
source = StrUtil.replaceChars(source, "时分秒", ":");

View File

@ -166,7 +166,6 @@ public class NumberValidator {
return false;
}
try {
//noinspection ResultOfMethodCallIgnored
Integer.decode(s);
} catch (final NumberFormatException e) {
return false;

View File

@ -884,7 +884,7 @@ public class ReUtil {
boolean result = matcher.find();
if (result) {
final Set<String> varNums = findAll(PatternPool.GROUP_VAR, replacementTemplate, 1,
new TreeSet<>(StrLengthComparator.INSTANCE.reversed()));
new TreeSet<>(StrLengthComparator.INSTANCE.reversed()));
final StringBuffer sb = new StringBuffer();
do {
String replacement = replacementTemplate;
@ -985,4 +985,24 @@ public class ReUtil {
}
return builder.toString();
}
/**
* 根据提供的匹配器和组名尝试获取匹配的字符串
* <p>
* 此方法旨在方便地从匹配器中提取指定名称的组匹配的字符串如果指定的组不存在
* 则通过捕获异常并返回null来优雅地处理错误
*
* @param matcher 匹配器对象用于查找和匹配文本
* @param name 组的名称用于指定要提取的匹配字符串的组
* @return 如果找到并成功提取了指定组的匹配字符串则返回该字符串如果组不存在则返回null
*/
public static String group(final Matcher matcher, final String name) {
try {
// 尝试根据组名获取匹配的字符串
return matcher.group(name);
} catch (final IllegalArgumentException e) {
// 如果组名无效捕获异常并返回null
return null;
}
}
}

View File

@ -0,0 +1,44 @@
package org.dromara.hutool.core.date;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.TimeZone;
public class DateBuilderTest {
@Test
public void testNormal() {
final DateBuilder builder = new DateBuilder();
builder.setYear(2019);
builder.setMonth(10);
builder.setDay(1);
final Date date = builder.toDate();
Assertions.assertEquals("2019-10-01", DateUtil.date(date).toDateStr());
}
@Test
public void testLocalDateTime() {
final DateBuilder builder = DateBuilder.of()
.setYear(2019)
.setMonth(10)
.setDay(1)
.setHour(10)
.setMinute(20)
.setSecond(30)
.setNs(900000000)
.setZone(TimeZone.getDefault());
final LocalDateTime dateTime = builder.toLocalDateTime();
Assertions.assertEquals("2019-10-01T10:20:30.900", builder.toLocalDateTime().toString());
}
@Test
public void testTimestamp() {
final String timestamp = "946656000";
final DateBuilder dateBuilder = DateBuilder.of().setUnixsecond(Long.parseLong(timestamp));
Assertions.assertEquals("2000-01-01T00:00", dateBuilder.toLocalDateTime().toString());
}
}

View File

@ -0,0 +1,12 @@
package org.dromara.hutool.core.date;
import org.dromara.hutool.core.lang.Console;
import org.junit.jupiter.api.Test;
public class IssueI8IUTBTest {
@Test
void parseTest() {
final DateTime parse = DateUtil.parse("May 8, 2009 5:57:51 PM");
Console.log(parse);
}
}

View File

@ -0,0 +1,16 @@
package org.dromara.hutool.core.date.format.parser;
import org.dromara.hutool.core.date.DateUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Date;
public class RegexDateParserTest {
@Test
void parsePureTest() {
final RegexDateParser parser = RegexDateParser.of("^(?<year>\\d{4})(?<month>\\d{2})(?<day>\\d{2})$");
final Date parse = parser.parse("20220101");
Assertions.assertEquals("2022-01-01", DateUtil.date(parse).toDateStr());
}
}