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 }