diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/VehicleUtil.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/VehicleUtil.java new file mode 100644 index 000000000..386ac1a5b --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/VehicleUtil.java @@ -0,0 +1,22 @@ +package org.dromara.hutool.extra.vehicle; + +import org.dromara.hutool.extra.vehicle.vin.Vin; + +/** + * 汽车工具类封装 + * + * @author VampireAchao + */ +public class VehicleUtil { + + /** + * 解析车辆识别代码 + * + * @param vin 车辆识别代码 + * @return 解析后的结果 + */ + public static Vin parseVin(String vin) { + return Vin.of(vin); + } + +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/AlphanumericVinCode.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/AlphanumericVinCode.java new file mode 100644 index 000000000..fadb39449 --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/AlphanumericVinCode.java @@ -0,0 +1,54 @@ +package org.dromara.hutool.extra.vehicle.vin; + +/** + * 可以是字母或者数字的字码 + * + * @author dax + * @since 2023 /5/14 17:56 + */ +class AlphanumericVinCode implements MaskVinCode { + private final String code; + private final int index; + private final int mask; + + + /** + * 该构造会校验字码是否符合GB16735标准 + * + * @param code 字码值 + * @param index 索引位 + * @throws IllegalArgumentException the illegal argument exception + */ + AlphanumericVinCode(String code, int index) { + this.code = code; + this.index = index; + int weight = WEIGHT_FACTORS.get(index); + mask = NUMERIC.matcher(this.code).matches() ? + Integer.parseInt(this.code) * weight : + VinCodeMaskEnum.valueOf(this.code).getMaskCode() * weight; + } + + @Override + public String getCode() { + return code; + } + + @Override + public int getMask() { + return mask; + } + + @Override + public int getIndex() { + return index; + } + + @Override + public String toString() { + return "AlphanumericVinCode{" + + "code='" + code + '\'' + + ", index=" + index + + ", mask=" + mask + + '}'; + } +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/MaskVinCode.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/MaskVinCode.java new file mode 100644 index 000000000..bcf664559 --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/MaskVinCode.java @@ -0,0 +1,36 @@ +package org.dromara.hutool.extra.vehicle.vin; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +/** + * 支持掩码的字码 + * + * @author dax + * @since 2023 /5/15 10:43 + */ +interface MaskVinCode extends VinCode { + /** + * 字码权重因子 + */ + List WEIGHT_FACTORS = Arrays.asList(8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2); + /** + * 数字位校验 + */ + Pattern NUMERIC = Pattern.compile("^\\d$"); + + /** + * 获取掩码,字码编码*加权值 + * + * @return the mask + */ + int getMask(); + + /** + * 所在位置索引,[0,16] + * + * @return the index + */ + int getIndex(); +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/NumericVinCode.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/NumericVinCode.java new file mode 100644 index 000000000..ed02b28f0 --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/NumericVinCode.java @@ -0,0 +1,55 @@ +package org.dromara.hutool.extra.vehicle.vin; + +/** + * 数字字码. + * + * @author dax + * @since 2023 /5/14 17:42 + */ +class NumericVinCode implements MaskVinCode { + private final String code; + private final int index; + private final int mask; + + /** + * 该构造会校验字码是否符合GB16735标准 + * + * @param code 字码值 + * @param index 索引位 + * @throws IllegalArgumentException 校验 + */ + NumericVinCode(String code, int index) throws IllegalArgumentException { + + if (!NUMERIC.matcher(code).matches()) { + throw new IllegalArgumentException("索引为 " + index + " 的字码必须是数字"); + } + this.code = code; + this.index = index; + this.mask = Integer.parseInt(code) * WEIGHT_FACTORS.get(index); + } + + @Override + public String getCode() { + return code; + } + + @Override + public int getMask() { + return mask; + } + + @Override + public int getIndex() { + return index; + } + + + @Override + public String toString() { + return "NumericCode{" + + "code='" + code + '\'' + + ", index=" + index + + ", mask=" + mask + + '}'; + } +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vds.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vds.java new file mode 100644 index 000000000..9878249cd --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vds.java @@ -0,0 +1,87 @@ +package org.dromara.hutool.extra.vehicle.vin; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * VDS + * + * @author dax + * @since 2023 /5/15 9:37 + */ +class Vds implements VinCode { + + private final List vdCode; + private final AlphanumericVinCode checksum; + private final String code; + private final int mask; + + /** + * Instantiates a new Vds. + * + * @param vdCode the vd code + */ + Vds(List vdCode) { + this.vdCode = vdCode.subList(0, 5); + this.checksum = vdCode.get(5); + this.code = vdCode.stream().map(AlphanumericVinCode::getCode).collect(Collectors.joining()); + this.mask = vdCode.stream().mapToInt(AlphanumericVinCode::getMask).sum(); + } + + /** + * 从VIN生成VDS + * + * @param vin the vin + * @return the vds + */ + public static Vds from(String vin) { + List vdCode = IntStream.range(3, 9) + .mapToObj(index -> + new AlphanumericVinCode(String.valueOf(vin.charAt(index)), index)) + .collect(Collectors.toList()); + return new Vds(vdCode); + } + + /** + * Gets vd code. + * + * @return the vd code + */ + List getVdCode() { + return vdCode; + } + + /** + * Gets checksum. + * + * @return the checksum + */ + AlphanumericVinCode getChecksum() { + return checksum; + } + + @Override + public String getCode() { + return code; + } + + /** + * Gets mask. + * + * @return the mask + */ + int getMask() { + return mask; + } + + @Override + public String toString() { + return "Vds{" + + "vdCode=" + vdCode + + ", checksum=" + checksum + + ", code='" + code + '\'' + + ", mask=" + mask + + '}'; + } +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vin.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vin.java new file mode 100644 index 000000000..2c2e45b1c --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vin.java @@ -0,0 +1,211 @@ +package org.dromara.hutool.extra.vehicle.vin; + + +import java.time.Year; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.IntStream; + +/** + * VIN是Vehicle Identification Number的缩写,即车辆识别号码。VIN码是全球通行的车辆唯一标识符,由17位数字和字母组成。 + *

+ * 不同位数代表着不同意义,具体解释如下: + *

    + *
  • 1-3位:制造商标示符,代表车辆制造商信息
  • + *
  • 4-8位:车型识别代码,代表车辆品牌、车系、车型及其排量等信息
  • + *
  • 9位:校验位,通过公式计算出来,用于验证VIN码的正确性
  • + *
  • 10位:年份代号,代表车辆生产的年份
  • + *
  • 11位:工厂代码,代表车辆生产工厂信息
  • + *
  • 12-17位:流水号,代表车辆的生产顺序号
  • + *
+ * VIN码可以找到汽车详细的个人、工程、制造方面的信息,是判定一个汽车合法性及其历史的重要依据。 + *

+ * 本实现参考以下标准: + *

+ * + * @author dax + * @since 2023 /5/15 9:40 + */ +public final class Vin implements VinCode { + private static final Pattern GB16735_VIN_REGEX = Pattern.compile("^[A-HJ-NPR-Z\\d]{8}[X\\d][A-HJ-NPR-Z\\d]{8}$"); + private final Wmi wmi; + private final Vds vds; + private final Vis vis; + private final String code; + + /** + * Instantiates a new Vin. + * + * @param wmi the wmi + * @param vds the vds + * @param vis the vis + * @param code the code + */ + Vin(Wmi wmi, Vds vds, Vis vis, String code) { + this.wmi = wmi; + this.vds = vds; + this.vis = vis; + this.code = code; + } + + /** + * 从VIN字符串生成{@code Vin}对象 + * + * @param vin VIN字符串 + * @return VIN对象 vin + */ + public static Vin of(String vin) throws IllegalArgumentException { + if (!GB16735_VIN_REGEX.matcher(vin).matches()) { + throw new IllegalArgumentException("VIN格式不正确,需满足正则 " + GB16735_VIN_REGEX.pattern()); + } + Wmi wmi = Wmi.from(vin); + Vds vds = Vds.from(vin); + Vis vis = Vis.from(vin); + int factor = (wmi.getMask() + vds.getMask() + vis.getMask()) % 11; + String checked = factor != 10 ? String.valueOf(factor) : "X"; + if (!Objects.equals(vds.getChecksum().getCode(), checked)) { + throw new IllegalArgumentException("VIN校验不通过"); + } + return new Vin(wmi, vds, vis, vin); + } + + /** + * 仅判断一个字符串是否符合VIN规则 + * + * @param vinStr vinStr + * @return {@code true} 符合 + */ + public static boolean isValidVinCode(String vinStr) { + if (GB16735_VIN_REGEX.matcher(vinStr).matches()) { + int weights = IntStream.range(0, 17) + .map(i -> calculateWeight(vinStr, i)) + .sum(); + int factor = weights % 11; + char checked = factor != 10 ? (char) (factor + '0') : 'X'; + return vinStr.charAt(8) == checked; + } + return false; + } + + private static int calculateWeight(String vinStr, int i) { + char c = vinStr.charAt(i); + Integer factor = MaskVinCode.WEIGHT_FACTORS.get(i); + return c <= '9' ? + Character.getNumericValue(c) * factor : + VinCodeMaskEnum.valueOf(String.valueOf(c)).getMaskCode() * factor; + } + + /** + * 标识一个国家或者地区 + * + * @return the string + */ + public String geoCode() { + String wmiCode = this.wmi.getCode(); + return wmiCode.substring(0, 2); + } + + /** + * 制造厂标识码 + *

+ * 年产量大于1000为符合GB16737规定的{@link Wmi},年产量小于1000固定为9,需要结合VIN的第12、13、14位字码确定唯一 + * + * @return 主机厂识别码 string + */ + public String manufacturerCode() { + String wmiCode = this.wmi.getCode(); + return isLessThan1000() ? + wmiCode.concat(this.vis.getProdNoStr().substring(0, 3)) : wmiCode; + } + + /** + * 是否是年产量小于1000的车辆制造厂 + * + * @return 是否年产量小于1000 boolean + */ + public boolean isLessThan1000() { + return this.wmi.isLessThan1000(); + } + + /** + * 获取WMI码 + * + * @return WMI值 string + */ + public String wmiCode() { + return wmi.getCode(); + } + + /** + * 获取车辆特征描述码 + * + * @return VDS值 string + */ + public String vdsCode() { + return this.vds.getCode().substring(0, 5); + } + + /** + * 获取默认车型年份,接近于本年度 + * + * @return the int + */ + public Year defaultYear() { + return this.year(1); + } + + /** + * 获取车型年份 + *

+ * 自1980年起,30年一个周期 + * + * @param multiple 1 代表从 1980年开始的第一个30年 + * @return 返回年份对象 year + * @see 年份编码模型 + */ + public Year year(int multiple) { + return this.vis.getYear(multiple); + } + + /** + * 生产序号 + *

+ * 年产量大于1000为6位,年产量小于1000的为3位 + * + * @return 生产序号 string + */ + public String prodNo() { + String prodNoStr = this.vis.getProdNoStr(); + return isLessThan1000() ? + prodNoStr.substring(3, 6) : prodNoStr; + } + + /** + * 获取装配厂字码 + * + * @return 由厂家自行定义的装配厂字码 string + */ + public String oemCode() { + return this.vis.getOem().getCode(); + } + + @Override + public String getCode() { + return code; + } + + @Override + public String toString() { + return "Vin{" + + "wmi=" + wmi + + ", vds=" + vds + + ", vis=" + vis + + ", code='" + code + '\'' + + '}'; + } + +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/VinCode.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/VinCode.java new file mode 100644 index 000000000..b79ce0be7 --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/VinCode.java @@ -0,0 +1,18 @@ +package org.dromara.hutool.extra.vehicle.vin; + + +/** + * 汽车Vin字码抽象. + * + * @author dax + * @since 2023 /5/14 17:42 + */ +interface VinCode { + + /** + * 获取字码值 + * + * @return the code + */ + String getCode(); +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/VinCodeMaskEnum.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/VinCodeMaskEnum.java new file mode 100644 index 000000000..fa0fd5783 --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/VinCodeMaskEnum.java @@ -0,0 +1,117 @@ +package org.dromara.hutool.extra.vehicle.vin; + +/** + * Vin掩码枚举 + * + * @author dax + * @since 2023 /5/15 10:49 + */ +enum VinCodeMaskEnum { + /** + * A 掩码. + */ + A(1), + /** + * B 掩码. + */ + B(2), + /** + * C 掩码. + */ + C(3), + /** + * D 掩码. + */ + D(4), + /** + * E 掩码. + */ + E(5), + /** + * F 掩码. + */ + F(6), + /** + * G 掩码. + */ + G(7), + /** + * H 掩码. + */ + H(8), + /** + * J 掩码. + */ + J(1), + /** + * K 掩码. + */ + K(2), + /** + * L 掩码. + */ + L(3), + /** + * M 掩码. + */ + M(4), + /** + * N 掩码. + */ + N(5), + /** + * P 掩码. + */ + P(7), + /** + * R 掩码. + */ + R(9), + /** + * S 掩码. + */ + S(2), + /** + * T 掩码. + */ + T(3), + /** + * U 掩码. + */ + U(4), + /** + * V 掩码. + */ + V(5), + /** + * W 掩码. + */ + W(6), + /** + * X 掩码. + */ + X(7), + /** + * Y 掩码. + */ + Y(8), + /** + * Z 掩码. + */ + Z(9); + + private final int maskCode; + + VinCodeMaskEnum(int maskCode) { + this.maskCode = maskCode; + } + + /** + * 获取掩码值. + * + * @return the mask code + */ + public int getMaskCode() { + return maskCode; + } +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vis.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vis.java new file mode 100644 index 000000000..ff6b42689 --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Vis.java @@ -0,0 +1,139 @@ +package org.dromara.hutool.extra.vehicle.vin; + +import java.time.Year; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * VIS + * + * @author dax + * @since 2023 /5/15 12:07 + */ +class Vis implements VinCode { + + private static final int YEAR_LOOP = 30; + private static final List YEAR_ID = Arrays.asList( + "A", "B", "C", "D", "E", + "F", "G", "H", "J", "K", + "L", "M", "N", "P", "R", + "S", "T", "V", "W", "X", + "Y", "1", "2", "3", "4", + "5", "6", "7", "8", "9"); + private static final Map YEAR_MAP = new HashMap<>(); + + static { + for (int i = 0; i < YEAR_ID.size(); i++) { + YEAR_MAP.put(YEAR_ID.get(i), i); + } + } + + private final AlphanumericVinCode year; + private final AlphanumericVinCode oem; + private final List prodNo; + private final String prodNoStr; + private final String code; + private final int mask; + + /** + * Instantiates a new Vis. + * + * @param year the year + * @param oem the oem + * @param prodNo the prod no + */ + Vis(AlphanumericVinCode year, AlphanumericVinCode oem, List prodNo) { + this.year = year; + this.oem = oem; + this.prodNo = prodNo; + this.prodNoStr = prodNo.stream().map(MaskVinCode::getCode).collect(Collectors.joining()); + this.code = year.getCode() + oem.getCode() + prodNo.stream() + .map(MaskVinCode::getCode) + .collect(Collectors.joining()); + this.mask = year.getMask() + oem.getMask() + prodNo.stream().mapToInt(MaskVinCode::getMask).sum(); + } + + /** + * 从VIN生成VIS + * + * @param vin the vin + * @return the vis + */ + static Vis from(String vin) { + AlphanumericVinCode year = new AlphanumericVinCode(String.valueOf(vin.charAt(9)), 9); + AlphanumericVinCode factory = new AlphanumericVinCode(String.valueOf(vin.charAt(10)), 10); + List codes = IntStream.range(11, 17) + .mapToObj(index -> index < 14 ? new AlphanumericVinCode(String.valueOf(vin.charAt(index)), index) : + new NumericVinCode(String.valueOf(vin.charAt(index)), index)) + .collect(Collectors.toList()); + return new Vis(year, factory, codes); + } + + /** + * Gets year. + * + * @param multiple the multiple + * @return the year + */ + Year getYear(int multiple) { + int year = 1980 + YEAR_LOOP * multiple + YEAR_MAP.get(this.year.getCode()) % YEAR_LOOP; + return Year.of(year); + } + + /** + * Gets oem. + * + * @return the oem + */ + AlphanumericVinCode getOem() { + return oem; + } + + + /** + * Gets prod no. + * + * @return the prod no + */ + List getProdNo() { + return prodNo; + } + + /** + * Gets prod no str. + * + * @return the prod no str + */ + String getProdNoStr() { + return prodNoStr; + } + + /** + * Gets mask. + * + * @return the mask + */ + int getMask() { + return mask; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String toString() { + return "Vis{" + + "year=" + year + + ", oem=" + oem + + ", prodNo=" + prodNo + + ", code='" + code + '\'' + + ", mask=" + mask + + '}'; + } +} diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Wmi.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Wmi.java new file mode 100644 index 000000000..8f03a3933 --- /dev/null +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/vehicle/vin/Wmi.java @@ -0,0 +1,83 @@ +package org.dromara.hutool.extra.vehicle.vin; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * The type Wmi. + * + * @author dax + * @since 2023 /5/15 9:25 + */ +class Wmi implements VinCode { + private final String code; + private final int mask; + /** + * The Wmi codes. + */ + List wmiCodes; + + /** + * Instantiates a new Wmi. + * + * @param wmiCodes the wmi codes + */ + Wmi(List wmiCodes) { + this.wmiCodes = wmiCodes; + AtomicInteger mask = new AtomicInteger(); + this.code = wmiCodes.stream() + .peek(alphanumericCode -> mask.addAndGet(alphanumericCode.getMask())) + .map(AlphanumericVinCode::getCode) + .collect(Collectors.joining()); + this.mask = mask.get(); + } + + /** + * 从VIN生成WMI + * + * @param vin the vin + * @return the wmi + */ + static Wmi from(String vin) { + List codes = IntStream.range(0, 3) + .mapToObj(index -> + new AlphanumericVinCode(String.valueOf(vin.charAt(index)), index)) + .collect(Collectors.toList()); + return new Wmi(codes); + } + + @Override + public String getCode() { + return code; + } + + @Override + public String toString() { + return "Wmi{" + + "wmiCodes=" + wmiCodes + + ", code='" + code + '\'' + + ", mask=" + mask + + '}'; + } + + /** + * Gets mask. + * + * @return the mask + */ + int getMask() { + return mask; + } + + + /** + * 是否是年产量小于1000的车辆制造厂 + * + * @return the boolean + */ + boolean isLessThan1000() { + return this.code.matches("^.*9$"); + } +} diff --git a/hutool-extra/src/test/java/org/dromara/hutool/extra/vehicle/VehicleTest.java b/hutool-extra/src/test/java/org/dromara/hutool/extra/vehicle/VehicleTest.java new file mode 100644 index 000000000..de1b904e0 --- /dev/null +++ b/hutool-extra/src/test/java/org/dromara/hutool/extra/vehicle/VehicleTest.java @@ -0,0 +1,41 @@ +package org.dromara.hutool.extra.vehicle; + +import org.dromara.hutool.extra.vehicle.vin.Vin; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Year; + +/** + * @author VampireAchao + * @since 2023/5/31 14:43 + */ +public class VehicleTest { + + @Test + public void parseVinTest() { + String vinStr = "HE9XR1C48PS083871"; + Vin vin = Vin.of(vinStr); + // VIN + Assertions.assertEquals("HE9XR1C48PS083871", vin.getCode()); + // 是否合法 + Assertions.assertTrue(Vin.isValidVinCode(vinStr)); + // 年产量<1000 + Assertions.assertTrue(vin.isLessThan1000()); + // WMI + Assertions.assertEquals("HE9", vin.wmiCode()); + // 地理区域码 + Assertions.assertEquals("HE", vin.geoCode()); + // 主机厂代码 + Assertions.assertEquals("HE9083", vin.manufacturerCode()); + // VDS + Assertions.assertEquals("XR1C4", vin.vdsCode()); + // 车型年份 + Assertions.assertEquals(Year.of(2023), vin.defaultYear()); + // OEM厂商 + Assertions.assertEquals("S", vin.oemCode()); + // 生产序号 + Assertions.assertEquals("871", vin.prodNo()); + } + +}