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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4a547d374960ed7a988ed11363ec67e020e70d7c3a09cbaeb605e9f142f0b645
4
+ data.tar.gz: c22a673bc326f22e1dc993309c4c7ee0edda786c2fcfed45d247f4d2119254b5
5
+ SHA512:
6
+ metadata.gz: c435c11b391665702e5889563e434e7ab9f0bae4e65d2415a3427a0d399c3c015a421a30e5a52ef432a85d75b3f9d54b48e4fbcf1ad5b8daefbe9c93988557d8
7
+ data.tar.gz: 1a22275112825f05cde5fab1e155dd1e2ab4fa9f57d15a2091c5fa55610fb2c9dd246c2b7969ec273b1d657b616193503316fd387551d087ac2495ad31f80656
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # brazil-validator (Ruby)
2
+
3
+ A validation, normalization, and formatting gem for Brazilian data: CPF, CNPJ, CEP, phone numbers, e-mail, PIX keys, and Inscrição Estadual.
4
+
5
+ This is the Ruby port of the [TypeScript implementation](../README.md), replicating the same officially-verified rules. Every module follows the same conceptual API:
6
+
7
+ ```ruby
8
+ Module.is_valid?(value) # -> true/false
9
+ Module.normalize(value) # -> String
10
+ Module.format(value) # -> String
11
+ ```
12
+
13
+ Two adaptations to Ruby convention: `is_valid?` carries the idiomatic `?` suffix for a predicate method (kept the `is_` prefix so the name still matches every other language's `isValid`/`is_valid`), and PIX's key-type lookup is named `key_type` (not `get_key_type`, per Ruby's convention of dropping `get_` on simple readers) and returns a symbol (`:cpf`, `:cnpj`, `:email`, `:phone`, `:evp`) or `nil` — not a string enum.
14
+
15
+ `Ie` extends the pattern with a second `uf` argument (the check-digit algorithm is defined per state, not nationally).
16
+
17
+ ## Installation
18
+
19
+ Not yet published to RubyGems. Add it straight from this repository to your `Gemfile`:
20
+
21
+ ```ruby
22
+ gem "brazil-validator", git: "https://github.com/matheuslm7/brazil-validator.git", glob: "ruby/*.gemspec"
23
+ ```
24
+
25
+ Or clone and build it locally:
26
+
27
+ ```bash
28
+ git clone https://github.com/matheuslm7/brazil-validator.git
29
+ cd brazil-validator/ruby
30
+ gem build brazil-validator.gemspec
31
+ gem install ./brazil-validator-0.1.0.gem
32
+ ```
33
+
34
+ *(the gem is published as `brazil-validator`, but the require path keeps its original name: `require "br_validator"` — a gem's published name and its require path don't have to match, e.g. the `activesupport` gem is required as `active_support`)*
35
+
36
+ ## Usage
37
+
38
+ ```ruby
39
+ require "br_validator"
40
+
41
+ BrValidator::Cpf.is_valid?("529.982.247-25") # true
42
+ BrValidator::Cpf.format("52998224725") # "529.982.247-25"
43
+
44
+ BrValidator::Cnpj.is_valid?("11.222.333/0001-81") # true
45
+
46
+ BrValidator::Ie.is_valid?("110.042.490.114", "SP") # true
47
+
48
+ BrValidator::Pix.key_type("+5511987654321") # :phone
49
+ ```
50
+
51
+ ## Modules
52
+
53
+ | Module | Notes |
54
+ | --- | --- |
55
+ | `Cpf` | |
56
+ | `Cnpj` | Numeric and alphanumeric |
57
+ | `Cep` | |
58
+ | `Phone` | National numbers only, no `+55` |
59
+ | `Email` | `format` returns the same value as `normalize` (no visual mask) |
60
+ | `Pix` | `key_type` returns a symbol: `:cpf`, `:cnpj`, `:email`, `:phone`, `:evp`, or `nil` |
61
+ | `Ie` | All 27 states/DF; `is_valid?`/`normalize`/`format` take `(value, uf)`. Each state is its own module under `BrValidator::Ie` — not part of the public API, only `BrValidator::Ie` itself is meant to be used directly |
62
+
63
+ See the [main README](../README.md) for the full behavior reference (official sources, normalize/format rules per validator, known gaps) — it applies identically here.
64
+
65
+ ## Conformance
66
+
67
+ `test/conformance_test.rb` checks this implementation against [`specification/`](../specification), the language-independent test vectors generated from the TypeScript implementation. If it passes, this port behaves identically to TypeScript (and to the [Go](../go), [Python](../python), [Java](../java), and [C#](../csharp) ports) for every vector on file.
68
+
69
+ ## Development
70
+
71
+ Zero runtime dependencies. Tests use Minitest (bundled with Ruby) — `rake` and `minitest` are declared as development dependencies only.
72
+
73
+ ```bash
74
+ cd ruby
75
+ bundle install
76
+ bundle exec rake test
77
+ ```
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shared"
4
+
5
+ module BrValidator
6
+ # Validates, normalizes, and formats Brazilian CEP (postal) codes.
7
+ module Cep
8
+ module_function
9
+
10
+ ALLOWED_CHARS = /\A[\d-]+\z/.freeze
11
+ private_constant :ALLOWED_CHARS
12
+
13
+ # Strips formatting and returns the canonical (digits-only)
14
+ # representation of value.
15
+ def normalize(value)
16
+ Shared.remove_non_digits(value)
17
+ end
18
+
19
+ # Returns true if value is a structurally valid CEP.
20
+ #
21
+ # A CEP has no check digit -- it is an 8-digit postal routing code
22
+ # (region, sub-region, sector, subsector and distribution suffix)
23
+ # defined by Correios -- so validity here means structural correctness
24
+ # (8 digits), not whether the code exists in Correios' address
25
+ # database.
26
+ def is_valid?(value)
27
+ return false unless value.match?(ALLOWED_CHARS)
28
+
29
+ normalize(value).length == 8
30
+ end
31
+
32
+ # Returns value as XXXXX-XXX.
33
+ #
34
+ # If the normalized value has an invalid length, value is returned
35
+ # unchanged.
36
+ def format(value)
37
+ cep = normalize(value)
38
+
39
+ return value unless cep.length == 8
40
+
41
+ "#{cep[0...5]}-#{cep[5...8]}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidator
4
+ # Validates, normalizes, and formats Brazilian CNPJ numbers.
5
+ #
6
+ # Covers both the traditional numeric format and the alphanumeric format
7
+ # introduced by Receita Federal.
8
+ module Cnpj
9
+ module_function
10
+
11
+ ALLOWED_CHARS = %r{\A[A-Za-z0-9.\-/\s]+\z}.freeze
12
+ FORMATTING_CHARS = %r{[.\-/\s]}.freeze
13
+ NORMALIZED_SHAPE = /\A[A-Z0-9]{12}\d{2}\z/.freeze
14
+ private_constant :ALLOWED_CHARS, :FORMATTING_CHARS, :NORMALIZED_SHAPE
15
+
16
+ # Strips formatting and uppercases value.
17
+ #
18
+ # A digit-only strip would destroy alphanumeric CNPJ values, so only
19
+ # separator characters are removed.
20
+ def normalize(value)
21
+ value.gsub(FORMATTING_CHARS, "").upcase
22
+ end
23
+
24
+ def character_value(char)
25
+ char.ord - 48
26
+ end
27
+ private_class_method :character_value
28
+
29
+ def calculate_check_digit(value)
30
+ sum = 0
31
+ weight = value.length == 12 ? 5 : 6
32
+
33
+ value.each_char do |char|
34
+ sum += character_value(char) * weight
35
+ weight -= 1
36
+ weight = 9 if weight == 1
37
+ end
38
+
39
+ remainder = sum % 11
40
+
41
+ return 0 if [0, 1].include?(remainder)
42
+
43
+ 11 - remainder
44
+ end
45
+ private_class_method :calculate_check_digit
46
+
47
+ # Returns true if value is a structurally and check-digit valid CNPJ,
48
+ # numeric or alphanumeric.
49
+ #
50
+ # Accepts raw or formatted input but rejects unexpected characters.
51
+ def is_valid?(value)
52
+ return false unless value.match?(ALLOWED_CHARS)
53
+
54
+ cnpj = normalize(value)
55
+
56
+ return false unless cnpj.match?(NORMALIZED_SHAPE)
57
+
58
+ first_digit = calculate_check_digit(cnpj[0...12])
59
+ return false unless first_digit == cnpj[12].to_i
60
+
61
+ second_digit = calculate_check_digit(cnpj[0...13])
62
+
63
+ second_digit == cnpj[13].to_i
64
+ end
65
+
66
+ # Returns value as XX.XXX.XXX/XXXX-XX.
67
+ #
68
+ # If the normalized value has an invalid length, value is returned
69
+ # unchanged.
70
+ def format(value)
71
+ cnpj = normalize(value)
72
+
73
+ return value unless cnpj.length == 14
74
+
75
+ "#{cnpj[0...2]}.#{cnpj[2...5]}.#{cnpj[5...8]}/#{cnpj[8...12]}-#{cnpj[12...14]}"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shared"
4
+
5
+ module BrValidator
6
+ # Validates, normalizes, and formats Brazilian CPF numbers.
7
+ module Cpf
8
+ module_function
9
+
10
+ ALLOWED_CHARS = /\A[\d.\-\s]+\z/.freeze
11
+ REPEATED_DIGITS = /\A(\d)\1{10}\z/.freeze
12
+ private_constant :ALLOWED_CHARS, :REPEATED_DIGITS
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
+ def calculate_check_digit(cpf, weight)
21
+ sum = 0
22
+
23
+ cpf.each_char.with_index do |char, i|
24
+ sum += char.to_i * (weight - i)
25
+ end
26
+
27
+ remainder = (sum * 10) % 11
28
+ remainder == 10 ? 0 : remainder
29
+ end
30
+ private_class_method :calculate_check_digit
31
+
32
+ # Returns true if value is a structurally and check-digit valid CPF.
33
+ #
34
+ # Accepts raw or formatted input but rejects unexpected characters.
35
+ def is_valid?(value)
36
+ return false unless value.match?(ALLOWED_CHARS)
37
+
38
+ cpf = normalize(value)
39
+
40
+ return false unless cpf.length == 11
41
+ return false if cpf.match?(REPEATED_DIGITS)
42
+
43
+ first_digit = calculate_check_digit(cpf[0...9], 10)
44
+ return false unless first_digit == cpf[9].to_i
45
+
46
+ second_digit = calculate_check_digit(cpf[0...10], 11)
47
+
48
+ second_digit == cpf[10].to_i
49
+ end
50
+
51
+ # Returns value as XXX.XXX.XXX-XX.
52
+ #
53
+ # If the normalized value has an invalid length, value is returned
54
+ # unchanged.
55
+ def format(value)
56
+ cpf = normalize(value)
57
+
58
+ return value unless cpf.length == 11
59
+
60
+ "#{cpf[0...3]}.#{cpf[3...6]}.#{cpf[6...9]}-#{cpf[9...11]}"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidator
4
+ # Validates, normalizes, and formats e-mail addresses.
5
+ module Email
6
+ module_function
7
+
8
+ # Sourced from the WHATWG HTML Living Standard's email state regex
9
+ # (used by browsers to validate <input type="email">). All quantifiers
10
+ # are bounded, so it cannot suffer catastrophic backtracking.
11
+ EMAIL_REGEX = /
12
+ \A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+
13
+ @[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
14
+ (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+\z
15
+ /x.freeze
16
+
17
+ MAX_EMAIL_LENGTH = 254
18
+ MAX_LOCAL_PART_LENGTH = 64
19
+ private_constant :EMAIL_REGEX, :MAX_EMAIL_LENGTH, :MAX_LOCAL_PART_LENGTH
20
+
21
+ # Trims whitespace and lowercases value.
22
+ def normalize(value)
23
+ value.strip.downcase
24
+ end
25
+
26
+ # Returns true if value is a structurally valid e-mail address, per the
27
+ # WHATWG regular expression plus RFC 5321 length limits.
28
+ def is_valid?(value)
29
+ email = normalize(value)
30
+
31
+ return false if email.empty? || email.length > MAX_EMAIL_LENGTH
32
+
33
+ local_part = email.split("@", 2).first
34
+
35
+ return false if local_part.nil? || local_part.empty? || local_part.length > MAX_LOCAL_PART_LENGTH
36
+
37
+ email.match?(EMAIL_REGEX)
38
+ end
39
+
40
+ # Returns the same canonical value as normalize.
41
+ #
42
+ # Unlike CPF/CNPJ/CEP/Phone, an e-mail address has no visual mask to
43
+ # apply.
44
+ def format(value)
45
+ normalize(value)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # AC: verified against the official SEFAZ-AC "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_AC.html),
10
+ # including its worked example (01.004.823/001-12). Format: 11 digits
11
+ # (always starting with "01") + 2 check digits.
12
+ module Ac
13
+ module_function
14
+
15
+ ALLOWED_CHARS = %r{\A[\d./\-\s]+\z}.freeze
16
+ FIRST_DIGIT_WEIGHTS = [4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
17
+ SECOND_DIGIT_WEIGHTS = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
18
+ private_constant :ALLOWED_CHARS, :FIRST_DIGIT_WEIGHTS, :SECOND_DIGIT_WEIGHTS
19
+
20
+ def normalize(value)
21
+ Shared.remove_non_digits(value)
22
+ end
23
+
24
+ def is_valid?(value)
25
+ return false unless value.match?(ALLOWED_CHARS)
26
+
27
+ v = normalize(value)
28
+
29
+ return false unless v.length == 13 && v.start_with?("01")
30
+
31
+ first_digit = Mod11.mod11_check_digit(Mod11.weighted_sum(v, FIRST_DIGIT_WEIGHTS))
32
+ return false unless first_digit == v[11].to_i
33
+
34
+ second_digit = Mod11.mod11_check_digit(Mod11.weighted_sum(v, SECOND_DIGIT_WEIGHTS))
35
+
36
+ second_digit == v[12].to_i
37
+ end
38
+
39
+ def format(value)
40
+ v = normalize(value)
41
+
42
+ return value unless v.length == 13
43
+
44
+ "#{v[0...2]}.#{v[2...5]}.#{v[5...8]}/#{v[8...11]}-#{v[11...13]}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # AL: verified against the official SEFAZ-AL "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_AL.html),
10
+ # including its worked example (24000004 -> check digit 8, i.e.
11
+ # 240000048).
12
+ #
13
+ # Format: "24" (fixed) + 1 "tipo de empresa" digit (0,3,5,7,8) + 5
14
+ # sequence digits + 1 check digit = 9 digits total. No official
15
+ # punctuation mask is published, so format returns the plain digits.
16
+ module Al
17
+ module_function
18
+
19
+ ALLOWED_CHARS = /\A[\d\s]+\z/.freeze
20
+ WEIGHTS = [9, 8, 7, 6, 5, 4, 3, 2].freeze
21
+ VALID_TYPE_DIGITS = %w[0 3 5 7 8].freeze
22
+ private_constant :ALLOWED_CHARS, :WEIGHTS, :VALID_TYPE_DIGITS
23
+
24
+ def normalize(value)
25
+ Shared.remove_non_digits(value)
26
+ end
27
+
28
+ def is_valid?(value)
29
+ return false unless value.match?(ALLOWED_CHARS)
30
+
31
+ v = normalize(value)
32
+
33
+ return false unless v.length == 9 && v.start_with?("24")
34
+ return false unless VALID_TYPE_DIGITS.include?(v[2])
35
+
36
+ check_digit = Mod11.mod11_times_ten_check_digit(Mod11.weighted_sum(v, WEIGHTS))
37
+
38
+ check_digit == v[8].to_i
39
+ end
40
+
41
+ def format(value)
42
+ normalize(value)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # AM: verified against the official SEFAZ-AM "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_AM.html).
10
+ # Format: 99.999.999-9 (8 digits + 1 check digit). Unlike most other
11
+ # states, when the weighted sum itself is below 11 the digit is 11
12
+ # minus the sum directly (skipping the modulo step).
13
+ module Am
14
+ module_function
15
+
16
+ ALLOWED_CHARS = /\A[\d.\-\s]+\z/.freeze
17
+ WEIGHTS = [9, 8, 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
+
27
+ return 11 - sum if sum < 11
28
+
29
+ Mod11.mod11_check_digit(sum)
30
+ end
31
+ private_class_method :calculate_check_digit
32
+
33
+ def is_valid?(value)
34
+ return false unless value.match?(ALLOWED_CHARS)
35
+
36
+ v = normalize(value)
37
+
38
+ return false unless v.length == 9
39
+
40
+ calculate_check_digit(v) == v[8].to_i
41
+ end
42
+
43
+ def format(value)
44
+ v = normalize(value)
45
+
46
+ return value unless v.length == 9
47
+
48
+ "#{v[0...2]}.#{v[2...5]}.#{v[5...8]}-#{v[8...9]}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # AP: verified against the official SEFAZ-AP "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_AP.html),
10
+ # including its worked example (030123459).
11
+ #
12
+ # Format: "03" (fixed) + 6 sequence digits + 1 check digit = 9 digits
13
+ # total. Unlike other states, the weighted sum starts from a constant
14
+ # "p" that depends on the numeric range of the registration, and a
15
+ # zero remainder maps to a range-dependent digit "d" instead of always
16
+ # 0.
17
+ module Ap
18
+ module_function
19
+
20
+ ALLOWED_CHARS = /\A[\d\s]+\z/.freeze
21
+ WEIGHTS = [9, 8, 7, 6, 5, 4, 3, 2].freeze
22
+ private_constant :ALLOWED_CHARS, :WEIGHTS
23
+
24
+ def normalize(value)
25
+ Shared.remove_non_digits(value)
26
+ end
27
+
28
+ def resolve_constants(base)
29
+ return [5, 0] if base <= 3_017_000
30
+ return [9, 1] if base <= 3_019_022
31
+
32
+ [0, 0]
33
+ end
34
+ private_class_method :resolve_constants
35
+
36
+ def calculate_check_digit(v)
37
+ base = v[0...8]
38
+ p, d = resolve_constants(base.to_i)
39
+ sum = p + Mod11.weighted_sum(base, WEIGHTS)
40
+ remainder = sum % 11
41
+
42
+ return 0 if remainder == 1
43
+ return d if remainder.zero?
44
+
45
+ 11 - remainder
46
+ end
47
+ private_class_method :calculate_check_digit
48
+
49
+ def is_valid?(value)
50
+ return false unless value.match?(ALLOWED_CHARS)
51
+
52
+ v = normalize(value)
53
+
54
+ return false unless v.length == 9 && v.start_with?("03")
55
+
56
+ calculate_check_digit(v) == v[8].to_i
57
+ end
58
+
59
+ def format(value)
60
+ normalize(value)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shared"
4
+ require_relative "mod11"
5
+
6
+ module BrValidator
7
+ module Ie
8
+ # BA: verified against the official SEFAZ-BA "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_BA.html),
10
+ # including its four worked examples (123456-63, 612345-57,
11
+ # 1000003-06 mod-10/mod-11 x 8/9-digit variants).
12
+ #
13
+ # Bahia has two lengths (8 or 9 digits) and, within each, two moduli:
14
+ # módulo 10 when the discriminating digit (the 1st digit for 8-digit
15
+ # IEs, the 2nd for 9-digit IEs) is one of 0,1,2,3,4,5,8, and módulo 11
16
+ # when it is 6, 7 or 9. The last check digit is calculated first (from
17
+ # the base digits alone), then the first check digit is calculated
18
+ # from the base plus that digit.
19
+ module Ba
20
+ module_function
21
+
22
+ ALLOWED_CHARS = /\A[\d\-\s]+\z/.freeze
23
+ MOD10_DIGITS = %w[0 1 2 3 4 5 8].freeze
24
+ MOD11_DIGITS = %w[6 7 9].freeze
25
+ private_constant :ALLOWED_CHARS, :MOD10_DIGITS, :MOD11_DIGITS
26
+
27
+ def normalize(value)
28
+ Shared.remove_non_digits(value)
29
+ end
30
+
31
+ def check_digit_for(sum, use_mod11)
32
+ use_mod11 ? Mod11.mod11_check_digit(sum) : Mod11.mod10_check_digit(sum)
33
+ end
34
+ private_class_method :check_digit_for
35
+
36
+ def descending_weights(length)
37
+ (0...length).map { |i| length + 1 - i }
38
+ end
39
+ private_class_method :descending_weights
40
+
41
+ def is_valid?(value)
42
+ return false unless value.match?(ALLOWED_CHARS)
43
+
44
+ v = normalize(value)
45
+
46
+ return false unless [8, 9].include?(v.length)
47
+
48
+ discriminant = v.length == 8 ? v[0] : v[1]
49
+ use_mod11 = MOD11_DIGITS.include?(discriminant)
50
+
51
+ return false if !use_mod11 && !MOD10_DIGITS.include?(discriminant)
52
+
53
+ base_length = v.length - 2
54
+ base = v[0...base_length]
55
+ last_digit = check_digit_for(Mod11.weighted_sum(base, descending_weights(base_length)), use_mod11)
56
+
57
+ return false unless last_digit == v[-1].to_i
58
+
59
+ base_with_last_digit = base + last_digit.to_s
60
+ first_digit = check_digit_for(
61
+ Mod11.weighted_sum(base_with_last_digit, descending_weights(base_length + 1)), use_mod11
62
+ )
63
+
64
+ first_digit == v[-2].to_i
65
+ end
66
+
67
+ def format(value)
68
+ v = normalize(value)
69
+
70
+ return value unless [8, 9].include?(v.length)
71
+
72
+ "#{v[0...-2]}-#{v[-2..]}"
73
+ end
74
+ end
75
+ end
76
+ 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
+ # CE: verified against the official SEFAZ-CE "Roteiro de Crítica da
9
+ # Inscrição Estadual" (sintegra.gov.br/Cad_Estados/cad_CE.html),
10
+ # including its worked example (06000001-5). Format: 8 digits + 1
11
+ # check digit.
12
+ module Ce
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