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.
- checksums.yaml +7 -0
- data/README.md +77 -0
- data/lib/br_validator/cep.rb +44 -0
- data/lib/br_validator/cnpj.rb +78 -0
- data/lib/br_validator/cpf.rb +63 -0
- data/lib/br_validator/email.rb +48 -0
- data/lib/br_validator/ie/ac.rb +48 -0
- data/lib/br_validator/ie/al.rb +46 -0
- data/lib/br_validator/ie/am.rb +52 -0
- data/lib/br_validator/ie/ap.rb +64 -0
- data/lib/br_validator/ie/ba.rb +76 -0
- data/lib/br_validator/ie/ce.rb +42 -0
- data/lib/br_validator/ie/df.rb +58 -0
- data/lib/br_validator/ie/es.rb +39 -0
- data/lib/br_validator/ie/go.rb +49 -0
- data/lib/br_validator/ie/ma.rb +41 -0
- data/lib/br_validator/ie/mg.rb +93 -0
- data/lib/br_validator/ie/mod11.rb +48 -0
- data/lib/br_validator/ie/ms.rb +45 -0
- data/lib/br_validator/ie/mt.rb +42 -0
- data/lib/br_validator/ie/pa.rb +44 -0
- data/lib/br_validator/ie/pb.rb +42 -0
- data/lib/br_validator/ie/pe.rb +52 -0
- data/lib/br_validator/ie/pi.rb +38 -0
- data/lib/br_validator/ie/pr.rb +53 -0
- data/lib/br_validator/ie/rj.rb +53 -0
- data/lib/br_validator/ie/rn.rb +58 -0
- data/lib/br_validator/ie/ro.rb +58 -0
- data/lib/br_validator/ie/rr.rb +47 -0
- data/lib/br_validator/ie/rs.rb +42 -0
- data/lib/br_validator/ie/sc.rb +42 -0
- data/lib/br_validator/ie/se.rb +42 -0
- data/lib/br_validator/ie/sp.rb +121 -0
- data/lib/br_validator/ie/to.rb +49 -0
- data/lib/br_validator/ie.rb +112 -0
- data/lib/br_validator/phone.rb +59 -0
- data/lib/br_validator/pix.rb +134 -0
- data/lib/br_validator/shared.rb +31 -0
- data/lib/br_validator/version.rb +5 -0
- data/lib/br_validator.rb +13 -0
- 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
|