brazil-validator 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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +77 -0
  3. data/lib/br_validator/cep.rb +44 -0
  4. data/lib/br_validator/cnpj.rb +78 -0
  5. data/lib/br_validator/cpf.rb +63 -0
  6. data/lib/br_validator/email.rb +48 -0
  7. data/lib/br_validator/ie/ac.rb +48 -0
  8. data/lib/br_validator/ie/al.rb +46 -0
  9. data/lib/br_validator/ie/am.rb +52 -0
  10. data/lib/br_validator/ie/ap.rb +64 -0
  11. data/lib/br_validator/ie/ba.rb +76 -0
  12. data/lib/br_validator/ie/ce.rb +42 -0
  13. data/lib/br_validator/ie/df.rb +58 -0
  14. data/lib/br_validator/ie/es.rb +39 -0
  15. data/lib/br_validator/ie/go.rb +49 -0
  16. data/lib/br_validator/ie/ma.rb +41 -0
  17. data/lib/br_validator/ie/mg.rb +93 -0
  18. data/lib/br_validator/ie/mod11.rb +48 -0
  19. data/lib/br_validator/ie/ms.rb +45 -0
  20. data/lib/br_validator/ie/mt.rb +42 -0
  21. data/lib/br_validator/ie/pa.rb +44 -0
  22. data/lib/br_validator/ie/pb.rb +42 -0
  23. data/lib/br_validator/ie/pe.rb +52 -0
  24. data/lib/br_validator/ie/pi.rb +38 -0
  25. data/lib/br_validator/ie/pr.rb +53 -0
  26. data/lib/br_validator/ie/rj.rb +53 -0
  27. data/lib/br_validator/ie/rn.rb +58 -0
  28. data/lib/br_validator/ie/ro.rb +58 -0
  29. data/lib/br_validator/ie/rr.rb +47 -0
  30. data/lib/br_validator/ie/rs.rb +42 -0
  31. data/lib/br_validator/ie/sc.rb +42 -0
  32. data/lib/br_validator/ie/se.rb +42 -0
  33. data/lib/br_validator/ie/sp.rb +121 -0
  34. data/lib/br_validator/ie/to.rb +49 -0
  35. data/lib/br_validator/ie.rb +112 -0
  36. data/lib/br_validator/phone.rb +59 -0
  37. data/lib/br_validator/pix.rb +134 -0
  38. data/lib/br_validator/shared.rb +31 -0
  39. data/lib/br_validator/version.rb +5 -0
  40. data/lib/br_validator.rb +13 -0
  41. metadata +84 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # RJ: verified against the official SEFAZ-RJ "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_RJ.html) for the
10
+ # check-digit rule, and cross-checked against a worked example
11
+ # (99.999.99-3) for the weights, which the Sintegra page itself does
12
+ # not enumerate.
13
+ module Rj
14
+ module_function
15
+
16
+ ALLOWED_CHARS = /\A[\d.\-\s]+\z/.freeze
17
+ WEIGHTS = [2, 7, 6, 5, 4, 3, 2].freeze
18
+ private_constant :ALLOWED_CHARS, :WEIGHTS
19
+
20
+ def normalize(value)
21
+ Shared.remove_non_digits(value)
22
+ end
23
+
24
+ def calculate_check_digit(v)
25
+ sum = Mod11.weighted_sum(v, WEIGHTS)
26
+ remainder = sum % 11
27
+
28
+ return 0 if remainder <= 1
29
+
30
+ 11 - remainder
31
+ end
32
+ private_class_method :calculate_check_digit
33
+
34
+ def is_valid?(value)
35
+ return false unless value.match?(ALLOWED_CHARS)
36
+
37
+ v = normalize(value)
38
+
39
+ return false unless v.length == 8
40
+
41
+ calculate_check_digit(v) == v[7].to_i
42
+ end
43
+
44
+ def format(value)
45
+ v = normalize(value)
46
+
47
+ return value unless v.length == 8
48
+
49
+ "#{v[0...2]}.#{v[2...5]}.#{v[5...7]}-#{v[7...8]}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # RN: verified against the official SEFAZ-RN "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_RN.html),
10
+ # including both of its worked examples (20.040.040-1 and
11
+ # 20.0.040.040-0). Format: always starts with "20", followed by either
12
+ # 7 or 8 more digits, plus 1 check digit (9 or 10 digits total, both
13
+ # still valid today per the official page).
14
+ module Rn
15
+ module_function
16
+
17
+ ALLOWED_CHARS = /\A[\d.\-\s]+\z/.freeze
18
+ private_constant :ALLOWED_CHARS
19
+
20
+ def normalize(value)
21
+ Shared.remove_non_digits(value)
22
+ end
23
+
24
+ def weights_for(base_length)
25
+ (0...base_length).map { |i| base_length + 1 - i }
26
+ end
27
+ private_class_method :weights_for
28
+
29
+ def is_valid?(value)
30
+ return false unless value.match?(ALLOWED_CHARS)
31
+
32
+ v = normalize(value)
33
+
34
+ return false unless [9, 10].include?(v.length) && v.start_with?("20")
35
+
36
+ base = v[0...-1]
37
+ check_digit = Mod11.mod11_times_ten_check_digit(
38
+ Mod11.weighted_sum(base, weights_for(base.length))
39
+ )
40
+
41
+ check_digit == v[-1].to_i
42
+ end
43
+
44
+ def format(value)
45
+ v = normalize(value)
46
+
47
+ case v.length
48
+ when 9
49
+ "#{v[0...2]}.#{v[2...5]}.#{v[5...8]}-#{v[8...9]}"
50
+ when 10
51
+ "#{v[0...2]}.#{v[2...3]}.#{v[3...6]}.#{v[6...9]}-#{v[9...10]}"
52
+ else
53
+ value
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # RO: verified against the official SEFAZ-RO "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_RO.html),
10
+ # including its worked example (0000000062521-3). Since 01/08/2000 the
11
+ # format is 13 digits + 1 check digit (the old "município + empresa"
12
+ # layout is superseded, with old registrations re-expressed by
13
+ # zero-padding into the new 13-digit field).
14
+ #
15
+ # The weights cycle 2-9 applied right to left over the 13 digits, i.e.
16
+ # [6,5,4,3,2,9,8,7,6,5,4,3,2] read left to right. Unlike most other
17
+ # states, a zero remainder maps to check digit 1, not 0 (the source
18
+ # explicitly says "subtract 10" from the 11-or-10 result, rather than
19
+ # mapping straight to 0).
20
+ module Ro
21
+ module_function
22
+
23
+ ALLOWED_CHARS = /\A[\d.\-\s]+\z/.freeze
24
+ WEIGHTS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
25
+ private_constant :ALLOWED_CHARS, :WEIGHTS
26
+
27
+ def normalize(value)
28
+ Shared.remove_non_digits(value)
29
+ end
30
+
31
+ def calculate_check_digit(base)
32
+ remainder = Mod11.weighted_sum(base, WEIGHTS) % 11
33
+ diff = 11 - remainder
34
+
35
+ diff > 9 ? diff - 10 : diff
36
+ end
37
+ private_class_method :calculate_check_digit
38
+
39
+ def is_valid?(value)
40
+ return false unless value.match?(ALLOWED_CHARS)
41
+
42
+ v = normalize(value)
43
+
44
+ return false unless v.length == 14
45
+
46
+ calculate_check_digit(v[0...13]) == v[13].to_i
47
+ end
48
+
49
+ def format(value)
50
+ v = normalize(value)
51
+
52
+ return value unless v.length == 14
53
+
54
+ "#{v[0...13]}-#{v[13...14]}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # RR: verified against the official SEFAZ-RR "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_RR.html),
10
+ # including its worked example (24006153-6) and the ten additional
11
+ # valid numbers it lists. Format: "24" (fixed) + 6 sequence digits + 1
12
+ # check digit = 9 digits total. Unlike every other state, the check
13
+ # digit uses módulo 9, and the weights are the digit's own 1-based
14
+ # position (ascending, not descending).
15
+ module Rr
16
+ module_function
17
+
18
+ ALLOWED_CHARS = /\A[\d\-\s]+\z/.freeze
19
+ WEIGHTS = [1, 2, 3, 4, 5, 6, 7, 8].freeze
20
+ private_constant :ALLOWED_CHARS, :WEIGHTS
21
+
22
+ def normalize(value)
23
+ Shared.remove_non_digits(value)
24
+ end
25
+
26
+ def is_valid?(value)
27
+ return false unless value.match?(ALLOWED_CHARS)
28
+
29
+ v = normalize(value)
30
+
31
+ return false unless v.length == 9 && v.start_with?("24")
32
+
33
+ check_digit = Mod11.weighted_sum(v, WEIGHTS) % 9
34
+
35
+ check_digit == v[8].to_i
36
+ end
37
+
38
+ def format(value)
39
+ v = normalize(value)
40
+
41
+ return value unless v.length == 9
42
+
43
+ "#{v[0...8]}-#{v[8...9]}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # RS: verified against the official SEFAZ-RS "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_RS.html),
10
+ # including its worked example (224/3658792). Format: 3 digits
11
+ # (município) + 6 digits (empresa) + 1 check digit = 10 digits total.
12
+ module Rs
13
+ module_function
14
+
15
+ ALLOWED_CHARS = %r{\A[\d/\s]+\z}.freeze
16
+ WEIGHTS = [2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
17
+ private_constant :ALLOWED_CHARS, :WEIGHTS
18
+
19
+ def normalize(value)
20
+ Shared.remove_non_digits(value)
21
+ end
22
+
23
+ def is_valid?(value)
24
+ return false unless value.match?(ALLOWED_CHARS)
25
+
26
+ v = normalize(value)
27
+
28
+ return false unless v.length == 10
29
+
30
+ Mod11.mod11_check_digit(Mod11.weighted_sum(v, WEIGHTS)) == v[9].to_i
31
+ end
32
+
33
+ def format(value)
34
+ v = normalize(value)
35
+
36
+ return value unless v.length == 10
37
+
38
+ "#{v[0...3]}/#{v[3...10]}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # SC: verified against the official SEFAZ-SC "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_SC.html),
10
+ # including its worked example (251.040.852). Format: 8 digits + 1
11
+ # check digit.
12
+ module Sc
13
+ module_function
14
+
15
+ ALLOWED_CHARS = /\A[\d.\s]+\z/.freeze
16
+ WEIGHTS = [9, 8, 7, 6, 5, 4, 3, 2].freeze
17
+ private_constant :ALLOWED_CHARS, :WEIGHTS
18
+
19
+ def normalize(value)
20
+ Shared.remove_non_digits(value)
21
+ end
22
+
23
+ def is_valid?(value)
24
+ return false unless value.match?(ALLOWED_CHARS)
25
+
26
+ v = normalize(value)
27
+
28
+ return false unless v.length == 9
29
+
30
+ Mod11.mod11_check_digit(Mod11.weighted_sum(v, WEIGHTS)) == v[8].to_i
31
+ end
32
+
33
+ def format(value)
34
+ v = normalize(value)
35
+
36
+ return value unless v.length == 9
37
+
38
+ "#{v[0...3]}.#{v[3...6]}.#{v[6...9]}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # SE: verified against the official SEFAZ-SE "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_SE.html),
10
+ # including its worked example (27123456-3). Format: 8 digits + 1
11
+ # check digit.
12
+ module Se
13
+ module_function
14
+
15
+ ALLOWED_CHARS = /\A[\d\-\s]+\z/.freeze
16
+ WEIGHTS = [9, 8, 7, 6, 5, 4, 3, 2].freeze
17
+ private_constant :ALLOWED_CHARS, :WEIGHTS
18
+
19
+ def normalize(value)
20
+ Shared.remove_non_digits(value)
21
+ end
22
+
23
+ def is_valid?(value)
24
+ return false unless value.match?(ALLOWED_CHARS)
25
+
26
+ v = normalize(value)
27
+
28
+ return false unless v.length == 9
29
+
30
+ Mod11.mod11_check_digit(Mod11.weighted_sum(v, WEIGHTS)) == v[8].to_i
31
+ end
32
+
33
+ def format(value)
34
+ v = normalize(value)
35
+
36
+ return value unless v.length == 9
37
+
38
+ "#{v[0...8]}-#{v[8...9]}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # SP: verified against the official SEFAZ-SP "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_SP.html),
10
+ # including both of its worked examples: the standard
11
+ # industrial/commercial format (110.042.490.114) and the "Produtor
12
+ # Rural" format (P-01100424.3/002).
13
+ #
14
+ # Note: the source restates the Produtor Rural example at the very end
15
+ # as "P-011000424.3/002" (14 characters) -- this contradicts both the
16
+ # document's own "13 caracteres" rule and its worked calculation
17
+ # (which sums exactly 8 digits to 91, matching the 13-character form).
18
+ # Treated as a typo in the source; the 13-character form is what this
19
+ # module expects.
20
+ module Sp
21
+ module_function
22
+
23
+ STANDARD_ALLOWED_CHARS = /\A[\d.\s]+\z/.freeze
24
+ PRODUTOR_RURAL_ALLOWED = %r{\A[Pp\d.\-/\s]+\z}.freeze
25
+ FORMATTING_CHARS = %r{[.\-/\s]}.freeze
26
+ PRODUTOR_RURAL_PREFIX = /\A\s*[Pp]/.freeze
27
+
28
+ STANDARD_FIRST_DIGIT_WEIGHTS = [1, 3, 4, 5, 6, 7, 8, 10].freeze
29
+ STANDARD_SECOND_DIGIT_WEIGHTS = [3, 2, 10, 9, 8, 7, 6, 5, 4, 3, 2].freeze
30
+ PRODUTOR_RURAL_WEIGHTS = [1, 3, 4, 5, 6, 7, 8, 10].freeze
31
+ private_constant :STANDARD_ALLOWED_CHARS, :PRODUTOR_RURAL_ALLOWED, :FORMATTING_CHARS,
32
+ :PRODUTOR_RURAL_PREFIX, :STANDARD_FIRST_DIGIT_WEIGHTS,
33
+ :STANDARD_SECOND_DIGIT_WEIGHTS, :PRODUTOR_RURAL_WEIGHTS
34
+
35
+ def calculate_check_digit(digits, weights)
36
+ sum = Mod11.weighted_sum(digits, weights)
37
+
38
+ sum % 11 == 10 ? 0 : sum % 11
39
+ end
40
+ private_class_method :calculate_check_digit
41
+
42
+ def produtor_rural?(value)
43
+ value.match?(PRODUTOR_RURAL_PREFIX)
44
+ end
45
+ private_class_method :produtor_rural?
46
+
47
+ def normalize_standard(value)
48
+ Shared.remove_non_digits(value)
49
+ end
50
+ private_class_method :normalize_standard
51
+
52
+ def valid_standard?(value)
53
+ return false unless value.match?(STANDARD_ALLOWED_CHARS)
54
+
55
+ v = normalize_standard(value)
56
+
57
+ return false unless v.length == 12
58
+
59
+ first_digit = calculate_check_digit(v, STANDARD_FIRST_DIGIT_WEIGHTS)
60
+ return false unless first_digit == v[8].to_i
61
+
62
+ second_digit = calculate_check_digit(v, STANDARD_SECOND_DIGIT_WEIGHTS)
63
+
64
+ second_digit == v[11].to_i
65
+ end
66
+ private_class_method :valid_standard?
67
+
68
+ def format_standard(value)
69
+ v = normalize_standard(value)
70
+
71
+ return value unless v.length == 12
72
+
73
+ "#{v[0...3]}.#{v[3...6]}.#{v[6...9]}.#{v[9...12]}"
74
+ end
75
+ private_class_method :format_standard
76
+
77
+ # Format: P0MMMSSSSD000 (13 characters) -- "P" (fixed) + "0" (fixed)
78
+ # + 3 município digits + 4 sequence digits + 1 check digit + 3
79
+ # unused digits.
80
+ def normalize_produtor_rural(value)
81
+ value.gsub(FORMATTING_CHARS, "").upcase
82
+ end
83
+ private_class_method :normalize_produtor_rural
84
+
85
+ def valid_produtor_rural?(value)
86
+ return false unless value.match?(PRODUTOR_RURAL_ALLOWED)
87
+
88
+ v = normalize_produtor_rural(value)
89
+
90
+ return false unless v.length == 13 && v[0] == "P" && v[1] == "0"
91
+
92
+ base = v[1...9]
93
+ check_digit = calculate_check_digit(base, PRODUTOR_RURAL_WEIGHTS)
94
+
95
+ check_digit == v[9].to_i
96
+ end
97
+ private_class_method :valid_produtor_rural?
98
+
99
+ def format_produtor_rural(value)
100
+ v = normalize_produtor_rural(value)
101
+
102
+ return value unless v.length == 13
103
+
104
+ "P-#{v[1...9]}.#{v[9...10]}/#{v[10...13]}"
105
+ end
106
+ private_class_method :format_produtor_rural
107
+
108
+ def is_valid?(value)
109
+ produtor_rural?(value) ? valid_produtor_rural?(value) : valid_standard?(value)
110
+ end
111
+
112
+ def normalize(value)
113
+ produtor_rural?(value) ? normalize_produtor_rural(value) : normalize_standard(value)
114
+ end
115
+
116
+ def format(value)
117
+ produtor_rural?(value) ? format_produtor_rural(value) : format_standard(value)
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # TO: verified against the official SEFAZ-TO "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_TO.html),
10
+ # including its worked example (29010227836). Format: 11 digits, where
11
+ # positions 3-4 hold a fixed "tipo" code (01 Produtor Rural, 02
12
+ # Indústria e Comércio, 03 Empresas Rudimentares, 99 Cadastro Antigo)
13
+ # that is excluded from the check-digit calculation, and position 11
14
+ # is the check digit.
15
+ module To
16
+ module_function
17
+
18
+ ALLOWED_CHARS = /\A[\d\s]+\z/.freeze
19
+ WEIGHTS = [9, 8, 7, 6, 5, 4, 3, 2].freeze
20
+ VALID_TYPE_CODES = %w[01 02 03 99].freeze
21
+ # 1-based positions used in the check-digit calculation (positions
22
+ # 3-4 are skipped).
23
+ DIGIT_POSITIONS = [1, 2, 5, 6, 7, 8, 9, 10].freeze
24
+ private_constant :ALLOWED_CHARS, :WEIGHTS, :VALID_TYPE_CODES, :DIGIT_POSITIONS
25
+
26
+ def normalize(value)
27
+ Shared.remove_non_digits(value)
28
+ end
29
+
30
+ def is_valid?(value)
31
+ return false unless value.match?(ALLOWED_CHARS)
32
+
33
+ v = normalize(value)
34
+
35
+ return false unless v.length == 11 && VALID_TYPE_CODES.include?(v[2...4])
36
+
37
+ sum = DIGIT_POSITIONS.each_with_index.sum do |pos, i|
38
+ v[pos - 1].to_i * WEIGHTS[i]
39
+ end
40
+
41
+ Mod11.mod11_check_digit(sum) == v[10].to_i
42
+ end
43
+
44
+ def format(value)
45
+ normalize(value)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ie/ac"
4
+ require_relative "ie/al"
5
+ require_relative "ie/am"
6
+ require_relative "ie/ap"
7
+ require_relative "ie/ba"
8
+ require_relative "ie/ce"
9
+ require_relative "ie/df"
10
+ require_relative "ie/es"
11
+ require_relative "ie/go"
12
+ require_relative "ie/ma"
13
+ require_relative "ie/mg"
14
+ require_relative "ie/ms"
15
+ require_relative "ie/mt"
16
+ require_relative "ie/pa"
17
+ require_relative "ie/pb"
18
+ require_relative "ie/pe"
19
+ require_relative "ie/pi"
20
+ require_relative "ie/pr"
21
+ require_relative "ie/rj"
22
+ require_relative "ie/rn"
23
+ require_relative "ie/ro"
24
+ require_relative "ie/rr"
25
+ require_relative "ie/rs"
26
+ require_relative "ie/sc"
27
+ require_relative "ie/se"
28
+ require_relative "ie/sp"
29
+ require_relative "ie/to"
30
+
31
+ module BrValidator
32
+ # Validates, normalizes, and formats Brazilian Inscrição Estadual
33
+ # numbers.
34
+ #
35
+ # Inscrição Estadual has no single national rule: each state (SEFAZ)
36
+ # defines its own digit count and check-digit algorithm, so every UF is
37
+ # modeled as its own module under BrValidator::Ie and registered in the
38
+ # STATES map below. A UF absent from that map is simply not supported
39
+ # yet.
40
+ module Ie
41
+ module_function
42
+
43
+ STATES = {
44
+ "AC" => Ac,
45
+ "AL" => Al,
46
+ "AM" => Am,
47
+ "AP" => Ap,
48
+ "BA" => Ba,
49
+ "CE" => Ce,
50
+ "DF" => Df,
51
+ "ES" => Es,
52
+ "GO" => Go,
53
+ "MA" => Ma,
54
+ "MG" => Mg,
55
+ "MS" => Ms,
56
+ "MT" => Mt,
57
+ "PA" => Pa,
58
+ "PB" => Pb,
59
+ "PE" => Pe,
60
+ "PI" => Pi,
61
+ "PR" => Pr,
62
+ "RJ" => Rj,
63
+ "RN" => Rn,
64
+ "RO" => Ro,
65
+ "RR" => Rr,
66
+ "RS" => Rs,
67
+ "SC" => Sc,
68
+ "SE" => Se,
69
+ "SP" => Sp,
70
+ "TO" => To
71
+ }.freeze
72
+ private_constant :STATES
73
+
74
+ def resolve_state(uf)
75
+ STATES[uf.strip.upcase]
76
+ end
77
+ private_class_method :resolve_state
78
+
79
+ # Returns true if value is a valid Inscrição Estadual for uf.
80
+ def is_valid?(value, uf)
81
+ state = resolve_state(uf)
82
+
83
+ return false if state.nil?
84
+
85
+ state.is_valid?(value)
86
+ end
87
+
88
+ # Strips formatting and returns the canonical representation of value
89
+ # for uf.
90
+ #
91
+ # If uf is not supported, value is returned unchanged.
92
+ def normalize(value, uf)
93
+ state = resolve_state(uf)
94
+
95
+ return value if state.nil?
96
+
97
+ state.normalize(value)
98
+ end
99
+
100
+ # Returns value in uf's standard human-readable representation.
101
+ #
102
+ # If uf is not supported or the normalized value has an invalid
103
+ # length, value is returned unchanged.
104
+ def format(value, uf)
105
+ state = resolve_state(uf)
106
+
107
+ return value if state.nil?
108
+
109
+ state.format(value)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shared"
4
+
5
+ module BrValidator
6
+ # Validates, normalizes, and formats Brazilian national phone numbers
7
+ # (mobile and landline, no +55 country code).
8
+ module Phone
9
+ module_function
10
+
11
+ ALLOWED_CHARS = /\A[\d\s()-]+\z/.freeze
12
+ private_constant :ALLOWED_CHARS
13
+
14
+ # Strips formatting and returns the canonical (digits-only)
15
+ # representation of value.
16
+ def normalize(value)
17
+ Shared.remove_non_digits(value)
18
+ end
19
+
20
+ # Returns true if value is a structurally valid Brazilian phone number.
21
+ #
22
+ # Enforces Anatel's numbering plan: the DDD must be one of the 67 codes
23
+ # actually assigned, mobile numbers (11 digits) must carry the "ninth
24
+ # digit" 9 (Resolução nº 553/2010), and landline numbers (10 digits)
25
+ # must start with 2-5.
26
+ def is_valid?(value)
27
+ return false unless value.match?(ALLOWED_CHARS)
28
+
29
+ phone = normalize(value)
30
+
31
+ return false unless [10, 11].include?(phone.length)
32
+ return false unless Shared::VALID_DDDS.include?(phone[0...2])
33
+
34
+ subscriber_first_digit = phone[2]
35
+
36
+ return subscriber_first_digit == "9" if phone.length == 11
37
+
38
+ "2345".include?(subscriber_first_digit)
39
+ end
40
+
41
+ # Returns value as (XX) XXXXX-XXXX (mobile) or (XX) XXXX-XXXX
42
+ # (landline).
43
+ #
44
+ # If the normalized value has an invalid length, value is returned
45
+ # unchanged.
46
+ def format(value)
47
+ phone = normalize(value)
48
+
49
+ case phone.length
50
+ when 11
51
+ "(#{phone[0...2]}) #{phone[2...7]}-#{phone[7...11]}"
52
+ when 10
53
+ "(#{phone[0...2]}) #{phone[2...6]}-#{phone[6...10]}"
54
+ else
55
+ value
56
+ end
57
+ end
58
+ end
59
+ end