[feature] 添加解析车辆识别代码

This commit is contained in:
VampireAchao 2023-05-31 14:52:07 +08:00
parent a55aed6a5a
commit 9c0a343491
11 changed files with 863 additions and 0 deletions

View File

@ -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);
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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<Integer> 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();
}

View File

@ -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 +
'}';
}
}

View File

@ -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<AlphanumericVinCode> vdCode;
private final AlphanumericVinCode checksum;
private final String code;
private final int mask;
/**
* Instantiates a new Vds.
*
* @param vdCode the vd code
*/
Vds(List<AlphanumericVinCode> 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<AlphanumericVinCode> 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<AlphanumericVinCode> 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 +
'}';
}
}

View File

@ -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位数字和字母组成
* <p>
* 不同位数代表着不同意义具体解释如下
* <ul>
* <li>1-3位制造商标示符代表车辆制造商信息</li>
* <li>4-8位车型识别代码代表车辆品牌车系车型及其排量等信息</li>
* <li>9位校验位通过公式计算出来用于验证VIN码的正确性</li>
* <li>10位年份代号代表车辆生产的年份</li>
* <li>11位工厂代码代表车辆生产工厂信息</li>
* <li>12-17位流水号代表车辆的生产顺序号</li>
* </ul>
* VIN码可以找到汽车详细的个人工程制造方面的信息是判定一个汽车合法性及其历史的重要依据
* <p>
* 本实现参考以下标准
* <ul>
* <li><a href="https://www.iso.org/standard/52200.html">ISO 3779</a></li>
* <li><a href="http://www.catarc.org.cn/upload/202004/24/202004241005284241.pdf">车辆识别代号管理办法</a></li>
* <li><a href="https://en.wikipedia.org/wiki/Vehicle_identification_number">Wikipedia</a></li>
* </ul>
*
* @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);
}
/**
* 制造厂标识码
* <p>
* 年产量大于1000为符合GB16737规定的{@link Wmi}年产量小于1000固定为9需要结合VIN的第121314位字码确定唯一
*
* @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);
}
/**
* 获取车型年份
* <p>
* 自1980年起30年一个周期
*
* @param multiple 1 代表从 1980年开始的第一个30年
* @return 返回年份对象 year
* @see <a href="https://en.wikipedia.org/wiki/Vehicle_identification_number#Model_year_encoding">年份编码模型</a>
*/
public Year year(int multiple) {
return this.vis.getYear(multiple);
}
/**
* 生产序号
* <p>
* 年产量大于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 + '\'' +
'}';
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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<String> 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<String, Integer> 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<MaskVinCode> 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<MaskVinCode> 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<MaskVinCode> 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<MaskVinCode> 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 +
'}';
}
}

View File

@ -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<AlphanumericVinCode> wmiCodes;
/**
* Instantiates a new Wmi.
*
* @param wmiCodes the wmi codes
*/
Wmi(List<AlphanumericVinCode> 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<AlphanumericVinCode> 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$");
}
}

View File

@ -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());
}
}