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:

  _  _     _  _  _  _  _
| _| _||_||_ |_   ||_||_|
||_  _|  | _||_|  ||_| _|

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 »

import unittest

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

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

if __name__ == '__main__':
    unittest.main()

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

def convert_v1(account):
    return "000000000"

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

import unittest

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

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

"""
        self.assertTrue(convert_v1(ACCOUNT), "111111111")

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 :

"""\

|
|

"""

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

    "\n".join([e[0:3] for e in full_account_number.split('\n')])

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 :

ONE = """\

|
|

"""
NUMBERS = { ONE : "1"}

La nouvelle méthode s’écrit alors :

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]

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

ACCOUNT="""\

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

"""

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:


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)

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 ».

ALL_DIFF = """\
    _  _     _  _  _  _  _
  | _| _||_||_ |_   ||_||_|
  ||_  _|  | _||_|  ||_| _|

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

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 :

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

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 « ? ».

ACCOUNT = """\
    _  _     _  _  _  _  _
  | _| _| _||_ |_   ||_||_|
  ||_  _|  | _||_|  ||_| _|

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

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 :

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

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:

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

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s