|
| 1 | +// |
| 2 | +// Validator |
| 3 | +// Copyright © 2025 Space Code. All rights reserved. |
| 4 | +// |
| 5 | + |
| 6 | +import Foundation |
| 7 | + |
| 8 | +// MARK: - PostalCodeValidationRule |
| 9 | + |
| 10 | +/// A validation rule used to verify postal/ZIP codes for different countries. |
| 11 | +/// |
| 12 | +/// `PostalCodeValidationRule` validates user input based on predefined |
| 13 | +/// country-specific postal code formats using regular expressions. |
| 14 | +/// |
| 15 | +/// # Example: |
| 16 | +/// ```swift |
| 17 | +/// let rule = PostalCodeValidationRule(country: .uk, error: "Invalid postal code") |
| 18 | +/// rule.validate(input: "SW1A 1AA") // true |
| 19 | +/// rule.validate(input: "123456") // false |
| 20 | +/// ``` |
| 21 | +/// |
| 22 | +public struct PostalCodeValidationRule: IValidationRule { |
| 23 | + // MARK: Types |
| 24 | + |
| 25 | + // swiftlint:disable identifier_name |
| 26 | + /// Supported countries with unique postal code formats. |
| 27 | + public enum Country { |
| 28 | + case us |
| 29 | + case uk |
| 30 | + case ca |
| 31 | + case de |
| 32 | + case fr |
| 33 | + case it |
| 34 | + case es |
| 35 | + case nl |
| 36 | + case be |
| 37 | + case ch |
| 38 | + case at |
| 39 | + case au |
| 40 | + case nz |
| 41 | + case jp |
| 42 | + case cn |
| 43 | + case `in` |
| 44 | + case br |
| 45 | + case mx |
| 46 | + case ar |
| 47 | + case za |
| 48 | + case ru |
| 49 | + case pl |
| 50 | + case se |
| 51 | + case no |
| 52 | + case dk |
| 53 | + case fi |
| 54 | + case pt |
| 55 | + case gr |
| 56 | + case cz |
| 57 | + case ie |
| 58 | + case sg |
| 59 | + case kr |
| 60 | + case il |
| 61 | + case tr |
| 62 | + } |
| 63 | + |
| 64 | + // swiftlint:enable identifier_name |
| 65 | + |
| 66 | + // MARK: Properties |
| 67 | + |
| 68 | + /// The country format to validate against. |
| 69 | + public let country: Country |
| 70 | + |
| 71 | + /// Validation error returned when validation fails. |
| 72 | + public let error: IValidationError |
| 73 | + |
| 74 | + // MARK: Initialization |
| 75 | + |
| 76 | + /// Creates a new postal code validation rule. |
| 77 | + /// |
| 78 | + /// - Parameters: |
| 79 | + /// - country: The country whose postal code format should be used. |
| 80 | + /// - error: Error object returned when validation fails. |
| 81 | + public init(country: Country, error: IValidationError) { |
| 82 | + self.country = country |
| 83 | + self.error = error |
| 84 | + } |
| 85 | + |
| 86 | + // MARK: ValidationRule |
| 87 | + |
| 88 | + public func validate(input: String) -> Bool { |
| 89 | + let trimmedInput = input.trimmingCharacters(in: .whitespacesAndNewlines) |
| 90 | + |
| 91 | + if trimmedInput.isEmpty { |
| 92 | + return false |
| 93 | + } |
| 94 | + |
| 95 | + if input != trimmedInput { |
| 96 | + return false |
| 97 | + } |
| 98 | + |
| 99 | + let pattern = getPattern(for: country) |
| 100 | + |
| 101 | + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { |
| 102 | + return false |
| 103 | + } |
| 104 | + |
| 105 | + let range = NSRange(location: 0, length: input.utf16.count) |
| 106 | + return regex.firstMatch(in: input, options: [], range: range) != nil |
| 107 | + } |
| 108 | + |
| 109 | + // MARK: Private Methods |
| 110 | + |
| 111 | + // swiftlint:disable function_body_length cyclomatic_complexity |
| 112 | + private func getPattern(for country: Country) -> String { |
| 113 | + switch country { |
| 114 | + case .us: |
| 115 | + #"^\d{5}(-\d{4})?$"# |
| 116 | + |
| 117 | + case .ca: |
| 118 | + #"^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] \d[ABCEGHJ-NPRSTV-Z]\d$"# |
| 119 | + |
| 120 | + case .mx: |
| 121 | + #"^\d{5}$"# |
| 122 | + |
| 123 | + case .br: |
| 124 | + #"^\d{5}-\d{3}$"# |
| 125 | + |
| 126 | + case .ar: |
| 127 | + #"^([A-Z]\d{4}([A-Z]{3})?|\d{4})$"# |
| 128 | + |
| 129 | + case .uk: |
| 130 | + #"^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$"# |
| 131 | + |
| 132 | + case .de: |
| 133 | + #"^\d{5}$"# |
| 134 | + |
| 135 | + case .fr: |
| 136 | + #"^\d{5}$"# |
| 137 | + |
| 138 | + case .it: |
| 139 | + #"^\d{5}$"# |
| 140 | + |
| 141 | + case .es: |
| 142 | + #"^\d{5}$"# |
| 143 | + |
| 144 | + case .nl: |
| 145 | + #"^\d{4} [A-Z]{2}$"# |
| 146 | + |
| 147 | + case .be: |
| 148 | + #"^\d{4}$"# |
| 149 | + |
| 150 | + case .ch: |
| 151 | + #"^\d{4}$"# |
| 152 | + |
| 153 | + case .at: |
| 154 | + #"^\d{4}$"# |
| 155 | + |
| 156 | + case .pt: |
| 157 | + #"^\d{4}-\d{3}$"# |
| 158 | + |
| 159 | + case .gr: |
| 160 | + #"^\d{3} \d{2}$"# |
| 161 | + |
| 162 | + case .ie: |
| 163 | + #"^[A-Z]\d{2} [A-Z0-9]{4}$"# |
| 164 | + |
| 165 | + case .se: |
| 166 | + #"^\d{3} \d{2}$"# |
| 167 | + |
| 168 | + case .no: |
| 169 | + #"^\d{4}$"# |
| 170 | + |
| 171 | + case .dk: |
| 172 | + #"^\d{4}$"# |
| 173 | + |
| 174 | + case .fi: |
| 175 | + #"^\d{5}$"# |
| 176 | + |
| 177 | + case .ru: |
| 178 | + #"^\d{6}$"# |
| 179 | + |
| 180 | + case .pl: |
| 181 | + #"^\d{2}-\d{3}$"# |
| 182 | + |
| 183 | + case .cz: |
| 184 | + #"^\d{3} \d{2}$"# |
| 185 | + |
| 186 | + case .jp: |
| 187 | + #"^\d{3}-\d{4}$"# |
| 188 | + |
| 189 | + case .cn: |
| 190 | + #"^\d{6}$"# |
| 191 | + |
| 192 | + case .in: |
| 193 | + #"^\d{6}$"# |
| 194 | + |
| 195 | + case .sg: |
| 196 | + #"^\d{6}$"# |
| 197 | + |
| 198 | + case .kr: |
| 199 | + #"^\d{5}$"# |
| 200 | + |
| 201 | + case .il: |
| 202 | + #"^\d{7}$"# |
| 203 | + |
| 204 | + case .tr: |
| 205 | + #"^\d{5}$"# |
| 206 | + |
| 207 | + case .au: |
| 208 | + #"^\d{4}$"# |
| 209 | + |
| 210 | + case .nz: |
| 211 | + #"^\d{4}$"# |
| 212 | + |
| 213 | + case .za: |
| 214 | + #"^\d{4}$"# |
| 215 | + } |
| 216 | + } |
| 217 | + // swiftlint:enable function_body_length cyclomatic_complexity |
| 218 | +} |
0 commit comments