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()
Portattu näköjään suoraan mureakuhan vastaavanlaisesta php koodivinkistä.
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.
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.
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ä :)
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/
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 :)
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.
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.
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.
Pitäisi varmaan päivittää tukemaan IBAN-muotoa. :)
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.
Kiitos hienosta kirjastosta!
Saako tätä käyttää sellaisenaan jollakin lisenssillä?
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ä.