1 | <?php |
||
2 | |||
3 | namespace kalanis\simple_vin; |
||
4 | |||
5 | |||
6 | use DateTime; |
||
7 | use Psr\Clock\ClockInterface; |
||
8 | |||
9 | |||
10 | /** |
||
11 | * Basic class for accessing VIN data |
||
12 | */ |
||
13 | class SimpleVin |
||
14 | { |
||
15 | protected const VALID_VIN_LENGTH = 17; |
||
16 | protected const CHECK_INDEX_ON_DIGIT = 8; |
||
17 | // Character weights for 17 characters in VIN |
||
18 | // Beware - some cars (like in Australia) does not have 17 characters VIN; then you cannot use this library |
||
19 | /** @var int[] */ |
||
20 | protected static array $CharacterWeights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; |
||
21 | |||
22 | protected readonly int $startYear; |
||
23 | protected readonly int $nextYear; |
||
24 | |||
25 | protected readonly Datasources\Years $years; |
||
26 | protected readonly Datasources\ValidCharacters $validCheckCharacters; |
||
27 | protected readonly Datasources\CharacterTransliteration $characterTransliteration; |
||
28 | protected readonly Datasources\WorldManufacturerIdentifiers $worldManufacturerIdentifiers; |
||
29 | |||
30 | 37 | public function __construct( |
|
31 | ?ClockInterface $clock = null, |
||
32 | ) |
||
33 | { |
||
34 | // no DI |
||
35 | 37 | $this->years = new Datasources\Years(); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
36 | 37 | $this->validCheckCharacters = new Datasources\ValidCharacters(); |
|
0 ignored issues
–
show
|
|||
37 | 37 | $this->characterTransliteration = new Datasources\CharacterTransliteration(); |
|
0 ignored issues
–
show
|
|||
38 | 37 | $this->worldManufacturerIdentifiers = new Datasources\WorldManufacturerIdentifiers(); |
|
0 ignored issues
–
show
|
|||
39 | |||
40 | 37 | $clocks = $clock ? $clock->now() : new DateTime(); // because it has not a correct interface! |
|
41 | |||
42 | 37 | $this->startYear = intdiv(intval($clocks->format('Y')), 30) * 30; // base year |
|
0 ignored issues
–
show
|
|||
43 | 37 | $this->nextYear = intval($clocks->format('Y')) + 1; // next year against current date |
|
0 ignored issues
–
show
|
|||
44 | } |
||
45 | |||
46 | 15 | public function isValid(string $vin): bool |
|
47 | { |
||
48 | 15 | if (static::VALID_VIN_LENGTH != strlen($vin)) { |
|
49 | 2 | return false; |
|
50 | } |
||
51 | |||
52 | 13 | $checkCharacter = $vin[static::CHECK_INDEX_ON_DIGIT]; |
|
53 | |||
54 | 13 | $calculated = $this->calculateChecksum($vin); |
|
55 | 13 | if (is_null($calculated)) { |
|
56 | 2 | return false; |
|
57 | } |
||
58 | |||
59 | 11 | return $calculated == $this->validCheckCharacters[$checkCharacter]; |
|
60 | } |
||
61 | |||
62 | 2 | public function restoreChecksum(string $vin): ?string |
|
63 | { |
||
64 | 2 | $char = $this->restoreChecksumCharacter($vin); |
|
65 | |||
66 | 2 | if (is_null($char)) { |
|
67 | 1 | return null; |
|
68 | } |
||
69 | |||
70 | 1 | $vin[static::CHECK_INDEX_ON_DIGIT] = $char; |
|
71 | 1 | return $vin; |
|
72 | } |
||
73 | |||
74 | 5 | public function restoreChecksumCharacter(string $vin): ?string |
|
75 | { |
||
76 | 5 | if (static::VALID_VIN_LENGTH != strlen($vin)) { |
|
77 | 2 | return null; |
|
78 | } |
||
79 | |||
80 | 3 | $calculated = $this->calculateChecksum($vin); |
|
81 | 3 | if (is_null($calculated)) { |
|
82 | 1 | return null; |
|
83 | } |
||
84 | |||
85 | 2 | $chars = array_map('strval', array_flip($this->validCheckCharacters->getArrayCopy())); |
|
86 | |||
87 | 2 | return $chars[$calculated] ?? null; |
|
88 | } |
||
89 | |||
90 | 16 | protected function calculateChecksum(string $vin): ?int |
|
91 | { |
||
92 | 16 | $value = 0; |
|
93 | |||
94 | 16 | for ($i = 0; $i < static::VALID_VIN_LENGTH; $i++) { |
|
95 | 16 | if (empty(static::$CharacterWeights[$i])) { |
|
96 | 13 | continue; |
|
97 | } |
||
98 | 16 | if (!isset($this->characterTransliteration[$vin[$i]])) { |
|
99 | 3 | return null; |
|
100 | } |
||
101 | 16 | $value += (static::$CharacterWeights[$i] * ($this->characterTransliteration[$vin[$i]])); |
|
102 | } |
||
103 | |||
104 | 13 | return $value % 11; |
|
105 | } |
||
106 | |||
107 | 6 | public function getWorldManufacturer(string $vinOrWmi): string |
|
108 | { |
||
109 | 6 | if (empty($vinOrWmi)) { |
|
110 | 1 | return ''; |
|
111 | } |
||
112 | |||
113 | 5 | if (2 > strlen($vinOrWmi)) { |
|
114 | 1 | return ''; |
|
115 | } |
||
116 | |||
117 | 4 | $prefix = substr($vinOrWmi, 0, 3); |
|
118 | 4 | if (2 < strlen($vinOrWmi) && isset($this->worldManufacturerIdentifiers[$prefix])) { |
|
119 | 2 | return $this->worldManufacturerIdentifiers[$prefix]; |
|
120 | } |
||
121 | |||
122 | 2 | $prefix = substr($vinOrWmi, 0, 2); |
|
123 | 2 | return $this->worldManufacturerIdentifiers[$prefix] ?? ''; |
|
124 | } |
||
125 | |||
126 | 17 | public function getModelYear(string $ident, ?int $startYear = null, ?int $nextYear = null): ?int |
|
127 | { |
||
128 | 17 | if (empty($ident)) { |
|
129 | 1 | return null; |
|
130 | } |
||
131 | |||
132 | 16 | if (empty($startYear)) { |
|
133 | 13 | $startYear = $this->startYear; |
|
134 | } |
||
135 | |||
136 | 16 | if (empty($nextYear)) { |
|
137 | 15 | $nextYear = $this->nextYear; |
|
138 | } |
||
139 | |||
140 | 16 | if (!$this->years->checkStart($startYear)) { |
|
141 | 1 | return null; |
|
142 | } |
||
143 | |||
144 | 15 | if (10 > strlen($ident)) { |
|
145 | 2 | return $this->getModelYearByNumber($ident, $startYear, $nextYear); |
|
146 | } |
||
147 | |||
148 | 13 | return $this->getModelYearByNumber($ident[9], $startYear, $nextYear); |
|
149 | } |
||
150 | |||
151 | 15 | protected function getModelYearByNumber(string $yearCharacter, int $startYear, int $nextYear): ?int |
|
152 | { |
||
153 | 15 | if (isset($this->years[$yearCharacter])) { |
|
154 | 12 | $year = $startYear + $this->years[$yearCharacter]; |
|
155 | 12 | if ($year > $nextYear) { |
|
156 | 6 | $year -= 30; |
|
157 | } |
||
158 | 12 | return $year; |
|
159 | } |
||
160 | |||
161 | 3 | return null; |
|
162 | } |
||
163 | } |
||
164 |