Skip to content

Commit 1878001

Browse files
authored
feat: add iban validation rule (#88)
1 parent be520ea commit 1878001

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ struct RegistrationView: View {
314314
| `ContainsValidationRule` | Validates that a string contains a specific substring | `ContainsValidationRule(substring: "@", error: "Must contain @")`
315315
| `EqualityValidationRule`| Validates that the input is equal to a given reference value | `EqualityValidationRule(compareTo: password, error: "Passwords do not match")`
316316
| `ComparisonValidationRule` | Validates that input against a comparison constraint | `ComparisonValidationRule(greaterThan: 0, error: "Must be greater than 0")`
317+
| `IBANValidationRule` | Validates that a string is a valid IBAN (International Bank Account Number) | `IBANValidationRule(error: "Invalid IBAN")`
317318

318319
## Custom Validators
319320

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// Validator
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
/// Validates that a string is a valid IBAN (International Bank Account Number).
7+
///
8+
/// Uses a simplified IBAN validation algorithm:
9+
/// 1. Checks length according to country code (2 letters + digits)
10+
/// 2. Moves first 4 characters to the end
11+
/// 3. Converts letters to numbers (A=10, B=11, ..., Z=35)
12+
/// 4. Checks if the resulting number mod 97 equals 1
13+
///
14+
/// # Example:
15+
/// ```swift
16+
/// let rule = IBANValidationRule(error: "Invalid IBAN")
17+
/// rule.validate(input: "GB82WEST12345698765432") // true
18+
/// rule.validate(input: "INVALIDIBAN") // false
19+
/// ```
20+
public struct IBANValidationRule: IValidationRule {
21+
// MARK: - Types
22+
23+
public typealias Input = String
24+
25+
// MARK: Properties
26+
27+
/// The validation error returned when validation fails.
28+
public let error: IValidationError
29+
30+
// MARK: Initialization
31+
32+
/// Creates an IBAN validation rule.
33+
///
34+
/// - Parameter error: The validation error returned if validation fails.
35+
public init(error: IValidationError) {
36+
self.error = error
37+
}
38+
39+
// MARK: IValidationRule
40+
41+
public func validate(input: String) -> Bool {
42+
let trimmed = input.replacingOccurrences(of: " ", with: "").uppercased()
43+
guard trimmed.count >= 4 else { return false }
44+
45+
let rearranged = String(trimmed.dropFirst(4) + trimmed.prefix(4))
46+
47+
var numericString = ""
48+
for char in rearranged {
49+
if let digit = char.wholeNumberValue {
50+
numericString.append(String(digit))
51+
} else if let ascii = char.asciiValue, ascii >= 65, ascii <= 90 {
52+
numericString.append(String(Int(ascii) - 55))
53+
} else {
54+
return false
55+
}
56+
}
57+
58+
var remainder = 0
59+
var chunk = ""
60+
for char in numericString {
61+
chunk.append(char)
62+
if let number = Int(chunk), number >= 97 {
63+
remainder = number % 97
64+
chunk = String(remainder)
65+
}
66+
}
67+
68+
remainder = (Int(chunk) ?? 0) % 97
69+
return remainder == 1
70+
}
71+
}

Sources/ValidatorCore/Validator.docc/Overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for
3737
- ``ContainsValidationRule``
3838
- ``EqualityValidationRule``
3939
- ``ComparisonValidationRule``
40+
- ``IBANValidationRule``
4041

4142
### Articles
4243

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// Validator
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
@testable import ValidatorCore
7+
import XCTest
8+
9+
// MARK: - IBANValidationRuleTests
10+
11+
final class IBANValidationRuleTests: XCTestCase {
12+
// MARK: Properties
13+
14+
private var sut: IBANValidationRule!
15+
16+
// MARK: XCTestCase
17+
18+
override func setUp() {
19+
super.setUp()
20+
sut = IBANValidationRule(error: String.error)
21+
}
22+
23+
override func tearDown() {
24+
sut = nil
25+
super.tearDown()
26+
}
27+
28+
// MARK: Tests
29+
30+
func test_thatValidationRuleSetsProperties() {
31+
// then
32+
XCTAssertEqual(sut.error.message, .error)
33+
}
34+
35+
func test_thatIBANValidationRuleValidatesInput_whenInputIsCorrectValue() {
36+
let validIBANs = [
37+
"GB82 WEST 1234 5698 7654 32",
38+
"DE89370400440532013000",
39+
"FR1420041010050500013M02606",
40+
]
41+
42+
for iban in validIBANs {
43+
XCTAssertTrue(sut.validate(input: iban))
44+
}
45+
}
46+
47+
func test_thatIBANValidationRuleValidatesInput_whenInputIsInCorrectValue() {
48+
let invalidIBANs = [
49+
"GB82WEST12345698765431",
50+
"INVALIDIBAN",
51+
"DE123",
52+
]
53+
54+
for iban in invalidIBANs {
55+
XCTAssertFalse(sut.validate(input: iban))
56+
}
57+
}
58+
59+
func test_thatIBANValidationRuleValidatesInput_whenInputIsEmpty() {
60+
XCTAssertFalse(sut.validate(input: ""))
61+
}
62+
63+
func test_thatIBANValidationRuleValidatesInput_whenInputHasInvalidCharacters() {
64+
XCTAssertFalse(sut.validate(input: "GB82WEST1234$5698765432"))
65+
}
66+
}
67+
68+
// MARK: Constants
69+
70+
private extension String {
71+
static let error = "Invalid IBAN"
72+
}

0 commit comments

Comments
 (0)