br-utils 0.1.1 → 0.2.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 +4 -4
- data/README.md +446 -22
- data/lib/brazilian-utils/area-code-utils.rb +60 -0
- data/lib/brazilian-utils/bank-account-utils.rb +82 -0
- data/lib/brazilian-utils/bank-utils.rb +61 -0
- data/lib/brazilian-utils/boleto-utils.rb +139 -0
- data/lib/brazilian-utils/caepf-utils.rb +91 -0
- data/lib/brazilian-utils/cbo-utils.rb +74 -0
- data/lib/brazilian-utils/cei-utils.rb +79 -0
- data/lib/brazilian-utils/cep-utils.rb +25 -1
- data/lib/brazilian-utils/certidao-utils.rb +171 -0
- data/lib/brazilian-utils/cfop-utils.rb +74 -0
- data/lib/brazilian-utils/cnae-utils.rb +104 -0
- data/lib/brazilian-utils/cnh-utils.rb +53 -0
- data/lib/brazilian-utils/cno-utils.rb +76 -0
- data/lib/brazilian-utils/cnpj-utils.rb +125 -12
- data/lib/brazilian-utils/cns-utils.rb +110 -0
- data/lib/brazilian-utils/cpf-utils.rb +13 -0
- data/lib/brazilian-utils/credit-card-utils.rb +47 -0
- data/lib/brazilian-utils/csosn-utils.rb +55 -0
- data/lib/brazilian-utils/cst-utils.rb +103 -0
- data/lib/brazilian-utils/currency-utils.rb +347 -228
- data/lib/brazilian-utils/data/area_codes.json +676 -0
- data/lib/brazilian-utils/data/banks.json +357 -0
- data/lib/brazilian-utils/data/cbo.json +1 -0
- data/lib/brazilian-utils/data/cfop.json +1 -0
- data/lib/brazilian-utils/data/cnae.json +1 -0
- data/lib/brazilian-utils/data/csosn.json +42 -0
- data/lib/brazilian-utils/data/cst.json +294 -0
- data/lib/brazilian-utils/data/legal_nature.json +900 -0
- data/lib/brazilian-utils/data/municipalities.json +1 -0
- data/lib/brazilian-utils/data/ncm.json +1 -0
- data/lib/brazilian-utils/data/states.json +218 -0
- data/lib/brazilian-utils/date-utils.rb +504 -256
- data/lib/brazilian-utils/iban-utils.rb +120 -0
- data/lib/brazilian-utils/ie-utils.rb +84 -0
- data/lib/brazilian-utils/legal-nature-utils.rb +238 -235
- data/lib/brazilian-utils/legal-process-utils.rb +31 -3
- data/lib/brazilian-utils/license-plate-utils.rb +21 -5
- data/lib/brazilian-utils/municipality-utils.rb +58 -0
- data/lib/brazilian-utils/ncm-utils.rb +91 -0
- data/lib/brazilian-utils/nfe-key-utils.rb +158 -0
- data/lib/brazilian-utils/number-utils.rb +123 -0
- data/lib/brazilian-utils/passport-utils.rb +65 -0
- data/lib/brazilian-utils/phone-utils.rb +491 -280
- data/lib/brazilian-utils/pis-utils.rb +16 -1
- data/lib/brazilian-utils/pix-key-utils.rb +70 -0
- data/lib/brazilian-utils/pix-payload-utils.rb +253 -0
- data/lib/brazilian-utils/registro-profissional-utils.rb +112 -0
- data/lib/brazilian-utils/renavam-utils.rb +14 -0
- data/lib/brazilian-utils/state-utils.rb +105 -0
- data/lib/brazilian-utils/text-utils.rb +105 -0
- data/lib/brazilian-utils/vin-utils.rb +46 -0
- data/lib/brazilian-utils/voter-id-utils.rb +36 -0
- metadata +39 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for the NCM (Nomenclatura Comum do Mercosul) table, the
|
|
5
|
+
# current 8-digit ("leaf") code list published by Siscomex.
|
|
6
|
+
module NCMUtils
|
|
7
|
+
DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'ncm.json')
|
|
8
|
+
|
|
9
|
+
# @private
|
|
10
|
+
def self.load_data
|
|
11
|
+
@data ||= JSON.parse(File.read(DATA_FILE))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private_class_method :load_data
|
|
15
|
+
|
|
16
|
+
# @private
|
|
17
|
+
def self.apply_mask(digits, group_sizes, separators)
|
|
18
|
+
chunks = []
|
|
19
|
+
idx = 0
|
|
20
|
+
group_sizes.each do |size|
|
|
21
|
+
break if idx >= digits.length
|
|
22
|
+
|
|
23
|
+
chunks << digits[idx, size]
|
|
24
|
+
idx += size
|
|
25
|
+
end
|
|
26
|
+
chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private_class_method :apply_mask
|
|
30
|
+
|
|
31
|
+
# Normalizes an NCM value to its bare 8 digits: bare digits (string or
|
|
32
|
+
# integer) are left-padded with zeros to 8; the `NNNN.NN.NN` mask is
|
|
33
|
+
# read as written. Any other string is rejected.
|
|
34
|
+
#
|
|
35
|
+
# @private
|
|
36
|
+
def self.normalize(value)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
return value.to_s.rjust(8, '0') if value.is_a?(Integer)
|
|
39
|
+
return nil unless value.is_a?(String)
|
|
40
|
+
|
|
41
|
+
raw = value.strip
|
|
42
|
+
return raw.rjust(8, '0') if raw.match?(/\A\d+\z/)
|
|
43
|
+
|
|
44
|
+
m = raw.match(/\A(\d{4})\.(\d{2})\.(\d{2})\z/)
|
|
45
|
+
m && "#{m[1]}#{m[2]}#{m[3]}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private_class_method :normalize
|
|
49
|
+
|
|
50
|
+
# Formats an NCM code with the mask `NNNN.NN.NN`; the mask is applied
|
|
51
|
+
# as far as the digits go (only the structure changes — use {is_valid}
|
|
52
|
+
# to check the code).
|
|
53
|
+
#
|
|
54
|
+
# @param value [String, Integer]
|
|
55
|
+
# @param options [Hash] `:pad` left-pads with zeros to 8 digits first.
|
|
56
|
+
# @return [String] An empty string when there is no digit at all.
|
|
57
|
+
def self.format(value, options = {})
|
|
58
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
59
|
+
digits = digits.rjust(8, '0') if options[:pad] || options['pad']
|
|
60
|
+
return '' if digits.empty?
|
|
61
|
+
|
|
62
|
+
apply_mask(digits, [4, 2, 2], ['.', '.'])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Checks whether an NCM code exists in the current table.
|
|
66
|
+
#
|
|
67
|
+
# @param value [String, Integer]
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def self.is_valid(value)
|
|
70
|
+
code = normalize(value)
|
|
71
|
+
return false unless code
|
|
72
|
+
|
|
73
|
+
load_data.key?(code)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class << self
|
|
77
|
+
alias valid? is_valid
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Removes NCM formatting and keeps only digits, capped to 8 digits
|
|
81
|
+
# (nothing is left-padded).
|
|
82
|
+
#
|
|
83
|
+
# @param value [String, Integer]
|
|
84
|
+
# @return [String]
|
|
85
|
+
def self.parse(value)
|
|
86
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
87
|
+
|
|
88
|
+
value.to_s.gsub(/\D/, '')[0, 8]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require_relative 'state-utils'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for the 44-digit DF-e (Documento Fiscal eletrônico) access
|
|
5
|
+
# key shared by NF-e, NFC-e, CT-e, MDF-e, CT-e OS, GTV-e, BP-e, NF3e and
|
|
6
|
+
# NFCom.
|
|
7
|
+
#
|
|
8
|
+
# @note For NFCom and NF3e (models 62 and 66) the numeric code (`cNF`)
|
|
9
|
+
# field is 7 digits rather than 8, which shifts the rest of the key;
|
|
10
|
+
# no test case exercised these two models, so {get_info} always uses
|
|
11
|
+
# the general 8-digit layout. Its `code`/`model` fields for a 62/66 key
|
|
12
|
+
# should be treated as unverified.
|
|
13
|
+
module NfeKeyUtils
|
|
14
|
+
LENGTH = 44
|
|
15
|
+
|
|
16
|
+
ID_PREFIXES = %w[NFCom NF3e NFe CTe MDFe BPe].freeze
|
|
17
|
+
|
|
18
|
+
VALID_MODELS = %w[55 65 57 58 67 64 63 66 62].freeze
|
|
19
|
+
|
|
20
|
+
# @private
|
|
21
|
+
def self.apply_mask(digits, group_size)
|
|
22
|
+
digits.chars.each_slice(group_size).map(&:join).join(' ')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private_class_method :apply_mask
|
|
26
|
+
|
|
27
|
+
# @private
|
|
28
|
+
def self.strip_id_prefix(raw)
|
|
29
|
+
prefix = ID_PREFIXES.find { |p| raw.start_with?(p) }
|
|
30
|
+
prefix ? raw[prefix.length..-1] : raw
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private_class_method :strip_id_prefix
|
|
34
|
+
|
|
35
|
+
# Removes the formatting of a DF-e access key and keeps only digits,
|
|
36
|
+
# capped to 44 digits. The XML `Id` prefixes are stripped first.
|
|
37
|
+
#
|
|
38
|
+
# @param value [String, Integer]
|
|
39
|
+
# @return [String]
|
|
40
|
+
def self.parse(value)
|
|
41
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
42
|
+
|
|
43
|
+
raw = strip_id_prefix(value.to_s)
|
|
44
|
+
raw.gsub(/\D/, '')[0, LENGTH]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Formats a DF-e access key into groups of 4 digits separated by
|
|
48
|
+
# spaces. Does not validate (use {is_valid}).
|
|
49
|
+
#
|
|
50
|
+
# @param value [String]
|
|
51
|
+
# @param options [Hash] `:pad` left-pads with zeros to 44 digits first.
|
|
52
|
+
# @return [String]
|
|
53
|
+
def self.format(value, options = {})
|
|
54
|
+
raw = value.is_a?(String) ? strip_id_prefix(value) : value.to_s
|
|
55
|
+
digits = raw.gsub(/\D/, '')
|
|
56
|
+
digits = digits.rjust(LENGTH, '0') if options[:pad] || options['pad']
|
|
57
|
+
return '' if digits.empty?
|
|
58
|
+
|
|
59
|
+
apply_mask(digits, 4)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @private
|
|
63
|
+
def self.normalize(value)
|
|
64
|
+
return nil unless value.is_a?(String)
|
|
65
|
+
|
|
66
|
+
raw = strip_id_prefix(value.strip)
|
|
67
|
+
return nil if raw.empty?
|
|
68
|
+
|
|
69
|
+
digits = raw.gsub(%r{[\s.\-/]}, '')
|
|
70
|
+
digits.match?(/\A\d{#{LENGTH}}\z/) ? digits : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private_class_method :normalize
|
|
74
|
+
|
|
75
|
+
# @private
|
|
76
|
+
def self.check_digit(base43)
|
|
77
|
+
weights = []
|
|
78
|
+
w = 2
|
|
79
|
+
base43.length.times do
|
|
80
|
+
weights << w
|
|
81
|
+
w = w == 9 ? 2 : w + 1
|
|
82
|
+
end
|
|
83
|
+
weights.reverse!
|
|
84
|
+
|
|
85
|
+
sum = base43.chars.each_with_index.sum { |d, i| d.to_i * weights[i] }
|
|
86
|
+
r = sum % 11
|
|
87
|
+
r < 2 ? 0 : 11 - r
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_class_method :check_digit
|
|
91
|
+
|
|
92
|
+
# @private
|
|
93
|
+
def self.cnf_ok?(cnf, nnf)
|
|
94
|
+
return false if cnf.chars.uniq.length == 1
|
|
95
|
+
return false if cnf.to_i == nnf.to_i
|
|
96
|
+
|
|
97
|
+
digits = cnf.chars.map(&:to_i)
|
|
98
|
+
ascending = digits.each_cons(2).all? { |a, b| b == (a + 1) % 10 }
|
|
99
|
+
descending = digits.each_cons(2).all? { |a, b| b == (a - 1) % 10 }
|
|
100
|
+
|
|
101
|
+
!(ascending || descending)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private_class_method :cnf_ok?
|
|
105
|
+
|
|
106
|
+
# Validates a 44-digit DF-e access key.
|
|
107
|
+
#
|
|
108
|
+
# @param value [String]
|
|
109
|
+
# @return [Boolean]
|
|
110
|
+
def self.is_valid(value)
|
|
111
|
+
digits = normalize(value)
|
|
112
|
+
return false unless digits
|
|
113
|
+
|
|
114
|
+
cuf = digits[0, 2]
|
|
115
|
+
model = digits[20, 2]
|
|
116
|
+
nnf = digits[25, 9]
|
|
117
|
+
tpemis = digits[34, 1]
|
|
118
|
+
cnf = digits[35, 8]
|
|
119
|
+
cdv = digits[43, 1]
|
|
120
|
+
|
|
121
|
+
return false unless StateUtils.get_by_ibge_code(cuf)
|
|
122
|
+
return false unless VALID_MODELS.include?(model)
|
|
123
|
+
return false unless tpemis.match?(/\A[1-9]\z/)
|
|
124
|
+
return false if nnf.to_i.zero?
|
|
125
|
+
return false if %w[55 65].include?(model) && !cnf_ok?(cnf, nnf)
|
|
126
|
+
|
|
127
|
+
cdv.to_i == check_digit(digits[0, 43])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class << self
|
|
131
|
+
alias valid? is_valid
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parses a DF-e access key into its fields.
|
|
135
|
+
#
|
|
136
|
+
# @param value [String]
|
|
137
|
+
# @return [Hash, nil] `nil` whenever {is_valid} would return false.
|
|
138
|
+
def self.get_info(value)
|
|
139
|
+
return nil unless is_valid(value)
|
|
140
|
+
|
|
141
|
+
digits = normalize(value)
|
|
142
|
+
state = StateUtils.get_by_ibge_code(digits[0, 2])
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
stateCode: state[:code],
|
|
146
|
+
year: 2000 + digits[2, 2].to_i,
|
|
147
|
+
month: digits[4, 2].to_i,
|
|
148
|
+
taxId: digits[6, 14],
|
|
149
|
+
model: digits[20, 2],
|
|
150
|
+
series: digits[22, 3].to_i,
|
|
151
|
+
number: digits[25, 9].to_i,
|
|
152
|
+
emissionType: digits[34, 1].to_i,
|
|
153
|
+
code: digits[35, 8],
|
|
154
|
+
checkDigit: digits[43, 1].to_i
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for writing numbers in Brazilian Portuguese cardinal words
|
|
5
|
+
# ("por extenso"), e.g. `1235` becomes `"mil duzentos e trinta e cinco"`.
|
|
6
|
+
module NumberUtils
|
|
7
|
+
MAX_ABS_VALUE = 999_999_999_999_999
|
|
8
|
+
|
|
9
|
+
FEMININE_GENDERS = %i[feminine feminino f].freeze
|
|
10
|
+
|
|
11
|
+
# Writes an integer in Brazilian Portuguese cardinal words.
|
|
12
|
+
#
|
|
13
|
+
# @param value [Numeric] The number to convert. Accepts integers from
|
|
14
|
+
# -999,999,999,999,999 to 999,999,999,999,999; a non-integer is
|
|
15
|
+
# truncated toward zero.
|
|
16
|
+
# @param options [Hash] `:gender` (`:masculine`, default, or
|
|
17
|
+
# `:feminine`) agrees "um/uma", "dois/duas" and the hundreds.
|
|
18
|
+
# @return [String] The textual representation, or an empty string for a
|
|
19
|
+
# value outside the supported range or not finite.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# convert_to_words(1235) #=> "mil duzentos e trinta e cinco"
|
|
23
|
+
# convert_to_words(100) #=> "cem"
|
|
24
|
+
# convert_to_words(-3) #=> "menos três"
|
|
25
|
+
# convert_to_words(2, gender: :feminine) #=> "duas"
|
|
26
|
+
def self.convert_to_words(value, options = {})
|
|
27
|
+
return '' unless value.is_a?(Numeric)
|
|
28
|
+
return '' if value.respond_to?(:finite?) && !value.finite?
|
|
29
|
+
|
|
30
|
+
int_value = value.to_i
|
|
31
|
+
return '' if int_value.abs > MAX_ABS_VALUE
|
|
32
|
+
|
|
33
|
+
gender = (options[:gender] || options['gender'] || :masculine).to_sym
|
|
34
|
+
negative = int_value.negative?
|
|
35
|
+
words = number_to_words(int_value.abs, gender)
|
|
36
|
+
negative ? "menos #{words}" : words
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @private
|
|
40
|
+
def self.number_to_words(number, gender = :masculine)
|
|
41
|
+
return 'zero' if number.zero?
|
|
42
|
+
|
|
43
|
+
scales = ['', 'mil', 'milhão', 'bilhão', 'trilhão']
|
|
44
|
+
scales_plural = ['', 'mil', 'milhões', 'bilhões', 'trilhões']
|
|
45
|
+
|
|
46
|
+
groups = []
|
|
47
|
+
temp = number
|
|
48
|
+
while temp > 0
|
|
49
|
+
groups << temp % 1000
|
|
50
|
+
temp /= 1000
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
parts = []
|
|
54
|
+
groups.each_with_index do |group, index|
|
|
55
|
+
next if group.zero?
|
|
56
|
+
|
|
57
|
+
group_text = convert_group(group, gender)
|
|
58
|
+
scale_name = group == 1 ? scales[index] : scales_plural[index]
|
|
59
|
+
|
|
60
|
+
parts << if scale_name.empty?
|
|
61
|
+
group_text
|
|
62
|
+
elsif index == 1 && group == 1
|
|
63
|
+
scale_name
|
|
64
|
+
else
|
|
65
|
+
"#{group_text} #{scale_name}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
parts.reverse!
|
|
70
|
+
return parts.first if parts.length == 1
|
|
71
|
+
|
|
72
|
+
final_group = groups.find { |g| !g.zero? }
|
|
73
|
+
last = parts.pop
|
|
74
|
+
separator = (final_group < 100 || (final_group % 100).zero?) ? ' e ' : ' '
|
|
75
|
+
"#{parts.join(' ')}#{separator}#{last}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private_class_method :number_to_words
|
|
79
|
+
|
|
80
|
+
# @private
|
|
81
|
+
def self.convert_group(number, gender = :masculine)
|
|
82
|
+
feminine = FEMININE_GENDERS.include?(gender)
|
|
83
|
+
|
|
84
|
+
ones = %w[zero um dois três quatro cinco seis sete oito nove]
|
|
85
|
+
ones_fem = %w[zero uma duas três quatro cinco seis sete oito nove]
|
|
86
|
+
tens = %w[dez onze doze treze quatorze quinze dezesseis dezessete dezoito dezenove]
|
|
87
|
+
tens_multiples = %w[_ _ vinte trinta quarenta cinquenta sessenta setenta oitenta noventa]
|
|
88
|
+
hundreds_m = %w[
|
|
89
|
+
_ cento duzentos trezentos quatrocentos quinhentos seiscentos setecentos oitocentos novecentos
|
|
90
|
+
]
|
|
91
|
+
hundreds_f = %w[
|
|
92
|
+
_ cento duzentas trezentas quatrocentas quinhentas seiscentas setecentas oitocentas novecentas
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
ones_words = feminine ? ones_fem : ones
|
|
96
|
+
hundreds = feminine ? hundreds_f : hundreds_m
|
|
97
|
+
|
|
98
|
+
return ones_words[number] if number < 10
|
|
99
|
+
return tens[number - 10] if number < 20
|
|
100
|
+
|
|
101
|
+
if number < 100
|
|
102
|
+
tens_digit = number / 10
|
|
103
|
+
ones_digit = number % 10
|
|
104
|
+
return tens_multiples[tens_digit] if ones_digit.zero?
|
|
105
|
+
|
|
106
|
+
return "#{tens_multiples[tens_digit]} e #{ones_words[ones_digit]}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
hundreds_digit = number / 100
|
|
110
|
+
remainder = number % 100
|
|
111
|
+
|
|
112
|
+
if number == 100
|
|
113
|
+
'cem'
|
|
114
|
+
elsif remainder.zero?
|
|
115
|
+
hundreds[hundreds_digit]
|
|
116
|
+
else
|
|
117
|
+
"#{hundreds[hundreds_digit]} e #{convert_group(remainder, gender)}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private_class_method :convert_group
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for formatting, validating and generating Brazilian passport
|
|
3
|
+
# numbers: 2 letters followed by 6 digits. There is no check digit.
|
|
4
|
+
module PassportUtils
|
|
5
|
+
# Removes the formatting symbols (`-`, `.`, `/` and whitespace),
|
|
6
|
+
# keeping everything else.
|
|
7
|
+
#
|
|
8
|
+
# @param value [String]
|
|
9
|
+
# @return [String]
|
|
10
|
+
def self.remove_symbols(value)
|
|
11
|
+
return '' unless value.is_a?(String)
|
|
12
|
+
|
|
13
|
+
value.gsub(%r{[-./\s]}, '')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Removes every non-alphanumeric character from a passport number,
|
|
17
|
+
# upper-cases it and caps it to 8 characters.
|
|
18
|
+
#
|
|
19
|
+
# @param value [String]
|
|
20
|
+
# @return [String]
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# parse("Ab123456") #=> "AB123456"
|
|
24
|
+
# parse(" AB 123 456 ") #=> "AB123456"
|
|
25
|
+
def self.parse(value)
|
|
26
|
+
return '' unless value.is_a?(String)
|
|
27
|
+
|
|
28
|
+
value.gsub(/[^a-zA-Z0-9]/, '').upcase[0, 8]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Formats a passport number: the same operation as {parse}.
|
|
32
|
+
#
|
|
33
|
+
# @param value [String]
|
|
34
|
+
# @return [String]
|
|
35
|
+
def self.format(value)
|
|
36
|
+
parse(value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Validates a Brazilian passport number: 2 letters followed by 6
|
|
40
|
+
# digits, after removing non-alphanumeric characters.
|
|
41
|
+
#
|
|
42
|
+
# @param value [String, Integer]
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def self.is_valid(value)
|
|
45
|
+
return false unless value.is_a?(String) || value.is_a?(Integer)
|
|
46
|
+
|
|
47
|
+
cleaned = value.to_s.gsub(/[^a-zA-Z0-9]/, '')
|
|
48
|
+
cleaned.match?(/\A[A-Za-z]{2}\d{6}\z/)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
alias valid? is_valid
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generates a random valid passport number: 2 uppercase letters
|
|
56
|
+
# followed by 6 digits.
|
|
57
|
+
#
|
|
58
|
+
# @return [String]
|
|
59
|
+
def self.generate
|
|
60
|
+
letters = 2.times.map { ('A'..'Z').to_a.sample }.join
|
|
61
|
+
digits = 6.times.map { rand(0..9) }.join
|
|
62
|
+
"#{letters}#{digits}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|