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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6475eea4d79e998ecd38cddabe1fde552506a8be42ebe49c8bb9e53537078ee1
4
- data.tar.gz: e8c623b974cbc84d7e7ae989db53ad4fb1a7db0cf1795be20b75204ec070a904
3
+ metadata.gz: 550dacaa281beb7bdba770c9dd0b54cf252f87b9aaf56fdfcaa9dbf7f3b22589
4
+ data.tar.gz: 3afd4ab32f6836e8995b473f2352a39d1b7e137c8aae7fac3a8815b21f3bce74
5
5
  SHA512:
6
- metadata.gz: 41f37e9ed4760dc95d0fba9f271e66bce6b1e05b7a0a6e00425953517b45c2c41c8f4974857faf5f6880157252bb89980ee4ecb11829ba57c67c1d14b6a5ce71
7
- data.tar.gz: 2a685abb6f8e64bd9a45d0796390e0e9389d65b6e0fc0e1d913ccc5c8274109836175c73074de3bb8a90adc380bb25b4604e90f24fce587d9ddf600a11f9ccb3
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. It provides a simple API to check the official modulus-11 checksum, extract the embedded components (category, office code, district code, sequence), and get a human-readable description of the category code.
4
-
5
- Thai ID Utils เป็น Ruby gem ที่ไม่ต้องพึ่งพาไลบรารีเสริม สำหรับตรวจสอบความถูกต้องและถอดรหัสหมายเลขบัตรประชาชนไทย โดยมี API ที่ใช้งานง่ายสำหรับตรวจสอบ checksum ตามมาตรฐาน modulus-11 ดึงส่วนประกอบต่างๆ (ประเภทผู้ลงทะเบียน รหัสหน่วยงาน รหัสอำเภอ และหมายเลขลำดับ) และแสดงคำอธิบายของรหัสประเภทในรูปแบบอ่านง่าย
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
+ [![Gem Version](https://badge.fury.io/rb/thai_id_utils.svg)](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
- id = "3012304567082"
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
- # Validate checksum / ตรวจสอบความถูกต้องของ checksum
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
- # Decode components / ถอดรหัสส่วนประกอบ
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
- # Get category description / คำอธิบายประเภท
27
- desc = ThaiIdUtils.category_description(info[:category])
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
- # Generate a new random valid ID / สร้างหมายเลขบัตรประชาชนใหม่แบบสุ่มที่ถูกต้อง
32
- new_id = ThaiIdUtils.generate
33
- puts new_id # => e.g. "3601205234518"
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ThaiIdUtils
4
- VERSION = '0.1.2'
4
+ VERSION = '0.3.0'
5
5
  end
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
- # Public: Validate checksum using Thailand’s modulus-11 algorithm
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
- # Public: Decode components present in a Thai ID
38
- # Returns a hash with keys:
39
- # :category => Integer
40
- # :office_code => String (4-digit registrar)
41
- # :district_code => String (last 2 of office_code)
42
- # :sequence => String (5-digit personal sequence)
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
- # Public: Generate a random, valid 13-digit Thai national ID.
57
- # You can override any component or let it be randomized.
58
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
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
- # Build and override office_code/district_code
64
- office_code = format('%04d', office_code || rand(1..9_999))
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/CyclomaticComplexity, Metrics/MethodLength
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
- # Public: Return a human-readable description for a category code
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.1.2
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: 2025-06-15 00:00:00.000000000 Z
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, office_code, district_code, sequence),
31
- random valid ID generation,
32
- human-readable category descriptions.
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
- - Gemfile
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://github.com/chayuto/thai_id_utils#readme
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.11
70
+ rubygems_version: 3.5.22
65
71
  signing_key:
66
72
  specification_version: 4
67
- summary: Validate and decode Thai national ID numbers
73
+ summary: Validate, decode, and generate Thai national ID numbers
68
74
  test_files: []
data/Gemfile DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- gemspec
data/Rakefile DELETED
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rake/testtask'
4
-
5
- Rake::TestTask.new do |t|
6
- t.libs << 'test'
7
- end
8
-
9
- task default: :test