IBAN Validator Swift
Asked Answered
C

4

6

I am writing an algorithm to validate IBAN (International Bank Account Number) in Swift 3 and not able to figure one of the validation.

Example IBAN - BE68539007547034

Here are the rules to validate -

  1. Input number should be of length 16.
  2. First 2 characters are country code (not numeric).
  3. Last 14 are numeric.
  4. Last 2 characters are the modulo 97 result of the previous 12 numeric characters.

While #1 - #3 are clear I need clarity on #4. If anyone have done this before and know about it then please let me know.

Confucianism answered 5/5, 2017 at 11:35 Comment(7)
see this gist.github.com/0xc010d/5301790Skillet
and this also once #46731Skillet
You need to take the hole IBAN, not just the 14 numeric, because Dutch IBAN numbers look like: NL70INGB0123456564. Have a look at this IBAN wikiIncoordinate
Or check this lib: github.com/readefries/IBAN-HelperIncoordinate
It's interesting to note all of the validators using the answers don't actually test for real countries, just alphabet letters.Sherwoodsherwynd
@StephenJ Real IBAN validation is a very complex thing. There is a list of country prefixes and every prefix has also a fixed IBAN length. Also, every country can have additional validation rules. However, frontend should usually check only for obvious typos, therefore calculcating mod97 is usually more than enough. The backend that actually uses the IBAN should actually verify that the IBAN exists and that's a whole different problem, accessing banking APIs that has that the information.Pacian
Yeah, I know anything with locations is complex as heck. It's just that it's for banking info, and I was like "Oh my, is this how we really test?" You gave me ease with the backend tidbit. Thank you.Sherwoodsherwynd
O
11

From Wikipedia

let IBAN = "GB82WEST12345698765432" // uppercase, no whitespace !!!!
var a = IBAN.utf8.map{ $0 }
while a.count < 4 {
    a.append(0)
}
let b = a[4..<a.count] + a[0..<4]
let c = b.reduce(0) { (r, u) -> Int in
    let i = Int(u)
    return i > 64 ? (100 * r + i - 55) % 97: (10 * r + i - 48) % 97
}
print( "IBAN \(IBAN) is", c == 1 ? "valid": "invalid")

prints

IBAN GB82WEST12345698765432 is valid

With IBAN from your question it prints

IBAN BE68539007547034 is valid
Oleson answered 5/5, 2017 at 17:44 Comment(2)
So many magic numbers... Why don't you use "A" instead of 64, "0" instead of 48 and so on? Your code will be much more readable.Pacian
@Pacian just to avoid create Int from "A" etc., which is required for computation :-). I like Int(String(char), radix: 36) from your answer!!!Oleson
P
24

The validation algorithm is rather simple if you follow the algorithm on wikipedia:

extension String {
    private func mod97() -> Int {
        let symbols: [Character] = Array(self)
        let swapped = symbols.dropFirst(4) + symbols.prefix(4)

        let mod: Int = swapped.reduce(0) { (previousMod, char) in
            let value = Int(String(char), radix: 36)! // "0" => 0, "A" => 10, "Z" => 35
            let factor = value < 10 ? 10 : 100          
            return (factor * previousMod + value) % 97
        }

        return mod
    }    

    func passesMod97Check() -> Bool {
        guard self.characters.count >= 4 else {
            return false
        }

        let uppercase = self.uppercased()

        guard uppercase.range(of: "^[0-9A-Z]*$", options: .regularExpression) != nil else {
            return false
        }

        return (uppercase.mod97() == 1)
    }
}

Usage:

let iban = "XX0000000..."
let valid = iban.passesMod97Check()

If you want to validate the format for a specific country, just modify the regular expression, e.g.

"^[A-Z]{2}[0-9]{14}$"

or directly

"^BE\\d{14}$"
Pacian answered 5/5, 2017 at 12:24 Comment(4)
Thanks much. This looks promising. Need some time to digest mod97 () :).Confucianism
This is actually coming from a production app.Pacian
@Retterdesdialogs The idea of your solution was correct, you could have changed the datatype to a 64-bit integer and it would work, at least for the Belgian IBAN. In a generic solution you would have to handle the A-Z characters, anyway.Pacian
@Pacian Ok, thank you, but your answer is so much better, so I just deleted my answer.Hilaryhilbert
O
11

From Wikipedia

let IBAN = "GB82WEST12345698765432" // uppercase, no whitespace !!!!
var a = IBAN.utf8.map{ $0 }
while a.count < 4 {
    a.append(0)
}
let b = a[4..<a.count] + a[0..<4]
let c = b.reduce(0) { (r, u) -> Int in
    let i = Int(u)
    return i > 64 ? (100 * r + i - 55) % 97: (10 * r + i - 48) % 97
}
print( "IBAN \(IBAN) is", c == 1 ? "valid": "invalid")

prints

IBAN GB82WEST12345698765432 is valid

With IBAN from your question it prints

IBAN BE68539007547034 is valid
Oleson answered 5/5, 2017 at 17:44 Comment(2)
So many magic numbers... Why don't you use "A" instead of 64, "0" instead of 48 and so on? Your code will be much more readable.Pacian
@Pacian just to avoid create Int from "A" etc., which is required for computation :-). I like Int(String(char), radix: 36) from your answer!!!Oleson
S
0

I finded a great solution that work for me in Objective-C https://gist.github.com/0xc010d/5301790 you can rewrite for Swift or use bridging header. Objective-C implementation of mod97 IBAN checking algorithm

#import <Foundation/Foundation.h>

@interface NSString (Mod97Check)

- (BOOL)passesMod97Check; // Returns result of mod 97 checking algorithm. Might be used to check IBAN.
                          // Expects string to contain digits and/or upper-/lowercase letters; space and all the rest symbols are not acceptable.

@end
#import "NSString+Mod97Check.h"

@implementation NSString (Mod97Check)

- (BOOL)passesMod97Check {
    NSString *string = [self uppercaseString];
    NSInteger mod = 0, length = [self length];
    for (NSInteger index = 4; index < length + 4; index ++) {
        unichar character = [string characterAtIndex:index % length];
        if (character >= '0' && character <= '9') {
            mod = (10 * mod + (character - '0')) % 97; // '0'=>0, '1'=>1, ..., '9'=>9
        }
        else if (character >= 'A' && character <= 'Z') {
            mod = (100 * mod + (character - 'A' + 10)) % 97; // 'A'=>10, 'B'=>11, ..., 'Z'=>35
        }
        else {
            return NO;
        }
    }
    return (mod == 1);
}

@end
-(BOOL)isValidIBAN {
    NSString *iban = self;
    static NSString* const LettersAndDecimals = @"ABCDEFGHIJKLKMNOPQRSTUVWXYZ0123456789";
    iban = [[iban stringByReplacingOccurrencesOfString:@" " withString:@""] uppercaseString];
    NSCharacterSet *invalidChars = [[NSCharacterSet characterSetWithCharactersInString:LettersAndDecimals] invertedSet];

    if ([iban rangeOfCharacterFromSet:invalidChars].location != NSNotFound)
    {
        return NO;
    }

    int checkDigit = [iban substringWithRange:NSMakeRange(2, 2)].intValue;
    iban = [NSString stringWithFormat:@"%@%@",[iban substringWithRange:NSMakeRange(4, iban.length - 4)], [iban substringWithRange:NSMakeRange(0, 4)]] ;

    for (int i = 0; i < iban.length; i++) {
        unichar c = [iban characterAtIndex:i];
        if (c >= 'A' && c <= 'Z') {
            iban = [NSString stringWithFormat:@"%@%d%@", [iban substringWithRange:NSMakeRange(0, i)], (c - 'A' + 10),[iban substringWithRange:NSMakeRange(i+1, iban.length - i - 1)]];
        }

    }
    iban = [[iban substringWithRange:NSMakeRange(0, iban.length - 2)] stringByAppendingString:@"00"];

    while(true)
    {
        int iMin = (int)MIN(iban.length, 9);
        NSString* strPart = [iban substringWithRange:NSMakeRange(0, iMin)];
        int decnumber = strPart.intValue;
        if(decnumber < 97 || iban.length < 3)
            break;
        int del = decnumber % 97;
        iban =  [NSString stringWithFormat:@"%d%@", del, [iban substringFromIndex:iMin]];
    }
    int check = 98 - iban.intValue;

    return checkDigit == check;
}
Shanda answered 24/8, 2017 at 16:38 Comment(1)
Your passesMod97Check function is very well-written. Its space and time complexity are neatly bounded.Shrug
C
-3

Here you go :

func isValidIBAN(text:String) -> Bool {
        let ibanRegEx = "[a-zA-Z]{2}+[0-9]{2}+[a-zA-Z0-9]{4}+[0-9]{7}([a-zA-Z0-9]?){0,16}"
        let ibanTest = NSPredicate(format:"SELF MATCHES %@", ibanRegEx)
        return ibanTest.evaluate(with: text)
    }

It's clean, and it works.

Chemisorb answered 17/8, 2017 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.