# frozen_string_literal: true require 'active_support/all' require_relative 'codici_catastali' module Cfita # Controllo codice fiscale italiano class CodiceFiscale attr_reader :fiscal_code, :sex, :birth_place, :birth_date, :errors def self.ccat @ccat ||= JSON.parse(open('ccat.json')) end def initialize( fiscal_code, birth_place: nil, birth_date: nil, name: nil, surname: nil, sex: nil ) @fiscal_code = fiscal_code.to_s.upcase.strip @birth_place = birth_place&.upcase @birth_date = birth_date @birth_date = Date.parse(birth_date) if birth_date.is_a?(String) @name = name&.parameterize&.upcase @surname = surname&.parameterize&.upcase @sex = sex&.upcase @errors = [] parse end def to_s fiscal_code end def valid? errors.empty? end def cf_sex return @cf_sex if @cf_sex case @fiscal_code[9] when nil when /[0-3LMNP]/ @cf_sex = 'M' when /[4-7QRST]/ @cf_sex = 'F' else @errors << 'Cifra decina giorno di nascita errata' nil end end def cf_birth_places return @cf_birth_places if @cf_birth_places @cf_birth_places = CODICI_CATASTALI[codice_catastale] end def cf_birth_date return @cf_birth_date if @cf_birth_date yy = cifre(6..7) day = cifre(9..10) return unless yy && day @errors << 'Cifra decina giorno di nascita errata' if day && day > 71 month = MESI.index(@fiscal_code[8]) @errors << 'Mese errato' unless month return unless yy && month && day @cf_birth_date = Date.new(yy2yyyy(yy), month + 1, day % 40) rescue nil end private def codice_catastale return @codice_catastale if @codice_catastale return unless letter = @fiscal_code[11] return unless numbers = @fiscal_code[12..14] numbers = numbers.chars.map do |c| i = OMOCODICI.index(c) i ? i.to_s : c end.join letter + numbers end def parse check_size check_chars return if errors.any? check_checksum return if errors.any? check_name check_surname check_birth_date check_birth_place check_sex end def check_name return unless @name a, b, c, d = consonants(@name) name_code = ( (d ? [a, c, d] : [a, b, c]).compact.join + vowels(@name).join + 'XXX' )[0..2] errors << "Il nome non corrisponde al codice '#{name_code}'" unless name_code == @fiscal_code[3..5] end def check_surname return unless @surname surname_code = ( consonants(@surname).join + vowels(@surname).join + 'XXX' )[0..2] errors << "Il cognome non corrisponde al codice '#{surname_code}'" unless surname_code == @fiscal_code[0..2] end VOWELS = 'AEIOU'.chars.freeze CONSONANTS = (('A'..'Z').to_a - VOWELS).freeze def vowels(word) word.chars.select { |char| char.in? VOWELS } end def consonants(word) word.chars.select { |char| char.in? CONSONANTS } end def check_size size = @fiscal_code.size errors << "Lunghezza errata (#{size})" unless size == 16 end def check_chars test = @fiscal_code == @fiscal_code[/^[A-Z0-9]*$/] errors << 'Caratteri non ammessi' unless test end def check_checksum errors << 'Checksum errato' if checksum != @fiscal_code.last end def check_sex if @sex errors << 'Sesso errato' if @sex != cf_sex else @sex = cf_sex end end def check_birth_place @errors << "Codice istat #{codice_catastale} non trovato" unless cf_birth_places if @birth_place unless cf_birth_places&.include?(@birth_place) @errors << "Luogo di nascita #{@birth_place} non coerente, al codice catastale #{codice_catastale} corrisponde a #{cf_birth_places.join(' o a ')}" end end end MESI = 'ABCDEHLMPRST' def yy2yyyy(year_as_yy) Date.today.year - (Date.today.year % 100 + 100 - year_as_yy) % 100 end def check_birth_date @errors << 'Data di nascita errata' unless cf_birth_date return if @errors.any? if @birth_date @errors << 'Data di nascita errata' if @birth_date != cf_birth_date else @birth_date = cf_birth_date end end def cifre(range) result = 0 range.each do |position| char = @fiscal_code[position] return nil unless char value = CIFRE.index(char) @errors << "Carattere '#{char}' errato in posizione #{position}" unless value return nil unless value result *= 10 result += value % 10 end result end DISPARI = [ 1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23 ].freeze OMOCODICI = 'LMNPQRSTUV' CIFRE = ('0123456789' + OMOCODICI).freeze def checksum tot = 0 @fiscal_code[0..14].bytes.first(15).each.with_index do |byte, i| next unless byte byte -= byte < 65 ? 48 : 65 sign = (i % 2).zero? tot += sign ? DISPARI[byte] : byte end (tot % 26 + 65).chr end end end