From ba3266aaeae770b0d1d4234a310b327763a8a414 Mon Sep 17 00:00:00 2001 From: ZhouXY108 Date: Tue, 22 Jul 2025 14:43:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor!:=20=E9=87=8D=E6=9E=84=E6=AD=A3?= =?UTF-8?q?=E5=88=99=E8=A1=A8=E8=BE=BE=E5=BC=8F=E7=9B=B8=E5=85=B3=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将正则相关根据移至 regex 包下 - 新增 PatternInfos 记录不同 pattern 的信息 - 新增 Chinese2ndIdCardNumberMatcher 和 LocalDateMatcher 作为日期和二代居民身份证的匹配结果,方便从中获取对应的 group --- .../model/Chinese2ndGenIDCardNumber.java | 4 +- .../commons/regex/AbstractMatcher.java | 38 +++ .../regex/Chinese2ndIdCardNumberMatcher.java | 60 ++++ .../commons/regex/LocalDateMatcher.java | 37 +++ .../{constant => regex}/PatternConsts.java | 2 +- .../plusone/commons/regex/PatternInfo.java | 38 +++ .../plusone/commons/regex/PatternInfos.java | 114 ++++++++ .../{constant => regex}/RegexConsts.java | 9 +- .../{constant => regex}/package-info.java | 14 +- .../plusone/commons/util/StringTools.java | 2 +- .../model/ValidatableStringRecordTests.java | 2 +- .../PatternConstsTests.java | 2 +- .../commons/regex/PatternInfosTests.java | 259 ++++++++++++++++++ 13 files changed, 559 insertions(+), 22 deletions(-) create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/AbstractMatcher.java create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/Chinese2ndIdCardNumberMatcher.java create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/LocalDateMatcher.java rename plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/{constant => regex}/PatternConsts.java (98%) create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternInfo.java create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternInfos.java rename plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/{constant => regex}/RegexConsts.java (90%) rename plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/{constant => regex}/package-info.java (65%) rename plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/{constant => regex}/PatternConstsTests.java (99%) create mode 100644 plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/regex/PatternInfosTests.java diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumber.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumber.java index 611780e..984334e 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumber.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumber.java @@ -33,7 +33,7 @@ import com.google.errorprone.annotations.Immutable; import xyz.zhouxy.plusone.commons.annotation.ReaderMethod; import xyz.zhouxy.plusone.commons.annotation.ValueObject; -import xyz.zhouxy.plusone.commons.constant.PatternConsts; +import xyz.zhouxy.plusone.commons.regex.PatternConsts; import xyz.zhouxy.plusone.commons.util.StringTools; /** @@ -44,7 +44,7 @@ import xyz.zhouxy.plusone.commons.util.StringTools; * * @author ZhouXY * @since 1.0.0 - * @see xyz.zhouxy.plusone.commons.constant.PatternConsts#CHINESE_2ND_ID_CARD_NUMBER + * @see xyz.zhouxy.plusone.commons.regex.PatternConsts#CHINESE_2ND_ID_CARD_NUMBER */ @ValueObject @Immutable diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/AbstractMatcher.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/AbstractMatcher.java new file mode 100644 index 0000000..29c2e22 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/AbstractMatcher.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.regex; + +import java.util.regex.Matcher; + +public abstract class AbstractMatcher { + private final Matcher matcher; + + AbstractMatcher(Matcher matcher) { + this.matcher = matcher; + } + + public final Matcher matcher() { + return this.matcher; + } + + public final boolean matches() { + return this.matcher.matches(); + } + + public final String getGroupValue(String groupName) { + return this.matcher.group(groupName); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/Chinese2ndIdCardNumberMatcher.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/Chinese2ndIdCardNumberMatcher.java new file mode 100644 index 0000000..f41662d --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/Chinese2ndIdCardNumberMatcher.java @@ -0,0 +1,60 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.regex; + +import java.util.regex.Matcher; + +import xyz.zhouxy.plusone.commons.model.Gender; + +public final class Chinese2ndIdCardNumberMatcher extends AbstractMatcher { + + Chinese2ndIdCardNumberMatcher(Matcher matcher) { + super(matcher); + } + + public final String getProvince() { + return getGroupValue("province"); + } + + public final String getCity() { + return getGroupValue("city"); + } + + public final String getCounty() { + return getGroupValue("county"); + } + + public final String getBirthDate() { + return getGroupValue("birthDate"); + } + + public final String getOrderCode() { + return getGroupValue("orderCode"); + } + + public final String getGenderCode() { + return getGroupValue("gender"); + } + + public final Gender getGender() { + final int genderCode = Integer.parseInt(getGenderCode()); + return genderCode % 2 == 0 ? Gender.FEMALE : Gender.MALE; + } + + public final String getCheckDigit() { + return getGroupValue("checkDigit"); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/LocalDateMatcher.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/LocalDateMatcher.java new file mode 100644 index 0000000..a2064c3 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/LocalDateMatcher.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.regex; + +import java.util.regex.Matcher; + +public final class LocalDateMatcher extends AbstractMatcher { + + LocalDateMatcher(Matcher matcher) { + super(matcher); + } + + public String getYear() { + return getGroupValue("yyyy"); + } + + public String getMonth() { + return getGroupValue("MM"); + } + + public String getDay() { + return getGroupValue("dd"); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/PatternConsts.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternConsts.java similarity index 98% rename from plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/PatternConsts.java rename to plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternConsts.java index afc80ec..7886101 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/PatternConsts.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternConsts.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package xyz.zhouxy.plusone.commons.constant; +package xyz.zhouxy.plusone.commons.regex; import java.util.regex.Pattern; diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternInfo.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternInfo.java new file mode 100644 index 0000000..e22de6a --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternInfo.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.regex; + +import java.util.regex.Pattern; + +public abstract class PatternInfo { + private final String regex; + private final Pattern pattern; + + PatternInfo(String regex, Pattern pattern) { + this.regex = regex; + this.pattern = pattern; + } + + public final String regex() { + return this.regex; + } + + public final Pattern pattern() { + return this.pattern; + } + + public abstract T matcher(String input); +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternInfos.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternInfos.java new file mode 100644 index 0000000..7843109 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/PatternInfos.java @@ -0,0 +1,114 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.regex; + +import java.util.regex.Matcher; + +public final class PatternInfos { + + // @see RegexConsts#BASIC_ISO_DATE + public static final PatternInfo BASIC_ISO_DATE = new PatternInfo( + RegexConsts.BASIC_ISO_DATE, + PatternConsts.BASIC_ISO_DATE) { + @Override + public LocalDateMatcher matcher(String input) { + Matcher matcher = pattern().matcher(input); + return new LocalDateMatcher(matcher); + } + }; + + // @see RegexConsts#ISO_LOCAL_DATE + public static final PatternInfo ISO_LOCAL_DATE = new PatternInfo( + RegexConsts.ISO_LOCAL_DATE, + PatternConsts.ISO_LOCAL_DATE) { + @Override + public LocalDateMatcher matcher(String input) { + Matcher matcher = pattern().matcher(input); + return new LocalDateMatcher(matcher); + } + }; + + // @see RegexConsts#PASSWORD + public static final PatternInfo PASSWORD = new PatternInfo( + RegexConsts.PASSWORD, + PatternConsts.PASSWORD) { + @Override + public Matcher matcher(String input) { + return pattern().matcher(input); + } + }; + + // @see RegexConsts#CAPTCHA + public static final PatternInfo CAPTCHA = new PatternInfo( + RegexConsts.CAPTCHA, + PatternConsts.CAPTCHA) { + @Override + public Matcher matcher(String input) { + return pattern().matcher(input); + } + }; + + // @see RegexConsts#EMAIL + public static final PatternInfo EMAIL = new PatternInfo( + RegexConsts.EMAIL, + PatternConsts.EMAIL) { + @Override + public Matcher matcher(String input) { + return pattern().matcher(input); + } + }; + + public static final PatternInfo MOBILE_PHONE = new PatternInfo( + RegexConsts.MOBILE_PHONE, + PatternConsts.MOBILE_PHONE) { + @Override + public Matcher matcher(String input) { + return pattern().matcher(input); + } + }; + + public static final PatternInfo USERNAME = new PatternInfo( + RegexConsts.USERNAME, + PatternConsts.USERNAME) { + @Override + public Matcher matcher(String input) { + return pattern().matcher(input); + } + }; + + public static final PatternInfo NICKNAME = new PatternInfo( + RegexConsts.NICKNAME, + PatternConsts.NICKNAME) { + @Override + public Matcher matcher(String input) { + return pattern().matcher(input); + } + }; + + public static final PatternInfo CHINESE_2ND_ID_CARD_NUMBER = new PatternInfo( + RegexConsts.CHINESE_2ND_ID_CARD_NUMBER, + PatternConsts.CHINESE_2ND_ID_CARD_NUMBER) { + @Override + public Chinese2ndIdCardNumberMatcher matcher(String input) { + Matcher matcher = pattern().matcher(input); + return new Chinese2ndIdCardNumberMatcher(matcher); + } + }; + + private PatternInfos() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/RegexConsts.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/RegexConsts.java similarity index 90% rename from plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/RegexConsts.java rename to plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/RegexConsts.java index 4cdaa58..2f94e88 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/RegexConsts.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/RegexConsts.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package xyz.zhouxy.plusone.commons.constant; +package xyz.zhouxy.plusone.commons.regex; /** * 正则表达式常量 @@ -41,12 +41,15 @@ public final class RegexConsts { public static final String MOBILE_PHONE = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$"; - public static final String USERNAME = "^[\\w-_.@]{4,36}$"; + public static final String USERNAME = "^[\\w-_]{4,36}$"; public static final String NICKNAME = "^[\\w-_.@]{4,36}$"; public static final String CHINESE_2ND_ID_CARD_NUMBER - = "^(?(?(?\\d{2})\\d{2})\\d{2})(?\\d{8})\\d{2}(?\\d)([\\dX])$"; + = "^(?(?(?\\d{2})\\d{2})\\d{2})" + + "(?\\d{8})" + + "(?\\d{2}(?\\d))" + + "(?[\\dX])$"; private RegexConsts() { throw new IllegalStateException("Utility class"); diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/package-info.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/package-info.java similarity index 65% rename from plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/package-info.java rename to plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/package-info.java index 0e1cfa9..6bf300e 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/constant/package-info.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/regex/package-info.java @@ -14,16 +14,4 @@ * limitations under the License. */ -/** - *

常量

- * - *

- * 1. 正则常量 - *

- * {@link RegexConsts} 包含常见正则表达式;{@link PatternConsts} 包含对应的 {@link Pattern} 对象。 - * - * @author ZhouXY - */ -package xyz.zhouxy.plusone.commons.constant; - -import java.util.regex.Pattern; +package xyz.zhouxy.plusone.commons.regex; diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/StringTools.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/StringTools.java index 3fa2f1b..c5f2dcf 100644 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/StringTools.java +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/util/StringTools.java @@ -26,7 +26,7 @@ import javax.annotation.Nullable; import com.google.common.annotations.Beta; -import xyz.zhouxy.plusone.commons.constant.PatternConsts; +import xyz.zhouxy.plusone.commons.regex.PatternConsts; /** * StringTools diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/model/ValidatableStringRecordTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/model/ValidatableStringRecordTests.java index 7bafc90..70e25d5 100644 --- a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/model/ValidatableStringRecordTests.java +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/model/ValidatableStringRecordTests.java @@ -31,7 +31,7 @@ import org.slf4j.LoggerFactory; import xyz.zhouxy.plusone.commons.annotation.StaticFactoryMethod; import xyz.zhouxy.plusone.commons.annotation.ValueObject; -import xyz.zhouxy.plusone.commons.constant.PatternConsts; +import xyz.zhouxy.plusone.commons.regex.PatternConsts; import java.util.Arrays; import java.util.Collections; diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/constant/PatternConstsTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/regex/PatternConstsTests.java similarity index 99% rename from plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/constant/PatternConstsTests.java rename to plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/regex/PatternConstsTests.java index 5a216a4..02a258e 100644 --- a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/constant/PatternConstsTests.java +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/regex/PatternConstsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package xyz.zhouxy.plusone.commons.constant; +package xyz.zhouxy.plusone.commons.regex; import static org.junit.jupiter.api.Assertions.*; diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/regex/PatternInfosTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/regex/PatternInfosTests.java new file mode 100644 index 0000000..3b6b1f5 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/regex/PatternInfosTests.java @@ -0,0 +1,259 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.zhouxy.plusone.commons.regex; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.regex.Matcher; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import lombok.extern.slf4j.Slf4j; +import xyz.zhouxy.plusone.commons.model.Gender; + +@Slf4j +public // +class PatternInfosTests { + + // ================================ + // #region - BASIC_ISO_DATE + // ================================ + + @Test + void testBasicIsoDate_ValidDate() { + LocalDateMatcher localDateMatcher = PatternInfos.BASIC_ISO_DATE.matcher("20241229"); + Matcher matcher = localDateMatcher.matcher(); + assertTrue(matcher.matches()); + assertTrue(localDateMatcher.matches()); + + assertEquals("2024", localDateMatcher.getYear()); + assertEquals("12", localDateMatcher.getMonth()); + assertEquals("29", localDateMatcher.getDay()); + } + + @ParameterizedTest + @ValueSource(strings = { + "20231301", // InvalidMonth + "20230230", // InvalidDay + "20210229", // NonLeapYearFeb29 + }) + void testBasicIsoDate_InvalidDate_butMatches(String date) { + // 虽然日期有误,但这个正则无法判断。实际工作中,应使用日期时间 API。 + LocalDateMatcher matcher = PatternInfos.BASIC_ISO_DATE.matcher(date); + assertTrue(matcher.matches()); + } + + @ParameterizedTest + @ValueSource(strings = { + "2023041", // TooShort + "99999999990415", // TooLong + "2023-04-15", // NonNumeric + }) + void testBasicIsoDate_InvalidDate_Mismatches(String date) { + LocalDateMatcher matcher = PatternInfos.BASIC_ISO_DATE.matcher(date); + assertFalse(matcher.matches()); + } + + // ================================ + // #endregion - BASIC_ISO_DATE + // ================================ + + // ================================ + // #region - ISO_LOCAL_DATE + // ================================ + + @Test + void testIsoLocalDate_ValidDate() { + LocalDateMatcher matcher = PatternInfos.ISO_LOCAL_DATE.matcher("2024-12-29"); + assertTrue(matcher.matches()); + assertEquals("2024", matcher.getYear()); + assertEquals("12", matcher.getMonth()); + assertEquals("29", matcher.getDay()); + + // LeapYearFeb29() + assertTrue(PatternInfos.ISO_LOCAL_DATE.matcher("2020-02-29").matches()); + + // BoundaryMin() + assertTrue(PatternInfos.ISO_LOCAL_DATE.matcher("0000-01-01").matches()); + + // BoundaryMax() + assertTrue(PatternInfos.ISO_LOCAL_DATE.matcher("999999999-12-31").matches()); + } + + @ParameterizedTest + @ValueSource(strings = { + "2023-13-01", // InvalidMonth + "2023-02-30", // InvalidDay + "2021-02-29", // NonLeapYearFeb29 + }) + void testIsoLocalDate_InvalidDate_butMatches(String date) { + // 虽然日期有误,但这个正则无法判断。实际工作中,应使用日期时间 API。 + LocalDateMatcher matcher = PatternInfos.ISO_LOCAL_DATE.matcher(date); + assertTrue(matcher.matches()); + } + + @ParameterizedTest + @ValueSource(strings = { + "2023-04-1", // TooShort + "9999999999-04-15", // TooLong + "20230415", + }) + void testIsoLocalDate_InvalidDate_Mismatches(String date) { + LocalDateMatcher matcher = PatternInfos.ISO_LOCAL_DATE.matcher(date); + assertFalse(matcher.matches()); + } + + // ================================ + // #endregion - ISO_LOCAL_DATE + // ================================ + + // ================================ + // #region - PASSWORD + // ================================ + + @Test + void testPassword_ValidPassword_Matches() { + assertTrue(PatternInfos.PASSWORD.matcher("Abc123!@#").matches()); + } + + @Test + void testPassword_InvalidPassword_Mismatches() { + assertFalse(PatternInfos.PASSWORD.matcher("Abc123 !@#").matches()); // 带空格 + assertFalse(PatternInfos.PASSWORD.matcher("Abc123!@# ").matches()); // 带空格 + assertFalse(PatternInfos.PASSWORD.matcher(" Abc123!@#").matches()); // 带空格 + assertFalse(PatternInfos.PASSWORD.matcher(" Abc123!@# ").matches()); // 带空格 + assertFalse(PatternInfos.PASSWORD.matcher("77553366998844113322").matches()); // 纯数字 + assertFalse(PatternInfos.PASSWORD.matcher("poiujhgbfdsazxcfvghj").matches()); // 纯小写字母 + assertFalse(PatternInfos.PASSWORD.matcher("POIUJHGBFDSAZXCFVGHJ").matches()); // 纯大写字母 + assertFalse(PatternInfos.PASSWORD.matcher("!#$%&'*\\+-/=?^`{|}~@()[]\",.;':").matches()); // 纯特殊字符 + assertFalse(PatternInfos.PASSWORD.matcher("sdfrghbv525842582752").matches()); // 没有小写字母 + assertFalse(PatternInfos.PASSWORD.matcher("SDFRGHBV525842582752").matches()); // 没有小写字母 + assertFalse(PatternInfos.PASSWORD.matcher("sdfrghbvSDFRGHBV").matches()); // 没有数字 + assertFalse(PatternInfos.PASSWORD.matcher("Abc1!").matches()); // 太短 + assertFalse(PatternInfos.PASSWORD.matcher("Abc1!Abc1!Abc1!Abc1!Abc1!Abc1!Abc1!").matches()); // 太长 + assertFalse(PatternInfos.PASSWORD.matcher("").matches()); + assertFalse(PatternInfos.PASSWORD.matcher(" ").matches()); + } + + // ================================ + // #endregion - PASSWORD + // ================================ + + // ================================ + // #region - EMAIL + // ================================ + + @Test + public void testValidEmails() { + assertTrue(PatternInfos.EMAIL.matcher("test@example.com").matches()); + assertTrue(PatternInfos.EMAIL.matcher("user.name+tag+sorting@example.com").matches()); + assertTrue(PatternInfos.EMAIL.matcher("user@sub.example.com").matches()); + assertTrue(PatternInfos.EMAIL.matcher("user@123.123.123.123").matches()); + } + + @Test + public void testInvalidEmails() { + assertFalse(PatternInfos.EMAIL.matcher(".username@example.com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("@missingusername.com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("plainaddress").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username..username@example.com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username.@example.com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@-example.com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@-example.com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@.com.com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@.com.my").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@.com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@com.").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@example..com").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@example.com-").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@example.com.").matches()); + assertFalse(PatternInfos.EMAIL.matcher("username@example").matches()); + } + + // ================================ + // #endregion - EMAIL + // ================================ + + // ================================ + // #region - Chinese2ndIdCardNumber + // ================================ + + @ParameterizedTest + @ValueSource(strings = { + "44520019900101456X", + "44520019900101456x", + "445200199001014566", + }) + void testChinese2ndIdCardNumber_ValidChinese2ndIdCardNumber(String value) { + Chinese2ndIdCardNumberMatcher chinese2ndIdCardNumberMatcher = PatternInfos.CHINESE_2ND_ID_CARD_NUMBER.matcher(value); + Matcher matcher = chinese2ndIdCardNumberMatcher.matcher(); + assertTrue(matcher.matches()); + assertEquals("44", chinese2ndIdCardNumberMatcher.getProvince()); + assertEquals("4452", chinese2ndIdCardNumberMatcher.getCity()); + assertEquals("445200", chinese2ndIdCardNumberMatcher.getCounty()); + assertEquals("19900101", chinese2ndIdCardNumberMatcher.getBirthDate()); + assertEquals("456", chinese2ndIdCardNumberMatcher.getOrderCode()); + assertEquals("6", chinese2ndIdCardNumberMatcher.getGenderCode()); + assertEquals(Gender.FEMALE, chinese2ndIdCardNumberMatcher.getGender()); + + String checkDigit = value.substring(value.length() - 1); + assertEquals(checkDigit, chinese2ndIdCardNumberMatcher.getCheckDigit()); + } + + @ParameterizedTest + @ValueSource(strings = { + "4452200199001014566", + "44520199001014566", + " ", + "", + }) + void testChinese2ndIdCardNumber_InvalidChinese2ndIdCardNumber(String value) { + assertFalse(PatternInfos.CHINESE_2ND_ID_CARD_NUMBER.matcher(value).matches()); + } + + // ================================ + // #endregion - Chinese2ndIdCardNumber + // ================================ + + // ================================ + // #region - invoke constructor + // ================================ + + @Test + void test_constructor_isNotAccessible_ThrowsIllegalStateException() { + Constructor[] constructors; + constructors = PatternInfos.class.getDeclaredConstructors(); + Arrays.stream(constructors) + .forEach(constructor -> { + assertFalse(constructor.isAccessible()); + constructor.setAccessible(true); + Throwable cause = assertThrows(Exception.class, constructor::newInstance) + .getCause(); + assertInstanceOf(IllegalStateException.class, cause); + assertEquals("Utility class", cause.getMessage()); + }); + } + + // ================================ + // #endregion - invoke constructor + // ================================ +}