Skip to content

Commit 33391d7

Browse files
committed
✨ feat: Add ShippingContainerChecker (ISO 6346) helper
- [x] Memvalidasi dan menghitung check digit untuk nomor kontainer standar ISO 6346. - [x] Menerima input fleksibel: `CSQU3054383`, `CSQU305438`, `CSQU 305438 3`. - [x] Menghitung check digit dari 10 karakter pertama (owner+category+serial). - [x] Memvalidasi nomor lengkap yang menyertakan check digit. - [x] Menormalisasi output ke format standar tanpa spasi.
1 parent 7f26709 commit 33391d7

File tree

5 files changed

+363
-0
lines changed

5 files changed

+363
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# 📦 Shipping Container Checker (ISO 6346)
2+
3+
Helper ini memvalidasi dan menghitung check digit untuk nomor kontainer standar ISO 6346.
4+
5+
Contoh nomor: `CSQU3054383` (diambil dari Wikipedia/ISO 6346), di mana:
6+
- `CSQ` = Owner code (3 huruf)
7+
- `U` = Category identifier (1 huruf)
8+
- `305438` = Serial number (6 digit)
9+
- `3` = Check digit (1 digit)
10+
11+
## ✨ Fitur
12+
- Terima input fleksibel: `CSQU3054383`, `CSQU305438`, `CSQU 305438 3`.
13+
- Hitung check digit dari 10 karakter pertama (owner+category+serial).
14+
- Validasi nomor lengkap yang menyertakan check digit.
15+
- Normalisasi output ke format standar tanpa spasi.
16+
17+
## 📦 Namespace & Kelas
18+
- Kelas: `Ay4t\Helper\Validation\ShippingContainerChecker`
19+
- Akses via facade: `Ay4t\Helper\HP::ShippingContainerChecker(...)`
20+
21+
## 🔧 Instalasi & Import
22+
Pastikan autoload Composer aktif. Di project ini sudah tersedia melalui `vendor/autoload.php`.
23+
24+
```php
25+
require_once __DIR__ . '/vendor/autoload.php';
26+
use Ay4t\Helper\HP;
27+
```
28+
29+
## 🚀 Penggunaan Dasar
30+
31+
### 1) Validasi nomor lengkap
32+
```php
33+
use Ay4t\Helper\HP;
34+
35+
$checker = HP::ShippingContainerChecker('CSQU3054383');
36+
$checker->isValid(); // true
37+
$checker->getResult(); // "CSQU3054383"
38+
```
39+
40+
### 2) Hitung check digit dari 10 karakter pertama
41+
```php
42+
$checker = HP::ShippingContainerChecker('CSQU305438');
43+
$checker->expectedCheckDigit(); // 3
44+
$checker->getResult(); // "CSQU3054383"
45+
```
46+
47+
### 3) Input dengan spasi atau pemisah lainnya
48+
```php
49+
$checker = HP::ShippingContainerChecker('CSQU 305438 3');
50+
$checker->isValid(); // true
51+
$checker->getResult(); // "CSQU3054383"
52+
```
53+
54+
### 4) Check digit salah
55+
```php
56+
$checker = HP::ShippingContainerChecker('CSQU3054384');
57+
$checker->isValid(); // false
58+
$checker->expectedCheckDigit(); // 3
59+
$checker->getResult(); // "CSQU3054383"
60+
```
61+
62+
## 🧠 API Reference
63+
64+
### set(string $container, ?string $category = null): self
65+
- Mengatur data kontainer dari satu string fleksibel.
66+
- Secara otomatis mengenali owner code (3 huruf), category (1 huruf), serial (6 digit), dan check digit (opsional).
67+
- Jika `$category` diberikan, akan override category hasil parsing.
68+
- Melempar `InvalidArgumentException` jika format tidak valid.
69+
70+
### calculateCheckDigit(?string $owner = null, ?string $category = null, ?string $serial = null): int
71+
- Hitung check digit dari gabungan 10 karakter pertama.
72+
- Bisa diberikan parameter komponen secara eksplisit; default gunakan nilai dari `set()`.
73+
74+
### isValid(): bool
75+
- Mengembalikan `true` jika input menyertakan check digit dan nilainya sesuai hasil perhitungan.
76+
- Jika check digit tidak diberikan di input, mengembalikan `false`.
77+
78+
### expectedCheckDigit(): int
79+
- Mengembalikan check digit yang benar berdasarkan komponen saat ini.
80+
81+
### getResult(): string
82+
- Mengembalikan nomor dalam format standar lengkap (owner+category+serial+check_digit).
83+
84+
## 🧮 Catatan Algoritma (ISO 6346)
85+
- Huruf dipetakan ke nilai numerik khusus ISO 6346:
86+
- A=10, B=12, C=13, D=14, E=15, F=16, G=17, H=18, I=19,
87+
- J=20, K=21, L=23, M=24, N=25, O=26, P=27, Q=28, R=29,
88+
- S=30, T=31, U=32, V=34, W=35, X=36, Y=37, Z=38
89+
- Bobot posisi untuk 10 karakter pertama adalah 2^pos (posisi mulai 0).
90+
- Jumlahkan (nilai_karakter * bobot).
91+
- Sisa bagi 11 => jika 10 maka check digit = 0, selain itu = sisa.
92+
93+
## 🧪 Contoh Lengkap (CLI)
94+
Lihat file contoh: `examples/shipping_container_example.php`
95+
96+
Jalankan:
97+
```bash
98+
php examples/shipping_container_example.php
99+
# atau
100+
php examples/shipping_container_example.php MSKU123456 CSQU3054383 "CSQU 305438 3"
101+
```
102+
103+
## ⚠️ Error & Validasi
104+
- `Invalid owner/category code` untuk prefix yang tidak memenuhi pola 4 huruf (3 owner + 1 kategori).
105+
- `Invalid serial` untuk serial yang tidak 6 digit angka.
106+
- `Invalid check digit` jika check digit bukan satu digit 0-9.
107+
108+
## 📚 Referensi
109+
- ISO 6346 — Shipping container coding, identification, and marking.
110+
- Contoh publik: `CSQU3054383` (Wikipedia / bahan referensi umum).
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
// Example CLI for ShippingContainerChecker
4+
// Usage:
5+
// php examples/shipping_container_example.php [CONTAINER ...]
6+
// If no arguments provided, a default set will be demonstrated.
7+
8+
require_once __DIR__ . '/../vendor/autoload.php';
9+
10+
use Ay4t\Helper\HP;
11+
12+
function clean($s) {
13+
return preg_replace('/\s+/', ' ', trim($s));
14+
}
15+
16+
function process_input(string $input): void {
17+
$raw = $input;
18+
$normalized = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $raw));
19+
$hasCheck = strlen($normalized) >= 11; // 4 letters + 6 digits + (optional) 1 check
20+
21+
echo "\n=== Input: " . clean($raw) . " ===\n";
22+
23+
try {
24+
$checker = HP::ShippingContainerChecker($raw);
25+
if ($hasCheck) {
26+
$valid = $checker->isValid();
27+
$expected = $checker->expectedCheckDigit();
28+
$result = $checker->getResult();
29+
$given = substr($normalized, 10, 1);
30+
31+
echo "Normalized : {$result}\n";
32+
echo "Given Check: {$given}\n";
33+
echo "Expected : {$expected}\n";
34+
echo "Valid : " . ($valid ? 'YES' : 'NO') . "\n";
35+
} else {
36+
$expected = $checker->expectedCheckDigit();
37+
$result = $checker->getResult();
38+
39+
echo "Normalized : {$result}\n";
40+
echo "Expected : {$expected}\n";
41+
echo "Info : (No check digit provided in input)\n";
42+
}
43+
} catch (Throwable $e) {
44+
echo "Error : " . $e->getMessage() . "\n";
45+
}
46+
}
47+
48+
$args = $argv;
49+
array_shift($args); // remove script name
50+
51+
if (count($args) === 0) {
52+
$samples = [
53+
'CSQU3054383', // valid full
54+
'CSQU 305438 3', // valid with spaces
55+
'CSQU305438', // without check digit -> expect 3
56+
'CSQU3054384', // invalid check digit (should be 3)
57+
'MSKU123456', // compute expected digit for sample
58+
'ABCU12X4567', // invalid: serial contains non-digit
59+
'ABC13054383', // invalid: 4th char must be a letter (category)
60+
];
61+
foreach ($samples as $s) {
62+
process_input($s);
63+
}
64+
echo "\nTip: You can pass your own inputs, e.g.\n";
65+
echo " php examples/shipping_container_example.php MSKU123456 CSQU3054383 \"CSQU 305438 3\"\n";
66+
} else {
67+
foreach ($args as $a) {
68+
process_input($a);
69+
}
70+
}

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ Beberapa dokumentasi tersedia:
8484
### 🔒 Security & Validation
8585
- [🔐 Security Helper](docs/Security/SecurityHelper.md)
8686
- [✅ Validation Helper](docs/Validation/ValidationHelper.md)
87+
- [📦 Shipping Container Checker (ISO 6346)](docs/Validation/ShippingContainerChecker.md)
8788

8889
### 🌐 Web
8990
- [🔗 URL Helper](docs/URL/URLHelper.md)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace Ay4t\Helper\Validation;
4+
5+
/**
6+
* Shipping Container Check Digit (ISO 6346) helper
7+
*
8+
* - Mendukung input: "CSQU3054383", "CSQU 305438 3", atau prefix+serial terpisah.
9+
* - Algoritma: map huruf ke nilai ISO, bobot 2^pos untuk 10 karakter pertama,
10+
* jumlah % 11; jika hasil 10 maka check digit = 0; selain itu = hasil.
11+
*/
12+
class ShippingContainerChecker
13+
{
14+
/** @var string */
15+
protected string $ownerCode = '';
16+
/** @var string */
17+
protected string $category = 'U';
18+
/** @var string */
19+
protected string $serial = '';
20+
/** @var int|null */
21+
protected ?int $providedCheck = null;
22+
23+
/**
24+
* Set container number in a flexible format.
25+
* Accepts: "CSQU3054383", "CSQU 305438 3", or parts
26+
*/
27+
public function set(string $container, ?string $category = null): self
28+
{
29+
// Normalize: remove spaces and non-alphanumeric
30+
$norm = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $container));
31+
32+
// Expect at least 10 chars (4 letters + 6 digits)
33+
if (strlen($norm) < 10) {
34+
throw new \InvalidArgumentException('Container number too short');
35+
}
36+
37+
$prefix = substr($norm, 0, 4);
38+
$serial = substr($norm, 4, 6);
39+
$check = strlen($norm) >= 11 ? substr($norm, 10, 1) : null;
40+
41+
if (!preg_match('/^[A-Z]{4}$/', $prefix)) {
42+
throw new \InvalidArgumentException('Invalid owner/category code');
43+
}
44+
if (!preg_match('/^[0-9]{6}$/', $serial)) {
45+
throw new \InvalidArgumentException('Invalid serial');
46+
}
47+
if ($check !== null && !preg_match('/^[0-9]$/', $check)) {
48+
throw new \InvalidArgumentException('Invalid check digit');
49+
}
50+
51+
$this->ownerCode = substr($prefix, 0, 3);
52+
$this->category = $category ? strtoupper($category) : substr($prefix, 3, 1);
53+
$this->serial = $serial;
54+
$this->providedCheck = $check !== null ? (int)$check : null;
55+
56+
return $this;
57+
}
58+
59+
/**
60+
* Hitung check digit dari owner+category+serial (10 karakter pertama)
61+
*/
62+
public function calculateCheckDigit(?string $owner = null, ?string $category = null, ?string $serial = null): int
63+
{
64+
$owner = $owner ?? $this->ownerCode;
65+
$category = $category ?? $this->category;
66+
$serial = $serial ?? $this->serial;
67+
68+
if (!preg_match('/^[A-Z]{3}$/', $owner) || !preg_match('/^[A-Z]{1}$/', $category) || !preg_match('/^[0-9]{6}$/', $serial)) {
69+
throw new \InvalidArgumentException('Invalid components for calculation');
70+
}
71+
72+
$first10 = $owner . $category . $serial; // 10 chars
73+
$sum = 0;
74+
for ($i = 0; $i < 10; $i++) {
75+
$char = $first10[$i];
76+
$value = ctype_alpha($char) ? self::letterValue($char) : (int)$char;
77+
$weight = 1 << $i; // 2^i
78+
$sum += $value * $weight;
79+
}
80+
$remainder = $sum % 11;
81+
$check = $remainder % 10; // if 10 => 0
82+
return $check;
83+
}
84+
85+
/**
86+
* Validasi nomor container (jika disediakan check digit)
87+
*/
88+
public function isValid(): bool
89+
{
90+
if ($this->providedCheck === null) {
91+
return false;
92+
}
93+
return $this->providedCheck === $this->calculateCheckDigit();
94+
}
95+
96+
/**
97+
* Dapatkan check digit yang diharapkan dari data saat ini
98+
*/
99+
public function expectedCheckDigit(): int
100+
{
101+
return $this->calculateCheckDigit();
102+
}
103+
104+
/**
105+
* Kembalikan format terstandardisasi: OWNER CATEGORY SERIAL CHECK
106+
* contoh: CSQU3054383
107+
*/
108+
public function getResult(): string
109+
{
110+
$check = $this->calculateCheckDigit();
111+
return sprintf('%s%s%s%d', $this->ownerCode, $this->category, $this->serial, $check);
112+
}
113+
114+
/**
115+
* Nilai huruf sesuai ISO 6346
116+
* A=10, B=12, C=13, D=14, E=15, F=16, G=17, H=18, I=19, J=20,
117+
* K=21, L=23, M=24, N=25, O=26, P=27, Q=28, R=29, S=30, T=31,
118+
* U=32, V=34, W=35, X=36, Y=37, Z=38
119+
*/
120+
public static function letterValue(string $letter): int
121+
{
122+
static $map = [
123+
'A' => 10, 'B' => 12, 'C' => 13, 'D' => 14, 'E' => 15, 'F' => 16, 'G' => 17, 'H' => 18, 'I' => 19,
124+
'J' => 20, 'K' => 21, 'L' => 23, 'M' => 24, 'N' => 25, 'O' => 26, 'P' => 27, 'Q' => 28, 'R' => 29,
125+
'S' => 30, 'T' => 31, 'U' => 32, 'V' => 34, 'W' => 35, 'X' => 36, 'Y' => 37, 'Z' => 38,
126+
];
127+
$letter = strtoupper($letter);
128+
if (!isset($map[$letter])) {
129+
throw new \InvalidArgumentException('Invalid letter: ' . $letter);
130+
}
131+
return $map[$letter];
132+
}
133+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Ay4t\Helper\Tests\Validation;
4+
5+
use Ay4t\Helper\Validation\ShippingContainerChecker;
6+
use Ay4t\Helper\HP;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class ShippingContainerCheckerTest extends TestCase
10+
{
11+
public function testCalculateCheckDigitFromParts()
12+
{
13+
$checker = new ShippingContainerChecker();
14+
$checker->set('CSQU305438'); // tanpa check digit
15+
$this->assertSame(3, $checker->calculateCheckDigit());
16+
}
17+
18+
public function testValidateFullNumber()
19+
{
20+
$checker = new ShippingContainerChecker();
21+
$checker->set('CSQU3054383');
22+
$this->assertTrue($checker->isValid());
23+
$this->assertSame('CSQU3054383', $checker->getResult());
24+
}
25+
26+
public function testInvalidNumber()
27+
{
28+
$checker = new ShippingContainerChecker();
29+
$checker->set('CSQU3054384'); // seharusnya 3, bukan 4
30+
$this->assertFalse($checker->isValid());
31+
$this->assertSame('CSQU3054383', $checker->getResult());
32+
}
33+
34+
public function testSetFlexibleInput()
35+
{
36+
$checker = new ShippingContainerChecker();
37+
$checker->set('CSQU 305438 3');
38+
$this->assertTrue($checker->isValid());
39+
$this->assertSame(3, $checker->expectedCheckDigit());
40+
}
41+
42+
public function testFacadeHP()
43+
{
44+
// Bisa dipanggil via HP facade
45+
$hp = HP::ShippingContainerChecker('CSQU3054383');
46+
$this->assertTrue($hp->isValid());
47+
$this->assertSame('CSQU3054383', $hp->getResult());
48+
}
49+
}

0 commit comments

Comments
 (0)