Skip to content

Commit c8f101e

Browse files
committed
feat: add postal code validation rule
1 parent 9775b7d commit c8f101e

File tree

4 files changed

+932
-0
lines changed

4 files changed

+932
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ struct RegistrationView: View {
316316
| `ComparisonValidationRule` | Validates that input against a comparison constraint | `ComparisonValidationRule(greaterThan: 0, error: "Must be greater than 0")`
317317
| `IBANValidationRule` | Validates that a string is a valid IBAN (International Bank Account Number) | `IBANValidationRule(error: "Invalid IBAN")`
318318
| `IPAddressValidationRule` | Validates that a string is a valid IPv4 or IPv6 address | `IPAddressValidationRule(version: .v4, error: ValidationError("Invalid IPv4"))`
319+
| `PostalCodeValidationRule` | Validates postal/ZIP codes for different countries | `PostalCodeValidationRule(country: .uk, error: "Invalid post code")`
319320

320321
## Custom Validators
321322

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

Sources/ValidatorCore/Validator.docc/Overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for
3939
- ``ComparisonValidationRule``
4040
- ``IBANValidationRule``
4141
- ``IPAddressValidationRule``
42+
- ``PostalCodeValidationRule``
4243

4344
### Articles
4445

0 commit comments

Comments
 (0)