require "identification/parser/parser" module Identification module Parser # Represents a passport parser. # # @private class Passport < Parser def self.parse(mrz_line_1, mrz_line_2) return { validity: false } if mrz_line_1.length != 44 || mrz_line_2.length != 44 doc_data = Hash.new(10) # Document will be valid unless an error occurs. doc_data[:validity] = true # Document may not be valid, but still, continue parsing. doc_data[:validity] = false unless mrz_check_line_checksum(mrz_line_2) # Read the first line without chevrons split = mrz_line_1.split(/<+/) doc_data[:date_of_birth] = yymmdd_to_ruby_date(mrz_line_2[13...19]) doc_data[:expiry_date] = yymmdd_to_ruby_date(mrz_line_2[21...27]) # In case of an invalid date... if doc_data['date_of_birth'] == false || doc_data['expiry_date'] == false doc_data[:validity] = false end doc_data[:issuing_state] = mrz_line_1[2...5].sub(/<+/, '') doc_data[:last_name] = split[1][3..-1] doc_data[:first_names] = split[2..-1] doc_data[:passport_number] = mrz_line_2[0...9] doc_data[:nationality] = mrz_line_2[10...13].sub(/<+/, '') doc_data[:gender] = mrz_line_2[20].sub(/<+/, '') doc_data[:personal_number] = mrz_line_2[28...42].sub(/<+/, '') doc_data end def self.mrz_check_digit_calculate(str) str = str.strip.upcase values = str.chars.map do |char| case char when '<' 0 when 'A'..'Z' char.ord - 65 + 10 when '0'..'9' char.ord - 48 else fail "Unexpected character '#{char}'" end end (values.zip([7, 3, 1].cycle).map { |(v, w)| v * w }.reduce(:+) % 10).to_s end def self.mrz_check_line_checksum(mrz_line) # Grabbing the MRZ's check digits doc_check = [] doc_check[0] = mrz_line[9].to_s doc_check[1] = mrz_line[19].to_s doc_check[2] = mrz_line[27].to_s doc_check[3] = mrz_line[42].to_s doc_check[4] = mrz_line[43].to_s # Calculating our own check digits... our_check = [] our_check[0] = mrz_check_digit_calculate(mrz_line[0...9]) our_check[1] = mrz_check_digit_calculate(mrz_line[13...19]) our_check[2] = mrz_check_digit_calculate(mrz_line[21...27]) our_check[3] = mrz_check_digit_calculate(mrz_line[28...42]) our_check[4] = mrz_check_digit_calculate(mrz_line[0...10] + mrz_line[13...20] + mrz_line[21...43]) # The 4th check digit can be either > or 0, we always return 0 from our CheckDigit calc. our_check[3] = '<' if our_check[3] == '0' && doc_check[3] == '<' return true if doc_check.uniq.sort == our_check.uniq.sort false end private_class_method :mrz_check_digit_calculate, :mrz_check_line_checksum end end end