This commit is contained in:
Looly 2024-07-31 18:10:11 +08:00
parent a22f4d686a
commit 68e0186ca7
3 changed files with 249 additions and 156 deletions

View File

@ -0,0 +1,236 @@
/*
* 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.data;
import org.dromara.hutool.core.regex.ReUtil;
import org.dromara.hutool.core.text.StrUtil;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 10位公民身份号码Citizen Identification Number一般用于台湾澳门香港
*
* @author Looly
* @since 6.0.0
*/
public class CIN10 {
private static final Pattern PATTERN_TW = Pattern.compile("^[a-zA-Z][0-9]{9}$");
private static final Pattern PATTERN_MC = Pattern.compile("^[157][0-9]{6}\\(?[0-9A-Z]\\)?$");
private static final Pattern PATTERN_HK = Pattern.compile("^[A-Z]{1,2}[0-9]{6}\\(?[0-9A]\\)?$");
/**
* 台湾身份首字母对应数字
*/
private static final Map<Character, Integer> TW_FIRST_CODE = new HashMap<>();
static {
TW_FIRST_CODE.put('A', 10);
TW_FIRST_CODE.put('B', 11);
TW_FIRST_CODE.put('C', 12);
TW_FIRST_CODE.put('D', 13);
TW_FIRST_CODE.put('E', 14);
TW_FIRST_CODE.put('F', 15);
TW_FIRST_CODE.put('G', 16);
TW_FIRST_CODE.put('H', 17);
TW_FIRST_CODE.put('J', 18);
TW_FIRST_CODE.put('K', 19);
TW_FIRST_CODE.put('L', 20);
TW_FIRST_CODE.put('M', 21);
TW_FIRST_CODE.put('N', 22);
TW_FIRST_CODE.put('P', 23);
TW_FIRST_CODE.put('Q', 24);
TW_FIRST_CODE.put('R', 25);
TW_FIRST_CODE.put('S', 26);
TW_FIRST_CODE.put('T', 27);
TW_FIRST_CODE.put('U', 28);
TW_FIRST_CODE.put('V', 29);
TW_FIRST_CODE.put('X', 30);
TW_FIRST_CODE.put('Y', 31);
TW_FIRST_CODE.put('W', 32);
TW_FIRST_CODE.put('Z', 33);
TW_FIRST_CODE.put('I', 34);
TW_FIRST_CODE.put('O', 35);
}
/**
* 创建并验证台湾澳门香港身份证号码
*
* @param code 台湾澳门香港身份证号码
* @return CIN10
*/
public static CIN10 of(final String code) {
return new CIN10(code);
}
private final String code;
private final String province;
private final Gender gender;
private final boolean verified;
/**
* 构造
*
* @param code 身份证号码
* @throws IllegalArgumentException 身份证格式不支持
*/
public CIN10(final String code) throws IllegalArgumentException {
this.code = code;
if (StrUtil.isNotBlank(code)) {
if (ReUtil.isMatch(PATTERN_TW, code)) { // 台湾
this.province = "台湾";
final char char2 = code.charAt(1);
if ('1' == char2) {
this.gender = Gender.MALE;
} else if ('2' == char2) {
this.gender = Gender.FEMALE;
} else {
this.gender = Gender.UNKNOWN;
}
this.verified = verifyTWCard(code);
return;
} else if (ReUtil.isMatch(PATTERN_MC, code)) { // 澳门
this.province = "澳门";
this.gender = Gender.UNKNOWN;
this.verified = true;
return;
} else if (ReUtil.isMatch(PATTERN_HK, code)) { // 香港
this.province = "香港";
this.gender = Gender.UNKNOWN;
this.verified = verfyHKCard(code);
return;
}
}
throw new IllegalArgumentException("Invalid CIN10 code!");
}
/**
* 获取CIN10码
*
* @return CIN10码
*/
public String getCode() {
return code;
}
/**
* 获取省份
*
* @return 省份
*/
public String getProvince() {
return province;
}
/**
* 获取性别
*
* @return 性别
*/
public Gender getGender() {
return gender;
}
/**
* 是否验证通过
*
* @return 是否验证通过
*/
public boolean isVerified() {
return verified;
}
/**
* 性别枚举
*/
public enum Gender {
/**
*
*/
MALE,
/**
*
*/
FEMALE,
/**
* 未知
*/
UNKNOWN
}
/**
* 验证台湾身份证号码
*
* @param code 身份证号码
* @return 验证码是否符合
*/
private static boolean verifyTWCard(final String code) {
final Integer iStart = TW_FIRST_CODE.get(code.charAt(0));
if (null == iStart) {
return false;
}
int sum = iStart / 10 + (iStart % 10) * 9;
final String mid = code.substring(1, 9);
final char[] chars = mid.toCharArray();
int iflag = 8;
for (final char c : chars) {
sum += Integer.parseInt(String.valueOf(c)) * iflag;
iflag--;
}
final String end = code.substring(9, 10);
return (sum % 10 == 0 ? 0 : (10 - sum % 10)) == Integer.parseInt(end);
}
/**
* 验证香港身份证号码(存在Bug部份特殊身份证无法检查)
* <p>
* 身份证前2位为英文字符如果只出现一个英文字符则表示第一位是空格对应数字58 前2位英文字符A-Z分别对应数字10-35 最后一位校验码为0-9的数字加上字符"A""A"代表10
* </p>
* <p>
* 将身份证号码全部转换为数字分别对应乘9-1相加的总和整除11则证件号码有效
* </p>
*
* @param code 身份证号码
* @return 验证码是否符合
*/
private static boolean verfyHKCard(final String code) {
String card = code.replaceAll("[()]", "");
int sum;
if (card.length() == 9) {
sum = (Character.toUpperCase(card.charAt(0)) - 55) * 9 + (Character.toUpperCase(card.charAt(1)) - 55) * 8;
card = card.substring(1, 9);
} else {
sum = 522 + (Character.toUpperCase(card.charAt(0)) - 55) * 8;
}
// 首字母A-ZA表示1以此类推
final String mid = card.substring(1, 7);
final String end = card.substring(7, 8);
final char[] chars = mid.toCharArray();
int iflag = 7;
for (final char c : chars) {
sum = sum + Integer.parseInt(String.valueOf(c)) * iflag;
iflag--;
}
if ("A".equalsIgnoreCase(end)) {
sum += 10;
} else {
sum += Integer.parseInt(end);
}
return sum % 11 == 0;
}
}

View File

@ -17,8 +17,6 @@ import org.dromara.hutool.core.date.DateUtil;
import org.dromara.hutool.core.text.StrUtil;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 身份证相关工具类参考标准GB 11643-1999<br>
@ -34,39 +32,6 @@ import java.util.Map;
*/
public class IdcardUtil {
/**
* 台湾身份首字母对应数字
*/
private static final Map<Character, Integer> TW_FIRST_CODE = new HashMap<>();
static {
TW_FIRST_CODE.put('A', 10);
TW_FIRST_CODE.put('B', 11);
TW_FIRST_CODE.put('C', 12);
TW_FIRST_CODE.put('D', 13);
TW_FIRST_CODE.put('E', 14);
TW_FIRST_CODE.put('F', 15);
TW_FIRST_CODE.put('G', 16);
TW_FIRST_CODE.put('H', 17);
TW_FIRST_CODE.put('J', 18);
TW_FIRST_CODE.put('K', 19);
TW_FIRST_CODE.put('L', 20);
TW_FIRST_CODE.put('M', 21);
TW_FIRST_CODE.put('N', 22);
TW_FIRST_CODE.put('P', 23);
TW_FIRST_CODE.put('Q', 24);
TW_FIRST_CODE.put('R', 25);
TW_FIRST_CODE.put('S', 26);
TW_FIRST_CODE.put('T', 27);
TW_FIRST_CODE.put('U', 28);
TW_FIRST_CODE.put('V', 29);
TW_FIRST_CODE.put('X', 30);
TW_FIRST_CODE.put('Y', 31);
TW_FIRST_CODE.put('W', 32);
TW_FIRST_CODE.put('Z', 33);
TW_FIRST_CODE.put('I', 34);
TW_FIRST_CODE.put('O', 35);
}
/**
* 将15位身份证号码转换为18位<br>
* 15位身份证号码遵循GB 11643-1989标准
@ -112,8 +77,7 @@ public class IdcardUtil {
return false;
}
case 10: {// 10位身份证港澳台地区
final String[] cardVal = isValidCard10(idCard);
return null != cardVal && "true".equals(cardVal[2]);
return isValidCard10(idCard);
}
default:
return false;
@ -208,122 +172,16 @@ public class IdcardUtil {
}
/**
* 验证10位身份编码是否合法
*
* @param idcard 身份编码
* @return 身份证信息数组
* <p>
* [0] - 台湾澳门香港 [1] - 性别(男M,女F,未知N) [2] - 是否合法(合法true,不合法false) 若不是身份证件号码则返回null
* </p>
* 是否有效的10位身份证号码一般用于判断和验证台湾澳门香港身份证
* @param idcard 台湾澳门香港身份证号码
* @return 是否有效的10位身份证号码
*/
public static String[] isValidCard10(final String idcard) {
if (StrUtil.isBlank(idcard)) {
return null;
}
final String[] info = new String[3];
final String card = idcard.replaceAll("[()]", "");
if (card.length() != 8 && card.length() != 9 && idcard.length() != 10) {
return null;
}
if (idcard.matches("^[a-zA-Z][0-9]{9}$")) { // 台湾
info[0] = "台湾";
final char char2 = idcard.charAt(1);
if ('1' == char2) {
info[1] = "M";
} else if ('2' == char2) {
info[1] = "F";
} else {
info[1] = "N";
info[2] = "false";
return info;
}
info[2] = isValidTWCard(idcard) ? "true" : "false";
} else if (idcard.matches("^[157][0-9]{6}\\(?[0-9A-Z]\\)?$")) { // 澳门
info[0] = "澳门";
info[1] = "N";
info[2] = "true";
} else if (idcard.matches("^[A-Z]{1,2}[0-9]{6}\\(?[0-9A]\\)?$")) { // 香港
info[0] = "香港";
info[1] = "N";
info[2] = isValidHKCard(idcard) ? "true" : "false";
} else {
return null;
}
return info;
}
/**
* 验证台湾身份证号码
*
* @param idcard 身份证号码
* @return 验证码是否符合
*/
public static boolean isValidTWCard(final String idcard) {
if (null == idcard || idcard.length() != 10) {
public static boolean isValidCard10(final String idcard){
try{
return CIN10.of(idcard).isVerified();
}catch (final IllegalArgumentException e){
return false;
}
final Integer iStart = TW_FIRST_CODE.get(idcard.charAt(0));
if (null == iStart) {
return false;
}
int sum = iStart / 10 + (iStart % 10) * 9;
final String mid = idcard.substring(1, 9);
final char[] chars = mid.toCharArray();
int iflag = 8;
for (final char c : chars) {
sum += Integer.parseInt(String.valueOf(c)) * iflag;
iflag--;
}
final String end = idcard.substring(9, 10);
return (sum % 10 == 0 ? 0 : (10 - sum % 10)) == Integer.parseInt(end);
}
/**
* 验证香港身份证号码(存在Bug部份特殊身份证无法检查)
* <p>
* 身份证前2位为英文字符如果只出现一个英文字符则表示第一位是空格对应数字58 前2位英文字符A-Z分别对应数字10-35 最后一位校验码为0-9的数字加上字符"A""A"代表10
* </p>
* <p>
* 将身份证号码全部转换为数字分别对应乘9-1相加的总和整除11则证件号码有效
* </p>
*
* @param idcard 身份证号码
* @return 验证码是否符合
*/
public static boolean isValidHKCard(final String idcard) {
if (StrUtil.isBlank(idcard)) {
return false;
}
if(false == idcard.matches("^[A-Z]{1,2}[0-9]{6}\\(?[0-9A]\\)?$")){
return false;
}
String card = idcard.replaceAll("[()]", "");
int sum;
if (card.length() == 9) {
sum = (Character.toUpperCase(card.charAt(0)) - 55) * 9 + (Character.toUpperCase(card.charAt(1)) - 55) * 8;
card = card.substring(1, 9);
} else {
sum = 522 + (Character.toUpperCase(card.charAt(0)) - 55) * 8;
}
// 首字母A-ZA表示1以此类推
final String mid = card.substring(1, 7);
final String end = card.substring(7, 8);
final char[] chars = mid.toCharArray();
int iflag = 7;
for (final char c : chars) {
sum = sum + Integer.parseInt(String.valueOf(c)) * iflag;
iflag--;
}
if ("A".equalsIgnoreCase(end)) {
sum += 10;
} else {
sum += Integer.parseInt(end);
}
return sum % 11 == 0;
}
/**

View File

@ -163,20 +163,20 @@ public class IdcardUtilTest {
@Test
public void isValidHKCardIdTest(){
final String hkCard="P174468(6)";
final boolean flag=IdcardUtil.isValidHKCard(hkCard);
final boolean flag=IdcardUtil.isValidCard10(hkCard);
assertTrue(flag);
}
@Test
public void isValidTWCardIdTest() {
final String twCard = "B221690311";
boolean flag = IdcardUtil.isValidTWCard(twCard);
boolean flag = IdcardUtil.isValidCard10(twCard);
assertTrue(flag);
final String errTwCard1 = "M517086311";
flag = IdcardUtil.isValidTWCard(errTwCard1);
flag = IdcardUtil.isValidCard10(errTwCard1);
assertFalse(flag);
final String errTwCard2 = "B2216903112";
flag = IdcardUtil.isValidTWCard(errTwCard2);
flag = IdcardUtil.isValidCard10(errTwCard2);
assertFalse(flag);
}
@ -196,8 +196,7 @@ public class IdcardUtilTest {
@Test
public void issueIAFOLITest() {
final String idcard = "H01487002";
assertFalse(IdcardUtil.isValidHKCard(idcard));
assertNull(IdcardUtil.isValidCard10(idcard));
assertFalse(IdcardUtil.isValidCard10(idcard));
assertFalse(IdcardUtil.isValidCard(idcard));
}
}