diff --git a/src/main/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumber.java b/src/main/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumber.java new file mode 100644 index 0000000..2f2bed2 --- /dev/null +++ b/src/main/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumber.java @@ -0,0 +1,153 @@ +package xyz.zhouxy.plusone.commons.model; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +/** + * 中国第二代居民身份证号 + */ +public class Chinese2ndGenIDCardNumber extends IDCardNumber { + private static final long serialVersionUID = 20241011231542L; + + /** 省份编码 */ + private final String provinceCode; + /** 市级编码 */ + private final String cityCode; + /** 县级编码 */ + private final String countyCode; + /** 性别 */ + private final Gender gender; + /** 出生日期 */ + private final LocalDate birthDate; + + public static final Pattern PATTERN = Pattern.compile("^(((\\d{2})\\d{2})\\d{2})(\\d{8})\\d{2}(\\d)(\\d|X|x)$"); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + private Chinese2ndGenIDCardNumber(String idNumber) { + super(idNumber, PATTERN, "Invalid ID number"); + + try { + final Matcher matcher = getMatcher(); + this.provinceCode = matcher.group(3); + this.cityCode = matcher.group(2); + this.countyCode = matcher.group(1); + + // 性别 + final String genderStr = matcher.group(5); + final int genderIndex = Integer.parseInt(genderStr); + this.gender = genderIndex % 2 == 0 ? Gender.FEMALE : Gender.MALE; + + // 出生日期 + final String birthDateStr = matcher.group(4); + this.birthDate = LocalDate.parse(birthDateStr, DATE_FORMATTER); + } + catch (DateTimeParseException e) { + throw new IllegalArgumentException(e); + } + } + + public static Chinese2ndGenIDCardNumber of(String idNumber) { + return new Chinese2ndGenIDCardNumber(idNumber); + } + + public String getProvinceCode() { + return provinceCode; + } + + public String getProvinceName() { + return PROVINCE_CODES.get(this.provinceCode); + } + + public String getFullProvinceCode() { + return Strings.padEnd(this.provinceCode, 12, '0'); + } + + public String getCityCode() { + return cityCode; + } + + public String getFullCityCode() { + return Strings.padEnd(this.cityCode, 12, '0'); + } + + public String getCountyCode() { + return countyCode; + } + + public String getFullCountyCode() { + return Strings.padEnd(this.countyCode, 12, '0'); + } + + @Override + public Gender getGender() { + return gender; + } + + @Override + public LocalDate getBirthDate() { + return birthDate; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + /** + * 省份代码表 + */ + public static final Map PROVINCE_CODES; + + static { + PROVINCE_CODES = ImmutableMap.builder() + .put("11", "北京") + .put("12", "天津") + .put("13", "河北") + .put("14", "山西") + .put("15", "内蒙古") + .put("21", "辽宁") + .put("22", "吉林") + .put("23", "黑龙江") + .put("31", "上海") + .put("32", "江苏") + .put("33", "浙江") + .put("34", "安徽") + .put("35", "福建") + .put("36", "江西") + .put("37", "山东") + .put("41", "河南") + .put("42", "湖北") + .put("43", "湖南") + .put("44", "广东") + .put("45", "广西") + .put("46", "海南") + .put("50", "重庆") + .put("51", "四川") + .put("52", "贵州") + .put("53", "云南") + .put("54", "西藏") + .put("61", "陕西") + .put("62", "甘肃") + .put("63", "青海") + .put("64", "宁夏") + .put("65", "新疆") + .put("71", "台湾") + .put("81", "香港") + .put("82", "澳门") + .put("83", "台湾") // 台湾身份证号码以83开头,但是行政区划为71 + .put("91", "国外") + .build(); + } +} diff --git a/src/main/java/xyz/zhouxy/plusone/commons/model/IDCardNumber.java b/src/main/java/xyz/zhouxy/plusone/commons/model/IDCardNumber.java new file mode 100644 index 0000000..f09a73a --- /dev/null +++ b/src/main/java/xyz/zhouxy/plusone/commons/model/IDCardNumber.java @@ -0,0 +1,46 @@ +package xyz.zhouxy.plusone.commons.model; + +import java.time.LocalDate; +import java.time.Period; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; + +/** + * 身份证号 + */ +public abstract class IDCardNumber extends ValidatableStringRecord { + private static final long serialVersionUID = 20241011231527L; + + protected IDCardNumber(@Nonnull String idNumber, @Nonnull Pattern pattern) + throws IllegalArgumentException{ + super(idNumber, pattern); + } + + protected IDCardNumber(@Nonnull String idNumber, @Nonnull Pattern pattern, + @Nonnull String errorMessage) { + super(idNumber, pattern, errorMessage); + } + + protected IDCardNumber(@Nonnull String idNumber, @Nonnull Pattern pattern, + @Nonnull Supplier errorMessage) { + super(idNumber, pattern, errorMessage); + } + + /** + * 根据身份证号判断性别 + */ + public abstract Gender getGender(); + + /** + * 获取出生日期 + */ + public abstract LocalDate getBirthDate(); + + /** 计算年龄 */ + public final int calculateAge() { + LocalDate now = LocalDate.now(); + return Period.between(getBirthDate(), now).getYears(); + } +} diff --git a/src/test/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumberTests.java b/src/test/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumberTests.java new file mode 100644 index 0000000..ff4ea2e --- /dev/null +++ b/src/test/java/xyz/zhouxy/plusone/commons/model/Chinese2ndGenIDCardNumberTests.java @@ -0,0 +1,57 @@ +package xyz.zhouxy.plusone.commons.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.regex.Matcher; +import org.junit.jupiter.api.Test; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Chinese2ndGenIDCardNumberTests { + + @Test + void testPattern() { + Matcher matcher = Chinese2ndGenIDCardNumber.PATTERN.matcher("11010520000101111X"); + assertTrue(matcher.matches()); + for (int i = 0; i < matcher.groupCount(); i++) { + log.info("{}: {}", i, matcher.group(i)); + } + } + + @Test + void test() throws CloneNotSupportedException { + Chinese2ndGenIDCardNumber idCardNumber = Chinese2ndGenIDCardNumber.of("11010520000101111X"); + assertEquals("11010520000101111X", idCardNumber.value()); + assertEquals(LocalDate.of(2000, 1, 1), idCardNumber.getBirthDate()); + assertEquals(Gender.MALE, idCardNumber.getGender()); + assertEquals("110105", idCardNumber.getCountyCode()); + assertEquals("110105000000", idCardNumber.getFullCountyCode()); + + assertEquals("1101", idCardNumber.getCityCode()); + assertEquals("110100000000", idCardNumber.getFullCityCode()); + + assertEquals("11", idCardNumber.getProvinceCode()); + assertEquals("110000000000", idCardNumber.getFullProvinceCode()); + + assertEquals("北京", idCardNumber.getProvinceName()); + + assertThrows(IllegalArgumentException.class, + () -> Chinese2ndGenIDCardNumber.of("1101520000101111")); + + assertThrows(IllegalArgumentException.class, + () -> Chinese2ndGenIDCardNumber.of("11010520002101111X")); + + try { + Chinese2ndGenIDCardNumber.of("11010520002101111X"); + } + catch (IllegalArgumentException e) { + log.error(e.getMessage(), e); + assertTrue(e.getCause() instanceof DateTimeParseException); + } + } +}