Skip to content

Instantly share code, notes, and snippets.

@stefanlindbohm
Last active June 27, 2018 18:37
Show Gist options
  • Save stefanlindbohm/db032cd9e249eccf0f8e8ae91431d3f0 to your computer and use it in GitHub Desktop.
Save stefanlindbohm/db032cd9e249eccf0f8e8ae91431d3f0 to your computer and use it in GitHub Desktop.
Swedish personal identity number validator & formatter

This is some code golfing that got out of hand. Written in a playground and as such uses simplistic test helpers that just return output strings. Copy and paste into Swift Playgrounds on iOS or a playground in XCode to run.

import Foundation
enum SwedishPersonalIdentityNumberError: Error {
case invalidFormat, incorrectChecksum
}
class SwedishPersonalIdentityNumber: CustomStringConvertible {
var description: String { return personalIdentityNumberString }
private let personalIdentityNumberString: String
private static var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd"
return dateFormatter
}()
init(string: String) throws {
var (centuryString, birthdateString, delimiter, birthNumberString, checksum) =
try SwedishPersonalIdentityNumber.extractComponents(string: string)
if (centuryString.count == 2) {
let yearsSinceBirthdate = SwedishPersonalIdentityNumber.yearsSince(
date: SwedishPersonalIdentityNumber.dateFormatter
.date(from: "\(centuryString)\(birthdateString)")!
)
if (yearsSinceBirthdate >= 100) {
delimiter = "+"
} else {
delimiter = "-"
}
} else if (delimiter.count == 0) {
delimiter = "-"
}
if (!SwedishPersonalIdentityNumber.luhnValidates(digits: "\(birthdateString)\(birthNumberString)\(checksum)")) {
throw SwedishPersonalIdentityNumberError.incorrectChecksum
}
personalIdentityNumberString = "\(birthdateString)\(delimiter)\(birthNumberString)\(checksum)"
}
// MARK: - Date handling
private static func yearsSince(date: Date) -> Int {
return Calendar.current.dateComponents(
[.year],
from: Calendar.current.startOfDay(for: date),
to: Calendar.current.startOfDay(for: Date())
).year!
}
// MARK: - Extracting personal identity number components
private static let personalIdentityNumberRegex = try! NSRegularExpression(
pattern: "^((?:(?:\\d{2})(?=\\d{6}\\D|\\d{10}))?)(\\d{6})([-+]?)(\\d{3})(\\d{1})$", options: []
)
private static func extractComponents(string: String) throws -> (centuryString: String, birthdateString: String, delimiter: String, birthNumberString: String, checksum: Int) {
guard
let result = SwedishPersonalIdentityNumber.personalIdentityNumberRegex
.firstMatch(in: string, options: [], range: NSMakeRange(0, string.count))
else {
throw SwedishPersonalIdentityNumberError.invalidFormat
}
return (
centuryString: String(string[Range(result.range(at: 1), in: string)!]),
birthdateString: String(string[Range(result.range(at: 2), in: string)!]),
delimiter: String(string[Range(result.range(at: 3), in: string)!]),
birthNumberString: String(string[Range(result.range(at: 4), in: string)!]),
checksum: Int(String(string[Range(result.range(at: 5), in: string)!]))!
)
}
// MARK: - Luhn check
private static func luhnValidates(digits: String) -> Bool {
return digits.flatMap { Int(String($0)) }
.reversed()
.enumerated()
.reduce(0) { sum, tuple in
let (i, digit) = tuple
if (i % 2 == 0) {
return sum + digit
} else {
return sum + (digit == 9 ? 9 : digit * 2 % 9)
}
} % 10 == 0
}
}
// MARK: - Test helpers
func expectToEqual<T: Equatable>(_ lhs: T, _ rhs: T) -> String {
return lhs == rhs ? "Pass" : "Fail - expected \(lhs) to equal \(rhs)"
}
func expectToThrow(error: Error, block: () throws -> ()) -> String {
var caughtError: Error?
do { try block() } catch { caughtError = error }
if let caughtError = caughtError {
return caughtError.localizedDescription == error.localizedDescription ?
"Pass" : "Fail - expected to throw \(error), got \(caughtError)"
} else {
return "Fail - expected to throw \(error), nothing was thrown"
}
}
// MARK: - Tests
// it initializes with correct personal number string
let pin1 = try? SwedishPersonalIdentityNumber(string: "811218-9876")
expectToEqual(pin1?.description, "811218-9876")
// it assumes "-" delimiter when no century nor delimiter is given
let pin2 = try? SwedishPersonalIdentityNumber(string: "8112189876")
expectToEqual(pin2?.description, "811218-9876")
// it uses "-" delimiter when given a 12 digit number and birthdate is less than 100 years ago
let pin3 = try? SwedishPersonalIdentityNumber(string: "198112189876")
expectToEqual(pin3?.description, "811218-9876")
// it uses "+" delimiter when given a 12 digit number and birthdate is more than 100 years ago
let pin4 = try? SwedishPersonalIdentityNumber(string: "19171218-9875")
expectToEqual(pin4?.description, "171218+9875")
// it throws .invalidFormat error when given an incorrectly formatted string
expectToThrow(error: SwedishPersonalIdentityNumberError.invalidFormat) {
try SwedishPersonalIdentityNumber(string: "11218-9876")
}
// it throws .incorrectChecksum error when given a number with incorrect checksum
expectToThrow(error: SwedishPersonalIdentityNumberError.incorrectChecksum) {
try SwedishPersonalIdentityNumber(string: "811218-9875")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment