Kata

KataBankOCR / Implémentation en Python

Voici une implémentation possible du KataBankOCR. Ce Kata est un classique dont l’énoncé peut être trouvé sur CodingDojo.org.


L’user-story à implémenter est la suivante :

Vous travaillez pour une banque, qui vient d’acheter une machine ingénieuse permettant de lier un compte bancaire à un document reçu (lettre ou fax). Cette machine scanne le document et produit un fichier avec un certain nombre d’entrée ressemblant à ceci:

[sourcecode language= »python »] _  _     _  _  _  _  _
| _| _||_||_ |_   ||_||_|
||_  _|  | _||_|  ||_| _|

[/sourcecode]

Chaque entrée fait 4 lignes et chaque ligne possède 27 caractères. Les trois premières lignes représentent un numéro de compte à l’aide de pipe et d’underscore. La quatrième ligne est blanche.
Chaque numéro de compte est composé de 9 nombres compris entre 0 et 9. Il est nécessaire d’écrire un programme permettant de transformer ces entrées en une chaîne de caractères composée de neuf nombres.
Il peut arriver qu’un nombre soit illisible. Dans ce cas, il est remplacé par un « ? ». 

Détections de numéro de comptes composés de digits identiques

Commençons par écrire le test permettant de contrôler un compte « 000000000 »

[sourcecode language= »python »]
import unittest

class test_id(unittest.TestCase):
def test_id_0(self):
ACCOUNT="""\
_  _  _  _  _  _  _  _  _
| || || || || || || || || |
|_||_||_||_||_||_||_||_||_|

"""
self.assertTrue(convert(ACCOUNT), "0000000000")

if __name__ == ‘__main__’:
unittest.main()
[/sourcecode]

Écrivons le fake qui nous permettra de commencer à écrire notre module

[sourcecode language= »python »]
def convert_v1(account):
return "000000000"
[/sourcecode]

On voit clairement les limites de cette méthode. En changeant ce numéro de compte par un nombre composé que de « 1 » ce test échouera. Cependant, elle nous permet de faire passer notre premier test et donc, de nous mettre le pied à l’étrier pour ce Kata.
Écrivons le test mettant en évidence les limitations de la méthode convert_v1

[sourcecode language= »python »]
import unittest

class test_id(unittest.TestCase):
def test_id_0(self):

def test_id_1(self):
ACCOUNT="""\

|  |  |  |  |  |  |  |  |
|  |  |  |  |  |  |  |  |

"""
self.assertTrue(convert_v1(ACCOUNT), "111111111")
[/sourcecode]

Inutile d’être un fin programmeur pour voir que ce test échoue bien! Écrivons donc le test permettant de contrôler les comptes composés de nombre identiques.
Un nombre s’écrit de la manière suivante :

[sourcecode language= »python »]
"""\

|
|

"""
[/sourcecode]

Chaque digit est codé sur trois caractères. L’extraction du premier nombre peut donc être écrit de la même façon

[sourcecode language= »python »]
"\n".join([e[0:3] for e in full_account_number.split(‘\n’)])
[/sourcecode]

Ceci reprend les trois premiers caractères de chaque ligne. Cette chaîne peut alors être associée à un nombre.
Ainsi, on construit un dictionnaire de référence :

[sourcecode language= »python »]
ONE = """\

|
|

"""
NUMBERS = { ONE : "1"}
[/sourcecode]

La nouvelle méthode s’écrit alors :

[sourcecode language= »python »]
def convert_v2(input):
first_number = ‘\n’.join( [e[0:3] for e in input.split(‘\n’)])
if first_number in NUMBERS:
return 9*NUMBERS[first_number]
[/sourcecode]

Le test marche marche ici si l’input est le numéro de compte suivant :

[sourcecode language= »python »]
ACCOUNT="""\

|  |  |  |  |  |  |  |  |
|  |  |  |  |  |  |  |  |

"""
[/sourcecode]

Afin que cela marche pour tout les numéros de compte identique, il suffit de compléter le dictionnaire « NUMBERS »
Écrivons d’abord les tests:

[sourcecode language= »python »]

ALL_0 = """\
_  _  _  _  _  _  _  _  _
| || || || || || || || || |
|_||_||_||_||_||_||_||_||_|

"""
ALL_1 = """\

|  |  |  |  |  |  |  |  |
|  |  |  |  |  |  |  |  |

"""

ALL_2 = """\
_ _ _ _ _ _ _ _ _
_| _| _| _| _| _| _| _| _|
|_ |_ |_ |_ |_ |_ |_ |_ |_

"""


ID_ACCOUNTS = [ALL_0, ALL_1, …, ALL_9]

class test_id(unittest.TestCase):
def test_id(self):
RESULTS = [9*str(e) for e in range(10)] # "000000000", "111111111", …
for ACCOUNT, result in zip(ID_ACCOUNTS, RESULTS)
self.assertTrue(convert_v2(ACCOUNT), result)
[/sourcecode]

Il suffit, pour convert_v2 de continuer le dictionnaire NUMBERS
Voilà une première étape, nous pouvons identifier tout les numéros de comptes composés de 9 digits identiques.
Généralisons et essayons de détecter un numéro de compte composé de digits différents.

Détections de numéro de comptes composés de digits différents

Écrivons un premier test simpliste correspondant à la détection du numéro de compte « 123456789 ».

[sourcecode language= »python »]
ALL_DIFF = """\
_ _ _ _ _ _ _
| _| _||_||_ |_ ||_||_|
||_ _| | _||_| ||_| _|

"""
class test_diff(unittest.TestCase):
def test_diff(self):
self.assertTrue(convert_v3(ALL_DIFF), "123456789")
[/sourcecode]

La méthode convert_v2, précédemment écrite, déduit le numéro de compte à partir du premier digit. Il suffit de reprendre cette méthode pour faire passer le test précédent de la manière suivante :

[sourcecode language= »python »]
def convert_v3(input):
output = ""
for idx in range(0,27,3):</span>
number = ‘\n’.join( [e[idx:idx+3] for e in input.split(‘\n’)])
if first_number in NUMBERS:
output += NUMBERS[number]
return output
[/sourcecode]

Cette méthode permet de faire passer le test précédent.

Ce dernier test peut largement être amélioré, notamment en générant de manière aléatoire un numéro OCR. Ceci permettrai de lever un éventuel fake (en effet, pour faire passer ce dernier test, un return « 123456789 » aurait marché!).

Cependant, à la lecture de convert_v3, il saute aux yeux qu’une condition n’est pas testée. Que se passe t’il si un nombre OCR est invalide?

Contrôle d’erreurs

Que se passe t’il si un numéro OCR est erroné? Reprenons le test précédent et corrompons le nombre « 4 ».
On souhaiterai que, lorsqu’un nombre n’est pas reconnu, il soit remplacé par un « ? ».

[sourcecode language= »python »]
ACCOUNT = """\
_ _ _ _ _ _ _
| _| _| _||_ |_ ||_||_|
||_ _| | _||_| ||_| _|

"""
class test_diff(unittest.TestCase):
def test_diff(self):
self.assertTrue(convert_v3(ACCOUNT), "123?56789")
[/sourcecode]

Le test précédent échoue et convert_v3 renvoie, comme numéro de compte « 12356789 ».
Ici le quatre « erroné » a été tout simplement ignoré.

La méthode permettant alors de faire passer le test est la suivante :

[sourcecode language= »python »]
def convert_v4(input):
output = ""
for idx in range(0,27,3):</span>
number = ‘\n’.join( [e[idx:idx+3] for e in input.split(‘\n’)])
if number in NUMBERS:
output += NUMBERS[number]
else:
output += ‘?’
return output
[/sourcecode]

Voilà qui finit ce Kata. Le code finalisé de la méthode convert et sa suite de test peut être trouvé sur gist.github.

Au vu du code un petit refactoring est possible pour optimiser le code final:

[sourcecode language= »python »]
def convert_v4(input):
output = ""
for idx in range(0,27,3):</span>
number = ‘\n’.join( [e[idx:idx+3] for e in input.split(‘\n’)])
output += NUMBERS.get(number, ‘?’)
return output
[/sourcecode]

 

 

Laisser un commentaire