CurpMX 0.3.0 → 1.0.1
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/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/Rakefile +12 -0
- data/curp_mx.gemspec +35 -0
- data/lib/curp_mx/validator.rb +151 -0
- data/lib/curp_mx/version.rb +5 -0
- data/lib/curp_mx.rb +3 -119
- data/sig/curp_mx.rbs +4 -0
- metadata +23 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b322b557bf790e15b26b0d5d950f370e1e56d458e49e74490746d2eac61433f
|
|
4
|
+
data.tar.gz: 7af4e03b90b08fab6cfedfa9d49484219ba48bb5eeff27f9bf587ac93b9a1d8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2133105cf7c325814b4a485abe0d457e2f3f35f6e5acc5b266f7b1346d6fa6eab620f5070ed50ccba8c9ffb4b92e73ddbc41d172dc460a34826db6af0c68e56
|
|
7
|
+
data.tar.gz: 39322eb5225a390fcaa7e9ab2d8119d1db09d62aaaa247268466f79c9ba69d873c5359f693baa2034cab76eb17872edde252b09ed6affc97c465669ebbc517d3
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/),
|
|
4
|
+
y este proyecto sigue [Versionado Semántico](https://semver.org/lang/es/).
|
|
5
|
+
|
|
6
|
+
## [1.0.1] - 2026-07-04
|
|
7
|
+
|
|
8
|
+
### Cambiado
|
|
9
|
+
- La gema se publica bajo el nombre existente `CurpMX` (RubyGems rechaza
|
|
10
|
+
`curp_mx` por ser demasiado similar). La ruta de `require` sigue siendo
|
|
11
|
+
`curp_mx`.
|
|
12
|
+
|
|
13
|
+
## [1.0.0] - 2026-07-04
|
|
14
|
+
|
|
15
|
+
Primera versión funcional y estable. `CurpMx::Validator` valida la formación de
|
|
16
|
+
un CURP conforme al Instructivo Normativo (DOF 18-10-2021) y a las Reglas para
|
|
17
|
+
la ejecución de los procedimientos para la asignación de la CURP.
|
|
18
|
+
|
|
19
|
+
### Agregado
|
|
20
|
+
- Validación del dígito verificador (posición 18) con el algoritmo del RENAPO,
|
|
21
|
+
disponible también como `CurpMx::Validator.check_digit`.
|
|
22
|
+
- Aceptación del marcador de sexo `X` (CURPs de género no binario).
|
|
23
|
+
- Aceptación de CURPs anteriores y posteriores al año 2000 (homoclave `0-9` o
|
|
24
|
+
`A-J` en la posición 17).
|
|
25
|
+
- Código de entidad `NE` (nacido en el extranjero).
|
|
26
|
+
- Normalización de la entrada a mayúsculas.
|
|
27
|
+
- Manejo seguro de entradas `nil` o que no son cadenas (regresan `format` en
|
|
28
|
+
lugar de lanzar una excepción).
|
|
29
|
+
|
|
30
|
+
### Cambiado
|
|
31
|
+
- Catálogo de palabras altisonantes ampliado a las 82 entradas del Anexo 01.
|
|
32
|
+
- Catálogo de entidades corregido a los 33 códigos del Anexo 03 (se eliminó el
|
|
33
|
+
código inexistente `CX`; la Ciudad de México es `DF`).
|
|
34
|
+
- Reescritura interna del validador: búsquedas con `Set`, extracción por
|
|
35
|
+
posición fija y `String#match?`, sin asignar `MatchData`. La validación
|
|
36
|
+
completa es ~2x más rápida que la implementación previa.
|
|
37
|
+
|
|
38
|
+
### Eliminado
|
|
39
|
+
- Dependencia de `parslet`. La gema ya no tiene dependencias en tiempo de
|
|
40
|
+
ejecución (solo la librería estándar de Ruby).
|
|
41
|
+
|
|
42
|
+
### Corregido
|
|
43
|
+
- Los errores de `state` y `problematic_name` lanzaban `NoMethodError` porque
|
|
44
|
+
el arreglo de errores nunca se inicializaba.
|
|
45
|
+
- El formato rechazaba todo CURP emitido a partir del año 2000 (posición 17 con
|
|
46
|
+
letra).
|
|
47
|
+
|
|
48
|
+
[1.0.1]: https://github.com/hslzr/curp_mx/releases/tag/v1.0.1
|
|
49
|
+
[1.0.0]: https://github.com/hslzr/curp_mx/releases/tag/v1.0.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 TODO: Write your name
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# CurpMx
|
|
2
|
+
|
|
3
|
+
Librería para validar CURPs. Nada complicado: esencialmente un regex más un
|
|
4
|
+
par de comprobaciones que regresan una lista de errores, de haber alguno.
|
|
5
|
+
|
|
6
|
+
Sin dependencias en tiempo de ejecución (solo usa la librería estándar de Ruby).
|
|
7
|
+
|
|
8
|
+
## Instalación
|
|
9
|
+
|
|
10
|
+
Agrega la gema a tu `Gemfile`:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "curp_mx"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Y luego:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
O instálala directamente:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gem install curp_mx
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Uso
|
|
29
|
+
|
|
30
|
+
### Validación rápida
|
|
31
|
+
|
|
32
|
+
Cuando solo te interesa saber si el CURP es válido o no:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
CurpMx::Validator.valid?("TOGG641009HJCRML99")
|
|
36
|
+
#=> true | false
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Validación a detalle
|
|
40
|
+
|
|
41
|
+
Cuando necesitas saber *por qué* un CURP es inválido:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
validator = CurpMx::Validator.new("TOGG641009HZZRML99")
|
|
45
|
+
# El método #validate se llama automáticamente al inicializar.
|
|
46
|
+
|
|
47
|
+
validator.valid?
|
|
48
|
+
#=> false
|
|
49
|
+
|
|
50
|
+
validator.errors
|
|
51
|
+
#=> { :state => ["Invalid state: 'ZZ'"] }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Si el formato no coincide con el de un CURP, la validación se detiene ahí y
|
|
55
|
+
solo se reporta el error de `format`:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
CurpMx::Validator.new("no-es-un-curp").errors
|
|
59
|
+
#=> { :format => ["Invalid format"] }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Un CURP válido regresa un hash de errores vacío:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
validator = CurpMx::Validator.new("TOGG641009HJCRML99")
|
|
66
|
+
validator.valid? #=> true
|
|
67
|
+
validator.errors #=> {}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Validaciones
|
|
71
|
+
|
|
72
|
+
`errors` es un `Hash` donde cada llave es el campo con problema y su valor es un
|
|
73
|
+
arreglo de mensajes.
|
|
74
|
+
|
|
75
|
+
| Llave | Significado |
|
|
76
|
+
|:--- |:---|
|
|
77
|
+
| `format` | El formato no coincide con el de un CURP |
|
|
78
|
+
| `state` | El estado no coincide con las abreviaciones del RENAPO |
|
|
79
|
+
| `problematic_name` | Las iniciales forman una palabra altisonante (ej. `CACA`) |
|
|
80
|
+
| `birth_day` | Día de nacimiento `<= 0` o `> 31` |
|
|
81
|
+
| `birth_month` | Mes de nacimiento `<= 0` o `> 12` |
|
|
82
|
+
| `birth_date` | Fecha de nacimiento inexistente (ej. `30/02/1989`) |
|
|
83
|
+
| `check_digit` | El dígito verificador (posición 18) no coincide con el calculado |
|
|
84
|
+
|
|
85
|
+
## Notas
|
|
86
|
+
|
|
87
|
+
- Se aceptan los marcadores de sexo `H`, `M` y `X`.
|
|
88
|
+
- Se aceptan CURPs anteriores y posteriores al año 2000 (la homoclave puede ser
|
|
89
|
+
dígito o letra).
|
|
90
|
+
- Se valida el dígito verificador (posición 18) con el algoritmo del RENAPO.
|
|
91
|
+
También se puede calcular por separado a partir de los primeros 17 caracteres:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
CurpMx::Validator.check_digit("BEBE900101HDFXXX0") #=> 7
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Dígito verificador
|
|
98
|
+
|
|
99
|
+
### Cómo se dedujo
|
|
100
|
+
|
|
101
|
+
El algoritmo del dígito verificador **no aparece publicado** en el Instructivo
|
|
102
|
+
Normativo: éste solo describe la posición 18 como *"un carácter asignado […] a
|
|
103
|
+
través de la aplicación de un algoritmo que permite calcular y verificar la
|
|
104
|
+
correcta conformación de la clave"*, sin dar la fórmula ni la tabla de valores.
|
|
105
|
+
|
|
106
|
+
Por eso el algoritmo de esta gema es, en parte, **ingeniería inversa**. Se
|
|
107
|
+
partió del algoritmo estándar del RENAPO que circula públicamente y se confirmó
|
|
108
|
+
de forma empírica contra CURPs reales, válidas y conocidas: para cada una se
|
|
109
|
+
calculó el dígito a partir de sus primeros 17 caracteres y se comparó con el
|
|
110
|
+
carácter 18 real. Varias CURPs independientes coinciden, así que la confianza es
|
|
111
|
+
alta, pero conviene tenerlo presente: **es la única regla de la gema que no está
|
|
112
|
+
respaldada por una fuente oficial directa**, sino por verificación empírica.
|
|
113
|
+
|
|
114
|
+
Sugiero ampliamente hacer validaciones con CURPs propias para corroborar que, en
|
|
115
|
+
CURPs ya existentes y en circulación, este algoritmo siga siendo válido. Si tu
|
|
116
|
+
CURP es detectada como no válida puedes crear un issue en este repo para refinar
|
|
117
|
+
el cálculo.
|
|
118
|
+
|
|
119
|
+
### Cómo funciona
|
|
120
|
+
|
|
121
|
+
Dados los primeros 17 caracteres del CURP:
|
|
122
|
+
|
|
123
|
+
1. **Valor de cada carácter.** Los dígitos `0`–`9` valen `0`–`9`. Las letras
|
|
124
|
+
`A`–`N` valen `10`–`23`, y las letras `O`–`Z` valen `25`–`36`.
|
|
125
|
+
|
|
126
|
+
2. **Suma ponderada.** Cada valor se multiplica por un peso descendente según su
|
|
127
|
+
posición: el primer carácter por `18`, el segundo por `17`, … y el carácter
|
|
128
|
+
17 por `2`. Se suman todos los productos.
|
|
129
|
+
|
|
130
|
+
3. **Complemento a 10.** El dígito verificador es `(10 - (suma mod 10)) mod 10`,
|
|
131
|
+
siempre un número del `0` al `9`.
|
|
132
|
+
|
|
133
|
+
Ejemplo con `BEBE900101HDFXXX0` (17 caracteres):
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
B E B E 9 0 0 1 0 1 H D F X X X 0
|
|
137
|
+
11 14 11 14 9 0 0 1 0 1 17 13 15 34 34 34 0 ← valor
|
|
138
|
+
×18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 ← peso
|
|
139
|
+
|
|
140
|
+
suma = 1693 ; 1693 mod 10 = 3 ; (10 - 3) mod 10 = 7 → dígito = 7
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Por eso `BEBE900101HDFXXX07` es válido.
|
|
144
|
+
|
|
145
|
+
Internamente el cálculo se hace sobre los bytes ASCII (los tres tramos de
|
|
146
|
+
valores son contiguos), sin construir tablas ni objetos, para que sea rápido.
|
|
147
|
+
|
|
148
|
+
## Desarrollo
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
bin/setup # instala dependencias
|
|
152
|
+
bundle exec rspec # corre las pruebas
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Normativa
|
|
156
|
+
La gema en su estado actual valida la formación del CURP de acuerdo al
|
|
157
|
+
**Instructivo Normativo Para La Asignación De La Clave Única De Registro De
|
|
158
|
+
Población**, publicado en el Diario Oficial de la Federación el día 18 de
|
|
159
|
+
Octubre de 2021. El documento en formato PDF puede ser leído [en este
|
|
160
|
+
enlace](https://www.gob.mx/cms/uploads/attachment/file/337251/Instructivo_Normativo_para_la_Asignacion_de_la_CURP.pdf)
|
|
161
|
+
|
|
162
|
+
Sin embargo, cabe aclarar que **no soy abogado**, así que mis fuentes podrían no
|
|
163
|
+
ser las más precisas. Durante mi investigación, encontré además las [REGLAS PARA
|
|
164
|
+
LA EJECUCIÓN DE LOS PROCEDIMIENTOS PARA LA ASIGNACIÓN DE LA CLAVE ÚNICA DE
|
|
165
|
+
POBLACIÓN](https://www.gob.mx/cms/uploads/attachment/file/960109/Reglas_para_la_Ejecucion_de_los_Procedimientos_para_la_Asignacion_de_la_CURP.pdf),
|
|
166
|
+
que si bien no contradice el documento anterior, ésta tiene ejemplos más
|
|
167
|
+
precisos y un par de anexos para la validación de los datos – la lista de
|
|
168
|
+
palabras altisonantes en la página 86, por ejemplo.
|
|
169
|
+
|
|
170
|
+
La gema actual permite el uso de la letra `X` en el
|
|
171
|
+
género del individuo a pesar de no estar dentro de la normativa; esto debido a
|
|
172
|
+
un antecedente de [una CURP emitida en
|
|
173
|
+
2023](https://quinto-poder.mx/orgullomx/2023/2/23/expiden-la-primera-curp-de-genero-no-binario-18792.html),
|
|
174
|
+
lo que implica que más CURPs con formato similar podrían existir allá afuera.
|
|
175
|
+
|
|
176
|
+
## Licencia
|
|
177
|
+
|
|
178
|
+
Disponible como software libre bajo los términos de la [licencia MIT](LICENSE.txt).
|
data/Rakefile
ADDED
data/curp_mx.gemspec
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/curp_mx/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "CurpMX"
|
|
7
|
+
spec.version = CurpMx::VERSION
|
|
8
|
+
spec.authors = ["Salazar"]
|
|
9
|
+
|
|
10
|
+
spec.summary = "Parse and validate CURP (Clave Única de Registro de Población) from Mexico."
|
|
11
|
+
spec.description = "Parse and validate CURP (Clave Única de Registro de Población) from Mexico."
|
|
12
|
+
spec.homepage = "https://github.com/hslzr/curp_mx"
|
|
13
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
|
|
18
|
+
|
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
(File.expand_path(f) == __FILE__) ||
|
|
24
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
spec.bindir = "exe"
|
|
28
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
29
|
+
spec.require_paths = ["lib"]
|
|
30
|
+
|
|
31
|
+
# No runtime dependencies: validation is a regex + stdlib Date.
|
|
32
|
+
|
|
33
|
+
# For more information and examples about making a new gem, check out our
|
|
34
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
35
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'set'
|
|
5
|
+
|
|
6
|
+
module CurpMx
|
|
7
|
+
# Validates a CURP's format and a few data points in it.
|
|
8
|
+
#
|
|
9
|
+
# Hot-path notes: the format check uses String#match? (no MatchData
|
|
10
|
+
# allocation) and fields are read by fixed offset. Lookups go through
|
|
11
|
+
# frozen Sets, not Array scans. See spec + benchmarks.
|
|
12
|
+
class Validator
|
|
13
|
+
attr_reader :errors, :raw_input
|
|
14
|
+
|
|
15
|
+
# Format only — no captures. Fields are sliced by offset afterwards.
|
|
16
|
+
# 0-3 name initials 4-5 year 6-7 month 8-9 day
|
|
17
|
+
# 10 sex 11-12 state 13-15 consonants
|
|
18
|
+
# 16 homoclave 17 check digit
|
|
19
|
+
#
|
|
20
|
+
# Position 16 (homoclave/century): 0-9 for births before 2000, A-J for
|
|
21
|
+
# 2000 onward, per the Instructivo Normativo (DOF 18-10-2021).
|
|
22
|
+
# Sex accepts X (non-binary CURPs issued since 2023) beyond the H/M
|
|
23
|
+
# in that same text.
|
|
24
|
+
FORMAT = /\A[A-Z]{4}\d{2}[0-1]\d[0-3]\d[HMX][A-Z]{2}[^AEIOU]{3}[0-9A-J]\d\z/.freeze
|
|
25
|
+
|
|
26
|
+
# Entity codes for positions 12-13, from Anexo 03 "Catálogo de
|
|
27
|
+
# Entidades Federativas para la conformación de la CURP" of the
|
|
28
|
+
# Instructivo Normativo (DOF 18-10-2021). 32 entities (Mexico City is
|
|
29
|
+
# DF; there is no CX) plus NE for people born abroad.
|
|
30
|
+
STATES_RENAPO = %w[AS BC BS CC CL CM CS CH DF DG GT GR HG JC MC MN MS
|
|
31
|
+
NT NL OC PL QT QR SP SL SR TC TS TL VZ YN ZS NE].freeze
|
|
32
|
+
|
|
33
|
+
# Problematic name initials (RENAPO substitutes the 2nd letter with X
|
|
34
|
+
# when the first four letters spell one of these). Full catalog of 82
|
|
35
|
+
# words from Anexo 01 "Catálogo de palabras altisonantes" of the
|
|
36
|
+
# Instructivo Normativo (DOF 18-10-2021).
|
|
37
|
+
NAME_ISSUES = %w[BACA BAKA BUEI BUEY CACA CACO CAGA CAGO CAKA CAKO
|
|
38
|
+
COGE COGI COJA COJE COJI COJO COLA CULO FALO FETO
|
|
39
|
+
GETA GUEI GUEY JETA JOTO KACA KACO KAGA KAGO KAKA
|
|
40
|
+
KAKO KOGE KOGI KOJA KOJE KOJI KOJO KOLA KULO LILO
|
|
41
|
+
LOCA LOCO LOKA LOKO MAME MAMO MEAR MEAS MEON MIAR
|
|
42
|
+
MION MOCO MOKO MULA MULO NACA NACO ORIN PEDA PEDO
|
|
43
|
+
PENE PIPI PITO POPO PUTA PUTO QULO RATA ROBA ROBE
|
|
44
|
+
ROBO RUIN SENO TETA VACA VAGA VAGO VAKA VUEI VUEY
|
|
45
|
+
WUEI WUEY].freeze
|
|
46
|
+
|
|
47
|
+
# O(1) lookup copies of the constants above (the public constants stay
|
|
48
|
+
# as readable frozen Arrays).
|
|
49
|
+
STATES = STATES_RENAPO.to_set.freeze
|
|
50
|
+
WORDS = NAME_ISSUES.to_set.freeze
|
|
51
|
+
|
|
52
|
+
# Computes the RENAPO check digit (0-9) from the first 17 characters.
|
|
53
|
+
#
|
|
54
|
+
# Works on raw bytes: RENAPO's value table is contiguous in ASCII —
|
|
55
|
+
# '0'-'9' = 48-57 (=> 0-9), 'A'-'N' = 65-78 (=> 10-23), 'O'-'Z' =
|
|
56
|
+
# 79-90 (=> 25-36). The unused value 24 is Ñ's slot in the table;
|
|
57
|
+
# Ñ never appears in a CURP (RENAPO substitutes it), so we only need
|
|
58
|
+
# its offset (the -54 shift from 'O' on), not the character itself.
|
|
59
|
+
# Returns nil for input shorter than 17 bytes.
|
|
60
|
+
def self.check_digit(str)
|
|
61
|
+
return nil if str.bytesize < 17
|
|
62
|
+
|
|
63
|
+
sum = 0
|
|
64
|
+
17.times do |i|
|
|
65
|
+
b = str.getbyte(i)
|
|
66
|
+
value = b < 58 ? b - 48 : (b < 79 ? b - 55 : b - 54)
|
|
67
|
+
sum += value * (18 - i)
|
|
68
|
+
end
|
|
69
|
+
(10 - sum % 10) % 10
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.valid?(curp)
|
|
73
|
+
new(curp).valid?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize(curp)
|
|
77
|
+
@raw_input = curp.is_a?(String) ? curp.upcase : curp
|
|
78
|
+
@errors = {}
|
|
79
|
+
|
|
80
|
+
validate
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def valid?
|
|
84
|
+
@errors.empty?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate
|
|
88
|
+
unless @raw_input.is_a?(String) && FORMAT.match?(@raw_input)
|
|
89
|
+
add_error(:format, 'Invalid format')
|
|
90
|
+
return false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
validate_state
|
|
94
|
+
validate_name_initials
|
|
95
|
+
validate_birth_date
|
|
96
|
+
validate_date_exists
|
|
97
|
+
validate_check_digit
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def add_error(key, message)
|
|
103
|
+
(@errors[key] ||= []) << message
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def validate_state
|
|
107
|
+
state = @raw_input[11, 2]
|
|
108
|
+
return if STATES.include?(state)
|
|
109
|
+
|
|
110
|
+
add_error(:state, "Invalid state: '#{state}'")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def validate_name_initials
|
|
114
|
+
initials = @raw_input[0, 4]
|
|
115
|
+
return unless WORDS.include?(initials)
|
|
116
|
+
|
|
117
|
+
add_error(:problematic_name, "Problematic name initials: '#{initials}'")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate_birth_date
|
|
121
|
+
day = @raw_input[8, 2].to_i
|
|
122
|
+
add_error(:birth_day, "Invalid birth day: '#{@raw_input[8, 2]}'") if day <= 0 || day > 31
|
|
123
|
+
|
|
124
|
+
month = @raw_input[6, 2].to_i
|
|
125
|
+
add_error(:birth_month, "Invalid birth month: '#{@raw_input[6, 2]}'") if month <= 0 || month > 12
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def validate_date_exists
|
|
129
|
+
return if Date.valid_date?(birth_year, @raw_input[6, 2].to_i, @raw_input[8, 2].to_i)
|
|
130
|
+
|
|
131
|
+
add_error(:birth_date,
|
|
132
|
+
"Invalid birth date (YYYY-mm-dd): #{birth_year}-#{@raw_input[6, 2]}-#{@raw_input[8, 2]}")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_check_digit
|
|
136
|
+
expected = self.class.check_digit(@raw_input)
|
|
137
|
+
return if expected && @raw_input[17].to_i == expected
|
|
138
|
+
|
|
139
|
+
add_error(:check_digit,
|
|
140
|
+
"Invalid check digit: expected '#{expected}', got '#{@raw_input[17]}'")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Full 4-digit year, using the homoclave to pick the century: a digit
|
|
144
|
+
# at position 17 means <2000, a letter means >=2000. Keeps leap-year
|
|
145
|
+
# checks (e.g. Feb 29) correct. Letters sort after '9' in ASCII.
|
|
146
|
+
def birth_year
|
|
147
|
+
century = @raw_input[16] >= 'A' ? 2000 : 1900
|
|
148
|
+
century + @raw_input[4, 2].to_i
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/curp_mx.rb
CHANGED
|
@@ -1,124 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "curp_mx/version"
|
|
4
|
+
require_relative "curp_mx/validator"
|
|
4
5
|
|
|
5
6
|
module CurpMx
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# Used to validate a CURPs format and a few data points in it
|
|
9
|
-
class Validator
|
|
10
|
-
attr_reader :errors, :raw_input
|
|
11
|
-
|
|
12
|
-
# Basic CURP regex structure
|
|
13
|
-
REGEX = /\A(?<father_initial>[A-Z]{2})
|
|
14
|
-
(?<mother_initial>[A-Z]{1})
|
|
15
|
-
(?<name_initial>[A-Z]{1})
|
|
16
|
-
(?<birth_year>[0-9]{2})
|
|
17
|
-
(?<birth_month>[0-1][0-9])
|
|
18
|
-
(?<birth_day>[0-3][0-9])
|
|
19
|
-
(?<sex>[HM])
|
|
20
|
-
(?<state>[A-Z]{2})
|
|
21
|
-
(?<father_consonant>[^AEIOU])
|
|
22
|
-
(?<mother_consonant>[^AEIOU])
|
|
23
|
-
(?<name_consonant>[^AEIOU])
|
|
24
|
-
(?<key>[0-9]{2})\z/x.freeze
|
|
25
|
-
|
|
26
|
-
# States' initials as listed in
|
|
27
|
-
# Registro Nacional de Población (RENAPO)
|
|
28
|
-
STATES_RENAPO = %w[AS BC BS CC CS CH CL CM DF CX DG GT GR HG JC MC MN MS
|
|
29
|
-
NT NL OC PL QT QR SP SL SR TC TS TL VZ YN ZS].freeze
|
|
30
|
-
|
|
31
|
-
# Problematic name initials
|
|
32
|
-
NAME_ISSUES = %w[BACA LOCO BUEI BUEY MAME CACA MAMO
|
|
33
|
-
CAGA MEAS CAGO MEON CAKA MIAR CAKO MION COGE
|
|
34
|
-
MOCO COGI MOKO COJA MULA COJE MULO COJI NACA
|
|
35
|
-
COJO NACO COLA PEDA CULO PEDO FALO PENE FETO
|
|
36
|
-
PIPI GETA PITO GUEI POPO GUEY PUTA JETA PUTO
|
|
37
|
-
JOTO QULO KACA RATA KACO ROBA KAGA ROBE KAGO
|
|
38
|
-
ROBO KAKA RUIN KAKO SENO KOGE TETA KOGI VACA
|
|
39
|
-
KOJA VAGA KOJE VAGO KOJI VAKA KOJO VUEI KOLA
|
|
40
|
-
VUEY KULO WUEI LILO WUEY LOCA CACO MEAR].freeze
|
|
41
|
-
|
|
42
|
-
def self.valid?(curp)
|
|
43
|
-
new(curp).valid?
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def initialize(curp)
|
|
47
|
-
@raw_input = curp
|
|
48
|
-
@errors = {}
|
|
49
|
-
|
|
50
|
-
validate
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def valid?
|
|
54
|
-
@errors.empty?
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def validate
|
|
58
|
-
@md = REGEX.match(@raw_input)
|
|
59
|
-
|
|
60
|
-
if @md.nil?
|
|
61
|
-
@errors[:format] ||= []
|
|
62
|
-
@errors[:format] << 'Invalid format'
|
|
63
|
-
return false
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
validate_state
|
|
67
|
-
validate_name_initials
|
|
68
|
-
validate_birth_date
|
|
69
|
-
validate_date_exists
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def validate_state
|
|
75
|
-
return if STATES_RENAPO.include? @md[:state]
|
|
76
|
-
|
|
77
|
-
@errors[:state] << "Invalid state: '#{@md[:state]}'"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def validate_name_initials
|
|
81
|
-
return unless NAME_ISSUES.include?(name_initials)
|
|
82
|
-
|
|
83
|
-
@errors[:problematic_name] << "Problematic name initials: '#{name_initials}'"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def validate_birth_date
|
|
87
|
-
validate_birth_day
|
|
88
|
-
validate_birth_month
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def validate_birth_day
|
|
92
|
-
birth_day = @md[:birth_day].to_i
|
|
93
|
-
return unless birth_day <= 0 || birth_day > 31
|
|
94
|
-
|
|
95
|
-
@errors[:birth_day] ||= []
|
|
96
|
-
@errors[:birth_day] << "Invalid birth day: '#{@md[:birth_day]}'"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def validate_birth_month
|
|
100
|
-
birth_month = @md[:birth_month].to_i
|
|
101
|
-
return unless birth_month <= 0 || birth_month > 12
|
|
102
|
-
|
|
103
|
-
@errors[:birth_month] ||= []
|
|
104
|
-
@errors[:birth_month] << "Invalid birth month: '#{@md[:birth_month]}'"
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def validate_date_exists
|
|
108
|
-
date_str = "#{@md[:birth_year]}-#{@md[:birth_month]}-#{@md[:birth_day]}"
|
|
109
|
-
return if valid_date?(date_str)
|
|
110
|
-
|
|
111
|
-
@errors[:birth_date] ||= []
|
|
112
|
-
@errors[:birth_date] << "Invalid birth date (YYYY-mm-dd): #{date_str}"
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def name_initials
|
|
116
|
-
[@md[:father_initial], @md[:mother_initial], @md[:name_initial]].join
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def valid_date?(date_str)
|
|
120
|
-
# Inner works: Date.valid_date? 2020, 7, 21
|
|
121
|
-
Date.valid_date?(*date_str.split('-').map(&:to_i))
|
|
122
|
-
end
|
|
123
|
-
end
|
|
7
|
+
class Error < StandardError; end
|
|
124
8
|
end
|
data/sig/curp_mx.rbs
ADDED
metadata
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: CurpMX
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Salazar
|
|
8
|
-
|
|
9
|
-
bindir: bin
|
|
8
|
+
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
|
-
description:
|
|
14
|
-
email:
|
|
15
|
-
- contacto@capisalazar.com
|
|
12
|
+
description: Parse and validate CURP (Clave Única de Registro de Población) from Mexico.
|
|
16
13
|
executables: []
|
|
17
14
|
extensions: []
|
|
18
15
|
extra_rdoc_files: []
|
|
19
16
|
files:
|
|
17
|
+
- ".rspec"
|
|
18
|
+
- ".rubocop.yml"
|
|
19
|
+
- CHANGELOG.md
|
|
20
|
+
- LICENSE.txt
|
|
21
|
+
- README.md
|
|
22
|
+
- Rakefile
|
|
23
|
+
- curp_mx.gemspec
|
|
20
24
|
- lib/curp_mx.rb
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
- lib/curp_mx/validator.rb
|
|
26
|
+
- lib/curp_mx/version.rb
|
|
27
|
+
- sig/curp_mx.rbs
|
|
28
|
+
homepage: https://github.com/hslzr/curp_mx
|
|
29
|
+
licenses: []
|
|
30
|
+
metadata:
|
|
31
|
+
homepage_uri: https://github.com/hslzr/curp_mx
|
|
32
|
+
source_code_uri: https://github.com/hslzr/curp_mx
|
|
33
|
+
changelog_uri: https://github.com/hslzr/curp_mx/blob/master/CHANGELOG.md
|
|
26
34
|
rdoc_options: []
|
|
27
35
|
require_paths:
|
|
28
36
|
- lib
|
|
@@ -30,15 +38,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
30
38
|
requirements:
|
|
31
39
|
- - ">="
|
|
32
40
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
41
|
+
version: 3.0.0
|
|
34
42
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
35
43
|
requirements:
|
|
36
44
|
- - ">="
|
|
37
45
|
- !ruby/object:Gem::Version
|
|
38
46
|
version: '0'
|
|
39
47
|
requirements: []
|
|
40
|
-
rubygems_version:
|
|
41
|
-
signing_key:
|
|
48
|
+
rubygems_version: 4.0.3
|
|
42
49
|
specification_version: 4
|
|
43
|
-
summary:
|
|
50
|
+
summary: Parse and validate CURP (Clave Única de Registro de Población) from Mexico.
|
|
44
51
|
test_files: []
|