IBAN
October 25, 2019
This is tedious, but not hard in a language that supports big-integer arithmetic. First we convert the string to a large integer:;
(define (iban str)
(let-values (((start end) (split 4 (string->list str))))
(let loop ((cs (append end start)) (zs (list)))
(cond ((null? cs)
(string->number
(list->string
(flatten
(reverse zs)))))
((char-alphabetic? (car cs))
(loop (cdr cs)
(cons (string->list
(number->string
(- (char->integer (car cs)) 55)))
zs)))
(else (loop (cdr cs) (cons (car cs) zs)))))))
Then it is easy to validate an IBAN or generate a hash code:
(define (valid? str) (= 1 (modulo (iban str) 97)))
(define (gen str) (- 98 (modulo (iban str) 97)))
Here are the example problems:
> (valid? "GB82WEST12345698765432")
#t
> (gen “GB00WEST12345698765432”)
82
You can run the program at https://ideone.com/k7S3Rg, but that calculates the answers incorrectly because the version of Chicken Scheme at ideone doesn’t compute big integers properly.
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 >>>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:
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') 82And 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])) % 97A 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_ ibanTestWhen 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
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:
calc_hashis not a fitting name for the function above. I think that reflects my original intent, although the function calculates a remainder, not a hash.