I wrote this Ruby Mod 11 (perhaps it's a bit generous to call it that) implementation to validate Brazilian CPF documents, which use a Mod 11 algorithm. CPF format is a 9 digit number followed by two checksums. It's often presented in this format 012.345.678-90
. Feedback is welcome.
class CPF
attr_reader :digits
LENGTH = 11
def self.valid?(number)
new(number).valid?
end
def self.mask(number)
new(number).mask
end
def initialize(number)
@digits = number.to_s.gsub(/\D/, '').each_char.map(&:to_i)
end
def valid?
correct_length? && !black_listed? && checksums_match?
end
# The `black_listed?` method checks common number patterns.
#
# It tests to see if the provided CPF is the same digit repeated n times.
#
def black_listed?
digits.join =~ /^12345678909|(\d)\1{10}$/
end
# A valid CPF must be 11 digits long.
#
def correct_length?
digits.size == LENGTH
end
# A CPF is only valid if the first and second checksum digits are what they should be.
#
def checksums_match?
first_checksum_matches? && second_checksum_matches?
end
# This returns the CPF is a human readable format.
#
# CPF.mask("01234567890")
# => "012.345.678-90"
#
def mask
[digits.first(9).each_slice(3).map(&:join).join("."), digits.last(2).join].join('-') if valid?
end
private
def first_checksum_matches?
checksum_one == digits.at(9)
end
def second_checksum_matches?
checksum_two == digits.at(10)
end
def checksum_one
digit = sum_at(10)
digit = calculate(digit)
digit = 0 if digit > 9
digit
end
def checksum_two
digit = sum_at(11) + (2 * checksum_one)
digit = calculate(digit)
digit = 0 if digit > 9
digit
end
def sum_at(position)
digits.slice(0..8).collect.with_index { |n, i| n * (position - i) }.reduce(:+)
end
def calculate(digit)
LENGTH - (digit % LENGTH)
end
end
Tests
require 'test_helper'
class CpfTest < ActiveSupport::TestCase
test 'black listed numbers are not' do
black_list = %w(00000000000 11111111111 22222222222 33333333333 44444444444
55555555555 66666666666 77777777777 88888888888 99999999999)
black_list.each do |number|
assert_invalid number
end
end
test 'nil is not a valid CPF' do
assert_invalid nil
end
test 'blank is not a valid CPF' do
assert_invalid ''
end
test 'valid CPF' do
assert_valid '01234567890'
end
test 'masked valid CPF' do
assert_valid '012.345.678-90'
end
test 'mask returns a masked CPF when CPF is valid' do
assert_equal '012.345.678-90', CPF.mask('01234567890')
end
test 'mask returns nil when CPF is not valid' do
assert_nil CPF.mask('0123456789')
end
private
def assert_invalid(number)
refute CPF.valid?(number), "Expected #{number || 'nil'} to be an invalid CPF"
end
def assert_valid(number)
assert CPF.valid?(number), "Expected #{number} to be a valid CPF"
end
end
mask
. I think the name is non-obvious, at least to me (might be obvious in your context, though). Why not simply use the defaultto_s
instead? – DarkDust Aug 29 '14 at 17:37to_s
. – Mohamad Aug 29 '14 at 19:12CPF.mask("01234567890") #=> "012.345.678-90"
– Mohamad Aug 29 '14 at 19:27