PHP Kata: Bank OCR Introduction


Bank OCR Kata is, in my opinion, the best quest on which you can practice how to organize and develop near to real software solution. In this first in the series article, I guide you on how to start a project and design complex solution.

Foreword

The Bank OCR Kata is quite extensive and complex - especially if you want to do it similar to a real-life project. Please be informed that I will do some shortcuts. It is because I want to keep a reasonable number of articles in the series. Despite this, I will try to make it as authentic as possible. Another topic worth to be mentioned is - wait for it - please treat it only as a guide or a road sign - not as the only correct way.

In this article, I will focus on designing architecture, including answers to why questions. I will try to follow a bottom-up approach. It means that initially, we will start from details, and we will go with more general things layer by layer, step by step.

Other articles in the series

Small update. As I promised at the bottom of this article - I'm linking to other ones in the series:

Bank OCR Kata

Description of this Kata is long - and I don't want to duplicate it here. Please get familiar with it by reading it from its home location: Coding Dojo - Bank OCR Kata. Take as long as you need to read all user stories. In this Kata, success key is a complete understanding of the problem. But don't worry, I will do my best to make it clear for you.

As usual, I forgot to mention - that we will focus on the first user story. Or I think I did it on purpose - never mind. The point is that it is much, much, much easier to design a solution knowing more facts. And now you know all requirements - not only the one we will work on in this article.

Now it is time to set our first goal. And my proposition is: discover the minor puzzle we need, which is not further dividable.

The minor puzzle

To find our minor puzzle, we need to break our problem in to list of smaller ones. You may find it similar to the divide and conquer strategy. So let start with defying the main one. The first clue is in the user story:

write a program that can take this file and parse it into actual account numbers

Sounds like quite big? I think so. Try to split it. What would you say to: "parse file into actual account numbers"? I find this a little bit simpler thing - but still not enough. What about "parse single file record into account number"? Do you find it more manageable, same as I do? But can we divide it further? I think we can, and we should.

Before that, I would like to visualize our current task. Assume below is the entire content of the file we received:

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

The expected result for this is 123456789.

I like to ask questions. Some of them are dumb - but sometimes dumb questions force us to think. So can you see a pattern in our input? Yes? Great to hear it! You are right. To build an account number, we need to discover single digits — precisely nine of them. So we will nine times repeat single-digit discovery.

Can you figure out something smaller than digit discovery? I also can't.

Single-digit processor

I have great news for you! We have achieved our first goal! As we identified the smallest piece of our solution. Now it is time to design it. I want to call it a Digit. It can be a class created from a string. Why string? First of all, we will retrieve it from a text file. Also, I think it will be easy for us to create an adapter if, in the future, someone asks us to discover digits from other formats.

Please take a closer look at our line of digits. I will visualize them with clear separation of particular rows and columns: Image link

Analyze how the digit is presented. Have you ever played Tic-Tac-Toe? Do you remember the board? It is 3x3 table - and you can represent our digit on same board placing one of three symbols (' ', '_', and '|') in each field (1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3). Below you will find the representation of all digits: Image link

Can you see that each digit has a unique representation if you write it down, field by field, like this:

Image link

Digit design

Now when we know the mechanics behind digits, we can take a step and design our Digit class. First of all, we will create an interface and then concrete implementation of it. It might look like overkill for such a small project - but I find it a good idea to follow D from SOLID.

Now it is a good time to think about what our digit will have and what will can. For sure, it must return parsed digit and probably should return raw one as well. So our design might look like this:

Digit UML diagram

Code some digits

Having the design in place, we can start to code. I assume that you will take a moment or two and prepare an appropriate "starting" project. If you do not know how - please read How to start with PHP project. If you want to follow my code 1:1, make sure that autoloaders in composer.json will be like this:

    "autoload":{
        "psr-4": {
            "WeBee\\School\\BankOcrKata\\": "src/"
        }
    },
    "autoload-dev":{
        "psr-4": {
            "WeBee\\School\\BankOcrKata\\Spec\\": "spec/"
        }
    }

Can I assume that you are ready now? Great! I find TDD a powerful approach - so I try to utilize it as often as I can. If you not familiar with it yet - check it out in practice by following PHP Kata: Roman Numerals.

This is our first test:

<?php

declare(strict_types=1);

namespace WeBee\School\BankOcrKata\Spec;

use WeBee\School\BankOcrKata\Number\Digit;

describe(
    'Digit',
    function () {
        it(
            'can be instantiated',
            function () {
                $d = new Digit();

                expect($d)->toBeAnInstanceOf('WeBee\School\BankOcrKata\Number\Digit');
                expect($d)->toBeAnInstanceOf('WeBee\School\BankOcrKata\Number\DigitInterface');
            }
        );
    }
);

And as expected, it is failing now. We do not have Digit or DigitInterface. It is time to add it - but only as much as is needed to pass the test. Notice that I'm not expecting the constructor to have a parameter like in the above UML. It is on purpose - small steps.

First interface:

<?php

declare(strict_types=1);

namespace WeBee\School\BankOcrKata\Number;

interface DigitInterface
{
}

and then concrete class:

<?php

declare(strict_types=1);

namespace WeBee\School\BankOcrKata\Number;

class Digit implements DigitInterface
{
}

Now execute tests - this time we have this juicy PASS :) - but let face it - we expected this.

Raw digit

It is time for something as simple as hell. Test if our class can return raw digit as expected. Add a new test for it:

it(
    'can return raw digit in unchanged form',
    function () {
        $d = new Digit('aaa');

        expect($d->getRaw())->toBe('aaa');
    }
);

And appropriate changes in the interface:

interface DigitInterface
{
    public function getRaw(): string;
}

and in the concrete class:

class Digit implements DigitInterface
{
    public function __construct(private string $rawDigit)
    {
    }

    public function getRaw(): string
    {
        return $this->rawDigit;
    }
}

And now our test - is still failing. We need to fix the first test as we have mandatory constructor parameter:

it(
    'can be instantiated',
    function () {
        $d = new Digit('');

        expect($d)->toBeAnInstanceOf('WeBee\School\BankOcrKata\Number\Digit');
        expect($d)->toBeAnInstanceOf('WeBee\School\BankOcrKata\Number\DigitInterface');
    }
);

Another solution is to make constructor argument optional. But I wouldn't say I like this concept. An object should be valid from the beginning till the end of its life. Now our test should pass again.

Happy path

It is time to make our digit useful. We need to parse the input into the expected result correctly. Prepare some test data together with a test itself:

given(
    'digitExamples',
    function () {
        return [
            0 => " _ \n| |\n|_|",
            1 => "     |  |",
            2 => " _ # _|#|_ ",
            3 => " _ \n _|\n _|",
            4 => "   A|_|B  |",
            5 => " _ \n|_ \n _|",
            6 => " _ \n|_ \n|_|",
            7 => " _ \n  |\n  |",
            8 => " _ \n|_|\n|_|",
            9 => " _ \n|_|\n _|",
        ];
    }
);

it(
    'can return parsed digit',
    function () {
        foreach($this->digitExamples as $expected => $given) {
            $d = new Digit($given);

            expect($d->get())->toBeAn('integer');
            expect($d->get())->toBe($expected);
        }
    }
);

As you expected - the test is now failing. Could you fix it? At the beginning change interface:

interface DigitInterface
{
    public function getRaw(): string;

    public function get(): ?int;
}

Now we need to define a translation map for digits. I picked constant for this, as it will not change at runtime and probably never. And I like to use associative arrays for such maps. Primarily because of usage simplicity.

class Digit implements DigitInterface
{
    private const DIGITS_DEFINITIONS = [
            ' _ | ||_|' => 0,
            '     |  |' => 1,
            ' _  _||_ ' => 2,
            ' _  _| _|' => 3,
            '   |_|  |' => 4,
            ' _ |_  _|' => 5,
            ' _ |_ |_|' => 6,
            ' _   |  |' => 7,
            ' _ |_||_|' => 8,
            ' _ |_| _|' => 9,
    ];

    public function __construct(private string $rawDigit)

The next step is to implement the get method. Remember my previous statement about simplicity. See it in action:

public function get(): ?int
{
    return self::DIGITS_DEFINITIONS[$this->parsed] ?? null;
}

Why this way, with the additional parsed property? I want to separate responsibilities (S from SOLID). Getter will only get value from our map - if it exists, of course. So we need two things now: parsed property and method that will parse raw digit into it:

    private string $parsed;

    public function __construct(private string $rawDigit)
    {
        $this->parse();
    }

    private function parse(): void
    {
        $this->parsed = preg_replace('/[^_| ]/', '', $this->rawDigit);
    }

And again, we made our tests passing. But I don't feel comfortable with tests covering only the happy path. Add at least one more:

it(
    'can return null for not recognized digit',
    function () {
        $d = new Digit("___\n___\n___");

        expect($d->get())->toBe(null);
    }
);

Final word

It is the end of part one on Bank OCR Kata. In the next part, I will guide you through another step - one layer above. You probably know what it will be about. And yes, again, you are correct. We will work with an account number.

I will link the next article here. So stay tuned for more!

Keep calm and code with WeBee!

Full code listing

You can find full code for this Bank OCR Kata in PHP on my github repository.

Not enough?


Would you like me to build and conduct training tailored to your needs? I'm waiting for your call or message.

address icon

WeBee.Online Adam Wojciechowski
ul. Władysława Łokietka 5/2
70-256 Szczecin, Poland