thai_id_utils 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/LICENSE +21 -0
- data/README.md +205 -21
- data/lib/thai_id_utils/version.rb +1 -1
- data/lib/thai_id_utils.rb +199 -18
- metadata +17 -11
- data/Gemfile +0 -5
- data/Rakefile +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 550dacaa281beb7bdba770c9dd0b54cf252f87b9aaf56fdfcaa9dbf7f3b22589
|
|
4
|
+
data.tar.gz: 3afd4ab32f6836e8995b473f2352a39d1b7e137c8aae7fac3a8815b21f3bce74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2689046128e65c474735e500e843fe7470e3dc06cfb4c76c9aa7adee1ffbd80be38ecb1163d4f928aad94418137e14dce5aa0cf5ba8ba63acd5e47e730c26b24
|
|
7
|
+
data.tar.gz: 551c61957b236cdcd61c44d47e4f5d2eb02da24bf3fc9f8e5e36825c40a337cb0284eb301958a571440eaa0f21091570e52ec04cd1bc214c0ba8ce605f4abfcf
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-03-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `DISTRICT_COUNTS` constant — maps all 77 province codes to their number of
|
|
12
|
+
administrative districts (amphoe/khet), used to constrain district generation
|
|
13
|
+
- `LASER_HARDWARE_VERSIONS` constant — known chip hardware-version prefixes
|
|
14
|
+
(`JC`, `AA`, `BB`, `GC`) observed on issued Thai ID cards
|
|
15
|
+
- `province_codes` — returns all valid 2-digit province code strings
|
|
16
|
+
- `generate_laser_id(hardware_version:, box_id:, position:)` — generates a
|
|
17
|
+
random, structurally valid laser ID in `XXN-NNNNNNN-NN` format
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `generate` now accepts a `province_code:` keyword argument (default: random
|
|
21
|
+
valid province). When `office_code:` is not given, the district code is
|
|
22
|
+
constrained to the province's known range via `DISTRICT_COUNTS`. Passing
|
|
23
|
+
`office_code:` explicitly retains the previous behaviour unchanged.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `generate` default no longer produces impossible province codes (e.g. `'00'`,
|
|
27
|
+
`'28'`). All generated IDs now have a geographically valid province by default.
|
|
28
|
+
|
|
29
|
+
## [0.2.0] - 2025-06-15
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- Province code lookup (`PROVINCE_CODES`) mapping all 77 Thai provinces
|
|
33
|
+
- `province_name(code)` — return province name from 2-digit code
|
|
34
|
+
- Laser ID validation (`laser_id_valid?`) using format `XXN-NNNNNNN-NN`
|
|
35
|
+
- Laser ID decoding (`laser_id_decode`) into hardware version, box ID, and position
|
|
36
|
+
- Buddhist Era conversion: `be_to_ce(year)` and `ce_to_be(year)`
|
|
37
|
+
- `province_name` field included in `decode` output hash
|
|
38
|
+
|
|
39
|
+
## [0.1.2] - 2025-06-15
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- Minor internal cleanup; no public API changes
|
|
43
|
+
|
|
44
|
+
## [0.1.1] - 2025-06-15
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- Gemspec corrections and metadata updates
|
|
48
|
+
|
|
49
|
+
## [0.1.0] - 2025-06-15
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- Initial release
|
|
53
|
+
- `valid?(id)` — checksum validation using Thailand's modulus-11 algorithm
|
|
54
|
+
- `decode(id)` — decode category, office code, province code, district code, sequence, and registration code
|
|
55
|
+
- `generate(...)` — generate a random valid 13-digit Thai national ID with optional overrides
|
|
56
|
+
- `category_description(category)` — human-readable description of ID category codes (0–8)
|
|
57
|
+
- `InvalidIDError` — raised on invalid IDs passed to `decode`
|
|
58
|
+
|
|
59
|
+
[0.3.0]: https://github.com/chayuto/thai_id_utils/compare/v0.2.0...v0.3.0
|
|
60
|
+
[0.2.0]: https://github.com/chayuto/thai_id_utils/compare/v0.1.2...v0.2.0
|
|
61
|
+
[0.1.2]: https://github.com/chayuto/thai_id_utils/compare/v0.1.1...v0.1.2
|
|
62
|
+
[0.1.1]: https://github.com/chayuto/thai_id_utils/compare/v0.1.0...v0.1.1
|
|
63
|
+
[0.1.0]: https://github.com/chayuto/thai_id_utils/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chayut Orapinpatipat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,34 +1,218 @@
|
|
|
1
1
|
# Thai ID Utils
|
|
2
2
|
|
|
3
|
-
Thai ID Utils is a zero-dependency Ruby gem for validating and decoding Thai national ID numbers.
|
|
4
|
-
|
|
5
|
-
Thai ID Utils เป็น Ruby gem ที่ไม่ต้องพึ่งพาไลบรารีเสริม สำหรับตรวจสอบความถูกต้องและถอดรหัสหมายเลขบัตรประชาชนไทย
|
|
3
|
+
Thai ID Utils is a zero-dependency Ruby gem for validating and decoding Thai national ID numbers.
|
|
4
|
+
|
|
5
|
+
Thai ID Utils เป็น Ruby gem ที่ไม่ต้องพึ่งพาไลบรารีเสริม สำหรับตรวจสอบความถูกต้องและถอดรหัสหมายเลขบัตรประชาชนไทย
|
|
6
|
+
|
|
7
|
+
[](https://badge.fury.io/rb/thai_id_utils)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features / ฟีเจอร์
|
|
12
|
+
|
|
13
|
+
- Checksum validation (modulus-11 algorithm)
|
|
14
|
+
- Component decoding (category, province, district, sequence)
|
|
15
|
+
- Province name lookup for all 77 provinces
|
|
16
|
+
- Random valid ID generation — province-constrained by default, population-weighted sampling ready
|
|
17
|
+
- Human-readable category descriptions (0–8)
|
|
18
|
+
- Laser ID validation, decoding, and generation
|
|
19
|
+
- Buddhist Era ↔ Common Era date conversion
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation / การติดตั้ง
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'thai_id_utils'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install directly:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
gem install thai_id_utils
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
6
36
|
|
|
7
37
|
## Usage / วิธีใช้งาน
|
|
8
38
|
|
|
9
39
|
```ruby
|
|
10
40
|
require "thai_id_utils"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Validate an ID / ตรวจสอบความถูกต้อง
|
|
11
44
|
|
|
12
|
-
|
|
45
|
+
```ruby
|
|
46
|
+
ThaiIdUtils.valid?("3012304567082") # => true
|
|
47
|
+
ThaiIdUtils.valid?("1234567890123") # => false
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Decode an ID / ถอดรหัสส่วนประกอบ
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
info = ThaiIdUtils.decode("3012304567082")
|
|
54
|
+
# => {
|
|
55
|
+
# category: 3,
|
|
56
|
+
# office_code: "0123",
|
|
57
|
+
# province_code: "01",
|
|
58
|
+
# province_name: nil, # nil if province code not recognized
|
|
59
|
+
# district_code: "23",
|
|
60
|
+
# sequence: "04567",
|
|
61
|
+
# registration_code: "08"
|
|
62
|
+
# }
|
|
63
|
+
```
|
|
13
64
|
|
|
14
|
-
|
|
15
|
-
if ThaiIdUtils.valid?(id)
|
|
16
|
-
puts "Valid!"
|
|
17
|
-
else
|
|
18
|
-
puts "Invalid ID"
|
|
19
|
-
end
|
|
65
|
+
Raises `ThaiIdUtils::InvalidIDError` if the ID fails checksum validation.
|
|
20
66
|
|
|
21
|
-
|
|
22
|
-
info = ThaiIdUtils.decode(id)
|
|
23
|
-
# => { category: 1, office_code: "6099", district_code: "99", sequence: "00257" }
|
|
24
|
-
puts info.inspect
|
|
67
|
+
### Province Name Lookup / ค้นหาชื่อจังหวัด
|
|
25
68
|
|
|
26
|
-
|
|
27
|
-
|
|
69
|
+
```ruby
|
|
70
|
+
ThaiIdUtils.province_name("10") # => "Bangkok"
|
|
71
|
+
ThaiIdUtils.province_name("83") # => "Phuket"
|
|
72
|
+
ThaiIdUtils.province_name("50") # => "Chiang Mai"
|
|
73
|
+
ThaiIdUtils.province_name("99") # => nil
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
All 77 Thai provinces are supported. Use `province_codes` to get the full list:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
ThaiIdUtils.province_codes
|
|
80
|
+
# => ["10", "11", "12", ..., "96"] (77 codes)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Category Description / คำอธิบายประเภทบัตร
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
ThaiIdUtils.category_description(1)
|
|
28
87
|
# => "Thai nationals who were born after 1 January 1984 and had their birth notified within the given deadline (15 days)."
|
|
29
|
-
puts desc
|
|
30
88
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
89
|
+
ThaiIdUtils.category_description(6)
|
|
90
|
+
# => "Foreign nationals who are living in Thailand temporarily and illegal migrants"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Generate a Valid ID / สร้างหมายเลขบัตรประชาชนแบบสุ่ม
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
ThaiIdUtils.generate
|
|
97
|
+
# => "1105312345671" (random valid province, district within that province's range)
|
|
98
|
+
|
|
99
|
+
# Pin to a specific province (district randomised within province's known range)
|
|
100
|
+
ThaiIdUtils.generate(province_code: "10") # Bangkok
|
|
101
|
+
ThaiIdUtils.generate(province_code: "83") # Phuket (3 districts)
|
|
102
|
+
|
|
103
|
+
# Full override via office_code bypasses province validation (backwards compatible)
|
|
104
|
+
ThaiIdUtils.generate(category: 1, office_code: "1001", sequence: "00001")
|
|
105
|
+
|
|
106
|
+
# Raises ArgumentError for unknown province codes
|
|
107
|
+
ThaiIdUtils.generate(province_code: "99") # => ArgumentError
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The default generates a geographically valid ID — province code is sampled uniformly from the 77 known codes and the district code is constrained to that province's actual district count via `DISTRICT_COUNTS`.
|
|
111
|
+
|
|
112
|
+
### Laser ID Validation / ตรวจสอบเลขเลเซอร์
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
ThaiIdUtils.laser_id_valid?("JC1-0002507-15") # => true
|
|
116
|
+
ThaiIdUtils.laser_id_valid?("INVALID") # => false
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Format: `XXN-NNNNNNN-NN` (two uppercase letters, one digit, hyphen, 7 digits, hyphen, 2 digits)
|
|
120
|
+
|
|
121
|
+
### Laser ID Decoding / ถอดรหัสเลขเลเซอร์
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
ThaiIdUtils.laser_id_decode("JC1-0002507-15")
|
|
125
|
+
# => {
|
|
126
|
+
# hardware_version: "JC1",
|
|
127
|
+
# box_id: "0002507",
|
|
128
|
+
# position: "15"
|
|
129
|
+
# }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Raises `ThaiIdUtils::InvalidIDError` if the format is invalid.
|
|
133
|
+
|
|
134
|
+
### Generate a Laser ID / สร้างเลขเลเซอร์
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
ThaiIdUtils.generate_laser_id
|
|
138
|
+
# => "JC2-0483921-07" (random, always matches LASER_ID_FORMAT)
|
|
139
|
+
|
|
140
|
+
# Override individual components
|
|
141
|
+
ThaiIdUtils.generate_laser_id(hardware_version: "JC1", box_id: 2507, position: 15)
|
|
142
|
+
# => "JC1-0002507-15"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Known hardware version prefixes (`LASER_HARDWARE_VERSIONS`): `JC`, `AA`, `BB`, `GC`.
|
|
146
|
+
The laser ID is a supply-chain tracking code with no mathematical link to the citizen ID.
|
|
147
|
+
|
|
148
|
+
### Buddhist Era Conversion / แปลงปี พ.ศ. ↔ ค.ศ.
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
ThaiIdUtils.be_to_ce(2567) # => 2024
|
|
152
|
+
ThaiIdUtils.ce_to_be(2024) # => 2567
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## API Reference / สรุป API
|
|
158
|
+
|
|
159
|
+
| Method | Description |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `valid?(id)` | Returns `true` if the 13-digit ID passes checksum |
|
|
162
|
+
| `decode(id)` | Returns a hash of decoded components; raises `InvalidIDError` on failure |
|
|
163
|
+
| `generate(category:, province_code:, office_code:, district_code:, sequence:)` | Generates a random valid 13-digit ID; defaults to a valid province |
|
|
164
|
+
| `province_name(code)` | Returns province name for a 2-digit code, or `nil` |
|
|
165
|
+
| `province_codes` | Returns all 77 valid 2-digit province code strings |
|
|
166
|
+
| `category_description(n)` | Returns human-readable category description |
|
|
167
|
+
| `laser_id_valid?(laser_id)` | Returns `true` if the laser ID format matches |
|
|
168
|
+
| `laser_id_decode(laser_id)` | Returns decoded laser ID hash; raises `InvalidIDError` on failure |
|
|
169
|
+
| `generate_laser_id(hardware_version:, box_id:, position:)` | Generates a random valid laser ID |
|
|
170
|
+
| `be_to_ce(year)` | Converts Buddhist Era year to Common Era |
|
|
171
|
+
| `ce_to_be(year)` | Converts Common Era year to Buddhist Era |
|
|
172
|
+
|
|
173
|
+
**Constants**
|
|
174
|
+
|
|
175
|
+
| Constant | Description |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `PROVINCE_CODES` | Hash mapping 77 province codes to English names |
|
|
178
|
+
| `DISTRICT_COUNTS` | Hash mapping province codes to their district count |
|
|
179
|
+
| `LASER_HARDWARE_VERSIONS` | Array of known chip hardware-version prefixes |
|
|
180
|
+
| `LASER_ID_FORMAT` | Regex for laser ID format validation |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Synthetic Dataset / ชุดข้อมูลสังเคราะห์
|
|
185
|
+
|
|
186
|
+
A 350,000-row fully synthetic dataset generated with this gem is published on HuggingFace:
|
|
187
|
+
|
|
188
|
+
**[huggingface.co/datasets/chayuto/thai-id-synthetic](https://huggingface.co/datasets/chayuto/thai-id-synthetic)**
|
|
189
|
+
|
|
190
|
+
- 332,500 valid IDs + 17,500 invalid IDs (bad checksum, impossible province, wrong category, wrong length)
|
|
191
|
+
- Population-weighted province sampling (NSO 2023), realistic category distribution
|
|
192
|
+
- train / test splits (315K / 35K)
|
|
193
|
+
- No real citizen data — 100% synthetic
|
|
194
|
+
|
|
195
|
+
To regenerate:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
cd dataset
|
|
199
|
+
ruby generate.rb --count 350000 --invalid-ratio 0.05 --seed 42
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Development / การพัฒนา
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
# Run tests
|
|
208
|
+
rake
|
|
209
|
+
|
|
210
|
+
# Or directly
|
|
211
|
+
ruby -Ilib -Itest test/test_thai_id_utils.rb
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## License / สัญญาอนุญาต
|
|
217
|
+
|
|
218
|
+
[MIT License](LICENSE)
|
data/lib/thai_id_utils.rb
CHANGED
|
@@ -21,9 +21,86 @@ module ThaiIdUtils
|
|
|
21
21
|
7 => 'Children of people of category 6 who were born in Thailand',
|
|
22
22
|
8 => 'Foreign nationals who are living in Thailand permanently or Thai nationals by naturalization'
|
|
23
23
|
}.freeze
|
|
24
|
+
|
|
25
|
+
# Mapping of 2-digit province codes (digits 2-3 of the ID) to province names
|
|
26
|
+
PROVINCE_CODES = {
|
|
27
|
+
'10' => 'Bangkok', '11' => 'Samut Prakan',
|
|
28
|
+
'12' => 'Nonthaburi', '13' => 'Pathum Thani',
|
|
29
|
+
'14' => 'Phra Nakhon Si Ayutthaya', '15' => 'Ang Thong',
|
|
30
|
+
'16' => 'Lopburi', '17' => 'Sing Buri',
|
|
31
|
+
'18' => 'Chainat', '19' => 'Saraburi',
|
|
32
|
+
'20' => 'Chonburi', '21' => 'Rayong',
|
|
33
|
+
'22' => 'Chanthaburi', '23' => 'Trat',
|
|
34
|
+
'24' => 'Chachoengsao', '25' => 'Prachin Buri',
|
|
35
|
+
'26' => 'Nakhon Nayok', '27' => 'Sa Kaeo',
|
|
36
|
+
'30' => 'Nakhon Ratchasima', '31' => 'Buri Ram',
|
|
37
|
+
'32' => 'Surin', '33' => 'Si Sa Ket',
|
|
38
|
+
'34' => 'Ubon Ratchathani', '35' => 'Yasothon',
|
|
39
|
+
'36' => 'Chaiyaphum', '37' => 'Amnat Charoen',
|
|
40
|
+
'38' => 'Bueng Kan', '39' => 'Nong Bua Lamphu',
|
|
41
|
+
'40' => 'Khon Kaen', '41' => 'Udon Thani',
|
|
42
|
+
'42' => 'Loei', '43' => 'Nong Khai',
|
|
43
|
+
'44' => 'Maha Sarakham', '45' => 'Roi Et',
|
|
44
|
+
'46' => 'Kalasin', '47' => 'Sakon Nakhon',
|
|
45
|
+
'48' => 'Nakhon Phanom', '49' => 'Mukdahan',
|
|
46
|
+
'50' => 'Chiang Mai', '51' => 'Lamphun',
|
|
47
|
+
'52' => 'Lampang', '53' => 'Uttaradit',
|
|
48
|
+
'54' => 'Phrae', '55' => 'Nan',
|
|
49
|
+
'56' => 'Phayao', '57' => 'Chiang Rai',
|
|
50
|
+
'58' => 'Mae Hong Son',
|
|
51
|
+
'60' => 'Nakhon Sawan', '61' => 'Uthai Thani',
|
|
52
|
+
'62' => 'Kamphaeng Phet', '63' => 'Tak',
|
|
53
|
+
'64' => 'Sukhothai', '65' => 'Phitsanulok',
|
|
54
|
+
'66' => 'Phichit', '67' => 'Phetchabun',
|
|
55
|
+
'70' => 'Ratchaburi', '71' => 'Kanchanaburi',
|
|
56
|
+
'72' => 'Suphanburi', '73' => 'Nakhon Pathom',
|
|
57
|
+
'74' => 'Samut Sakhon', '75' => 'Samut Songkhram',
|
|
58
|
+
'76' => 'Phetchaburi', '77' => 'Prachuap Khiri Khan',
|
|
59
|
+
'80' => 'Nakhon Si Thammarat', '81' => 'Krabi',
|
|
60
|
+
'82' => 'Phangnga', '83' => 'Phuket',
|
|
61
|
+
'84' => 'Surat Thani', '85' => 'Ranong',
|
|
62
|
+
'86' => 'Chumphon',
|
|
63
|
+
'90' => 'Songkhla', '91' => 'Satun',
|
|
64
|
+
'92' => 'Trang', '93' => 'Phatthalung',
|
|
65
|
+
'94' => 'Pattani', '95' => 'Yala',
|
|
66
|
+
'96' => 'Narathiwat'
|
|
67
|
+
}.freeze
|
|
24
68
|
# rubocop:enable Layout/LineLength
|
|
25
69
|
|
|
26
|
-
#
|
|
70
|
+
# Mapping of province codes to the number of administrative districts
|
|
71
|
+
# (amphoe for provinces, khet for Bangkok). Used to constrain district code
|
|
72
|
+
# generation to realistic ranges within generate().
|
|
73
|
+
# Counts are approximate and reflect post-2011 administrative divisions.
|
|
74
|
+
DISTRICT_COUNTS = {
|
|
75
|
+
'10' => 50, '11' => 11, '12' => 6, '13' => 7, '14' => 16,
|
|
76
|
+
'15' => 7, '16' => 11, '17' => 6, '18' => 8, '19' => 13,
|
|
77
|
+
'20' => 11, '21' => 8, '22' => 10, '23' => 7, '24' => 11,
|
|
78
|
+
'25' => 7, '26' => 4, '27' => 9,
|
|
79
|
+
'30' => 32, '31' => 23, '32' => 17, '33' => 22, '34' => 25,
|
|
80
|
+
'35' => 9, '36' => 16, '37' => 7, '38' => 8, '39' => 6,
|
|
81
|
+
'40' => 26, '41' => 20, '42' => 14, '43' => 18, '44' => 13,
|
|
82
|
+
'45' => 20, '46' => 18, '47' => 18, '48' => 12, '49' => 7,
|
|
83
|
+
'50' => 25, '51' => 8, '52' => 13, '53' => 9, '54' => 8,
|
|
84
|
+
'55' => 15, '56' => 9, '57' => 18, '58' => 7,
|
|
85
|
+
'60' => 15, '61' => 8, '62' => 11, '63' => 8, '64' => 9,
|
|
86
|
+
'65' => 9, '66' => 12, '67' => 11,
|
|
87
|
+
'70' => 10, '71' => 13, '72' => 10, '73' => 7, '74' => 7,
|
|
88
|
+
'75' => 3, '76' => 8, '77' => 8,
|
|
89
|
+
'80' => 23, '81' => 8, '82' => 8, '83' => 3, '84' => 19,
|
|
90
|
+
'85' => 5, '86' => 8,
|
|
91
|
+
'90' => 16, '91' => 7, '92' => 10, '93' => 11, '94' => 12,
|
|
92
|
+
'95' => 8, '96' => 9
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
LASER_ID_FORMAT = /\A[A-Z]{2}\d-\d{7}-\d{2}\z/.freeze
|
|
96
|
+
|
|
97
|
+
# Known chip hardware-version prefixes observed on issued Thai ID cards.
|
|
98
|
+
LASER_HARDWARE_VERSIONS = %w[JC AA BB GC].freeze
|
|
99
|
+
|
|
100
|
+
# Validate a Thai national ID using Thailand's modulus-11 checksum algorithm.
|
|
101
|
+
#
|
|
102
|
+
# @param id [String, Integer] 13-digit Thai national ID number
|
|
103
|
+
# @return [Boolean] true if the checksum is valid, false otherwise
|
|
27
104
|
def self.valid?(id)
|
|
28
105
|
digits = id.to_s.chars.map(&:to_i)
|
|
29
106
|
return false unless digits.size == 13
|
|
@@ -34,12 +111,18 @@ module ThaiIdUtils
|
|
|
34
111
|
false
|
|
35
112
|
end
|
|
36
113
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
114
|
+
# Decode the components encoded in a Thai national ID number.
|
|
115
|
+
#
|
|
116
|
+
# @param id [String, Integer] 13-digit Thai national ID number
|
|
117
|
+
# @return [Hash] decoded fields:
|
|
118
|
+
# - `:category` [Integer] — registration category (0–8)
|
|
119
|
+
# - `:office_code` [String] — 4-digit registrar code (province + district)
|
|
120
|
+
# - `:province_code` [String] — first 2 digits of office_code
|
|
121
|
+
# - `:province_name` [String, nil] — province name, or nil if unknown
|
|
122
|
+
# - `:district_code` [String] — last 2 digits of office_code
|
|
123
|
+
# - `:sequence` [String] — 5-digit personal sequence number
|
|
124
|
+
# - `:registration_code` [String] — 2-digit chronological sequence marker
|
|
125
|
+
# @raise [InvalidIDError] if the ID fails checksum validation
|
|
43
126
|
def self.decode(id)
|
|
44
127
|
raise InvalidIDError, 'Invalid ID' unless valid?(id)
|
|
45
128
|
|
|
@@ -48,42 +131,140 @@ module ThaiIdUtils
|
|
|
48
131
|
{
|
|
49
132
|
category: d[0].to_i,
|
|
50
133
|
office_code: d[1..4].join,
|
|
134
|
+
province_code: d[1..2].join,
|
|
135
|
+
province_name: PROVINCE_CODES[d[1..2].join],
|
|
51
136
|
district_code: d[3..4].join,
|
|
52
|
-
sequence: d[5..9].join
|
|
137
|
+
sequence: d[5..9].join,
|
|
138
|
+
registration_code: d[10..11].join
|
|
53
139
|
}
|
|
54
140
|
end
|
|
55
141
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
142
|
+
# Generate a random, valid 13-digit Thai national ID.
|
|
143
|
+
# Any component can be overridden; the rest is randomised and the checksum
|
|
144
|
+
# is computed. When neither +office_code+ nor +province_code+ is given, a
|
|
145
|
+
# valid province is selected at random and a district code within that
|
|
146
|
+
# province's known range is generated.
|
|
147
|
+
#
|
|
148
|
+
# @param category [Integer] ID category (1–8), default: random 1–6
|
|
149
|
+
# @param province_code [String, nil] 2-digit province code (e.g. "10").
|
|
150
|
+
# Must be a key in PROVINCE_CODES. Ignored when +office_code+ is given.
|
|
151
|
+
# @param office_code [Integer, String, nil] 4-digit registrar code override.
|
|
152
|
+
# When supplied, bypasses province_code and district_code validation.
|
|
153
|
+
# @param district_code [String, nil] 2-digit district override (applied on
|
|
154
|
+
# top of whatever office_code is built).
|
|
155
|
+
# @param sequence [Integer, String, nil] 5-digit personal sequence, default: random
|
|
156
|
+
# @return [String] a valid 13-digit Thai national ID
|
|
157
|
+
# @raise [ArgumentError] if province_code is given but not in PROVINCE_CODES
|
|
158
|
+
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
59
159
|
def self.generate(category: rand(1..6),
|
|
160
|
+
province_code: PROVINCE_CODES.keys.sample,
|
|
60
161
|
office_code: nil,
|
|
61
162
|
district_code: nil,
|
|
62
163
|
sequence: nil)
|
|
63
|
-
|
|
64
|
-
|
|
164
|
+
office_code = if office_code
|
|
165
|
+
format('%04d', office_code)
|
|
166
|
+
else
|
|
167
|
+
pcode = province_code.to_s
|
|
168
|
+
raise ArgumentError, "Unknown province_code: #{pcode.inspect}" unless PROVINCE_CODES.key?(pcode)
|
|
169
|
+
|
|
170
|
+
"#{pcode}#{format('%02d', rand(1..DISTRICT_COUNTS[pcode]))}"
|
|
171
|
+
end
|
|
65
172
|
office_code[2..3] = district_code.to_s.rjust(2, '0') if district_code
|
|
66
173
|
|
|
67
|
-
# Sequence (5 digits) and classification (2 digits)
|
|
68
174
|
sequence = format('%05d', sequence || rand(0..99_999))
|
|
69
175
|
classification = format('%02d', rand(0..99))
|
|
70
176
|
|
|
71
|
-
# First 12 digits: category + office_code + sequence + classification
|
|
72
177
|
digits = [category.to_i] +
|
|
73
178
|
office_code.chars.map(&:to_i) +
|
|
74
179
|
sequence.chars.map(&:to_i) +
|
|
75
180
|
classification.chars.map(&:to_i)
|
|
76
181
|
|
|
77
|
-
# Checksum
|
|
78
182
|
sum = digits.each_with_index.sum { |d, i| d * (13 - i) }
|
|
79
183
|
check = (11 - (sum % 11)) % 10
|
|
80
184
|
|
|
81
185
|
(digits + [check]).join
|
|
82
186
|
end
|
|
83
|
-
# rubocop:enable Metrics/AbcSize, Metrics/
|
|
187
|
+
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
188
|
+
|
|
189
|
+
# Return all valid 2-digit province code strings.
|
|
190
|
+
#
|
|
191
|
+
# @return [Array<String>] all keys of PROVINCE_CODES
|
|
192
|
+
def self.province_codes
|
|
193
|
+
PROVINCE_CODES.keys
|
|
194
|
+
end
|
|
84
195
|
|
|
85
|
-
#
|
|
196
|
+
# Return the human-readable description for a Thai ID category code.
|
|
197
|
+
#
|
|
198
|
+
# @param category [Integer, String] category digit (0–8)
|
|
199
|
+
# @return [String] description, or "Unknown category" if not found
|
|
86
200
|
def self.category_description(category)
|
|
87
201
|
CATEGORY_DESCRIPTIONS[category.to_i] || 'Unknown category'
|
|
88
202
|
end
|
|
203
|
+
|
|
204
|
+
# Return the province name for a 2-digit province code.
|
|
205
|
+
#
|
|
206
|
+
# @param code [String] 2-digit province code (e.g., "10" for Bangkok)
|
|
207
|
+
# @return [String, nil] province name, or nil if the code is not recognized
|
|
208
|
+
def self.province_name(code)
|
|
209
|
+
PROVINCE_CODES[code.to_s]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Convert a Buddhist Era year to Common Era (subtract 543).
|
|
213
|
+
#
|
|
214
|
+
# @param year [Integer, String] Buddhist Era year (e.g., 2567)
|
|
215
|
+
# @return [Integer] Common Era year (e.g., 2024)
|
|
216
|
+
def self.be_to_ce(year)
|
|
217
|
+
year.to_i - 543
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Convert a Common Era year to Buddhist Era (add 543).
|
|
221
|
+
#
|
|
222
|
+
# @param year [Integer, String] Common Era year (e.g., 2024)
|
|
223
|
+
# @return [Integer] Buddhist Era year (e.g., 2567)
|
|
224
|
+
def self.ce_to_be(year)
|
|
225
|
+
year.to_i + 543
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Validate the format of a Thai ID card laser ID (printed on the card back).
|
|
229
|
+
# Expected format: XXN-NNNNNNN-NN (e.g., JC1-0002507-15)
|
|
230
|
+
#
|
|
231
|
+
# @param laser_id [String] the laser ID string to validate
|
|
232
|
+
# @return [Boolean] true if format matches, false otherwise
|
|
233
|
+
def self.laser_id_valid?(laser_id)
|
|
234
|
+
LASER_ID_FORMAT.match?(laser_id.to_s)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Decode a Thai ID card laser ID into its components.
|
|
238
|
+
#
|
|
239
|
+
# @param laser_id [String] laser ID string (e.g., "JC1-0002507-15")
|
|
240
|
+
# @return [Hash] decoded fields:
|
|
241
|
+
# - `:hardware_version` [String] — chip generation code (e.g., "JC1")
|
|
242
|
+
# - `:box_id` [String] — distribution box number (e.g., "0002507")
|
|
243
|
+
# - `:position` [String] — slot within the box (e.g., "15")
|
|
244
|
+
# @raise [InvalidIDError] if the laser ID format is invalid
|
|
245
|
+
def self.laser_id_decode(laser_id)
|
|
246
|
+
raise InvalidIDError, 'Invalid laser ID' unless laser_id_valid?(laser_id)
|
|
247
|
+
|
|
248
|
+
parts = laser_id.to_s.split('-')
|
|
249
|
+
{
|
|
250
|
+
hardware_version: parts[0],
|
|
251
|
+
box_id: parts[1],
|
|
252
|
+
position: parts[2]
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Generate a random, valid Thai ID card laser ID.
|
|
257
|
+
# Format: XXN-NNNNNNN-NN (e.g., JC1-0002507-15)
|
|
258
|
+
#
|
|
259
|
+
# @param hardware_version [String, nil] full 3-char chip code (e.g. "JC1").
|
|
260
|
+
# Defaults to a random prefix from LASER_HARDWARE_VERSIONS + digit 1–3.
|
|
261
|
+
# @param box_id [Integer, nil] distribution box number (1–9,999,999)
|
|
262
|
+
# @param position [Integer, nil] slot within the box (1–60)
|
|
263
|
+
# @return [String] a laser ID string matching LASER_ID_FORMAT
|
|
264
|
+
def self.generate_laser_id(hardware_version: nil, box_id: nil, position: nil)
|
|
265
|
+
hw = hardware_version || "#{LASER_HARDWARE_VERSIONS.sample}#{rand(1..3)}"
|
|
266
|
+
box = format('%07d', box_id || rand(1..9_999_999))
|
|
267
|
+
pos = format('%02d', position || rand(1..60))
|
|
268
|
+
"#{hw}-#{box}-#{pos}"
|
|
269
|
+
end
|
|
89
270
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: thai_id_utils
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chayut Orapinpatipat
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-03-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -25,27 +25,33 @@ dependencies:
|
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '5.0'
|
|
27
27
|
description: |
|
|
28
|
-
Zero-dependency Ruby utilities for:
|
|
28
|
+
Zero-dependency Ruby utilities for Thai national ID numbers:
|
|
29
29
|
• checksum validation (modulus-11),
|
|
30
|
-
• component decoding (category,
|
|
31
|
-
•
|
|
32
|
-
•
|
|
30
|
+
• component decoding (category, province, district, sequence),
|
|
31
|
+
• province-constrained valid ID generation with DISTRICT_COUNTS,
|
|
32
|
+
• province name lookup for all 77 provinces,
|
|
33
|
+
• laser ID validation, decoding, and generation,
|
|
34
|
+
• human-readable category descriptions (0–8),
|
|
35
|
+
• Buddhist Era ↔ Common Era date conversion.
|
|
33
36
|
email:
|
|
34
37
|
- chayut_o@hotmail.com
|
|
35
38
|
executables: []
|
|
36
39
|
extensions: []
|
|
37
40
|
extra_rdoc_files: []
|
|
38
41
|
files:
|
|
39
|
-
-
|
|
42
|
+
- CHANGELOG.md
|
|
43
|
+
- LICENSE
|
|
40
44
|
- README.md
|
|
41
|
-
- Rakefile
|
|
42
45
|
- lib/thai_id_utils.rb
|
|
43
46
|
- lib/thai_id_utils/version.rb
|
|
44
47
|
homepage: https://github.com/chayuto/thai_id_utils
|
|
45
48
|
licenses:
|
|
46
49
|
- MIT
|
|
47
50
|
metadata:
|
|
48
|
-
documentation_uri: https://
|
|
51
|
+
documentation_uri: https://rubydoc.info/gems/thai_id_utils
|
|
52
|
+
source_code_uri: https://github.com/chayuto/thai_id_utils
|
|
53
|
+
changelog_uri: https://github.com/chayuto/thai_id_utils/blob/main/CHANGELOG.md
|
|
54
|
+
rubygems_mfa_required: 'true'
|
|
49
55
|
post_install_message:
|
|
50
56
|
rdoc_options: []
|
|
51
57
|
require_paths:
|
|
@@ -61,8 +67,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
61
67
|
- !ruby/object:Gem::Version
|
|
62
68
|
version: '0'
|
|
63
69
|
requirements: []
|
|
64
|
-
rubygems_version: 3.5.
|
|
70
|
+
rubygems_version: 3.5.22
|
|
65
71
|
signing_key:
|
|
66
72
|
specification_version: 4
|
|
67
|
-
summary: Validate and
|
|
73
|
+
summary: Validate, decode, and generate Thai national ID numbers
|
|
68
74
|
test_files: []
|
data/Gemfile
DELETED