Skip to content

Commit

Permalink
Add initial logic
Browse files Browse the repository at this point in the history
  • Loading branch information
romanmazeev committed Jun 15, 2021
1 parent c33f290 commit c95b7b2
Show file tree
Hide file tree
Showing 17 changed files with 567 additions and 33 deletions.
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions MRZParser/.gitignore

This file was deleted.

This file was deleted.

3 changes: 0 additions & 3 deletions MRZParser/README.md

This file was deleted.

3 changes: 0 additions & 3 deletions MRZParser/Sources/MRZParser/MRZParser.swift

This file was deleted.

11 changes: 0 additions & 11 deletions MRZParser/Tests/MRZParserTests/MRZParserTests.swift

This file was deleted.

File renamed without changes.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# MRZParser
Library for parsing MRZ code

A description of this package.
56 changes: 56 additions & 0 deletions Sources/MRZParser/MRZField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// MRZField.swift
//
//
// Created by Roman Mazeev on 15.06.2021.
//

import Foundation

struct MRZField {
enum FieldType {
case documentType, countryCode, names, documentNumber, nationality, birthdate, sex, expiryDate, personalNumber, optionalData, hash
}

let value: Any?
let rawValue: String
let checkDigit: String?
let isValid: Bool?

init(value: Any?, rawValue: String, checkDigit: String?) {
self.value = value
self.rawValue = rawValue
self.checkDigit = checkDigit
self.isValid = (checkDigit == nil) ? nil : MRZField.isValueValid(rawValue, checkDigit: checkDigit!)
}

// MARK: Static
static func isValueValid(_ value: String, checkDigit: String) -> Bool {
guard let numericCheckDigit = Int(checkDigit) else {
return checkDigit == "<" ? value.trimmingFillers().isEmpty : false
}

var total = 0

for (index, character) in value.enumerated() {
guard let unicodeScalar = character.unicodeScalars.first else { return false }
let charValue: Int

if CharacterSet.uppercaseLetters.contains(unicodeScalar) {
charValue = Int(10 + unicodeScalar.value) - 65
} else if CharacterSet.decimalDigits.contains(unicodeScalar) {
charValue = Int(String(character))!
} else if character == "<" {
charValue = 0
} else {
return false
}

total += charValue * [7, 3, 1][index % 3]
}

return total % 10 == numericCheckDigit
}
}


137 changes: 137 additions & 0 deletions Sources/MRZParser/MRZFieldFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//
// MRZFieldFormatter.swift
//
//
// Created by Roman Mazeev on 15.06.2021.
//

import Foundation

class MRZFieldFormatter {
private let ocrCorrection: Bool

private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(abbreviation: "GMT+0:00")
return formatter
}()

init(ocrCorrection: Bool) {
self.ocrCorrection = ocrCorrection
}

func createField(
type: MRZField.FieldType,
from string: String,
at startIndex: Int,
length: Int,
checkDigitFollows: Bool = false
) -> MRZField {
let endIndex = (startIndex + length)
var rawValue = string.substring(startIndex, to: (endIndex - 1))
var checkDigit = checkDigitFollows ? string.substring(endIndex, to: endIndex) : nil

if ocrCorrection {
rawValue = correct(rawValue, fieldType: type)
checkDigit = (checkDigit == nil) ? nil : correct(checkDigit!, fieldType: type)
}

return MRZField(value: format(rawValue, as: type), rawValue: rawValue, checkDigit: checkDigit)
}

func format(_ string: String, as fieldType: MRZField.FieldType) -> Any? {
switch fieldType {
case .names:
return names(from: string)
case .birthdate:
return birthdate(from: string)
case .sex:
return sex(from: string)
case .expiryDate:
return expiryDate(from: string)
case .documentType, .documentNumber, .countryCode, .nationality, .personalNumber, .optionalData, .hash:
return text(from: string)
}
}

func correct(_ string: String, fieldType: MRZField.FieldType) -> String {
switch fieldType {
case .birthdate, .expiryDate, .hash:
// TODO: Check correction of dates (month & day)
return replaceLetters(in: string)
case .names, .documentType, .countryCode, .nationality:
// TODO: Check documentType, countryCode and nationality against possible (allowed) values
return replaceDigits(in: string)
case .sex:
// TODO: Improve correction (take into account "M" & "<" too)
return string.replace("P", with: "F")
default:
return string
}
}

// MARK: Value Formatters
private func names(from string: String) -> (primary: String, secondary: String) {
let identifiers = string.trimmingFillers().components(separatedBy: "<<").map({ $0.replace("<", with: " ") })
let secondaryID = identifiers.indices.contains(1) ? identifiers[1] : ""
return (primary: identifiers[0], secondary: secondaryID)
}

private func sex(from string: String) -> String? {
switch string {
case "M": return "MALE"
case "F": return "FEMALE"
case "<": return "UNSPECIFIED" // X
default: return nil
}
}

private func birthdate(from string: String) -> Date? {
guard CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string)) else {
return nil
}

let currentYear = Calendar.current.component(.year, from: Date()) - 2000
let parsedYear = Int(string.substring(0, to: 1))!
let centennial = (parsedYear > currentYear) ? "19" : "20"

return dateFormatter.date(from: centennial + string)
}

private func expiryDate(from string: String) -> Date? {
guard CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string)) else {
return nil
}

let parsedYear = Int(string.substring(0, to: 1))!
let centennial = (parsedYear >= 70) ? "19" : "20"

return dateFormatter.date(from: centennial + string)
}

private func text(from string: String) -> String {
return string.trimmingFillers().replace("<", with: " ")
}

// MARK: Utils
private func replaceDigits(in string: String) -> String {
return string
.replace("0", with: "O")
.replace("1", with: "I")
.replace("2", with: "Z")
.replace("8", with: "B")
}

private func replaceLetters(in string: String) -> String {
return string
.replace("O", with: "0")
.replace("Q", with: "0")
.replace("U", with: "0")
.replace("D", with: "0")
.replace("I", with: "1")
.replace("Z", with: "2")
.replace("B", with: "8")
}
}
70 changes: 70 additions & 0 deletions Sources/MRZParser/MRZParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// MRZParser.swift
//
//
// Created by Roman Mazeev on 15.06.2021.
//

import Foundation

public class MRZParser {
let formatter: MRZFieldFormatter

enum MRZFormat: Int {
case td1, td2, td3, invalid
}

public init(ocrCorrection: Bool = false) {
formatter = MRZFieldFormatter(ocrCorrection: ocrCorrection)
}

// MARK: Parsing
public func parse(mrzLines: [String]) -> MRZResult? {
switch self.mrzFormat(from: mrzLines) {
case .td1:
return TD1(from: mrzLines, using: formatter).result
case .td2:
return TD2(from: mrzLines, using: formatter).result
case .td3:
return TD3(from: mrzLines, using: formatter).result
case .invalid:
return nil
}
}

public func parse(mrzString: String) -> MRZResult? {
return parse(mrzLines: mrzString.components(separatedBy: "\n"))
}

// MARK: MRZ-Format detection
fileprivate func mrzFormat(from mrzLines: [String]) -> MRZFormat {
switch mrzLines.count {
case 2:
let lineLength = uniformedLineLength(for: mrzLines)
let possibleFormats = [MRZFormat.td2: TD2.lineLength, .td3: TD3.lineLength]

for (format, requiredLineLength) in possibleFormats where lineLength == requiredLineLength {
return format
}

return .invalid
case 3:
return (uniformedLineLength(for: mrzLines) == TD1.lineLength) ? .td1 : .invalid
default:
return .invalid
}
}

fileprivate func uniformedLineLength(for mrzLines: [String]) -> Int? {
guard let lineLength = mrzLines.first?.count else {
return nil
}

if mrzLines.contains(where: { $0.count != lineLength }) {
return nil
}

return lineLength
}
}

33 changes: 33 additions & 0 deletions Sources/MRZParser/MRZResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// MRZResult.swift
//
//
// Created by Roman Mazeev on 15.06.2021.
//

import Foundation

public struct MRZResult {
public let documentType: String
public let countryCode: String
public let surnames: String
public let givenNames: String
public let documentNumber: String
public let nationalityCountryCode: String
/// `nil` if formatting failed
public let birthdate: Date?
/// `nil` if formatting failed
public let sex: String?
/// `nil` if formatting failed
public let expiryDate: Date?
public let personalNumber: String
/// `nil` if not provided
public let personalNumber2: String?

public let isDocumentNumberValid: Bool
public let isBirthdateValid: Bool
public let isExpiryDateValid: Bool
public let isPersonalNumberValid: Bool?
public let allCheckDigitsValid: Bool
}

Loading

0 comments on commit c95b7b2

Please sign in to comment.