Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ struct RegistrationView: View {
| `ContainsValidationRule` | Validates that a string contains a specific substring | `ContainsValidationRule(substring: "@", error: "Must contain @")`
| `EqualityValidationRule`| Validates that the input is equal to a given reference value | `EqualityValidationRule(compareTo: password, error: "Passwords do not match")`
| `ComparisonValidationRule` | Validates that input against a comparison constraint | `ComparisonValidationRule(greaterThan: 0, error: "Must be greater than 0")`
| `IBANValidationRule` | Validates that a string is a valid IBAN (International Bank Account Number) | `IBANValidationRule(error: "Invalid IBAN")`

## Custom Validators

Expand Down
71 changes: 71 additions & 0 deletions Sources/ValidatorCore/Classes/Rules/IBANValidationRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// Validator
// Copyright © 2025 Space Code. All rights reserved.
//

/// Validates that a string is a valid IBAN (International Bank Account Number).
///
/// Uses a simplified IBAN validation algorithm:
/// 1. Checks length according to country code (2 letters + digits)
/// 2. Moves first 4 characters to the end
/// 3. Converts letters to numbers (A=10, B=11, ..., Z=35)
/// 4. Checks if the resulting number mod 97 equals 1
///
/// # Example:
/// ```swift
/// let rule = IBANValidationRule(error: "Invalid IBAN")
/// rule.validate(input: "GB82WEST12345698765432") // true
/// rule.validate(input: "INVALIDIBAN") // false
/// ```
public struct IBANValidationRule: IValidationRule {
// MARK: - Types

public typealias Input = String

// MARK: Properties

/// The validation error returned when validation fails.
public let error: IValidationError

// MARK: Initialization

/// Creates an IBAN validation rule.
///
/// - Parameter error: The validation error returned if validation fails.
public init(error: IValidationError) {
self.error = error
}

// MARK: IValidationRule

public func validate(input: String) -> Bool {
let trimmed = input.replacingOccurrences(of: " ", with: "").uppercased()
guard trimmed.count >= 4 else { return false }

let rearranged = String(trimmed.dropFirst(4) + trimmed.prefix(4))

var numericString = ""
for char in rearranged {
if let digit = char.wholeNumberValue {
numericString.append(String(digit))
} else if let ascii = char.asciiValue, ascii >= 65, ascii <= 90 {
numericString.append(String(Int(ascii) - 55))
} else {
return false
}
}

var remainder = 0
var chunk = ""
for char in numericString {
chunk.append(char)
if let number = Int(chunk), number >= 97 {
remainder = number % 97
chunk = String(remainder)
}
}

remainder = (Int(chunk) ?? 0) % 97
return remainder == 1
}
}
1 change: 1 addition & 0 deletions Sources/ValidatorCore/Validator.docc/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for
- ``ContainsValidationRule``
- ``EqualityValidationRule``
- ``ComparisonValidationRule``
- ``IBANValidationRule``

### Articles

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// Validator
// Copyright © 2025 Space Code. All rights reserved.
//

@testable import ValidatorCore
import XCTest

// MARK: - IBANValidationRuleTests

final class IBANValidationRuleTests: XCTestCase {
// MARK: Properties

private var sut: IBANValidationRule!

// MARK: XCTestCase

override func setUp() {
super.setUp()
sut = IBANValidationRule(error: String.error)
}

override func tearDown() {
sut = nil
super.tearDown()
}

// MARK: Tests

func test_thatValidationRuleSetsProperties() {
// then
XCTAssertEqual(sut.error.message, .error)
}

func test_thatIBANValidationRuleValidatesInput_whenInputIsCorrectValue() {
let validIBANs = [
"GB82 WEST 1234 5698 7654 32",
"DE89370400440532013000",
"FR1420041010050500013M02606",
]

for iban in validIBANs {
XCTAssertTrue(sut.validate(input: iban))
}
}

func test_thatIBANValidationRuleValidatesInput_whenInputIsInCorrectValue() {
let invalidIBANs = [
"GB82WEST12345698765431",
"INVALIDIBAN",
"DE123",
]

for iban in invalidIBANs {
XCTAssertFalse(sut.validate(input: iban))
}
}

func test_thatIBANValidationRuleValidatesInput_whenInputIsEmpty() {
XCTAssertFalse(sut.validate(input: ""))
}

func test_thatIBANValidationRuleValidatesInput_whenInputHasInvalidCharacters() {
XCTAssertFalse(sut.validate(input: "GB82WEST1234$5698765432"))
}
}

// MARK: Constants

private extension String {
static let error = "Invalid IBAN"
}