diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/MRZParser/.gitignore b/MRZParser/.gitignore
deleted file mode 100644
index bb460e7..0000000
--- a/MRZParser/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-.DS_Store
-/.build
-/Packages
-/*.xcodeproj
-xcuserdata/
-DerivedData/
-.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
diff --git a/MRZParser/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MRZParser/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/MRZParser/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/MRZParser/README.md b/MRZParser/README.md
deleted file mode 100644
index 6fc5494..0000000
--- a/MRZParser/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# MRZParser
-
-A description of this package.
diff --git a/MRZParser/Sources/MRZParser/MRZParser.swift b/MRZParser/Sources/MRZParser/MRZParser.swift
deleted file mode 100644
index 597d771..0000000
--- a/MRZParser/Sources/MRZParser/MRZParser.swift
+++ /dev/null
@@ -1,3 +0,0 @@
-struct MRZParser {
- var text = "Hello, World!"
-}
diff --git a/MRZParser/Tests/MRZParserTests/MRZParserTests.swift b/MRZParser/Tests/MRZParserTests/MRZParserTests.swift
deleted file mode 100644
index bc25ade..0000000
--- a/MRZParser/Tests/MRZParserTests/MRZParserTests.swift
+++ /dev/null
@@ -1,11 +0,0 @@
- import XCTest
- @testable import MRZParser
-
- final class MRZParserTests: XCTestCase {
- func testExample() {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct
- // results.
- XCTAssertEqual(MRZParser().text, "Hello, World!")
- }
- }
diff --git a/MRZParser/Package.swift b/Package.swift
similarity index 100%
rename from MRZParser/Package.swift
rename to Package.swift
diff --git a/README.md b/README.md
index fbd3724..6fc5494 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
# MRZParser
-Library for parsing MRZ code
+
+A description of this package.
diff --git a/Sources/MRZParser/MRZField.swift b/Sources/MRZParser/MRZField.swift
new file mode 100644
index 0000000..d813fde
--- /dev/null
+++ b/Sources/MRZParser/MRZField.swift
@@ -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
+ }
+}
+
+
diff --git a/Sources/MRZParser/MRZFieldFormatter.swift b/Sources/MRZParser/MRZFieldFormatter.swift
new file mode 100644
index 0000000..e80c906
--- /dev/null
+++ b/Sources/MRZParser/MRZFieldFormatter.swift
@@ -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")
+ }
+}
diff --git a/Sources/MRZParser/MRZParser.swift b/Sources/MRZParser/MRZParser.swift
new file mode 100644
index 0000000..5908156
--- /dev/null
+++ b/Sources/MRZParser/MRZParser.swift
@@ -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
+ }
+}
+
diff --git a/Sources/MRZParser/MRZResult.swift b/Sources/MRZParser/MRZResult.swift
new file mode 100644
index 0000000..b35fa5a
--- /dev/null
+++ b/Sources/MRZParser/MRZResult.swift
@@ -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
+}
+
diff --git a/Sources/MRZParser/Parsers/TD1.swift b/Sources/MRZParser/Parsers/TD1.swift
new file mode 100644
index 0000000..e765990
--- /dev/null
+++ b/Sources/MRZParser/Parsers/TD1.swift
@@ -0,0 +1,71 @@
+//
+// TD1.swift
+// QKMRZParser
+//
+// Created by Matej Dorcak on 14/10/2018.
+//
+
+import Foundation
+
+class TD1 {
+ static let lineLength = 30
+ fileprivate let finalCheckDigit: String
+ let documentType: MRZField
+ let countryCode: MRZField
+ let documentNumber: MRZField
+ let optionalData: MRZField
+ let birthdate: MRZField
+ let sex: MRZField
+ let expiryDate: MRZField
+ let nationality: MRZField
+ let optionalData2: MRZField
+ let names: MRZField
+
+ fileprivate lazy var allCheckDigitsValid: Bool = {
+ let compositedValue = [documentNumber, optionalData, birthdate, expiryDate, optionalData2].reduce("", { ($0 + $1.rawValue + ($1.checkDigit ?? "")) })
+ let isCompositedValueValid = MRZField.isValueValid(compositedValue, checkDigit: finalCheckDigit)
+ return (documentNumber.isValid! && birthdate.isValid! && expiryDate.isValid! && isCompositedValueValid)
+ }()
+
+ lazy var result: MRZResult = {
+ let (surnames, givenNames) = names.value as! (String, String)
+
+ return MRZResult(
+ documentType: documentType.value as! String,
+ countryCode: countryCode.value as! String,
+ surnames: surnames,
+ givenNames: givenNames,
+ documentNumber: documentNumber.value as! String,
+ nationalityCountryCode: nationality.value as! String,
+ birthdate: birthdate.value as! Date?,
+ sex: sex.value as! String?,
+ expiryDate: expiryDate.value as! Date?,
+ personalNumber: optionalData.value as! String,
+ personalNumber2: (optionalData2.value as! String),
+
+ isDocumentNumberValid: documentNumber.isValid!,
+ isBirthdateValid: birthdate.isValid!,
+ isExpiryDateValid: expiryDate.isValid!,
+ isPersonalNumberValid: nil,
+ allCheckDigitsValid: allCheckDigitsValid
+ )
+ }()
+
+ init(from mrzLines: [String], using formatter: MRZFieldFormatter) {
+ let (firstLine, secondLine, thirdLine) = (mrzLines[0], mrzLines[1], mrzLines[2])
+
+ documentType = formatter.createField(type: .documentType, from: firstLine, at: 0, length: 2)
+ countryCode = formatter.createField(type: .countryCode, from: firstLine, at: 2, length: 3)
+ documentNumber = formatter.createField(type: .documentNumber, from: firstLine, at: 5, length: 9, checkDigitFollows: true)
+ optionalData = formatter.createField(type: .optionalData, from: firstLine, at: 15, length: 15)
+
+ birthdate = formatter.createField(type: .birthdate, from: secondLine, at: 0, length: 6, checkDigitFollows: true)
+ sex = formatter.createField(type: .sex, from: secondLine, at: 7, length: 1)
+ expiryDate = formatter.createField(type: .expiryDate, from: secondLine, at: 8, length: 6, checkDigitFollows: true)
+ nationality = formatter.createField(type: .nationality, from: secondLine, at: 15, length: 3)
+ optionalData2 = formatter.createField(type: .optionalData, from: secondLine, at: 18, length: 11)
+ finalCheckDigit = formatter.createField(type: .hash, from: secondLine, at: 29, length: 1).rawValue
+
+ names = formatter.createField(type: .names, from: thirdLine, at: 0, length: 29)
+ }
+}
diff --git a/Sources/MRZParser/Parsers/TD2.swift b/Sources/MRZParser/Parsers/TD2.swift
new file mode 100644
index 0000000..2fa6d5b
--- /dev/null
+++ b/Sources/MRZParser/Parsers/TD2.swift
@@ -0,0 +1,75 @@
+//
+// TD2.swift
+// QKMRZParser
+//
+// Created by Matej Dorcak on 14/10/2018.
+//
+
+import Foundation
+
+class TD2 {
+ static let lineLength = 36
+ fileprivate let finalCheckDigit: String?
+ let documentType: MRZField
+ let countryCode: MRZField
+ let names: MRZField
+ let documentNumber: MRZField
+ let nationality: MRZField
+ let birthdate: MRZField
+ let sex: MRZField
+ let expiryDate: MRZField
+ let optionalData: MRZField
+
+ fileprivate lazy var allCheckDigitsValid: Bool = {
+ if let checkDigit = finalCheckDigit {
+ let compositedValue = [documentNumber, birthdate, expiryDate, optionalData].reduce("", { ($0 + $1.rawValue + ($1.checkDigit ?? "")) })
+ let isCompositedValueValid = MRZField.isValueValid(compositedValue, checkDigit: checkDigit)
+ return (documentNumber.isValid! && birthdate.isValid! && expiryDate.isValid! && isCompositedValueValid)
+ }
+ else {
+ return (documentNumber.isValid! && birthdate.isValid! && expiryDate.isValid!)
+ }
+ }()
+
+ lazy var result: MRZResult = {
+ let (surnames, givenNames) = names.value as! (String, String)
+
+ return MRZResult(
+ documentType: documentType.value as! String,
+ countryCode: countryCode.value as! String,
+ surnames: surnames,
+ givenNames: givenNames,
+ documentNumber: documentNumber.value as! String,
+ nationalityCountryCode: nationality.value as! String,
+ birthdate: birthdate.value as! Date?,
+ sex: sex.value as! String?,
+ expiryDate: expiryDate.value as! Date?,
+ personalNumber: optionalData.value as! String,
+ personalNumber2: nil,
+
+ isDocumentNumberValid: documentNumber.isValid!,
+ isBirthdateValid: birthdate.isValid!,
+ isExpiryDateValid: expiryDate.isValid!,
+ isPersonalNumberValid: nil,
+ allCheckDigitsValid: allCheckDigitsValid
+ )
+ }()
+
+ init(from mrzLines: [String], using formatter: MRZFieldFormatter) {
+ let (firstLine, secondLine) = (mrzLines[0], mrzLines[1])
+ /// MRV-B type
+ let isVisaDocument = (firstLine.substring(0, to: 0) == "V")
+
+ documentType = formatter.createField(type: .documentType, from: firstLine, at: 0, length: 2)
+ countryCode = formatter.createField(type: .countryCode, from: firstLine, at: 2, length: 3)
+ names = formatter.createField(type: .names, from: firstLine, at: 5, length: 31)
+
+ documentNumber = formatter.createField(type: .documentNumber, from: secondLine, at: 0, length: 9, checkDigitFollows: true)
+ nationality = formatter.createField(type: .nationality, from: secondLine, at: 10, length: 3)
+ birthdate = formatter.createField(type: .birthdate, from: secondLine, at: 13, length: 6, checkDigitFollows: true)
+ sex = formatter.createField(type: .sex, from: secondLine, at: 20, length: 1)
+ expiryDate = formatter.createField(type: .expiryDate, from: secondLine, at: 21, length: 6, checkDigitFollows: true)
+ optionalData = formatter.createField(type: .optionalData, from: secondLine, at: 28, length: isVisaDocument ? 8 : 7)
+ finalCheckDigit = isVisaDocument ? nil : formatter.createField(type: .hash, from: secondLine, at: 35, length: 1).rawValue
+ }
+}
diff --git a/Sources/MRZParser/Parsers/TD3.swift b/Sources/MRZParser/Parsers/TD3.swift
new file mode 100644
index 0000000..2427476
--- /dev/null
+++ b/Sources/MRZParser/Parsers/TD3.swift
@@ -0,0 +1,83 @@
+//
+// TD3.swift
+// QKMRZParser
+//
+// Created by Matej Dorcak on 14/10/2018.
+//
+
+import Foundation
+
+class TD3 {
+ static let lineLength = 44
+ fileprivate let finalCheckDigit: String?
+ let documentType: MRZField
+ let countryCode: MRZField
+ let names: MRZField
+ let documentNumber: MRZField
+ let nationality: MRZField
+ let birthdate: MRZField
+ let sex: MRZField
+ let expiryDate: MRZField
+ let personalNumber: MRZField
+
+ fileprivate lazy var allCheckDigitsValid: Bool = {
+ if let checkDigit = finalCheckDigit {
+ let compositedValue = [documentNumber, birthdate, expiryDate, personalNumber].reduce("", { ($0 + $1.rawValue + $1.checkDigit!) })
+ let isCompositedValueValid = MRZField.isValueValid(compositedValue, checkDigit: checkDigit)
+ return (documentNumber.isValid! && birthdate.isValid! && expiryDate.isValid! && personalNumber.isValid! && isCompositedValueValid)
+ }
+ else {
+ return (documentNumber.isValid! && birthdate.isValid! && expiryDate.isValid!)
+ }
+ }()
+
+ lazy var result: MRZResult = {
+ let (surnames, givenNames) = names.value as! (String, String)
+
+ return MRZResult(
+ documentType: documentType.value as! String,
+ countryCode: countryCode.value as! String,
+ surnames: surnames,
+ givenNames: givenNames,
+ documentNumber: documentNumber.value as! String,
+ nationalityCountryCode: nationality.value as! String,
+ birthdate: birthdate.value as! Date?,
+ sex: sex.value as! String?,
+ expiryDate: expiryDate.value as! Date?,
+ personalNumber: personalNumber.value as! String,
+ personalNumber2: nil,
+
+ isDocumentNumberValid: documentNumber.isValid!,
+ isBirthdateValid: birthdate.isValid!,
+ isExpiryDateValid: expiryDate.isValid!,
+ isPersonalNumberValid: personalNumber.isValid,
+ allCheckDigitsValid: allCheckDigitsValid
+ )
+ }()
+
+ init(from mrzLines: [String], using formatter: MRZFieldFormatter) {
+ let (firstLine, secondLine) = (mrzLines[0], mrzLines[1])
+
+ /// MRV-A type
+ let isVisaDocument = (firstLine.substring(0, to: 0) == "V")
+
+ documentType = formatter.createField(type: .documentType, from: firstLine, at: 0, length: 2)
+ countryCode = formatter.createField(type: .countryCode, from: firstLine, at: 2, length: 3)
+ names = formatter.createField(type: .names, from: firstLine, at: 5, length: 39)
+
+ documentNumber = formatter.createField(type: .documentNumber, from: secondLine, at: 0, length: 9, checkDigitFollows: true)
+ nationality = formatter.createField(type: .nationality, from: secondLine, at: 10, length: 3)
+ birthdate = formatter.createField(type: .birthdate, from: secondLine, at: 13, length: 6, checkDigitFollows: true)
+ sex = formatter.createField(type: .sex, from: secondLine, at: 20, length: 1)
+ expiryDate = formatter.createField(type: .expiryDate, from: secondLine, at: 21, length: 6, checkDigitFollows: true)
+
+ if isVisaDocument {
+ personalNumber = formatter.createField(type: .optionalData, from: secondLine, at: 28, length: 16)
+ finalCheckDigit = nil
+ }
+ else {
+ personalNumber = formatter.createField(type: .personalNumber, from: secondLine, at: 28, length: 14, checkDigitFollows: true)
+ finalCheckDigit = formatter.createField(type: .hash, from: secondLine, at: 43, length: 1).rawValue
+ }
+ }
+}
diff --git a/Sources/MRZParser/String+TrimmingFillers.swift b/Sources/MRZParser/String+TrimmingFillers.swift
new file mode 100644
index 0000000..5e9a596
--- /dev/null
+++ b/Sources/MRZParser/String+TrimmingFillers.swift
@@ -0,0 +1,28 @@
+//
+// String+TrimmingFillers.swift
+//
+//
+// Created by Roman Mazeev on 15.06.2021.
+//
+
+import Foundation
+
+// MARK: Parser related
+extension String {
+ func trimmingFillers() -> String {
+ return trimmingCharacters(in: CharacterSet(charactersIn: "<"))
+ }
+}
+
+// MARK: Generic
+extension String {
+ func replace(_ target: String, with: String) -> String {
+ return replacingOccurrences(of: target, with: with, options: .literal, range: nil)
+ }
+
+ func substring(_ from: Int, to: Int) -> String {
+ let fromIndex = index(startIndex, offsetBy: from)
+ let toIndex = index(startIndex, offsetBy: to + 1)
+ return String(self[fromIndex..