cpf-dv 0.0.0 β 1.0.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/LICENSE +9 -0
- data/README.md +288 -0
- data/src/cpf-dv/cpf_check_digits.rb +178 -0
- data/src/cpf-dv/errors.rb +132 -0
- data/src/cpf-dv/version.rb +2 -2
- data/src/cpf-dv.rb +30 -4
- metadata +31 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4c1b2490462860148180946616219c920d96507426d6fc6c848dc1dca7b74c2
|
|
4
|
+
data.tar.gz: 80fa518ec814ef85f84b4c76a78105e895e24acaf3ae88f95a86e830286f67b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cae6be7695a8892a7ea1b1b6398d05fc2a8e67a7133040e7da356d53635092a82ad427046ad6e632f1aa1a270390b96148f7c7bbb72273850da60593d54fec6a
|
|
7
|
+
data.tar.gz: bf145363a9df1a42912a13b19d495a27d80098f988aa6f2b643eb2d90e95b8172c4bf8d0a447683fdb8dd3b84dd12c49240b7944927801f00d59098a8a62b1a1
|
data/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julio L. Muller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
cpf-dv for Ruby
|
|
2
|
+
|
|
3
|
+
[Gem Version](https://rubygems.org/gems/cpf-dv)
|
|
4
|
+
[Downloads Count](https://rubygems.org/gems/cpf-dv)
|
|
5
|
+
[Ruby Version](https://www.ruby-lang.org/)
|
|
6
|
+
[Test Status](https://github.com/LacusSolutions/br-utils-ruby/actions)
|
|
7
|
+
[Last Update Date](https://github.com/LacusSolutions/br-utils-ruby)
|
|
8
|
+
[Project License](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
> π [Acessar documentaΓ§Γ£o em portuguΓͺs](https://github.com/LacusSolutions/br-utils-ruby/blob/main/packages/cpf-dv/README.pt.md)
|
|
11
|
+
|
|
12
|
+
A Ruby utility to calculate check digits on CPF (Brazilian Individual's Taxpayer ID).
|
|
13
|
+
|
|
14
|
+
## Ruby Support
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
| Ruby 3.2 | Ruby 3.3 | Ruby 3.4 |
|
|
18
|
+
| --------- | --------- | --------- |
|
|
19
|
+
| Passing β | Passing β | Passing β |
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- β
**Flexible input**: Accepts `String` or `Array` of strings
|
|
27
|
+
- β
**Format agnostic**: Strips non-numeric characters from string input
|
|
28
|
+
- β
**Auto-expansion**: Multi-character strings in arrays are joined and parsed like a single string
|
|
29
|
+
- β
**Input validation**: Rejects ineligible CPFs (9 identical digits in the base β repeated-digit pattern)
|
|
30
|
+
- β
**Lazy evaluation**: Check digits are calculated only when accessed (via methods)
|
|
31
|
+
- β
**Caching**: Calculated values are cached for subsequent access
|
|
32
|
+
- β
**Minimal dependencies**: Only [`lacus-utils`](https://rubygems.org/gems/lacus-utils)
|
|
33
|
+
- β
**Error handling**: API misuse vs domain errors with a `CpfDV::Error` marker for library-wide rescue
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Install the gem directly:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
gem install cpf-dv
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or add it to your `Gemfile` and run `bundle install`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
gem 'cpf-dv'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Require
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
require 'cpf-dv'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
require 'cpf-dv'
|
|
65
|
+
|
|
66
|
+
check_digits = CpfDV::CpfCheckDigits.new('054496519')
|
|
67
|
+
|
|
68
|
+
check_digits.first # => '1'
|
|
69
|
+
check_digits.second # => '0'
|
|
70
|
+
check_digits.both # => '10'
|
|
71
|
+
check_digits.cpf # => '05449651910'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
The main resource of this package is the class `CpfDV::CpfCheckDigits`. Through an instance, you access CPF check-digit information:
|
|
79
|
+
|
|
80
|
+
- `initialize`: `CpfDV::CpfCheckDigits.new(cpf_input)` β `cpf_input` must be a `String` or an `Array` of strings. After sanitization, the value must have 9β11 digits (formatting stripped from strings). Only the **first 9** digits are used as the base; if you pass 10 or 11 digits (e.g. a full CPF including prior check digits), digits 10β11 are **ignored** and the check digits are recalculated. There are **no options**, keyword arguments, or configuration objects β the constructor takes only the CPF input.
|
|
81
|
+
- `first`: First check digit (10th digit of the full CPF). Lazy, cached.
|
|
82
|
+
- `second`: Second check digit (11th digit of the full CPF). Lazy, cached.
|
|
83
|
+
- `both`: Both check digits concatenated as a string.
|
|
84
|
+
- `cpf`: The complete CPF as a string of 11 digits (9 base digits + 2 check digits).
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
### Input formats
|
|
89
|
+
|
|
90
|
+
The `CpfCheckDigits` class accepts multiple input formats:
|
|
91
|
+
|
|
92
|
+
**String input:** plain digits or formatted CPF (e.g. `054.496.519-10`, `123.456.789`). Non-numeric characters are removed. Leading zeros are preserved.
|
|
93
|
+
|
|
94
|
+
**Array of strings:** each element must be a string; values are concatenated and then parsed like a single string (e.g. `['0','5','4',β¦]`, `['054','496','519']`, `['054496519']`). Non-string elements are not allowed.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
require 'cpf-dv'
|
|
98
|
+
|
|
99
|
+
# String β plain, formatted, or with existing check digits (only first 9 digits used)
|
|
100
|
+
CpfDV::CpfCheckDigits.new('054496519')
|
|
101
|
+
CpfDV::CpfCheckDigits.new('054.496.519-10')
|
|
102
|
+
CpfDV::CpfCheckDigits.new('05449651910')
|
|
103
|
+
|
|
104
|
+
# Array of strings β single- or multi-character elements
|
|
105
|
+
CpfDV::CpfCheckDigits.new(%w[0 5 4 4 9 6 5 1 9])
|
|
106
|
+
CpfDV::CpfCheckDigits.new(%w[054 496 519])
|
|
107
|
+
CpfDV::CpfCheckDigits.new(%w[054496519])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
### Error handling
|
|
113
|
+
|
|
114
|
+
Errors fall into two categories:
|
|
115
|
+
|
|
116
|
+
| Category | Meaning |
|
|
117
|
+
|---|---|
|
|
118
|
+
| **API misuse** | The caller invoked the library incorrectly (wrong type). Detectable from the call shape. |
|
|
119
|
+
| **Domain error** | The call was structurally correct, but a value violates a business rule (length, eligibility). |
|
|
120
|
+
|
|
121
|
+
Every custom error includes the `CpfDV::Error` marker module. Domain failures (`InvalidLengthError`, `ValidationError`) inherit from `CpfDV::DomainError` (`RangeError`).
|
|
122
|
+
|
|
123
|
+
#### Summary
|
|
124
|
+
|
|
125
|
+
| Class | Inherits from | Category | Trigger condition |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| `CpfDV::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | Argument has the wrong data type |
|
|
128
|
+
| `CpfDV::InvalidLengthError` | `CpfDV::DomainError` | Domain error | Sanitized length is not 9β11 |
|
|
129
|
+
| `CpfDV::ValidationError` | `CpfDV::DomainError` | Domain error | First 9 digits are all identical (repeated-digit pattern) |
|
|
130
|
+
|
|
131
|
+
#### `CpfDV::Error` (marker module)
|
|
132
|
+
|
|
133
|
+
- **Inheritance:** module marker mixed into every library error via `include` (not a class).
|
|
134
|
+
- **Category:** N/A (rescue target only) β not a failure mode by itself.
|
|
135
|
+
- **When it is raised:** Never raised directly; included by every custom error the library raises.
|
|
136
|
+
- **Example:** N/A
|
|
137
|
+
- **How to rescue it:**
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
rescue CpfDV::Error
|
|
141
|
+
# everything this library raises
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### `CpfDV::DomainError`
|
|
145
|
+
|
|
146
|
+
- **Inheritance:** `CpfDV::DomainError < RangeError` (includes `CpfDV::Error`)
|
|
147
|
+
- **Category:** Domain error β ancestor for numeric/length domain failures.
|
|
148
|
+
- **When it is raised:** Not raised directly; prefer raising a leaf subclass.
|
|
149
|
+
- **Example:** Prefer `raise CpfDV::InvalidLengthError` over raising `DomainError` directly.
|
|
150
|
+
- **How to rescue it:**
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
rescue CpfDV::DomainError
|
|
154
|
+
# InvalidLengthError, ValidationError, and any other DomainError subclass
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### `CpfDV::TypeMismatchError`
|
|
158
|
+
|
|
159
|
+
- **Inheritance:** `CpfDV::TypeMismatchError < TypeError` (includes `CpfDV::Error`)
|
|
160
|
+
- **Category:** API misuse β the caller passed a value of the wrong type.
|
|
161
|
+
- **When it is raised:** Raised when the CPF input is not a `String` or an `Array` of strings (or an array contains a non-string element).
|
|
162
|
+
- **Example:**
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
CpfDV::CpfCheckDigits.new(12_345_678_901) # raises CpfDV::TypeMismatchError
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- **How to rescue it:**
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
rescue CpfDV::TypeMismatchError
|
|
172
|
+
# this library's type-contract violation
|
|
173
|
+
|
|
174
|
+
rescue TypeError
|
|
175
|
+
# native type errors, including this library's TypeMismatchError
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### `CpfDV::InvalidLengthError`
|
|
179
|
+
|
|
180
|
+
- **Inheritance:** `CpfDV::InvalidLengthError < CpfDV::DomainError < RangeError` (includes `CpfDV::Error`)
|
|
181
|
+
- **Category:** Domain error β a collection or string length violates a business rule.
|
|
182
|
+
- **When it is raised:** Raised when the sanitized CPF input does not contain 9 to 11 digits.
|
|
183
|
+
- **Example:**
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
CpfDV::CpfCheckDigits.new('12345678') # raises CpfDV::InvalidLengthError
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- **How to rescue it:**
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
rescue CpfDV::InvalidLengthError
|
|
193
|
+
# this exact length violation
|
|
194
|
+
|
|
195
|
+
rescue CpfDV::DomainError
|
|
196
|
+
# RangeError-rooted domain failures from this library
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `CpfDV::ValidationError`
|
|
200
|
+
|
|
201
|
+
- **Inheritance:** `CpfDV::ValidationError < CpfDV::DomainError < RangeError` (includes `CpfDV::Error`)
|
|
202
|
+
- **Category:** Domain error β a value fails a non-numeric, non-length domain rule.
|
|
203
|
+
- **When it is raised:** Raised when the first 9 digits are all the same digit (repeated-digit pattern).
|
|
204
|
+
- **Example:**
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
CpfDV::CpfCheckDigits.new('111111111') # raises CpfDV::ValidationError
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- **How to rescue it:**
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
rescue CpfDV::ValidationError
|
|
214
|
+
# this exact domain validation failure
|
|
215
|
+
|
|
216
|
+
rescue CpfDV::DomainError
|
|
217
|
+
# RangeError-rooted domain failures from this library
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### Rescue granularity
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
# 1) Single native class β catches type misuse from this library (and other TypeErrors).
|
|
224
|
+
rescue TypeError
|
|
225
|
+
# CpfDV::TypeMismatchError and any other TypeError (library or not)
|
|
226
|
+
|
|
227
|
+
# 2) CpfDV::DomainError β catches business-rule violations under DomainError.
|
|
228
|
+
rescue CpfDV::DomainError
|
|
229
|
+
# CpfDV::InvalidLengthError, CpfDV::ValidationError, and other DomainError subclasses
|
|
230
|
+
|
|
231
|
+
# 3) CpfDV::Error β catches everything the library raises.
|
|
232
|
+
rescue CpfDV::Error
|
|
233
|
+
# every custom error that includes CpfDV::Error
|
|
234
|
+
|
|
235
|
+
# 4) Specific leaf class β catches only that exact failure mode.
|
|
236
|
+
rescue CpfDV::InvalidLengthError
|
|
237
|
+
# only CpfDV::InvalidLengthError
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Notable attributes on raised errors:
|
|
241
|
+
|
|
242
|
+
- `TypeMismatchError`: `actual_input`, `actual_type`, `expected_type`
|
|
243
|
+
- `InvalidLengthError`: `actual_input`, `evaluated_input`, `min_expected_length`, `max_expected_length`
|
|
244
|
+
- `ValidationError`: `actual_input`, `reason`
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
### Other available resources
|
|
249
|
+
|
|
250
|
+
After `require 'cpf-dv'`:
|
|
251
|
+
|
|
252
|
+
- `CpfDV::CPF_MIN_LENGTH`: `9`
|
|
253
|
+
- `CpfDV::CPF_MAX_LENGTH`: `11`
|
|
254
|
+
- **Errors**: see above (`CpfDV::Error`, `DomainError`, and raised leaves)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
## Calculation algorithm
|
|
259
|
+
|
|
260
|
+
The package calculates CPF check digits using the official Brazilian modulo-11 algorithm:
|
|
261
|
+
|
|
262
|
+
1. **First check digit (10th position):** apply to the first **9** base digits; weights **10, 9, 8, 7, 6, 5, 4, 3, 2** (left to right); let `remainder = 11 - (sum(digit Γ weight) % 11)`. The digit is `0` if `remainder > 9`, otherwise `remainder`.
|
|
263
|
+
2. **Second check digit (11th position):** apply to the first 9 base digits **plus** the first check digit; weights **11, 10, 9, 8, 7, 6, 5, 4, 3, 2** (left to right); same formula for `remainder`.
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
## Contribution & Support
|
|
268
|
+
|
|
269
|
+
We welcome contributions! Please see our [Contributing Guidelines](https://github.com/LacusSolutions/br-utils-ruby/blob/main/CONTRIBUTING.md) for details. If you find this project helpful, please consider:
|
|
270
|
+
|
|
271
|
+
- β Starring the repository
|
|
272
|
+
- π€ Contributing to the codebase
|
|
273
|
+
- π‘ [Suggesting new features](https://github.com/LacusSolutions/br-utils-ruby/issues)
|
|
274
|
+
- π [Reporting bugs](https://github.com/LacusSolutions/br-utils-ruby/issues)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
This project is licensed under the MIT License β see the [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) file for details.
|
|
281
|
+
|
|
282
|
+
## Changelog
|
|
283
|
+
|
|
284
|
+
See [CHANGELOG](./CHANGELOG.md) for a list of changes and version history.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
Made with β€οΈ by [Lacus Solutions](https://github.com/LacusSolutions)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
|
|
5
|
+
module CpfDV
|
|
6
|
+
# Minimum number of digits required for the CPF check digits calculation.
|
|
7
|
+
CPF_MIN_LENGTH = 9
|
|
8
|
+
|
|
9
|
+
# Maximum number of digits accepted as input for the CPF check digits
|
|
10
|
+
# calculation.
|
|
11
|
+
CPF_MAX_LENGTH = 11
|
|
12
|
+
|
|
13
|
+
DELTA_FACTOR = '0'.ord
|
|
14
|
+
DIGIT_CHARS = '0123456789'
|
|
15
|
+
|
|
16
|
+
private_constant :DELTA_FACTOR, :DIGIT_CHARS
|
|
17
|
+
|
|
18
|
+
# Calculates and exposes CPF check digits from a valid base input.
|
|
19
|
+
#
|
|
20
|
+
# Validates length and rejects repeated-digit sequences.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# check_digits = CpfDV::CpfCheckDigits.new('054496519')
|
|
24
|
+
# check_digits.first # => '1'
|
|
25
|
+
# check_digits.second # => '0'
|
|
26
|
+
# check_digits.both # => '10'
|
|
27
|
+
# check_digits.cpf # => '05449651910'
|
|
28
|
+
class CpfCheckDigits
|
|
29
|
+
# Creates a calculator for the given CPF base (9 to 11 digits).
|
|
30
|
+
#
|
|
31
|
+
# @param cpf_input [String, Array<String>] digits with or without formatting,
|
|
32
|
+
# or an array of strings
|
|
33
|
+
# @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
|
|
34
|
+
# @raise [InvalidLengthError] when digit count is not between 9 and 11
|
|
35
|
+
# @raise [ValidationError] when all first 9 digits are the same (repeated
|
|
36
|
+
# digits, e.g. +777.777.777-...+)
|
|
37
|
+
def initialize(cpf_input)
|
|
38
|
+
parsed_input = parse_input(cpf_input)
|
|
39
|
+
|
|
40
|
+
validate_length(parsed_input, cpf_input)
|
|
41
|
+
validate_non_repeated_digits(parsed_input, cpf_input)
|
|
42
|
+
|
|
43
|
+
@cpf_digits = parsed_input[0, CPF_MIN_LENGTH]
|
|
44
|
+
@cached_first_digit = nil
|
|
45
|
+
@cached_second_digit = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# First check digit (10th digit of the full CPF).
|
|
49
|
+
#
|
|
50
|
+
# @return [String] a single numeric character (+"0"+β+"9"+)
|
|
51
|
+
def first
|
|
52
|
+
@cached_first_digit = _calculate(@cpf_digits) if @cached_first_digit.nil?
|
|
53
|
+
|
|
54
|
+
DIGIT_CHARS[@cached_first_digit]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Second check digit (11th digit of the full CPF).
|
|
58
|
+
#
|
|
59
|
+
# @return [String] a single numeric character (+"0"+β+"9"+)
|
|
60
|
+
def second
|
|
61
|
+
@cached_second_digit = _calculate([*@cpf_digits, first]) if @cached_second_digit.nil?
|
|
62
|
+
|
|
63
|
+
DIGIT_CHARS[@cached_second_digit]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Both check digits concatenated (10th and 11th digits).
|
|
67
|
+
#
|
|
68
|
+
# @return [String] two-character numeric string
|
|
69
|
+
def both
|
|
70
|
+
first + second
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Full 11-digit CPF (base 9 digits concatenated with the 2 check digits).
|
|
74
|
+
#
|
|
75
|
+
# @return [String] 11-digit CPF
|
|
76
|
+
def cpf
|
|
77
|
+
@cpf_digits.join + both
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Protected (not private) so test spy subclasses can override and call +super+.
|
|
81
|
+
# Leading underscore matches the cross-language helper name (`_calculate`).
|
|
82
|
+
protected
|
|
83
|
+
|
|
84
|
+
# Computes a single check digit using the standard CPF modulo-11 algorithm.
|
|
85
|
+
#
|
|
86
|
+
# @param cpf_sequence [Array<String>] digit characters used in the weighted sum
|
|
87
|
+
# @return [Integer] check digit in the range 0β9
|
|
88
|
+
def _calculate(cpf_sequence)
|
|
89
|
+
factor = cpf_sequence.length + 1
|
|
90
|
+
sum_result = 0
|
|
91
|
+
|
|
92
|
+
cpf_sequence.each do |digit_char|
|
|
93
|
+
sum_result += (digit_char.ord - DELTA_FACTOR) * factor
|
|
94
|
+
factor -= 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
remainder = 11 - (sum_result % 11)
|
|
98
|
+
|
|
99
|
+
remainder > 9 ? 0 : remainder
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Parses a string or an array of strings into digit characters.
|
|
105
|
+
#
|
|
106
|
+
# @param cpf_input [Object] candidate CPF input
|
|
107
|
+
# @return [Array<String>] digit characters
|
|
108
|
+
# @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
|
|
109
|
+
def parse_input(cpf_input)
|
|
110
|
+
return parse_string_input(cpf_input) if cpf_input.is_a?(String)
|
|
111
|
+
return parse_array_input(cpf_input) if cpf_input.is_a?(Array)
|
|
112
|
+
|
|
113
|
+
raise TypeMismatchError.new(cpf_input, 'string or string[]')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Strips non-digit characters and keeps the remainder as characters.
|
|
117
|
+
#
|
|
118
|
+
# @param cpf_string [String] raw or formatted CPF string
|
|
119
|
+
# @return [Array<String>] digit characters
|
|
120
|
+
def parse_string_input(cpf_string)
|
|
121
|
+
result = []
|
|
122
|
+
|
|
123
|
+
cpf_string.each_char do |char|
|
|
124
|
+
code = char.ord
|
|
125
|
+
result << char if code.between?(48, 57)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Concatenates an array of strings and parses the result.
|
|
132
|
+
#
|
|
133
|
+
# @param cpf_array [Array] candidate array of string chunks
|
|
134
|
+
# @return [Array<String>] digit characters
|
|
135
|
+
# @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
|
|
136
|
+
def parse_array_input(cpf_array)
|
|
137
|
+
return [] if cpf_array.empty?
|
|
138
|
+
|
|
139
|
+
raise TypeMismatchError.new(cpf_array, 'string or string[]') unless cpf_array.all?(String)
|
|
140
|
+
|
|
141
|
+
parse_string_input(cpf_array.join)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Ensures digit count is between {CPF_MIN_LENGTH} and {CPF_MAX_LENGTH}.
|
|
145
|
+
#
|
|
146
|
+
# @param cpf_digits [Array<String>] normalized digit characters
|
|
147
|
+
# @param original_input [String, Array<String>] original caller input
|
|
148
|
+
# @raise [InvalidLengthError] when digit count is not between 9 and 11
|
|
149
|
+
def validate_length(cpf_digits, original_input)
|
|
150
|
+
digits_count = cpf_digits.length
|
|
151
|
+
|
|
152
|
+
return if digits_count.between?(CPF_MIN_LENGTH, CPF_MAX_LENGTH)
|
|
153
|
+
|
|
154
|
+
raise InvalidLengthError.new(
|
|
155
|
+
original_input,
|
|
156
|
+
cpf_digits.join,
|
|
157
|
+
CPF_MIN_LENGTH,
|
|
158
|
+
CPF_MAX_LENGTH
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Rejects inputs where all first 9 digits are the same.
|
|
163
|
+
#
|
|
164
|
+
# @param cpf_digits [Array<String>] normalized digit characters
|
|
165
|
+
# @param original_input [String, Array<String>] original caller input
|
|
166
|
+
# @raise [ValidationError] when all first 9 digits are the same (repeated
|
|
167
|
+
# digits, e.g. +777.777.777-...+)
|
|
168
|
+
def validate_non_repeated_digits(cpf_digits, original_input)
|
|
169
|
+
first_char = cpf_digits[0]
|
|
170
|
+
return unless cpf_digits[1, CPF_MIN_LENGTH - 1].all? { |char| char == first_char }
|
|
171
|
+
|
|
172
|
+
raise ValidationError.new(
|
|
173
|
+
original_input,
|
|
174
|
+
'Repeated digits are not considered valid.'
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'lacus-utils'
|
|
5
|
+
|
|
6
|
+
module CpfDV
|
|
7
|
+
# Formats the original input for inclusion in an exception message.
|
|
8
|
+
module FormatActualInput
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# @param actual_input [Object] the original input value
|
|
12
|
+
# @return [String] a quoted string, or compact JSON for arrays
|
|
13
|
+
def call(actual_input)
|
|
14
|
+
return "\"#{actual_input}\"" if actual_input.is_a?(String)
|
|
15
|
+
|
|
16
|
+
JSON.generate(actual_input)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
private_constant :FormatActualInput
|
|
20
|
+
|
|
21
|
+
# Marker module mixed into every custom error raised by this library.
|
|
22
|
+
#
|
|
23
|
+
# Use +rescue CpfDV::Error+ to catch every library error regardless of native
|
|
24
|
+
# ancestry.
|
|
25
|
+
module Error; end
|
|
26
|
+
|
|
27
|
+
# API misuse error raised when an argument's runtime type does not match the
|
|
28
|
+
# type required by the API contract.
|
|
29
|
+
#
|
|
30
|
+
# Raised when the input provided to {CpfCheckDigits} is not a +String+ or
|
|
31
|
+
# +Array<String>+. The error message includes both the actual type of the
|
|
32
|
+
# input and the expected type.
|
|
33
|
+
class TypeMismatchError < TypeError
|
|
34
|
+
include Error
|
|
35
|
+
|
|
36
|
+
# @return [Object] the offending input value
|
|
37
|
+
attr_reader :actual_input
|
|
38
|
+
|
|
39
|
+
# @return [String] human-readable type of {#actual_input}
|
|
40
|
+
attr_reader :actual_type
|
|
41
|
+
|
|
42
|
+
# @return [String] description of the expected type
|
|
43
|
+
attr_reader :expected_type
|
|
44
|
+
|
|
45
|
+
# @param actual_input [Object] the offending input value (the whole array
|
|
46
|
+
# when a non-string element is found)
|
|
47
|
+
# @param expected_type [String] description of the expected type (e.g.
|
|
48
|
+
# +"string or string[]"+)
|
|
49
|
+
def initialize(actual_input, expected_type)
|
|
50
|
+
actual_type = LacusUtils.describe_type(actual_input)
|
|
51
|
+
|
|
52
|
+
super("CPF input must be of type #{expected_type}. Got #{actual_type}.")
|
|
53
|
+
@actual_input = actual_input
|
|
54
|
+
@actual_type = actual_type
|
|
55
|
+
@expected_type = expected_type
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Domain error ancestor for business-rule failures (length, validation, and
|
|
60
|
+
# other domain leaves). Prefer raising a leaf subclass.
|
|
61
|
+
class DomainError < RangeError
|
|
62
|
+
include Error
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Domain error raised when a string, array, or other collection has a length
|
|
66
|
+
# outside the bounds required by the domain rule.
|
|
67
|
+
#
|
|
68
|
+
# A valid CPF input must contain between 9 and 11 digits. The error message
|
|
69
|
+
# distinguishes between the original input and the evaluated one (which strips
|
|
70
|
+
# punctuation characters).
|
|
71
|
+
class InvalidLengthError < DomainError
|
|
72
|
+
# @return [String, Array<String>] the original input
|
|
73
|
+
attr_reader :actual_input
|
|
74
|
+
|
|
75
|
+
# @return [String] the digits-only string used for counting
|
|
76
|
+
attr_reader :evaluated_input
|
|
77
|
+
|
|
78
|
+
# @return [Integer] minimum expected length (9)
|
|
79
|
+
attr_reader :min_expected_length
|
|
80
|
+
|
|
81
|
+
# @return [Integer] maximum expected length (11)
|
|
82
|
+
attr_reader :max_expected_length
|
|
83
|
+
|
|
84
|
+
# @param actual_input [String, Array<String>] the original input
|
|
85
|
+
# @param evaluated_input [String] the digits-only string
|
|
86
|
+
# @param min_expected_length [Integer] minimum expected length
|
|
87
|
+
# @param max_expected_length [Integer] maximum expected length
|
|
88
|
+
def initialize(actual_input, evaluated_input, min_expected_length, max_expected_length)
|
|
89
|
+
super(build_message(actual_input, evaluated_input, min_expected_length, max_expected_length))
|
|
90
|
+
@actual_input = actual_input
|
|
91
|
+
@evaluated_input = evaluated_input
|
|
92
|
+
@min_expected_length = min_expected_length
|
|
93
|
+
@max_expected_length = max_expected_length
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def build_message(actual_input, evaluated_input, min_len, max_len)
|
|
99
|
+
fmt_actual = FormatActualInput.call(actual_input)
|
|
100
|
+
fmt_evaluated =
|
|
101
|
+
if actual_input == evaluated_input
|
|
102
|
+
evaluated_input.length.to_s
|
|
103
|
+
else
|
|
104
|
+
"#{evaluated_input.length} in \"#{evaluated_input}\""
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
"CPF input #{fmt_actual} does not contain #{min_len} to #{max_len} digits. " \
|
|
108
|
+
"Got #{fmt_evaluated}."
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Domain error raised when a value has a valid type and length but violates a
|
|
113
|
+
# validation rule that is not length-based (e.g. repeated digits).
|
|
114
|
+
#
|
|
115
|
+
# This is a business-logic exception; callers should catch it and handle it
|
|
116
|
+
# appropriately.
|
|
117
|
+
class ValidationError < DomainError
|
|
118
|
+
# @return [String, Array<String>] the original input
|
|
119
|
+
attr_reader :actual_input
|
|
120
|
+
|
|
121
|
+
# @return [String] human-readable reason why the input is invalid
|
|
122
|
+
attr_reader :reason
|
|
123
|
+
|
|
124
|
+
# @param actual_input [String, Array<String>] the original input
|
|
125
|
+
# @param reason [String] human-readable reason why the input is invalid
|
|
126
|
+
def initialize(actual_input, reason)
|
|
127
|
+
super("CPF input #{FormatActualInput.call(actual_input)} is invalid. #{reason}")
|
|
128
|
+
@actual_input = actual_input
|
|
129
|
+
@reason = reason
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
data/src/cpf-dv/version.rb
CHANGED
data/src/cpf-dv.rb
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'cpf-dv/version'
|
|
4
|
+
require_relative 'cpf-dv/errors'
|
|
5
|
+
require_relative 'cpf-dv/cpf_check_digits'
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# Check-digit calculation for Brazilian CPF (numeric format).
|
|
8
|
+
#
|
|
9
|
+
# Errors fall into two categories:
|
|
10
|
+
#
|
|
11
|
+
# - *API misuse* β the caller invoked the library incorrectly (wrong type).
|
|
12
|
+
# Raised as {CpfDV::TypeMismatchError} (+TypeError+).
|
|
13
|
+
# - *Domain errors* β the call shape was valid, but a value violates a business
|
|
14
|
+
# rule (invalid length, repeated digits). Length failures raise
|
|
15
|
+
# {CpfDV::InvalidLengthError}; other domain failures raise
|
|
16
|
+
# {CpfDV::ValidationError} (both under {CpfDV::DomainError} / +RangeError+).
|
|
17
|
+
#
|
|
18
|
+
# Every custom error includes the {CpfDV::Error} marker module so consumers can
|
|
19
|
+
# +rescue CpfDV::Error+ for a library-wide catch.
|
|
20
|
+
#
|
|
21
|
+
# Public API:
|
|
22
|
+
#
|
|
23
|
+
# - {CpfDV::CpfCheckDigits}
|
|
24
|
+
# - {CpfDV::CPF_MIN_LENGTH}, {CpfDV::CPF_MAX_LENGTH}
|
|
25
|
+
# - Error marker {CpfDV::Error}; domain ancestor {CpfDV::DomainError};
|
|
26
|
+
# raised leaves {CpfDV::TypeMismatchError}, {CpfDV::InvalidLengthError},
|
|
27
|
+
# {CpfDV::ValidationError}
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# require 'cpf-dv'
|
|
31
|
+
#
|
|
32
|
+
# check_digits = CpfDV::CpfCheckDigits.new('054496519')
|
|
33
|
+
# check_digits.cpf # => '05449651910'
|
|
34
|
+
module CpfDV
|
|
9
35
|
end
|
metadata
CHANGED
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cpf-dv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julio L. Muller
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
12
|
-
dependencies:
|
|
13
|
-
|
|
11
|
+
date: 2026-07-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: lacus-utils
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 1.1.0
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 2.0.0
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 1.1.0
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 2.0.0
|
|
33
|
+
description: Utility to calculate check digits on CPF (Brazilian Individual's Taxpayer
|
|
34
|
+
ID)
|
|
14
35
|
email:
|
|
15
36
|
- juliolmuller@outlook.com
|
|
16
37
|
executables: []
|
|
17
38
|
extensions: []
|
|
18
39
|
extra_rdoc_files: []
|
|
19
40
|
files:
|
|
41
|
+
- LICENSE
|
|
42
|
+
- README.md
|
|
20
43
|
- src/cpf-dv.rb
|
|
44
|
+
- src/cpf-dv/cpf_check_digits.rb
|
|
45
|
+
- src/cpf-dv/errors.rb
|
|
21
46
|
- src/cpf-dv/version.rb
|
|
22
47
|
homepage: https://github.com/LacusSolutions/br-utils-ruby
|
|
23
48
|
licenses:
|
|
24
49
|
- MIT
|
|
25
50
|
metadata:
|
|
26
51
|
source_code_uri: https://github.com/LacusSolutions/br-utils-ruby
|
|
27
|
-
changelog_uri: https://github.com/LacusSolutions/br-utils-ruby/blob/main/CHANGELOG.md
|
|
28
52
|
rubygems_mfa_required: 'true'
|
|
29
53
|
post_install_message:
|
|
30
54
|
rdoc_options: []
|
|
@@ -34,7 +58,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
34
58
|
requirements:
|
|
35
59
|
- - ">="
|
|
36
60
|
- !ruby/object:Gem::Version
|
|
37
|
-
version: '3.
|
|
61
|
+
version: '3.1'
|
|
38
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
63
|
requirements:
|
|
40
64
|
- - ">="
|
|
@@ -44,5 +68,5 @@ requirements: []
|
|
|
44
68
|
rubygems_version: 3.4.19
|
|
45
69
|
signing_key:
|
|
46
70
|
specification_version: 4
|
|
47
|
-
summary: Check-digit calculation for CPF (Brazilian
|
|
71
|
+
summary: Check-digit calculation for CPF (Brazilian Individual's Taxpayer ID)
|
|
48
72
|
test_files: []
|