Kirjautuminen

Haku

Tehtävät

Koodit: Python: Tilinumeron tarkistus

Kirjoittaja: Chiman

Kirjoitettu: 02.10.2004 – 30.10.2012

Tagit: algoritmit, ohjelmointitavat, teksti, yhteiskunta, koodi näytille, vinkki

Pythonilla (2.6-2.7) tehty suomalaisen tilinumeron tarkistus ja IBAN-muunnos.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Validate and convert Finnish bank account number

# More info:
# http://www.fkl.fi/teemasivut/sepa/tekninen_dokumentaatio/

import re

class AccountNumberException(Exception):
    pass

# create compiled validation regular expressions for efficiency
old_re = re.compile(r'([0-9]{6})-([0-9]{2,8})')
mlf_re = re.compile(r'[0-9]{14}')
fi_iban_re = re.compile(r'FI[0-9]{16}')

# account number prefix: (bank name, BIC code, position to add zeros)
bank_data = {'1': ('Nordea Pankki (Nordea)', 'NDEAFIHH', 6),
             '2': ('Nordea Pankki (Nordea)', 'NDEAFIHH', 6),
             '31': ('Handelsbanken', 'HANDFIHH', 6),
             '33': ('Skandinaviska Enskilda Banken (SEB)', 'ESSEFIHX', 6),
             '34': ('Danske Bank', 'DABAFIHX', 6),
             '36': ('Tapiola Pankki', 'TAPIFI22', 6),
             '37': ('DNB Bank ASA, Finland Branch', 'DNBAFIHX', 6),
             '38': ('Swedbank', 'SWEDFIHH', 6),
             '39': ('S-Pankki', 'SBANFIHH', 6),
             '4': ('Aktia Pankki, Säästöpankit (Sp) ja Paikallisosuuspankit (POP)', 'HELSFIHH', 7),
             '5': ('Pohjola Pankki (OP-Pohjola-ryhmän pankkien keskusrahalaitos)', 'OKOYFIHH', 7),
             '6': ('Ålandsbanken (ÅAB)', 'AABAFI22', 6),
             '8': ('Sampo Pankki', 'DABAFIHH', 6),
             '711': ('Calyon', 'BSUIFIHH', 6),  # IBAN only
             '713': ('Citibank', 'CITIFIHX', 6),  # IBAN only
             '715': ('Itella Pankki', 'ITELFIHH', 6)  # IBAN only
            }

def old_to_mlf(old_s):
    """Return old Finnish account number in machine format language.

    Raise exception if account number is invalid.

    """

    # Check if number format is correct
    m = old_re.match(old_s)
    if m is None:
        raise AccountNumberException('Number format incorrect')

    number = ''.join(m.groups())
    for k, v in bank_data.items():
        if k.startswith('7'):
            continue  # only used in IBAN, ignore

        if number.startswith(k):
            br_point = v[2] # the position of zeroes depends on bank prefix
            zeroes = '0' * (14 - len(number))
            mlf_s = number[:br_point] + zeroes + number[br_point:]
            if not validate_mlf(mlf_s):
                raise AccountNumberException('Invalid account number')
            return mlf_s

    raise AccountNumberException('Prefix not in use')

def mlf_to_iban(mlf_s):
    """return IBAN for given Finnish account number machine language format"""
    raw_iban = mlf_s + '151800'  # '15' = 'F', '18' = 'I'
    check_digits = '{0:02d}'.format(98 - int(raw_iban) % 97)
    return 'FI' + check_digits + mlf_s

def old_to_iban(old_s, spaced=False):
    """return IBAN version of given old account number"""
    iban_s = mlf_to_iban(old_to_mlf(old_s))
    return iban_s if not spaced else spaced_iban(iban_s)

def spaced_iban(iban_s):
    """return IBAN in four character groups, separated by spaces"""
    iban_s = iban_s.replace(' ', '')
    iban_spaced = [(' ' if i and i % 4 == 0 else '') + c for i, c in enumerate(iban_s)]
    return ''.join(iban_spaced)

def validate_old(old_s):
    """return True for valid old format account number, False otherwise"""
    try:
        return validate_mlf(old_to_mlf(old_s))
    except AccountNumberException:
        return False

def validate_mlf(mlf_s):
    """return True for valid machine language format, False otherwise"""

    if mlf_re.match(mlf_s) is None:
        return False

    weights = (2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2)  # weights for 13 first digits
    mlf_numbers = map(int, mlf_s)

    # 13 first account number digits are multiplied by weights
    products = map(lambda (a, b): str(a * b), zip(weights, mlf_numbers))

    # products are put together in a single string and the sum of digits is calculated
    total = sum(int(x) for x in ''.join(products))

    # modulo check for 13 first digits must match the last (14th) digit
    return (10 - total) % 10 == mlf_numbers[-1]

def validate_fi_iban(iban_s):
    """return True for valid Finnish IBAN, False otherwise"""
    iban_s = iban_s.replace(' ', '')

    if fi_iban_re.match(iban_s) is None:
        return False

    # number format = machine language format + 'FI' as digits + check digits
    # 'A' = '10', ..., 'Z' = '35', 'FI' = '1518'
    number_format = iban_s[4:] + '1518' + iban_s[2:4]
    return int(number_format) % 97 == 1

def _get_bank_data_by_iban(iban_s, data_i):
    if not validate_fi_iban(iban_s):
        raise AccountNumberException('Invalid IBAN')

    iban_s = iban_s.replace(' ', '')
    acc_number_part = iban_s[4:]
    for k, v in bank_data.items():
        if acc_number_part.startswith(k):
            return v[data_i]

    raise AccountNumberException('Unknown bank')

def bic_of_iban(iban_s):
    """return bank identification code (BIC) of given IBAN"""
    return _get_bank_data_by_iban(iban_s, 1)

def bank_name_of_iban(iban_s):
    """return name of the bank of given IBAN"""
    return _get_bank_data_by_iban(iban_s, 0)

def main():
    # examples

    old = '123456-785'
    print 'Account number in old format: {0:s}'.format(old)
    print 'Machine language format: {0:s}'.format(old_to_mlf(old))
    iban = old_to_iban(old)
    print 'IBAN: {0:s}'.format(iban)
    print 'BIC: {0:s}'.format(bic_of_iban(iban))
    print 'Bank name: {0:s}'.format(bank_name_of_iban(iban))

    real_iban = 'FI3715903000000776'
    result = validate_fi_iban(real_iban)
    print '{0:s} is {1:s} IBAN'.format(real_iban, 'valid' if result else 'invalid')

    fake_iban = 'FI3715903000000777'
    result = validate_fi_iban(fake_iban)
    print '{0:s} is {1:s} IBAN'.format(fake_iban, 'valid' if result else 'invalid')

    fake_old = '123457-785'
    result = validate_old(fake_old)
    print '{0:s} is {1:s}'.format(fake_old, 'valid' if result else 'invalid')

    # error handling example:
    try:
        print '{0:s} to IBAN: {1:s}'.format(fake_old, old_to_iban(fake_old))
    except AccountNumberException as e:
        print 'Unable to convert: {0:s}'.format(e)

if __name__ == '__main__':
    main()

Kommentit

tuomas [02.10.2004 15:50:29]

#

Portattu näköjään suoraan mureakuhan vastaavanlaisesta php koodivinkistä.

Antti Laaksonen [02.10.2004 15:59:40]

#

Ei nyt sentään, toteutus on kokonaan eri. Koodissa on sitä paitsi käytetty hyväksi Python-kielen erikoisominaisuuksia, minkä vuoksi se on myös hyvä ohjelmointiesimerkki.

Chiman [02.10.2004 21:07:39]

#

Mureakuhasta sain tosiaan ajatuksen tuohon, mutta tein sen suoraan Pankkiyhdistyksen dokumentin pohjalta. Ideana oli hankkia lisää koodauskokemusta Pythonilla. Laitoin sitten koodin myös näkyville, jos siitä vaikka sattuisi olemaan jollekin iloa tai saisin vinkkejä koodin laadun parantamiseen.

Aku2 [07.10.2004 01:15:09]

#

En ole tuota koittanut enkä kyllä kykenisikään koittamaan, mutta pienoinen kysymys ja pyyntö olisi tuosta.

Ensin pyyntö:
Voisko joku kehittää vastaavan VB6:lle alla olevin täydennyksin.

Ja sitten kysymykset.
- Jokin hyöty, jos tilinumeron perusteella osaa ohjelma kertoa mikä pankki on kyseessä? Kuitenkin pankit erotellaan vieläkin tarkemmin, esim. Joensuun osuuspankki, tai EKPS (Eteläkarjalan säästöpankki).
- Ilmeisesti kyseinen koodi laskee myös tarkisteeen ja vertaa sitä annettuun tilinumeroon, eli pitääkö tilinumero yleensäkin paikkansa?
- Ottaako huomioon lyhyen ja pitkän tilinumeron?

Lisäysehdotus:
- Jos on syötetty lyhyt tilinumero, niin muuttaa/näyttää sen pitkäksi (käytetään mm. viivakoodeissa pitkää tilinumeroa). Samoin mahdollisesti näyttäisi myös IBAN numerona.

Niin ja tietysti noi olisi kiva nähdä myös VB:lle tehtynä :)

Chiman [07.10.2004 12:21:36]

#

Aku2, kiitos kommenteista. Tuota koodia voi kokeilla helposti, jos vain asentaa Pythonin.

- Tilinumerosta taitaa saada vielä nykyäänkin selville myös pankkikonttorin, mutta en ole nähnyt sellaista listaa missään netissä. Kyseessä olisi sitä paitsi aikamoinen listaus, sillä konttoreita riittää. Olisihan sekin tietysti mukava lisäominaisuus tuohon koodiin.

Kyllä, tarkiste eli tarkistusnumero lasketaan tuossa modulo checkissä. Tilinumeron konekielisestä muodosta otetaan 13 ensimmäistä numeroa ja lasketaan niiden tulot _weights-painokertoimien kanssa. Tulojen kaikki yksittäiset numerot lasketaan yhteen ja summan viimeinen numero vähennetään luvusta 10. Näin saadaan tarkiste, jonka pitää täsmätä tilinumeron viimeiseen numeroon. Yksinkertaista ;)

- Tuo koodi ottaa huomioon kaikki nykyiset suomalaiset tilinumerot siinä muodossa, kun ne yleisesti ilmoitetaan. Joillakin pankeilla on lyhyempiä numeroita (loppuosan pituus vaihtelee 2 ja 8 numeron välillä).

Jos tarkoitat pitkällä tilinumerolla sen konekielistä muotoa (esim. 123456-785 -> 12345600000785), sitä käytetään tuossa koodin sisällä number_mlf-muuttujassa, josta sen voisi tulostaa. Valitettavasti koodivinkkiä ei taida pystyä muokkaamaan jälkikäteen.

Tuo IBAN-muoto oli hyvä idea. En tosin tiedä kannattaako sen laskemista lisätä tänne omaksi koodivinkikseen, mutta ehkä lisään sen ominaisuuden lähipäivinä tästä alla olevasta linkistä löytyvään koodiin.

http://koti.mbnet.fi/heh/py/CheckAccNbr/CheckAccNbr.py.html

VB:lle voi joku muu tehdä tuon saman koodin. Se ei tosin ole yhtä näppärää, sillä Pythonia on vaikea päihittää tuollaisten operaatioiden toteuttamisen helppoudessa :)

Aku2 [08.10.2004 00:06:30]

#

lainaus:

Aku2, kiitos kommenteista. Tuota koodia voi kokeilla helposti, jos vain asentaa Pythonin.

Lähinnä ei ole mielenkiintoa pythoniin eikä jaksa opetella TAAS uutta kieltä, vaikka ei toi niin erikoiselta vaikutakaan. :)

lainaus:

- Tilinumerosta taitaa saada vielä nykyäänkin selville myös pankkikonttorin, mutta en ole nähnyt sellaista listaa missään netissä. Kyseessä olisi sitä paitsi aikamoinen listaus, sillä konttoreita riittää. Olisihan sekin tietysti mukava lisäominaisuus tuohon koodiin.

Menisi aika monimutkaiseksi, koska pankkikonttoreita tulee uusia ja vanhoja kuolee ja nimet muuttuu. Menisi aikamoiseksi päivitysrumbaksi.
Enemmänkin kaipasin selitystä miksi ilmoittaa pankkia. Mikä hyöty ja missä sitä voisi soveltaa käytännössä?
Ihan mukava lisä jos ei tilinumerosta itse osaa päätellä mikä pankki on kyseessä.

lainaus:

Jos tarkoitat pitkällä tilinumerolla sen konekielistä muotoa (esim. 123456-785 -> 12345600000785), sitä käytetään tuossa koodin sisällä number_mlf-muuttujassa, josta sen voisi tulostaa. Valitettavasti koodivinkkiä ei taida pystyä muokkaamaan jälkikäteen.

Juu. Tuota tarkoitin, kun eri pankeilla ne "välinollat" tulee eritavoin. Lähinnä kiinnostaa missä pankissa ne tulee mitenkin.

Chiman [08.10.2004 01:15:32]

#

lainaus:

Enemmänkin kaipasin selitystä miksi ilmoittaa pankkia. Mikä hyöty ja missä sitä voisi soveltaa käytännössä? Ihan mukava lisä jos ei tilinumerosta itse osaa päätellä mikä pankki on kyseessä.

Laitoin sen tuohon lähinnä siitä syystä, että se tieto oli saatavilla ja helposti koodattavissa :) Sitä tietoahan ei ole pakko käyttää hyväksi (tulostaa), vaikka se ylemmän funktion paluuarvona tuleekin.

Mikäli ylläoleva koodi sijoitettaisiin erilliseen tiedostoon, sitä voitaisiin käyttää toisesta tiedostosta käsin (import-lauseen avulla). Siinä tapauksessa tuota alaosassa olevaa if __name__ -kohtaa ei suoriteta lainkaan, vaan ainoastaan silloin, kun tuo tiedosto suoritetaan yksinään. Tuon yleisen if __name__ -rakenteen tarkoituksena on toimia muun koodin käytön esittelynä ja/tai moduulitestauksen apuna.

lainaus:

Tuota tarkoitin, kun eri pankeilla ne "välinollat" tulee eritavoin. Lähinnä kiinnostaa missä pankissa ne tulee mitenkin.

Se tieto näkyy koodin yläosasta, bank_prefix-muuttujan määrittelystä. Säästö- ja osuuspankeilla nollat lisätään seitsemän numeron jälkeen, muilla pankeilla kuuden eli sen väliviivan kohdalle.

mike patto [14.10.2004 13:52:41]

#

Pankkitietoa voi hyödyntää esim sovelluksessa,
jossa pitää tilinumeron perusteella "niputtaa" tietoja.
Ajettele yritystä jolla on pankkitili useassa eri pankissa
ja maksut halutaan maksaa tyyliin Nordeaan menevät maksut Nordean tilitä jne.

Metabolix [28.10.2012 02:57:08]

#

Pitäisi varmaan päivittää tukemaan IBAN-muotoa. :)

Chiman [30.10.2012 13:28:50]

#

Vinkki päivitetty. Ominaisuudet:

Pyrin pitämään koodin yksinkertaisena. Jätin pois esim. yksikkötestit, joita varten Pythonissa on hyvä unittest-moduuli.

Muoks klo 14.07: Täydensin muototarkistuksia.

vesi [25.02.2014 18:12:07]

#

Kiitos hienosta kirjastosta!

Saako tätä käyttää sellaisenaan jollakin lisenssillä?

Chiman [25.02.2014 21:02:05]

#

vesi kirjoitti:

Saako tätä käyttää sellaisenaan jollakin lisenssillä?

Koodia saa minun puolestani käyttää vapaasti omalla vastuullaan. Koodasin sen koodausajankohtana saatavilla olevien dokumenttien perusteella, mutten voi taata täyttä virheettömyyttä.

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta