sat_rfc 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +92 -0
- data/Rakefile +8 -0
- data/lib/rfc/discardable_terms/natural_person.rb +35 -0
- data/lib/rfc/generator.rb +92 -0
- data/lib/rfc/homoclave_calculator.rb +87 -0
- data/lib/rfc/natural_ten_digits_code_calculator.rb +115 -0
- data/lib/rfc/verification_digit_calculator.rb +39 -0
- data/lib/rfc/version.rb +5 -0
- data/lib/rfc.rb +3 -0
- data/lib/sat_rfc.rb +18 -0
- data/sig/sat_rfc.rbs +3 -0
- metadata +70 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d546b4ca5245b4d86c98325bf2872a14268e945b8b51c0f6bee2c26e7dc9b1af
|
|
4
|
+
data.tar.gz: 6e129b4c04bfc6bb2ec397609bc408c8368094a49bc4a292fb561ff0f9bc9088
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 69a47948fcfa654cacfe91d7abd919704b44be4e3fa1690a078ca48a90dec715e5635b9781103b4e3f5e8944b891063361746567b9368d14ad2267e64411fcb9
|
|
7
|
+
data.tar.gz: 928596bf9a5c0fcfd3426b839e8db8254e90c76b892ed785159a688e3d721ccc0771308f057fd04ea06daacf76e52fbd8be5cdf04a9b65c667be073e695180d6
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RFC Contributors
|
|
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,92 @@
|
|
|
1
|
+
# sat_rfc
|
|
2
|
+
|
|
3
|
+
Ruby gem to generate Mexican **RFC** tax identifiers for **natural persons** (personas físicas), following the [SAT](https://www.sat.gob.mx/) algorithm.
|
|
4
|
+
|
|
5
|
+
Legal entity support (personas morales) is planned for a future release.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "sat_rfc"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install from the repository:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "sat_rfc", git: "https://github.com/elosnaya/rfc.git"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Local development install:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle exec rake install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "sat_rfc"
|
|
31
|
+
|
|
32
|
+
rfc = Rfc::Generator.new.for_natural_person(
|
|
33
|
+
name: "Juan Carlos",
|
|
34
|
+
first_last_name: "Perez",
|
|
35
|
+
second_last_name: "Garcia",
|
|
36
|
+
date_of_birth: "15/03/1990"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
puts rfc # => "PEGJ900315PE9"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
You can also pass the birth date as separate values:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Rfc::Generator.new.for_natural_person(
|
|
46
|
+
name: "Juan Carlos",
|
|
47
|
+
first_last_name: "Perez",
|
|
48
|
+
second_last_name: "Garcia",
|
|
49
|
+
day: 15,
|
|
50
|
+
month: 3,
|
|
51
|
+
year: 1990
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`date_of_birth` accepts `DD/MM/YYYY` format or any string parseable by Ruby's `Date`.
|
|
56
|
+
|
|
57
|
+
### Errors
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
begin
|
|
61
|
+
Rfc::Generator.new.for_natural_person(name: "Ana", first_last_name: "Lopez", second_last_name: "Garcia")
|
|
62
|
+
rescue Rfc::Generator::InvalidDateError => e
|
|
63
|
+
puts e.message # => "Date information missing"
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bin/setup # install dependencies
|
|
71
|
+
bundle exec rspec # run tests
|
|
72
|
+
bin/console # interactive console
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Example in the console:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
Rfc::Generator.new.for_natural_person(
|
|
79
|
+
name: "Juan Carlos",
|
|
80
|
+
first_last_name: "Perez",
|
|
81
|
+
second_last_name: "Garcia",
|
|
82
|
+
date_of_birth: "15/03/1990"
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Contributing
|
|
87
|
+
|
|
88
|
+
Bug reports and pull requests are welcome on GitHub at [elosnaya/rfc](https://github.com/elosnaya/rfc).
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rfc
|
|
4
|
+
module DiscardableTerms
|
|
5
|
+
# Dictionary of discardable terms and forbidden words for natural persons (personas físicas).
|
|
6
|
+
# Based on SAT specification.
|
|
7
|
+
module NaturalPerson
|
|
8
|
+
ARTICLES_AND_PREPOSITIONS = %w[
|
|
9
|
+
DE LA LAS DEL LOS Y
|
|
10
|
+
EN CON SUS PARA POR AL E
|
|
11
|
+
OF THE AND
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
COMPANY_TYPE_ABBREVIATIONS = %w[
|
|
15
|
+
SA SC SRL SCS CIA COOP SOC CO COMPANY
|
|
16
|
+
CV RL S A C V
|
|
17
|
+
COMPAÑIA
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
DISCARDABLE_TERMS = %w[MC VON MAC VAN MI].freeze
|
|
21
|
+
|
|
22
|
+
ALL = (
|
|
23
|
+
ARTICLES_AND_PREPOSITIONS +
|
|
24
|
+
COMPANY_TYPE_ABBREVIATIONS +
|
|
25
|
+
DISCARDABLE_TERMS
|
|
26
|
+
).freeze
|
|
27
|
+
|
|
28
|
+
FORBIDDEN_WORDS = %w[
|
|
29
|
+
BUEI BUEY CACA CACO CAGA KOGE KAKA MAME KOJO KULO
|
|
30
|
+
CAGO COGE COJE COJO FETO JOTO KACO KAGO MAMO MEAR MEON
|
|
31
|
+
MION MOCO MULA PEDA PEDO PENE PUTA PUTO QULO RATA RUIN
|
|
32
|
+
].freeze
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Rfc
|
|
6
|
+
class Generator
|
|
7
|
+
class InvalidDateError < Error; end
|
|
8
|
+
|
|
9
|
+
def initialize(
|
|
10
|
+
natural_ten_digits_calculator: NaturalTenDigitsCodeCalculator,
|
|
11
|
+
homoclave_calculator: HomoclaveCalculator,
|
|
12
|
+
verification_digit_calculator: VerificationDigitCalculator
|
|
13
|
+
)
|
|
14
|
+
@natural_ten_digits_calculator = natural_ten_digits_calculator
|
|
15
|
+
@homoclave_calculator = homoclave_calculator
|
|
16
|
+
@verification_digit_calculator = verification_digit_calculator
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def for_natural_person(
|
|
20
|
+
name:,
|
|
21
|
+
first_last_name:,
|
|
22
|
+
second_last_name:,
|
|
23
|
+
day: nil,
|
|
24
|
+
month: nil,
|
|
25
|
+
year: nil,
|
|
26
|
+
date_of_birth: nil
|
|
27
|
+
)
|
|
28
|
+
normalized_day, normalized_month, normalized_year = normalize_date(day, month, year, date_of_birth)
|
|
29
|
+
|
|
30
|
+
ten_digits_code = @natural_ten_digits_calculator.new(
|
|
31
|
+
name: name,
|
|
32
|
+
first_last_name: first_last_name,
|
|
33
|
+
second_last_name: second_last_name,
|
|
34
|
+
day: normalized_day,
|
|
35
|
+
month: normalized_month,
|
|
36
|
+
year: normalized_year
|
|
37
|
+
).calculate
|
|
38
|
+
|
|
39
|
+
homoclave = @homoclave_calculator.new(
|
|
40
|
+
name: name,
|
|
41
|
+
first_last_name: first_last_name,
|
|
42
|
+
second_last_name: second_last_name
|
|
43
|
+
).calculate
|
|
44
|
+
|
|
45
|
+
build_rfc(ten_digits_code, homoclave)
|
|
46
|
+
rescue ArgumentError => e
|
|
47
|
+
raise InvalidDateError, "Invalid date parameters: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def build_rfc(ten_digits_code, homoclave)
|
|
53
|
+
rfc12_digits = "#{ten_digits_code.gsub("-", "")}#{homoclave}"
|
|
54
|
+
verification_digit = @verification_digit_calculator.new(rfc12_digits).calculate
|
|
55
|
+
|
|
56
|
+
"#{ten_digits_code}#{homoclave}#{verification_digit}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def normalize_date(day, month, year, date_string)
|
|
60
|
+
if !day.nil? && !month.nil? && !year.nil?
|
|
61
|
+
[day, month, year]
|
|
62
|
+
elsif present?(date_string)
|
|
63
|
+
parse_date_string(date_string)
|
|
64
|
+
else
|
|
65
|
+
raise InvalidDateError, "Date information missing"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_date_string(date_string)
|
|
70
|
+
parsed_date = try_parse_date(date_string)
|
|
71
|
+
raise InvalidDateError, "Invalid date format: #{date_string}" unless parsed_date
|
|
72
|
+
|
|
73
|
+
[parsed_date.day, parsed_date.month, parsed_date.year]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def try_parse_date(date_string)
|
|
77
|
+
return nil unless present?(date_string)
|
|
78
|
+
|
|
79
|
+
Date.strptime(date_string, "%d/%m/%Y")
|
|
80
|
+
rescue ArgumentError
|
|
81
|
+
begin
|
|
82
|
+
Date.parse(date_string.to_s)
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def present?(value)
|
|
89
|
+
!value.nil? && !value.to_s.strip.empty?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "i18n"
|
|
4
|
+
|
|
5
|
+
module Rfc
|
|
6
|
+
class HomoclaveCalculator
|
|
7
|
+
HOMOCLAVE_DIGITS = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".freeze
|
|
8
|
+
FULL_NAME_MAPPING = {
|
|
9
|
+
" " => "00", "0" => "00", "1" => "01", "2" => "02", "3" => "03", "4" => "04",
|
|
10
|
+
"5" => "05", "6" => "06", "7" => "07", "8" => "08", "9" => "09", "&" => "10",
|
|
11
|
+
"A" => "11", "B" => "12", "C" => "13", "D" => "14", "E" => "15", "F" => "16",
|
|
12
|
+
"G" => "17", "H" => "18", "I" => "19", "J" => "21", "K" => "22", "L" => "23",
|
|
13
|
+
"M" => "24", "N" => "25", "O" => "26", "P" => "27", "Q" => "28", "R" => "29",
|
|
14
|
+
"S" => "32", "T" => "33", "U" => "34", "V" => "35", "W" => "36", "X" => "37",
|
|
15
|
+
"Y" => "38", "Z" => "39", "Ñ" => "40"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(name:, first_last_name:, second_last_name:)
|
|
19
|
+
@name = name
|
|
20
|
+
@first_last_name = first_last_name
|
|
21
|
+
@second_last_name = second_last_name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def calculate
|
|
25
|
+
normalize_full_name
|
|
26
|
+
map_full_name_to_digits_code
|
|
27
|
+
sum_pairs_of_digits
|
|
28
|
+
build_homoclave
|
|
29
|
+
|
|
30
|
+
@homoclave
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_homoclave
|
|
36
|
+
last_three_digits = (@pairs_of_digits_sum % 1000)
|
|
37
|
+
quo = (last_three_digits / 34)
|
|
38
|
+
reminder = (last_three_digits % 34)
|
|
39
|
+
@homoclave = "#{HOMOCLAVE_DIGITS[quo]}#{HOMOCLAVE_DIGITS[reminder]}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def sum_pairs_of_digits
|
|
43
|
+
@pairs_of_digits_sum = 0
|
|
44
|
+
(0..@mapped_full_name.length - 2).each do |i|
|
|
45
|
+
num1 = @mapped_full_name[i..i + 1].to_i
|
|
46
|
+
num2 = @mapped_full_name[i + 1..i + 1].to_i
|
|
47
|
+
|
|
48
|
+
@pairs_of_digits_sum += num1 * num2
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def map_full_name_to_digits_code
|
|
53
|
+
@mapped_full_name = +"0"
|
|
54
|
+
@full_name.each_char do |character|
|
|
55
|
+
@mapped_full_name << map_character_to_two_digit_code(character)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def map_character_to_two_digit_code(character)
|
|
60
|
+
return FULL_NAME_MAPPING[character] if FULL_NAME_MAPPING.key?(character)
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "No two-digit-code mapping for char: #{character}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def normalize_full_name
|
|
66
|
+
raw_full_name = full_name_string.upcase
|
|
67
|
+
@full_name = I18n.transliterate(raw_full_name).dup
|
|
68
|
+
@full_name.gsub!(/[-.']/, "")
|
|
69
|
+
@full_name.gsub!(/[^A-Z0-9\s]/, " ")
|
|
70
|
+
@full_name.gsub!(/\s+/, " ")
|
|
71
|
+
@full_name.strip!
|
|
72
|
+
add_missing_char_to_full_name(raw_full_name, "Ñ")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def full_name_string
|
|
76
|
+
"#{@first_last_name} #{@second_last_name} #{@name}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_missing_char_to_full_name(raw_full_name, missing_char)
|
|
80
|
+
index = raw_full_name.index(missing_char)
|
|
81
|
+
until index.nil?
|
|
82
|
+
@full_name[index] = missing_char
|
|
83
|
+
index = raw_full_name.index(missing_char, index + 1)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rfc
|
|
4
|
+
class NaturalTenDigitsCodeCalculator
|
|
5
|
+
VOWEL_PATTERN = /[AEIOU]+/
|
|
6
|
+
DISCARDABLE_TERMS = DiscardableTerms::NaturalPerson::ALL
|
|
7
|
+
FORBIDDEN_WORDS = DiscardableTerms::NaturalPerson::FORBIDDEN_WORDS
|
|
8
|
+
|
|
9
|
+
def initialize(name:, first_last_name:, second_last_name:, day:, month:, year:)
|
|
10
|
+
@name = name
|
|
11
|
+
@first_last_name = first_last_name
|
|
12
|
+
@second_last_name = second_last_name
|
|
13
|
+
@day = day
|
|
14
|
+
@month = month
|
|
15
|
+
@year = year
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def calculate
|
|
19
|
+
obfuscate_forbidden_words(name_code) + birthday_code
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def obfuscate_forbidden_words(name_code)
|
|
25
|
+
FORBIDDEN_WORDS.each do |forbidden|
|
|
26
|
+
return "#{name_code[0..2]}X" if forbidden == name_code
|
|
27
|
+
end
|
|
28
|
+
name_code
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def name_code
|
|
32
|
+
return first_last_name_empty_form if first_last_name_empty?
|
|
33
|
+
return second_last_name_empty_form if second_last_name_empty?
|
|
34
|
+
return first_last_name_too_short_form if first_last_name_is_too_short?
|
|
35
|
+
|
|
36
|
+
normal_form
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def second_last_name_empty_form
|
|
40
|
+
first_two_letters_of(@first_last_name) + first_two_letters_of(filter_name(@name))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def birthday_code
|
|
44
|
+
"#{@year.to_s[-2, 2]}#{format('%02d', @month)}#{format('%02d', @day)}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def second_last_name_empty?
|
|
48
|
+
normalized = normalize(@second_last_name)
|
|
49
|
+
normalized.nil? || normalized.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def first_last_name_empty_form
|
|
53
|
+
first_two_letters_of(@second_last_name) + first_two_letters_of(filter_name(@name))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def first_last_name_empty?
|
|
57
|
+
normalized = normalize(@first_last_name)
|
|
58
|
+
normalized.nil? || normalized.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def first_last_name_too_short_form
|
|
62
|
+
first_letter_of(@first_last_name) +
|
|
63
|
+
first_letter_of(@second_last_name) +
|
|
64
|
+
first_two_letters_of(filter_name(@name))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def first_two_letters_of(word)
|
|
68
|
+
normalized_word = normalize(word)
|
|
69
|
+
normalized_word[0..1]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def first_last_name_is_too_short?
|
|
73
|
+
normalize(@first_last_name).length <= 2
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def normal_form
|
|
77
|
+
first_letter_of(@first_last_name) +
|
|
78
|
+
first_vowel_excluding_first_character_of(@first_last_name) +
|
|
79
|
+
first_letter_of(@second_last_name) +
|
|
80
|
+
first_letter_of(filter_name(@name))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def filter_name(name)
|
|
84
|
+
normalize(name).strip.sub(/^(MA|MA\.|MARIA|JOSE)\s+/, "")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def first_letter_of(word)
|
|
88
|
+
normalized_word = normalize(word)
|
|
89
|
+
normalized_word[0]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def normalize(word)
|
|
93
|
+
return word if word.nil? || word.empty?
|
|
94
|
+
|
|
95
|
+
normalized_word = I18n.transliterate(word).upcase
|
|
96
|
+
remove_discardable_terms(normalized_word, DISCARDABLE_TERMS)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def remove_discardable_terms(word, discardable_terms)
|
|
100
|
+
new_word = word.dup
|
|
101
|
+
discardable_terms.each do |term|
|
|
102
|
+
new_word.gsub!("#{term} ", "")
|
|
103
|
+
end
|
|
104
|
+
new_word
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def first_vowel_excluding_first_character_of(word)
|
|
108
|
+
normalized_word = normalize(word)[1..]
|
|
109
|
+
match = VOWEL_PATTERN.match(normalized_word)
|
|
110
|
+
raise ArgumentError, "Word doesn't contain a vowel: #{normalized_word}" if match.nil?
|
|
111
|
+
|
|
112
|
+
match.to_s[0]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rfc
|
|
4
|
+
class VerificationDigitCalculator
|
|
5
|
+
MAPPING = {
|
|
6
|
+
"0" => 0, "1" => 1, "2" => 2, "3" => 3, "4" => 4, "5" => 5, "6" => 6,
|
|
7
|
+
"7" => 7, "8" => 8, "9" => 9, "A" => 10, "B" => 11, "C" => 12, "D" => 13,
|
|
8
|
+
"E" => 14, "F" => 15, "G" => 16, "H" => 17, "I" => 18, "J" => 19, "K" => 20,
|
|
9
|
+
"L" => 21, "M" => 22, "N" => 23, "&" => 24, "O" => 25, "P" => 26, "Q" => 27,
|
|
10
|
+
"R" => 28, "S" => 29, "T" => 30, "U" => 31, "V" => 32, "W" => 33, "X" => 34,
|
|
11
|
+
"Y" => 35, "Z" => 36, " " => 37, "Ñ" => 38
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(rfc12_digits)
|
|
15
|
+
@rfc12_digits = rfc12_digits
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def calculate
|
|
19
|
+
rfc12_padded = @rfc12_digits.ljust(12, " ")
|
|
20
|
+
sum = 0
|
|
21
|
+
(0..11).each do |i|
|
|
22
|
+
sum += map_digit(rfc12_padded[i]) * (13 - i)
|
|
23
|
+
end
|
|
24
|
+
reminder = sum % 11
|
|
25
|
+
|
|
26
|
+
return "0" if reminder.zero?
|
|
27
|
+
|
|
28
|
+
(11 - reminder).to_s(16).upcase
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def map_digit(character)
|
|
34
|
+
return MAPPING[character] if MAPPING.key?(character)
|
|
35
|
+
|
|
36
|
+
0
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/rfc/version.rb
ADDED
data/lib/rfc.rb
ADDED
data/lib/sat_rfc.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "i18n"
|
|
4
|
+
|
|
5
|
+
I18n.available_locales = [:en]
|
|
6
|
+
I18n.default_locale = :en
|
|
7
|
+
|
|
8
|
+
require_relative "rfc/version"
|
|
9
|
+
require_relative "rfc/discardable_terms/natural_person"
|
|
10
|
+
require_relative "rfc/verification_digit_calculator"
|
|
11
|
+
require_relative "rfc/homoclave_calculator"
|
|
12
|
+
require_relative "rfc/natural_ten_digits_code_calculator"
|
|
13
|
+
|
|
14
|
+
module Rfc
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require_relative "rfc/generator"
|
data/sig/sat_rfc.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sat_rfc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- elOsnaya
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: i18n
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
description: Ruby library that generates RFC tax identifiers for Mexican natural persons
|
|
27
|
+
using the SAT algorithm. Supports name normalization, homoclave, and verification
|
|
28
|
+
digit calculation.
|
|
29
|
+
email:
|
|
30
|
+
- luisosnet@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- lib/rfc.rb
|
|
39
|
+
- lib/rfc/discardable_terms/natural_person.rb
|
|
40
|
+
- lib/rfc/generator.rb
|
|
41
|
+
- lib/rfc/homoclave_calculator.rb
|
|
42
|
+
- lib/rfc/natural_ten_digits_code_calculator.rb
|
|
43
|
+
- lib/rfc/verification_digit_calculator.rb
|
|
44
|
+
- lib/rfc/version.rb
|
|
45
|
+
- lib/sat_rfc.rb
|
|
46
|
+
- sig/sat_rfc.rbs
|
|
47
|
+
homepage: https://github.com/elosnaya/rfc
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
homepage_uri: https://github.com/elosnaya/rfc
|
|
52
|
+
source_code_uri: https://github.com/elosnaya/rfc
|
|
53
|
+
rdoc_options: []
|
|
54
|
+
require_paths:
|
|
55
|
+
- lib
|
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 3.2.0
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
requirements: []
|
|
67
|
+
rubygems_version: 4.0.3
|
|
68
|
+
specification_version: 4
|
|
69
|
+
summary: Generate Mexican RFC codes for natural persons (personas físicas).
|
|
70
|
+
test_files: []
|