IBAN

October 25, 2019

The International Bank Account Number (IBAN) is an internationally agreed standard of identifying bank accounts with a hash number to reduce errors. The first two characters of an IBAN are the two-letter country code, the next two characters are a two-digit hash, and the remaining characters identify the bank routing number and depositor account number. For instance, here is a fictition British IBAN: GB82 WEST 1234 5698 7654 32. The full IBAN standard specifies the range of valid bank account numbers and depositor account numbers for each country; we are interested only in the hash code.

In the code shown above, the hash code consists of the two digits 82, which are validated as follows:

1) Move the first four characters from the beginning of the string to the end: WEST 1234 5698 7654 32GB 82.

2) Replace letters with two-digit numbers according to the scheme A = 10, …, Z = 35:
3214 2829 1234 5698 7654 3216 1182.

3) Treating the string as a large integer, divide by 97 and calculate the remainder.

4) If the remainder is not 1, the IBAN is not valid.

To generate a hash code, perform the same procedure as above with the two hash digits initially set to 00, then subtract the hash code from 98 and insert it in the IBAN.

Your task is to write functions to validate existing IBAN numbers and generate hash codes for new IBAN numbers. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below.

Advertisement

Pages: 1 2

8 Responses to “IBAN”

  1. Alex B said

    Python 3.8 solution:

    #! /usr/bin/env python3
    
    # IBAN
    # https://programmingpraxis.com/2019/10/25/iban/
    
    IBAN_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    
    class IBAN(object):
        """International Bank Account Number"""
    
        def __init__(self, iban):
            self.iban = self.normalise(iban)
            self.country_code = self.iban[:2]
            self.hash_code = self.iban[2:4]
            self.account_details = self.iban[4:]
    
        def __repr__(self):
            return f"IBAN.IBAN('{self.iban}')"
    
        def __str__(self):
            chunks = [self.iban[i:i+4] for i in range(0, len(self.iban), 4)]
            return ' '.join(chunks)
    
        def __eq__(self, other):
            return self.iban == other.iban
    
        def is_valid(self):
            istr = self.iban[4:] + self.iban[:4]
            istr = self.alpha_to_num(istr)
            chk = int(istr) % 97
            return chk == 1
    
        @classmethod
        def from_details(cls, country_code, details):
            iban = cls.normalise(details + country_code) + '00'
            istr = cls.alpha_to_num(iban)
            chk = 98 - (int(istr) % 97)
            iban = iban[-4:-2] + str(chk) + iban[:-4]
            return cls(iban)
    
        @staticmethod
        def normalise(iban):
            return ''.join(i for c in iban if (i := c.upper()) in IBAN_CHARS)
    
        @staticmethod
        def alpha_to_num(iban):
            return ''.join(c if c.isdigit() else str(ord(c)-55) for c in iban)
    

    Used as follows:

    >>> import IBAN
    >>> pp_iban = IBAN.IBAN('GB82 WEST 1234 5698 7654 32')
    >>> pp_iban
    IBAN.IBAN('GB82WEST12345698765432')
    >>> str(pp_iban)
    'GB82 WEST 1234 5698 7654 32'
    >>> pp_iban.is_valid()
    True
    >>> pp_iban.country_code
    'GB'
    >>> pp_iban.hash_code
    '82'
    >>> pp_iban.account_details
    'WEST12345698765432'
    >>> bad_iban = IBAN.IBAN('GB82 WEST 1234 5698 7654 36')
    >>> bad_iban.is_valid()
    False
    >>> new_iban = IBAN.IBAN.from_details('GB', 'West 123456-9876-5432')
    >>> new_iban.is_valid()
    True
    >>> new_iban == pp_iban
    True
    >>> 
    
  2. chaw said

    Here is a simple solution in R7R6 Scheme and a few helpers from
    popular SRFIs.

    It emphasizes clarity over efficiency and makes rather liberal use of
    potentially expensive string operations and conversions. On the other
    hand, given the fixed size and format of IBAN, it is a Theta(1)
    solution.

    It also allows human-friendly spaces (and potentially other
    punctuation) in the IBANs.

    (import (scheme base)
            (scheme write)
            (scheme char)
            (only (srfi 1) split-at append-map iota filter)
            (only (srfi 8) receive))
    
    (define iban-sample "GB82 WEST 1234 5698 7654 32")
    (define iban-sample-1 "GB82:WEST:1234-5698-7654-32")
    
    ;; 0..9 -> "0".."9"; A .. Z -> "10" .. "35"; a .. z -> "10" .. "35"; else "".
    (define iban-char->number-string
      (let ((letter-values (map cons
                                (map integer->char
                                     (iota 26 (char->integer #\A)))
                                (map number->string
                                     (iota 26 10)))))
        (lambda (c)
          (cond ((char-numeric? c) (string c))
                ((assv (char-upcase c) letter-values) => cdr)
                (else "")))))
    
    (define (iban->number str)
      (receive (pre suf) (split-at (map iban-char->number-string
                                        (string->list str))
                                   4)
        (string->number (apply string-append (append suf pre)))))
    
    (define (iban-hash-valid? str)
      (= 1 (remainder (iban->number str)
                      97)))
    
    (define (iban-hash str)
      (receive (pre suf) (split-at (filter (lambda (c)
                                             (or (char-numeric? c)
                                                 (char-alphabetic? c)))
                                           (string->list str))
                                   2)
        (- 98
           (remainder (iban->number (apply string
                                           (append pre
                                                   '(#\0 #\0)
                                                   (cddr suf))))
                      97))))
    
    (define (iban-demo str)
      (display (list str
                   (iban-hash str)
                   (iban-hash-valid? str)))
      (newline))
    
    (for-each iban-demo (list iban-sample iban-sample-1))
    

    Output:

    (GB82 WEST 1234 5698 7654 32 82 #t)
    (GB82:WEST:1234-5698-7654-32 82 #t)
    

  3. matthew said

    I’m not going to try to compete with Alex’s nicely engineered solution, so here’s a Python two-liner (it would go into one, but it feels like cheating to go over 80 chars in a line):

    >>> chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    >>> def iban(s): return int("".join(str(chars.index(c)) for c in s[4:]+s[:4])) % 97
    ... 
    >>> iban('GB82WEST12345698765432')
    1
    >>> 98-iban('GB00WEST12345698765432')
    82
    
  4. matthew said

    And in fact, we can use the Python string -> int conversion with base 36 and get an under 80 chars one-liner (I got this from the Rosetta Code IBAN page):

    def iban(s): return int("".join(str(int(c,36)) for c in s[4:]+s[:4])) % 97
    
  5. Globules said

    A Haskell version.

    import Control.Monad ((>=>))
    import Data.List (lookup)
    import Data.Maybe (maybe)
    import Numeric (readDec)
    import System.Environment (getArgs)
    import Text.Printf (printf)
    
    -- Return a "broken out" IBAN, consisting of a two character country code, two
    -- check digits and the remaining BBAN (Basic Bank Account Number).  Return
    -- nothing if the IBAN doesn't have enough characters.
    ibanSplit :: String -> Maybe (String, String, String)
    ibanSplit (cc0:cc1:cd0:cd1:bban) = Just ([cc0, cc1], [cd0, cd1], bban)
    ibanSplit _                      = Nothing
    
    -- A mapping from IBAN characters to their corresponding decimal strings.
    digits :: [(Char, String)]
    digits = zip (['0'..'9'] ++ ['A'..'Z']) $ map show [0..]
    
    -- Convert a string consisting of the characters '0' through '9' and 'A' through
    -- 'Z' to an integer.  Return nothing if the string contains any characters not
    -- in those ranges.
    toInt :: String -> Maybe Integer
    toInt cs = case readDec . concat <$> mapM (`lookup` digits) cs of
                 Just [(n, "")] -> Just n
                 _              -> Nothing
    
    -- Validate a broken out IBAN.  Return true if all the characters are valid and
    -- the check digits are correct, otherwise return false.
    ibanValidate' :: (String, String, String) -> Bool
    ibanValidate' (cc, cd, bban) = maybe False remOk (toInt $ bban ++ cc ++ cd)
      where remOk n = n `rem` 97 == 1
    
    -- Validate an IBAN.  Return true if all the characters are valid and the check
    -- digits are correct, otherwise return false.
    ibanValidate :: String -> Bool
    ibanValidate = maybe False ibanValidate' . ibanSplit
    
    -- Given a broken out IBAN return the IBAN containing generated check digits.
    ibanChecksum' :: (String, String, String) -> Maybe String
    ibanChecksum' (cc, _, bban) = newIban <$> toInt (bban ++ cc ++ "00")
      where newIban n = cc ++ checkDigits n ++ bban
            checkDigits n = printf "%02d" $ 98 - (n `rem` 97)
    
    -- Given an IBAN return a new IBAN containing generated check digits.
    ibanChecksum :: String -> Maybe String
    ibanChecksum = ibanSplit >=> ibanChecksum'
    
    ibanTest :: String -> IO ()
    ibanTest iban = do
      printf "%s: valid? %s\n" iban (show $ ibanValidate iban)
      printf "%s: new check digits: %s\n" iban (show $ ibanChecksum iban)
    
    main :: IO ()
    main = getArgs >>= mapM_ ibanTest
    
    $ ./iban GB82WEST12345698765432 GBXXWEST12345698765432 G@82WEST12345698765432
    GB82WEST12345698765432: valid? True
    GB82WEST12345698765432: new check digits: Just "GB82WEST12345698765432"
    GBXXWEST12345698765432: valid? False
    GBXXWEST12345698765432: new check digits: Just "GB82WEST12345698765432"
    G@82WEST12345698765432: valid? False
    G@82WEST12345698765432: new check digits: Nothing
    
  6. Steve said

    When I tried to run your solution in Gambit Scheme, I got the following:

    (define-syntax let-values
    (syntax-rules ()
    ((_ () f1 f2 …) (let () f1 f2 …))
    ((_ ((fmls1 expr1) (fmls2 expr2) …) f1 f2 …)
    (let-values-help fmls1 () () expr1 ((fmls2 expr2) …) (f1 f2 …)))))
    *** ERROR IN (console)@56.17 — Ill-formed expression

    3> (define-syntax let-values-help
    (syntax-rules ()
    ((_ (x1 . fmls) (x …) (t …) e m b)
    (let-values-help fmls (x … x1) (t … tmp) e m b))
    ((_ () (x …) (t …) e m b)
    (call-with-values
    (lambda () e)
    (lambda (t …)
    (let-values m (let ((x t) …) . b)))))
    ((_ xr (x …) (t …) e m b)
    (call-with-values
    (lambda () e)
    (lambda (t … . tmpr)
    (let-values m (let ((x t) … (xr tmpr)) . b)))))))
    *** ERROR IN (console)@61.17 — Ill-formed expression

  7. Daniel said

    Here’s a solution in C.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int calc_hash(char* iban) {
      int result = 0;
      int n = strlen(iban);
      for (int i = 0; i < n; ++i) {
        char c = iban[(i + 4) % n];
        if (c >= 'A' && c <= 'Z') {
          result = (result * 100) + (10 + c - 'A');
        } else if (c >= '0' && c <= '9') {
          result = (result * 10) + (c - '0');
        } else {
          // Unknown character
        }
        result %= 97;
      }
      return result;
    }
    
    int main(int argc, char* argv[]) {
      if (argc != 2) {
        return EXIT_FAILURE;
      }
      int hash = calc_hash(argv[1]);
      printf("%d\n", hash);
      return EXIT_SUCCESS;
    }
    

    Example usage:

    $ ./iban "GB82 WEST 1234 5698 7654 32"
    1
    
    $ expr 98 - $(./iban "GB00 WEST 1234 5698 7654 32")
    82
    
  8. Daniel said

    calc_hash is not a fitting name for the function above. I think that reflects my original intent, although the function calculates a remainder, not a hash.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: