Skip to content

Commit 0f3dc6f

Browse files
authored
Add methods to format, match and check verifying digits (#10)
* Add raw Format * format as argument require to be in the list * Formater.format requires a string as document number * Add documentation for raw method * Add matcher to match with CPF and CNPJ formats * Add verification digit checker for CPF generator * Add verification digit checker for CNPJ generator * Rubocop issue: add empty line and avoit long line * Add Changelog * Add readme to help using * Test just against ruby 3 versions * Revert "Test just against ruby 3 versions" This reverts commit 8b22575. * Add Newer ruby versions * Remove syntax using that works only in ruby 3.1
1 parent bb65746 commit 0f3dc6f

13 files changed

+534
-60
lines changed

.travis.yml

+2
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ rvm:
66
- 2.6.6
77
- 2.5.8
88
- 2.4.10
9+
- 3.0.3
10+
- 3.1.1
911

1012
before_install: gem install bundler -v 2.1.4

CHANGELOG.md

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11

22
# Changelog
3+
34
All notable changes to this project will be documented in this file.
45

56
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
67
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
78

89
## [Unreleased]
910

11+
### Added
12+
13+
- Add Formatter.raw method to make a document number to have only numbers;
14+
- Add Matcher.match? to check if a CPF or CNPJ has a valid format;
15+
- Add CPFGenerator.valid_verification_digit? to check if a CPF has a valid verification digit
16+
- Add CNPJGenerator.valid_verification_digit? to check if a CNPJ has a valid verification digit
17+
1018
## [1.0.0] - 2020-22-10
19+
1120
### Added
12-
- CPF and CNPJ number formatter
13-
- CPF number generator
14-
- CNPJ number generator
15-
- RSpec CPF matcher helpers
16-
- RSpec CNPJ matcher helpers
21+
22+
- CPF and CNPJ number formatter;
23+
- CPF number generator;
24+
- CNPJ number generator;
25+
- RSpec CPF matcher helpers;
26+
- RSpec CNPJ matcher helpers;

README.md

+45
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ BraDocuments::CPFGenerator.generate(person_number: '123123123', formatted: true)
4040
#=> "123.123.123-87"
4141
```
4242

43+
### CPF digit verification
44+
45+
```rb
46+
BraDocuments::CPFGenerator.valid_verification_digit?(document: '86027265892')
47+
#=> true
48+
49+
BraDocuments::CPFGenerator.valid_verification_digit?(document: '123.123.123-87')
50+
#=> true
51+
52+
BraDocuments::CPFGenerator.valid_verification_digit?(document: '123.123.123-88')
53+
#=> false
54+
```
55+
4356
### CNPJ Generation
4457

4558
```rb
@@ -62,6 +75,19 @@ BraDocuments::CNPJGenerator.generate(company_number: '53855973', matrix_subsidia
6275
#=> "53.855.973/0001-79"
6376
```
6477

78+
### CNPJ digit verification
79+
80+
```rb
81+
BraDocuments::CNPJGenerator.valid_verification_digit?(document: '62885807804809')
82+
#=> true
83+
84+
BraDocuments::CNPJGenerator.valid_verification_digit?(document: '53.855.973/0001-79')
85+
#=> true
86+
87+
BraDocuments::CNPJGenerator.valid_verification_digit?(document: '53855973000177')
88+
#=> false
89+
```
90+
6591
### Formatting
6692

6793
```rb
@@ -70,6 +96,25 @@ BraDocuments::Formatter.format('86027265892', as: :cpf)
7096

7197
BraDocuments::Formatter.format('53855973879456', as: :cnpj)
7298
#=> "53.855.973/8794-56"
99+
100+
BraDocuments::Formatter.raw('53.855.973/8794-56')
101+
#=> "53855973879456"
102+
```
103+
104+
### Matching
105+
106+
```rb
107+
BraDocuments::Matcher.match?('11111111111', kind: :cpf, mode: :raw)
108+
#=> true
109+
110+
BraDocuments::Matcher.match?('11111111111', kind: :cpf, mode: :formatted)
111+
#=> false
112+
113+
BraDocuments::Matcher.match?('11111111111', kind: :cnpj, mode: :raw)
114+
#=> false
115+
116+
BraDocuments::Matcher.match?('90.978.812/0001-07', kind: :cnpj, mode: :formatted)
117+
#=> true
73118
```
74119

75120
### Tests Matching

lib/bra_documents.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
require 'bra_documents/cpf_generator'
55
require 'bra_documents/cnpj_generator'
66
require 'bra_documents/formatter'
7-
require "bra_documents/version"
8-
7+
require 'bra_documents/matcher'
8+
require 'bra_documents/version'
99

1010
module BraDocuments
1111
end

lib/bra_documents/cnpj_generator.rb

+44-6
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,27 @@ class << self
1212
# Generates a random CNPJ document number or add verifying digits to one if it's given.
1313
# It can return only numbers or formatted with mask
1414
#
15-
# BraDocuments::CNPJGenerator.generate # => "62885807804809"
16-
# BraDocuments::CNPJGenerator.generate(formatted: true) # => "53.855.973/0664-39"
17-
# BraDocuments::CNPJGenerator.generate(company_number: '53855973') # => "53855973879456"
18-
# BraDocuments::CNPJGenerator.generate(company_number: '53855973', formatted: true) # => "53.855.973/8189-02"
19-
# BraDocuments::CNPJGenerator.generate(company_number: '53855973', matrix_subsidiary_number: '0001') # => "53855973000179"
20-
# BraDocuments::CNPJGenerator.generate(company_number: '53855973', matrix_subsidiary_number: '0001', formatted: true) # => "53.855.973/0001-79"
15+
# BraDocuments::CNPJGenerator.generate
16+
# # => "62885807804809"
17+
#
18+
# BraDocuments::CNPJGenerator.generate(formatted: true)
19+
# # => "53.855.973/0664-39"
20+
#
21+
# BraDocuments::CNPJGenerator.generate(company_number: '53855973')
22+
# # => "53855973879456"
23+
#
24+
# BraDocuments::CNPJGenerator.generate(company_number: '53855973', formatted: true)
25+
# # => "53.855.973/8189-02"
26+
#
27+
# BraDocuments::CNPJGenerator.generate(company_number: '53855973', matrix_subsidiary_number: '0001')
28+
# # => "53855973000179"
29+
#
30+
# BraDocuments::CNPJGenerator.generate(
31+
# company_number: '53855973',
32+
# matrix_subsidiary_number: '0001',
33+
# formatted: true
34+
# )
35+
# # => "53.855.973/0001-79"
2136
def generate(company_number: nil, matrix_subsidiary_number: nil, formatted: false)
2237
company_number = number_for('Company', COMPANY_NUMBER_SIZE, company_number)
2338
matrix_subsidiary_number = number_for('Matrix or subsidiary', MATRIX_SUBSIDIARY_SIZE, matrix_subsidiary_number)
@@ -28,6 +43,29 @@ def generate(company_number: nil, matrix_subsidiary_number: nil, formatted: fals
2843
formatted ? Formatter.format(full_number, as: :cnpj) : full_number
2944
end
3045

46+
# Returns if a CPF has a valid verification digit.
47+
#
48+
# BraDocuments::CPFGenerator.valid_verification_digit?(document: '11.111.111/1111-11')
49+
# # => false
50+
#
51+
# BraDocuments::CPFGenerator.valid_verification_digit?(document: '20.163.606/0001-55')
52+
# # => true
53+
#
54+
# BraDocuments::CPFGenerator.valid_verification_digit?(document: '29432530000190')
55+
# # => true
56+
def valid_verification_digit?(document:)
57+
raw_document = Formatter.raw(document)
58+
return false if raw_document.chars.uniq.size == 1
59+
60+
company_number = raw_document.slice(0..(COMPANY_NUMBER_SIZE - 1))
61+
matrix_subsidiary_number = raw_document
62+
.slice(COMPANY_NUMBER_SIZE..(COMPANY_NUMBER_SIZE + MATRIX_SUBSIDIARY_SIZE - 1))
63+
verified_digit = raw_document.slice(-2..-1)
64+
65+
generate(company_number: company_number, matrix_subsidiary_number: matrix_subsidiary_number)
66+
.end_with?(verified_digit)
67+
end
68+
3169
private
3270

3371
def verification_digit_multiplicators_for(numbers)

lib/bra_documents/cpf_generator.rb

+32-5
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,42 @@ class << self
1111
# Generates a random CPF document number or add verifying digits to one if it's given.
1212
# It can return only numbers or formatted with mask
1313
#
14-
# BraDocuments::CPFGenerator.generate # => "86027265892"
15-
# BraDocuments::CPFGenerator.generate(formatted: true) # => "038.857.544-10"
16-
# BraDocuments::CPFGenerator.generate(person_number: '123123123') # => "12312312387"
17-
# BraDocuments::CPFGenerator.generate(person_number: '123123123', formatted: true) # => "123.123.123-87"
14+
# BraDocuments::CPFGenerator.generate
15+
# # => "86027265892"
16+
#
17+
# BraDocuments::CPFGenerator.generate(formatted: true)
18+
# # => "038.857.544-10"
19+
#
20+
# BraDocuments::CPFGenerator.generate(person_number: '123123123')
21+
# # => "12312312387"
22+
#
23+
# BraDocuments::CPFGenerator.generate(person_number: '123123123', formatted: true)
24+
# # => "123.123.123-87"
1825
def generate(person_number: nil, formatted: false)
1926
numbers = number_for('Person', PERSON_NUMBER_SIZE, person_number)
2027
full_number = complete!(numbers)
2128

22-
formatted ? Formatter.format(full_number, as: :cpf) : full_number
29+
formatted ? Formatter.format(full_number, as: :cpf) : full_number
30+
end
31+
32+
# Returns if a CPF has a valid verification digit.
33+
#
34+
# BraDocuments::CPFGenerator.valid_verification_digit?(document: '111.111.111-11')
35+
# # => false
36+
#
37+
# BraDocuments::CPFGenerator.valid_verification_digit?(document: '123.456.700-88')
38+
# # => true
39+
#
40+
# BraDocuments::CPFGenerator.valid_verification_digit?(document: '12345670088')
41+
# # => true
42+
def valid_verification_digit?(document:)
43+
raw_document = Formatter.raw(document)
44+
return false if raw_document.chars.uniq.size == 1
45+
46+
person_number = raw_document.slice(0..(PERSON_NUMBER_SIZE - 1))
47+
verified_digit = raw_document.slice(PERSON_NUMBER_SIZE..(raw_document.size - 1))
48+
49+
generate(person_number: person_number).end_with?(verified_digit)
2350
end
2451

2552
private

lib/bra_documents/formatter.rb

+36-9
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,47 @@
22

33
module BraDocuments
44
class Formatter
5+
NOT_NUMBER = /\D/
56
FORMATS = {
67
cpf: { pattern: /\A(\d{3})(\d{3})(\d{3})(\d{2})\z/, mask: '%s.%s.%s-%s' },
7-
cnpj: { pattern: /\A(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})\z/, mask: '%s.%s.%s/%s-%s'}
8+
cnpj: { pattern: /\A(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})\z/, mask: '%s.%s.%s/%s-%s' }
89
}.freeze
910

10-
# Formats a only numbers CPF or CNPJ in their own mask
11-
#
12-
# BraDocuments::Formatter.format('86027265892', as: :cpf) # => "860.272.658-92"
13-
# BraDocuments::Formatter.format('53855973879456', as: :cnpj) # => "53.855.973/8794-56"
14-
def self.format(number, as:)
15-
format_data = FORMATS[as]
11+
class << self
12+
# Formats a only numbers CPF or CNPJ in their own mask
13+
#
14+
# BraDocuments::Formatter.format('86027265892', as: :cpf) # => "860.272.658-92"
15+
# BraDocuments::Formatter.format('53855973879456', as: :cnpj) # => "53.855.973/8794-56"
16+
def format(number, as:)
17+
raise ArgumentError, "\"#{number.inspect}\" must be a String." unless number.is_a?(String)
18+
unless known_format?(as)
19+
raise ArgumentError, "Format \"#{as}\" is not know. Known formats: #{known_formats.join(', ')}."
20+
end
1621

17-
Kernel.format(format_data[:mask], *format_data[:pattern].match(number).captures)
22+
format_data = FORMATS[as]
23+
24+
Kernel.format(format_data[:mask], *format_data[:pattern].match(number).captures)
25+
end
26+
27+
# Formats removing all not number caracters from string.
28+
#
29+
# BraDocuments::Formatter.raw('860.272.658-9') # => "286027265892"
30+
# BraDocuments::Formatter.format('53.855.973/8794-56') # => "53855973879456"
31+
def raw(number)
32+
raise ArgumentError, "\"#{number.inspect}\" must be a String." unless number.is_a?(String)
33+
34+
number.gsub(NOT_NUMBER, '')
35+
end
36+
37+
private
38+
39+
def known_formats
40+
@known_formats ||= FORMATS.keys
41+
end
42+
43+
def known_format?(format)
44+
known_formats.include?(format)
45+
end
1846
end
1947
end
2048
end
21-

lib/bra_documents/matcher.rb

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
module BraDocuments
4+
class Matcher
5+
FORMATS = {
6+
cpf: { formatted: /\A(\d{3}\.){2}\d{3}-\d{2}\z/, raw: /\A\d{11}\z/ },
7+
cnpj: { formatted: /\A\d{2}.\d{3}\.\d{3}\/\d{4}-\d{2}\z/, raw: /\A\d{14}\z/ }
8+
}.freeze
9+
10+
class << self
11+
# Macthes with Brazilian CPF and CNPJ documents.
12+
#
13+
# BraDocuments::Matcher.match?('11111111111', kind: :cpf, mode: :raw)
14+
# # => true
15+
#
16+
# BraDocuments::Matcher.match?('11111111111', kind: :cpf, mode: :formatted)
17+
# # => false
18+
#
19+
# BraDocuments::Matcher.match?('11111111111', kind: :cnpj, mode: :raw)
20+
# # => false
21+
#
22+
# BraDocuments::Matcher.match?('90.978.812/0001-07', kind: :cnpj, mode: :formatted)
23+
# # => true
24+
def match?(number, kind:, mode:)
25+
raise ArgumentError, "\"#{number.inspect}\" must be a String." unless number.is_a?(String)
26+
27+
unless known_format?(kind)
28+
raise ArgumentError, "Unknown document kind \"#{kind.inspect}\". Known documents: #{known_formats.join(', ')}."
29+
end
30+
31+
unless known_mode?(mode)
32+
raise ArgumentError, "Unknown document format mode \"#{mode.inspect}\". Known modes: #{known_modes.join(', ')}."
33+
end
34+
35+
formats_to_match(kind, mode).any? { |format| format.match?(number) }
36+
end
37+
38+
private
39+
40+
def known_formats
41+
@known_formats ||= FORMATS.keys
42+
end
43+
44+
def known_modes
45+
@known_modes ||= FORMATS[known_formats.first].keys << :any
46+
end
47+
48+
def known_format?(format)
49+
known_formats.include?(format)
50+
end
51+
52+
def known_mode?(mode)
53+
known_modes.include?(mode)
54+
end
55+
56+
def formats_to_match(kind, mode)
57+
document_formats = FORMATS[kind]
58+
59+
mode == :any ? document_formats.values : [document_formats[mode]]
60+
end
61+
end
62+
end
63+
end

lib/bra_documents/national_register_base.rb

+7-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
module BraDocuments
44
class NationalRegisterBase
55
class << self
6-
NOT_NUMBER_PATTERN = /\D/.freeze
76
BASE = 11
87

98
private
@@ -14,9 +13,12 @@ def complete!(numbers)
1413
end
1514

1615
def number_for(number_description, number_size, given_value)
17-
given_value = only_digits_for(given_value)
16+
given_value = Formatter.raw(given_value.to_s)
1817
if !given_value.to_s.empty?
19-
raise ArgumentError, "#{number_description} number must be a number with #{number_size} digits." unless given_value.size == number_size
18+
unless given_value.size == number_size
19+
raise ArgumentError, "#{number_description} number must be a number with #{number_size} digits."
20+
end
21+
2022
given_value.split('').map(&:to_i)
2123
else
2224
number_with(number_size)
@@ -27,10 +29,6 @@ def number_with(size)
2729
size.times.map { rand(10) }
2830
end
2931

30-
def only_digits_for(number)
31-
number.to_s.gsub(NOT_NUMBER_PATTERN, '')
32-
end
33-
3432
def verification_digit_for(numbers)
3533
verification_digit_multiplicators = verification_digit_multiplicators_for(numbers)
3634
sum_and_multiplication = sum_and_multiply(numbers, verification_digit_multiplicators)
@@ -43,7 +41,8 @@ def verified_digit(sum_and_multiplication)
4341
end
4442

4543
def sum_and_multiply(numbers, multiplicators)
46-
multiplicators.map
44+
multiplicators
45+
.map
4746
.with_index { |multiplicator, position| numbers[position] * multiplicator }
4847
.sum
4948
end

0 commit comments

Comments
 (0)