1 module iban.validation;
2 
3 import std.array : array, empty;
4 import std.algorithm.iteration : joiner, map;
5 import std.algorithm.searching : all, startsWith;
6 import std.ascii : isAlpha, isAlphaNum, isUpper;
7 import std.conv : to;
8 import std.range : takeExactly;
9 import std.typecons : Nullable, nullable;
10 import std.uni : isNumber;
11 import std.utf : byChar;
12 import std.stdio;
13 import std.format;
14 
15 import iban.ibans;
16 import iban.structures;
17 
18 @safe:
19 
20 string removeWhite(string input) {
21 	import std.array : replace;
22 	return input.replace(" ", "");
23 }
24 
25 Nullable!string extractCountryPrefix(string input) {
26 	import std.ascii : isUpper;
27 	return input.length > 1
28 			&& input[0].isUpper()
29 			&& input[1].isUpper()
30 		? nullable(input[0 .. 2])
31 		: Nullable!(string).init;
32 }
33 
34 bool isValidIBAN(string toTest) {
35 	Nullable!string specKey = extractCountryPrefix(toTest);
36 
37 	if(specKey.isNull()) {
38 		return false;
39 	}
40 
41 	auto spec = specKey.get() in getIBANs();
42 
43 	if(spec is null) {
44 		return false;
45 	}
46 
47 	return isValidIBAN(toTest, *spec);
48 }
49 
50 bool isValidIBAN(string toTest, IBANData spec) {
51 	toTest = toTest.removeWhite();
52 
53 	return validateLength(spec, toTest)
54 		&& validateCharacters(toTest)
55 		&& validateFormat(spec, toTest)
56 		&& validateChecksum(toTest);
57 }
58 
59 bool validateCharacters(string toTest) {
60 	import std.regex : regex, matchFirst;
61 	import std.algorithm.searching : startsWith;
62 	auto re = regex(`[A-Z]{2}\d{2}[A-Z]*`);
63 	auto m = matchFirst(toTest, re);
64 	return m.length == 1 && toTest.startsWith(m[0]);
65 }
66 
67 unittest {
68 	import iban.testdata;
69 	foreach(it; valid) {
70 		string pp = it.removeWhite();
71 		assert(pp.validateCharacters(), it);
72 		assert(pp.validateLength(), it);
73 		assert(pp.validateFormat(), it);
74 		assert(pp.validateChecksum(), it);
75 	}
76 }
77 
78 unittest {
79 	import iban.testdata;
80 	foreach(it; invalid) {
81 		string pp = it.removeWhite();
82 		assert(pp.validateCharacters()
83 			|| pp.validateLength()
84 			|| pp.validateFormat()
85 			|| pp.validateChecksum(), it);
86 	}
87 }
88 
89 bool validateLength(string iban) {
90 	import std.typecons : Nullable;
91 	Nullable!string specKey = extractCountryPrefix(iban);
92 
93 	if(specKey.isNull()) {
94 		return false;
95 	}
96 
97 	auto spec = specKey.get() in getIBANs();
98 	return spec !is null && validateLength(*spec, iban);
99 }
100 
101 bool validateLength(IBANData spec, string iban) {
102 	return iban.length == spec.ibanLength;
103 }
104 
105 bool matchDirect(string iban, Parse p) {
106 	return iban.startsWith(p.direct);
107 }
108 
109 bool matchSpaces(string iban, Parse p) {
110 	return iban.length >= p.number
111 		&& iban.byChar.map!(it => ' ')
112 			.takeExactly(p.number)
113 			.all;
114 }
115 
116 bool matchNumber(string iban, Parse p) {
117 	bool ret = iban.length >= p.number
118 		&& iban.byChar
119 			.map!(it => isNumber(it))
120 			.takeExactly(p.number)
121 			.all;
122 
123 	return ret;
124 }
125 
126 bool matchAlpha(string iban, Parse p) {
127 	return iban.length >= p.number
128 		&& iban.byChar.map!(it => isAlpha(it) && isUpper(it))
129 			.takeExactly(p.number)
130 			.all;
131 }
132 
133 bool matchAlphaNum(string iban, Parse p) {
134 	return iban.length >= p.number
135 		&& iban.byChar
136 			.map!(it => isAlphaNum(it))
137 			.takeExactly(p.number)
138 			.all;
139 }
140 
141 struct MatchResult {
142 	bool matches;
143 	string cutString;
144 }
145 
146 MatchResult match(string iban, Parse p) {
147 	MatchResult ret;
148 	final switch(p.type) {
149 		case ParseType.direct:
150 			ret.matches = matchDirect(iban, p);
151 			break;
152 		case ParseType.number:
153 			ret.matches = matchNumber(iban, p);
154 			break;
155 		case ParseType.alpha:
156 			ret.matches = matchAlpha(iban, p);
157 			break;
158 		case ParseType.alphanum:
159 			ret.matches = matchAlphaNum(iban, p);
160 			break;
161 		case ParseType.space:
162 			ret.matches = matchSpaces(iban, p);
163 			break;
164 	}
165 	ret.cutString = iban.length >= p.number
166 		? iban[p.number .. $]
167 		: iban;
168 
169 	return ret;
170 }
171 
172 bool validateFormat(string iban) {
173 	Nullable!string specKey = extractCountryPrefix(iban);
174 
175 	if(specKey.isNull()) {
176 		return false;
177 	}
178 
179 	auto spec = specKey.get() in getIBANs();
180 	return spec !is null && validateFormat(*spec, iban);
181 }
182 
183 bool validateFormat(IBANData spec, string iban) {
184 	string tmp = iban;
185 	foreach(idx, it; spec.ibanSpecRegex) {
186 		MatchResult ne = match(tmp, it);
187 		if(!ne.matches) {
188 			return false;
189 		}
190 		tmp = ne.cutString;
191 	}
192 	return tmp.empty;
193 }
194 
195 bool validateChecksum(string iban) {
196 	import std.bigint;
197 
198 	if(iban.length <= 4) {
199 		return false;
200 	}
201 
202 	long chkSum = to!long(iban[2 .. 4]);
203 
204 	// The bank number + account number
205 	string bban = iban[4 .. $]
206 		.map!(it => it >= 'A'
207 				? to!string(to!int(it - 'A' + 10))
208 				: to!string(it)
209 		)
210 		.joiner("")
211 		.to!string();
212 
213 	// the prefix to postfix conversion for the checksum check
214 	string prefix = iban[0 .. 4]
215 		.map!(it => it >= 'A'
216 				? to!int(it - 'A' + 10)
217 				: 0
218 		)
219 		.map!(it => to!string(it))
220 		.joiner("")
221 		.to!string();
222 
223 	// the prefix to postfix conversion for % 97 == 1 check
224 	string prefix2 = iban[0 .. 4]
225 		.map!(it => it >= 'A'
226 				? to!int(it - 'A' + 10)
227 				: to!int(it - '0')
228 		)
229 		.map!(it => to!string(it))
230 		.joiner("")
231 		.to!string();
232 
233 	string cc = bban ~ prefix;
234 	BigInt num = cc;
235 	long mod = num % 97;
236 	long chkSumTT = 98 - mod;
237 
238 	string cc2 = bban ~ prefix2;
239 	BigInt num2 = cc2;
240 	long mod2 = num2 % 97;
241 
242 	return chkSumTT == chkSum && mod2 == 1;
243 }