Passed
Push — master ( 2d8139...671277 )
by Petr
02:20
created

SimpleVin::calculateChecksum()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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