Module: ThaiIdUtils
- Defined in:
- lib/thai_id_utils.rb,
lib/thai_id_utils/version.rb
Overview
ThaiIdUtils: Validate and decode Thai national ID numbers, providing checksum validation, component decoding, ID generation, and human-readable category descriptions.
Defined Under Namespace
Classes: InvalidIDError
Constant Summary collapse
- CATEGORY_DESCRIPTIONS =
Mapping of category codes to their descriptions
rubocop:disable Layout/LineLength
{ 0 => '(Not found on cards of Thai nationals but may be found in the other issued identity cards below)', 1 => 'Thai nationals who were born after 1 January 1984 and had their birth notified within the given deadline (15 days).', 2 => 'Thai nationals who were born after 1 January 1984 but failed to have their birth notified in time.', 3 => 'Thai nationals or foreign nationals with identification cards who were born and whose names were included in a house registration book before 1 January 1984', 4 => 'Thai nationals who were born before 1 January 1984 but were not included in a house registration book at that time, for example due to moving residences', 5 => 'Thai nationals who missed the official census or other special cases, for instance those of dual nationality', 6 => 'Foreign nationals who are living in Thailand temporarily and illegal migrants', 7 => 'Children of people of category 6 who were born in Thailand', 8 => 'Foreign nationals who are living in Thailand permanently or Thai nationals by naturalization' }.freeze
- PROVINCE_CODES =
Mapping of 2-digit province codes (digits 2-3 of the ID) to province names
{ '10' => 'Bangkok', '11' => 'Samut Prakan', '12' => 'Nonthaburi', '13' => 'Pathum Thani', '14' => 'Phra Nakhon Si Ayutthaya', '15' => 'Ang Thong', '16' => 'Lopburi', '17' => 'Sing Buri', '18' => 'Chainat', '19' => 'Saraburi', '20' => 'Chonburi', '21' => 'Rayong', '22' => 'Chanthaburi', '23' => 'Trat', '24' => 'Chachoengsao', '25' => 'Prachin Buri', '26' => 'Nakhon Nayok', '27' => 'Sa Kaeo', '30' => 'Nakhon Ratchasima', '31' => 'Buri Ram', '32' => 'Surin', '33' => 'Si Sa Ket', '34' => 'Ubon Ratchathani', '35' => 'Yasothon', '36' => 'Chaiyaphum', '37' => 'Amnat Charoen', '38' => 'Bueng Kan', '39' => 'Nong Bua Lamphu', '40' => 'Khon Kaen', '41' => 'Udon Thani', '42' => 'Loei', '43' => 'Nong Khai', '44' => 'Maha Sarakham', '45' => 'Roi Et', '46' => 'Kalasin', '47' => 'Sakon Nakhon', '48' => 'Nakhon Phanom', '49' => 'Mukdahan', '50' => 'Chiang Mai', '51' => 'Lamphun', '52' => 'Lampang', '53' => 'Uttaradit', '54' => 'Phrae', '55' => 'Nan', '56' => 'Phayao', '57' => 'Chiang Rai', '58' => 'Mae Hong Son', '60' => 'Nakhon Sawan', '61' => 'Uthai Thani', '62' => 'Kamphaeng Phet', '63' => 'Tak', '64' => 'Sukhothai', '65' => 'Phitsanulok', '66' => 'Phichit', '67' => 'Phetchabun', '70' => 'Ratchaburi', '71' => 'Kanchanaburi', '72' => 'Suphanburi', '73' => 'Nakhon Pathom', '74' => 'Samut Sakhon', '75' => 'Samut Songkhram', '76' => 'Phetchaburi', '77' => 'Prachuap Khiri Khan', '80' => 'Nakhon Si Thammarat', '81' => 'Krabi', '82' => 'Phangnga', '83' => 'Phuket', '84' => 'Surat Thani', '85' => 'Ranong', '86' => 'Chumphon', '90' => 'Songkhla', '91' => 'Satun', '92' => 'Trang', '93' => 'Phatthalung', '94' => 'Pattani', '95' => 'Yala', '96' => 'Narathiwat' }.freeze
- DISTRICT_COUNTS =
Mapping of province codes to the number of administrative districts (amphoe for provinces, khet for Bangkok). Used to constrain district code generation to realistic ranges within generate(). Counts are approximate and reflect post-2011 administrative divisions.
{ '10' => 50, '11' => 11, '12' => 6, '13' => 7, '14' => 16, '15' => 7, '16' => 11, '17' => 6, '18' => 8, '19' => 13, '20' => 11, '21' => 8, '22' => 10, '23' => 7, '24' => 11, '25' => 7, '26' => 4, '27' => 9, '30' => 32, '31' => 23, '32' => 17, '33' => 22, '34' => 25, '35' => 9, '36' => 16, '37' => 7, '38' => 8, '39' => 6, '40' => 26, '41' => 20, '42' => 14, '43' => 18, '44' => 13, '45' => 20, '46' => 18, '47' => 18, '48' => 12, '49' => 7, '50' => 25, '51' => 8, '52' => 13, '53' => 9, '54' => 8, '55' => 15, '56' => 9, '57' => 18, '58' => 7, '60' => 15, '61' => 8, '62' => 11, '63' => 8, '64' => 9, '65' => 9, '66' => 12, '67' => 11, '70' => 10, '71' => 13, '72' => 10, '73' => 7, '74' => 7, '75' => 3, '76' => 8, '77' => 8, '80' => 23, '81' => 8, '82' => 8, '83' => 3, '84' => 19, '85' => 5, '86' => 8, '90' => 16, '91' => 7, '92' => 10, '93' => 11, '94' => 12, '95' => 8, '96' => 9 }.freeze
- LASER_ID_FORMAT =
/\A[A-Z]{2}\d-\d{7}-\d{2}\z/.freeze
- LASER_HARDWARE_VERSIONS =
Known chip hardware-version prefixes observed on issued Thai ID cards.
%w[JC AA BB GC].freeze
- VERSION =
'0.3.0'
Class Method Summary collapse
-
.be_to_ce(year) ⇒ Integer
Convert a Buddhist Era year to Common Era (subtract 543).
-
.category_description(category) ⇒ String
Return the human-readable description for a Thai ID category code.
-
.ce_to_be(year) ⇒ Integer
Convert a Common Era year to Buddhist Era (add 543).
-
.decode(id) ⇒ Hash
Decode the components encoded in a Thai national ID number.
-
.generate(category: rand(1..6), province_code: PROVINCE_CODES.keys.sample, office_code: nil, district_code: nil, sequence: nil) ⇒ String
Generate a random, valid 13-digit Thai national ID.
-
.generate_laser_id(hardware_version: nil, box_id: nil, position: nil) ⇒ String
Generate a random, valid Thai ID card laser ID.
-
.laser_id_decode(laser_id) ⇒ Hash
Decode a Thai ID card laser ID into its components.
-
.laser_id_valid?(laser_id) ⇒ Boolean
Validate the format of a Thai ID card laser ID (printed on the card back).
-
.province_codes ⇒ Array<String>
Return all valid 2-digit province code strings.
-
.province_name(code) ⇒ String?
Return the province name for a 2-digit province code.
-
.valid?(id) ⇒ Boolean
Validate a Thai national ID using Thailand’s modulus-11 checksum algorithm.
Class Method Details
.be_to_ce(year) ⇒ Integer
Convert a Buddhist Era year to Common Era (subtract 543).
216 217 218 |
# File 'lib/thai_id_utils.rb', line 216 def self.be_to_ce(year) year.to_i - 543 end |
.category_description(category) ⇒ String
Return the human-readable description for a Thai ID category code.
200 201 202 |
# File 'lib/thai_id_utils.rb', line 200 def self.category_description(category) CATEGORY_DESCRIPTIONS[category.to_i] || 'Unknown category' end |
.ce_to_be(year) ⇒ Integer
Convert a Common Era year to Buddhist Era (add 543).
224 225 226 |
# File 'lib/thai_id_utils.rb', line 224 def self.ce_to_be(year) year.to_i + 543 end |
.decode(id) ⇒ Hash
Decode the components encoded in a Thai national ID number.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/thai_id_utils.rb', line 126 def self.decode(id) raise InvalidIDError, 'Invalid ID' unless valid?(id) d = id.to_s.chars { category: d[0].to_i, office_code: d[1..4].join, province_code: d[1..2].join, province_name: PROVINCE_CODES[d[1..2].join], district_code: d[3..4].join, sequence: d[5..9].join, registration_code: d[10..11].join } end |
.generate(category: rand(1..6), province_code: PROVINCE_CODES.keys.sample, office_code: nil, district_code: nil, sequence: nil) ⇒ String
Generate a random, valid 13-digit Thai national ID. Any component can be overridden; the rest is randomised and the checksum is computed. When neither office_code nor province_code is given, a valid province is selected at random and a district code within that province’s known range is generated.
rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/thai_id_utils.rb', line 159 def self.generate(category: rand(1..6), province_code: PROVINCE_CODES.keys.sample, office_code: nil, district_code: nil, sequence: nil) office_code = if office_code format('%04d', office_code) else pcode = province_code.to_s raise ArgumentError, "Unknown province_code: #{pcode.inspect}" unless PROVINCE_CODES.key?(pcode) "#{pcode}#{format('%02d', rand(1..DISTRICT_COUNTS[pcode]))}" end office_code[2..3] = district_code.to_s.rjust(2, '0') if district_code sequence = format('%05d', sequence || rand(0..99_999)) classification = format('%02d', rand(0..99)) digits = [category.to_i] + office_code.chars.map(&:to_i) + sequence.chars.map(&:to_i) + classification.chars.map(&:to_i) sum = digits.each_with_index.sum { |d, i| d * (13 - i) } check = (11 - (sum % 11)) % 10 (digits + [check]).join end |
.generate_laser_id(hardware_version: nil, box_id: nil, position: nil) ⇒ String
Generate a random, valid Thai ID card laser ID. Format: XXN-NNNNNNN-NN (e.g., JC1-0002507-15)
264 265 266 267 268 269 |
# File 'lib/thai_id_utils.rb', line 264 def self.generate_laser_id(hardware_version: nil, box_id: nil, position: nil) hw = hardware_version || "#{LASER_HARDWARE_VERSIONS.sample}#{rand(1..3)}" box = format('%07d', box_id || rand(1..9_999_999)) pos = format('%02d', position || rand(1..60)) "#{hw}-#{box}-#{pos}" end |
.laser_id_decode(laser_id) ⇒ Hash
Decode a Thai ID card laser ID into its components.
245 246 247 248 249 250 251 252 253 254 |
# File 'lib/thai_id_utils.rb', line 245 def self.laser_id_decode(laser_id) raise InvalidIDError, 'Invalid laser ID' unless laser_id_valid?(laser_id) parts = laser_id.to_s.split('-') { hardware_version: parts[0], box_id: parts[1], position: parts[2] } end |
.laser_id_valid?(laser_id) ⇒ Boolean
Validate the format of a Thai ID card laser ID (printed on the card back). Expected format: XXN-NNNNNNN-NN (e.g., JC1-0002507-15)
233 234 235 |
# File 'lib/thai_id_utils.rb', line 233 def self.laser_id_valid?(laser_id) LASER_ID_FORMAT.match?(laser_id.to_s) end |
.province_codes ⇒ Array<String>
Return all valid 2-digit province code strings.
192 193 194 |
# File 'lib/thai_id_utils.rb', line 192 def self.province_codes PROVINCE_CODES.keys end |
.province_name(code) ⇒ String?
Return the province name for a 2-digit province code.
208 209 210 |
# File 'lib/thai_id_utils.rb', line 208 def self.province_name(code) PROVINCE_CODES[code.to_s] end |
.valid?(id) ⇒ Boolean
Validate a Thai national ID using Thailand’s modulus-11 checksum algorithm.
104 105 106 107 108 109 110 111 112 |
# File 'lib/thai_id_utils.rb', line 104 def self.valid?(id) digits = id.to_s.chars.map(&:to_i) return false unless digits.size == 13 sum = digits[0..11].each_with_index.sum { |d, i| d * (13 - i) } ((11 - (sum % 11)) % 10) == digits.last rescue StandardError false end |