Code kata time - Roman numerals+Jest+LiveReload

Learning from the lessons of the previous kata I’m going to create my github repository before initializing the project, and make sure to commit code whenever I reach a green working test.

It also sure would be handy if all three automatic watching things could be done at the same time. And look, npm-run-all is designed for that task.

The roman numerals kata is one where you convert from normal (arabic) numbers to roman numerals.

The initialization process for starting a new project stems from four main things:

  • version control - github.com
  • test - Jest
  • build - Browserify, using the Watchify version of it instead
  • server - handled by watch-http-server

The process of creating a new project is:

  • create a folder for the kata, I’m creating one called kata/roman-numerals/

  • shift+right-click on the folder and open a powershell or command shell there

  • at github.com create a new repository

    • called roman-numerals-2017-12-29
    • initialized with a readme file
    • .gitignore set for Node
    • with an MIT license
  • clone the github repository into the roman-numerals. folder

    • with the command git clone https://github.com/pmw57/roman-numerals-2017-12-29
  • start a new node project with `npm init and accept the defaults, except for:

    • description: Roman Numerals kata
    • test: jest --watch
    • keywords: roman numerals kata
    • author: Paul Wilkins
    • license: MIT
  • create files

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Roman Numerals kata</title>
</head>
<body>
    <h1>Roman Numerals kata</h1>
    <script src="bundle.js"></script>
</body>
</html>

The bundle.js file gets supplied by watchify converting index.js

index.js

/*jslint browser */
var romanNumerals = require("./roman-numerals.js");

var results = document.querySelector(".results");
var numbers = new Array(100).fill().map((v, i) => i + 1);
results.innerHTML = numbers.map(romanNumerals.convert)
    .join(", ");

An array from 1 to 100 is converted and shown on the page
This requires roman-numerals.js, so create that, and it needs a test file

roman-numerals.test.js

/*jslint browser */ /*global test, expect */
(function iife() {
    "use strict";
    const romanNumerals = require("./roman-numerals");

    test("function exists", function () {
        expect(romanNumerals()).not.toBeUndefined();
    });
}());
  • jest

    • install it with: npm install --save-dev jest
    • test that it works with npm run test
    • Use Turbotop to make the console window always stay on top
  • watchify

    • install it with: `npm install --save-dev watchify
    • add build script to package.json with: "build": "watchify index.js -o bundle.js"
    • test that it works with: npm run build:watch
  • install watch-http-server (trying a different one) with:

    • npm install --save-dev watch-http-server
    • add start script to package.json with: "start": "watch-http-server"
  • install npm-run-all with:

    • npm install --save-dev npm-run-all
    • add watch script to package.json with: "watch": "run-p test build start"

You are now all ready to start, by issuing the one command of:

  • npm run watch

which will have the test, the build, and the server all watching and updating for you.

You can now create roman-numerals.js if you didn’t do so already, and give it the following code:

roman-numerals.js

module.exports = (function iife() {
    "use strict";
    function convert() {
        return;
    }
    return {
        convert
    };
}());

which will make the test pass, and you should see the webpage automatically update too.

I was about to carry on but then I realised, I need to commit these changes.

The status command shows that we are currently on the master branch, which is not a good place to be.

> git status
On branch master

We need to create a branch for the development code, and use that one instead.

> git branch develop
> git checkout -b develop

We can now add the untracked files:

> git add bundle.js
> git add index.html
> git add index.js
> git add package.json
> git add roman-numerals.js
> git add roman-numerals.test.js

and commit this working code:

> git commit -m "git commit -m "Testing with Jest, Building with Watchify (variation of Browserify), and serving with watch-http-server"

Our first push uses -u to tell git that the default upstream branch is develop.

> git push -u origin develop

After this though we only need to issue a simple git push command to push to the same place.

The first proper test

A good first test checks the nil condition, what happens when the function is given nothing, or zero.

    test("no input gives an empty string", function () {
        expect(convert).toBe("");
    });

Returning an empty string is the simplest way to pass this test.

    function convert() {
        return "";
    }

The test passes, and git status shows that we have two modified files. We add the files with:

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "No input"

You don’t have to push the commit every time you make them. Normally it’s better to leave off the push until you’ve made significant progress, or at the end of the day.

No matter what though, only pass code that is passing all of the tests. If you want to experiment with something then do that on another branch. Committing only passing tests means that every commit is fully working code, which is really satisfying.

The next test is for 0, because we might get that at some stage.

    test("0 gives an empty string", function () {
        expect(convert(0)).toBe("");
    });

That passes immediately, so add and commit the code:

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "0 gives and empty string"

The first real test

After checking for standard nil conditions, we can start testing for when numbers are used.

    test("1 converts to I", function () {
        expect(convert(1)).toBe("I");
    });

We can now add a parameter to the convert function, and do an early return for empty or 0 values of n.

We can then return I and the test passes.

    function convert(n) {
        if (!n || n < 1) {
            return "";
        }
        return "I";
    }

Add and commit that code:

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "1 converts to I"

Now that the code is passing, does any refactoring need to take place? There’s not much to refactor at this stage, so it’s on to the next text.

There’s an excellent Roman numerals kata with commentary done by Corey Haines. The order of his tests do make it easier, but I’ll ignore that and just go through the numbers sequentially here.

Converting 2 to II

This test is fairly straight forward:

    test("2 converts to II", function () {
        expect(convert(2)).toBe("II");
    });

And we can easily make it pass by checking if n is 1, otherwise giving II

        if (n === 1) {
            return "I";
        }
        return "II";

The code passes, so add and commit the code:

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "2 converts to II"

Is there anything to refactor yet? There is some duplication, but I’ll wait for three examples of duplication before doing anything about it.

3 converts to III

The next test is:

    test("3 converts to III", function () {
        expect(convert(3)).toBe("III");
    });

And we make it pass in the same way that we made the previous one pass.
It’s important that we do nothing fancy here yet. That can wait until we refactor the code to improve it.

        if (n === 1) {
            return "I";
        }
        if (n === 2) {
            return "II";
        }
        return "III";

And all tests pass, so commit the code.

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "3 converts to III"

Refactoring 3

Now that the test is passing, and the passing code has been committed, we are now free to refactor the code and make it better, while making sure that that the tests all remain in the green.

We can use recursion to help us refactor the code. First, we split up the III

        return "II" + "I";

which should still pass.

The II is already handled when the function is given 2, so we can use that instead.

        return convert(2) + "I";

Since n is 3, we can also automatically get 2 by doing n - 1 instead.

        return convert(n - 1) + "I";

And that should work with the II line as well.

            // return "II";
            return convert(n - 1) + "I";

Because the code for returning 2 and 3 are now both the same, we can completely remove the if statement.

        // if (n === 2) {
        //     return convert(n - 1) + "I";
        // }
        return convert(n - 1) + "I";

And the tests still all pass.
Having passing tests is a very freeing experience, for you no longer fear breaking the code. If anything does break, you get immediate feedback about it and can either undo the change that you made, or fix it.

Can we also use that technique on the n === 1 code too?

        if (n === 1) {
            // return "I";
            return convert(n - 1) + "I";
        }

It sure works, and we can delete all of that code too.

        // if (n === 1) {
        //     return convert(n - 1) + "I";
        // }
        return convert(n - 1) + "I";

After all of that refactoring we are now left with only the following in the convert function:

    function convert(n) {
        if (!n || n < 1) {
            return "";
        }
        return convert(n - 1) + "I";
    }

We should also commit our code here too.

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Refactor to use recursion, and remove duplication"

This is a good time to end this post, push the current progress to the server, and grab a cuppa.

> git push

The next tests are for what what happens from 4 onwards.

Convert 4 to IV

    test("4 converts to IV", function () {
        expect(convert(4)).toBe("IV");
    });

We can make that pass by testing for 4 and returning it before the ones.

        if (n === 4) {
            return "IV";
        }
        return convert(n - 1) + "I";

The tests are showing quite some duplication now. We should be able to use an array to hold the values being converted, and loop through them to perform the tests.

    let convertedValues = [
        "I", "II", "III", "IV"
    ];
    convertedValues.forEach(function (roman, index) {
        const arabic = index + 1;
        test(`${arabic} converts to ${roman}`, function () {
            expect(convert(arabic)).toBe(roman);
        });
    });

While that works, I don’t like how I have to convert the index number to an arabic value.
I don’t want to include 0 in there either, because it doesn’t belong as a roman value.

The following test code would work wonderfully:

    let convertedValues = [
        {"1": "I"},
        {"2": "II"},
        {"3": "III"},
        {"4": "IV"}
    ];
    Object.entries(convertedValues).forEach(function ([arabic, roman]) {
        test(`${arabic} converts to ${roman}`, function () {
            expect(convert(arabic)).toBe(roman);
        });
    });

But, Jest doesn’t understand the ES6 Object.entries() code.

So instead, another alternative is to use arrays instead of objects, which gives us:

    let convertedValues = [
        [1, "I"],
        [2, "II"],
        [3, "III"],
        [4, "IV"]
    ];
    convertedValues.forEach(function ([arabic, roman]) {
        test(`${arabic} converts to ${roman}`, function () {
            expect(convert(arabic)).toBe(roman);
        });
    });

This way, we can just add move values to the array, for each new test.

So now that the tests are all passing, we need to commit this update:

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Refactor the tests to use an array for each conversion test"

Convert 5 to V

It’s now easier to add the test for this conversion. Below the number 4 test, we just add another test for 5.

        [4, "IV"],
        [5, "V"]

Making this pass can be done by adding an if statement to the code:

        if (n === 5) {
            return "V";
        }
        return convert(n - 1) + "I";

But where should it go? It works both above and below the one that checks for number 4.

Never mind, the code works so commit, and then considering this can be done when refactoring.

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Convert 5 to V"

Before refactoring, I’ll wait for a third if statement, which should be when we get to number 10.

The tests for 6 through to 8 all pass, with the tests just being:

        [5, "V"],
        [6, "VI"],
        [7, "VII"],
        [8, "VIII"]

Those are all worth committing too.

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Tests for converting 6 through to 8 all pass without needing to change the code"

Convert 9 to IX

The number 9 test is easy to add to the array in the test code:

        [8, "VIII"],
        [9, "IX"]

We can add a new if condition for 9:

        if (n === 9) {
            return "IX";
        }
        return convert(n - 1) + "I";

And all the tests pass. Commit the code.

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Convert 9 to IX"

Is the code worth refactoring yet? I think that we’ll really see how it should be refactored when we get to 10.

Convert 10 to X

The test to add for this is added below the 9 test:

        [9, "IX"],
        [10, "X"]

And I thought that the code to make this pass would be another if statement:

        if (n === 10) {
            return "X";
        }
        return convert(n - 1) + "I";

But the code doesn’t pass. Instead of getting "X" I get "IXI", so that if statement for 10 needs to come before the one for 9.

        if (n === 10) {
            return "X";
        }
        if (n === 9) {
            return "IX";
        }

And the test passes. I should make a similar change to the other if statements too, with the highest values coming before the lower values:

        if (n === 10) {
            return "X";
        }
        if (n === 9) {
            return "IX";
        }
        if (n === 5) {
            return "V";
        }
        if (n === 4) {
            return "IV";
        }
        return convert(n - 1) + "I";

And now it’s a good time to commit this working code:

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Convert 10 to X"

We can now focus on refactoring the code, to remove some of that duplication.

A few possibilities exist for reducing the number of if statements. We could check if the value of the next roman numeral is larger, and take certain steps. But, that it likely to get very messy when dealing with L, C, D, and M.

As with the tests, I do like the idea of having the information in an array.

        var CONVERSION_FACTORS = [
            [10, "X"],
            [9, "IX"],
            [5, "V"],
            [4, "IV"]
        ];

And the code can then get the roman characters from that array.

        if (n === 10) {
            return CONVERSION_FACTORS[0][1];
        }
        if (n === 9) {
            return CONVERSION_FACTORS[1][1];
        }
        if (n === 5) {
            return CONVERSION_FACTORS[2][1];
        }
        if (n === 4) {
            return CONVERSION_FACTORS[3][1];
        }

And that works, all the tests pass, but is it an improvement?

I could search the CONVERSION_FACTORS array for the first item that’s greater or equal to n, that might work well.

        if (n === 10) {
            return CONVERSION_FACTORS.find(function ([arabic, ignore]) {
                return (arabic >= n);
            }).pop();
        }

Yes, that works well. I don’t want to add all of that to the other tests though.

Can I use that for other tests, such as with 9 as well?

        if (n >= 9) {
            return CONVERSION_FACTORS.find(function ([arabic, ignore]) {
                return (arabic >= n);
            }).pop();
        }

No, the test fails. It gets X instead of IX. If the array was reversed, then we might really have something.

        var CONVERSION_FACTORS = [
            [4, "IV"],
            [5, "V"],
            [9, "IX"],
            [10, "X"]
        ];
        if (n >= 9) {
            return CONVERSION_FACTORS.find(function ([arabic, ignore]) {
                return (arabic >= n);
            }).pop();
        }
        if (n === 5) {
            return CONVERSION_FACTORS[1][1];
        }
        if (n === 4) {
            return CONVERSION_FACTORS[0][1];
        }

Yes, that works. It’s time to commit this while we’re ahead, because I don’t want too much time to pass between commits, and things are currently all working.

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Partway through refactoring so that a CONVERSION_FACTORS array is used"

With that committed, I feel better about moving on to finish this refactoring.

Improving the conversion

When I try to lower the if condition to 8, the code doesn’t work, getting IX instead of VIII
The comparison is wrong inside of the find statement. It’s getting higher roman values than it should.

Returning the if condition back up to 9, I can flip >= to <= which corrects that problem, but the tests still don’t pass.
I need to flip the array back around to how it began, with high values at the top.

        var CONVERSION_FACTORS = [
            [10, "X"],
            [9, "IX"],
            [5, "V"],
            [4, "IV"]
        ];
        // ...
        if (n === 5) {
            return CONVERSION_FACTORS[2][1];
        }
        if (n === 4) {
            return CONVERSION_FACTORS[3][1];
        }

and then -

No, I don’t go looking at 8. I’ve fixed a problem in the code and all tests pass, so before exploring further, I commit that fix.

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Using correct <= comparison to get roman characters within range of n"

Getting 8 and lower working with the find function

Changing the if statement to (n >= 8) I can see now from the test, that it’s getting “V” instead of “VIII”
I just need to subtract the arabic value and return the roman character along with another call to the convert function.

To do that, I need to have a separate if statement inside of the find function.

                // return (arabic <= n);
                if (arabic <= n) {
                    return true;
                }

I can now subtract the arabic value when inside of that if condition, and add on the converted value of what remains afterwards:

           return CONVERSION_FACTORS.find(function ([arabic, ignore]) {
                if (arabic <= n) {
                    n -= arabic;
                    return true;
                }
            }).pop() + convert(n);

That works. and lowering the if condition down to 4, it still passes all the tests.

Going down to 3 though it doesn’t work, because it can’t find anything in the array.
Can I deal with that by adding “I” to the array?

            [4, "IV"],
            [1, "I"]

Yes that works. Can I go all the way now, and remove the if statement completely?

        // if (n >= 3) {
            return CONVERSION_FACTORS.find(function ([arabic, ignore]) {
                if (arabic <= n) {
                    n -= arabic;
                    return true;
                }
            }).pop() + convert(n);
        // }

Hey yes that works! Let’s remove the other code that was below it:

        // if (n === 5) {
        //     return CONVERSION_FACTORS[2][1];
        // }
        // if (n === 4) {
        //     return CONVERSION_FACTORS[3][1];
        // }
        // return convert(n - 1) + "I";

And it looks like we now have a working algorithm. Time to commit this quick.

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Using CONVERSION_FACTORS for all of the conversions now, instead of separate if statements"

Finishing off the tests

Based on the IV, V and IX X pattern, we can finish off the rest of the tests in a rather rapid order, for 40 and 50, 90 and 100, 400 and 500, and 1000. One at a time though, ending up with test code of:

        // ...
        [10, "X"],
        [40, "XL"],
        [50, "L"],
        [90, "XC"],
        [100, "C"],
        [400, "CD"],
        [500, "D"],
        [1000, "M"]

And the following code that makes it pass:

        var CONVERSION_FACTORS = [
            [1000, "M"],
            [500, "D"],
            [400, "CD"],
            [100, "C"],
            [90, "XC"],
            [50, "L"],
            [40, "XL"],
            [10, "X"],
            // ...

Commit this working code, and there’s only one more thing to go:

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Add remaining tests and code for 40, 50, 90, 100, 400, 500, and 1000"

A final test

To make sure that everything’s working properly, one final test to convert 1999 should help to make sure that everything working correctly. I don’t want to use the output of my program for the converted value, so Roman numerals converter tells me that 1999 converts to MCMXCIX

The test is:

        [1999, "MCMXCIX"]

And the code fails. What? It shouldn’t fail.

    Expected value to be (using Object.is):
      "MCMXCIX"
    Received:
      "MDCDXCIX"

I went too fast with those tests, and forgot to put in 900.

The tests are:

        [900, "CM"],
        [1000, "M"],
        [1999, "MCMXCIX"]

And the updated code is:

        var CONVERSION_FACTORS = [
            [1000, "M"],
            [900, "CM"],

All 21 tests now pass, so I can commit and push this final lot of code:

> git status
bundle.js
roman-numerals.js
roman-numerals.test.js

> git add *.js

> git commit -m "Added CM to the conversions, and a final check of converting 1999 to MCMXCIX"

> git push

The index page shows all of the roman numerals from 1 to 100, and would do them up to any value we like.
This kata is done, and all of the develop commits commits can now be seen.

Merge develop branch into master

I can now merge this develop branch to master. Before merging though, close the window that’s watching test and build, so that files like bundle.js don’t get updated. It had updated for me after my last commit, so I followed the git status instructions to toss it away:

git checkout -- bundle.js

With the merge, the checkout switches to my local branch, and the pull retrieves any updates from the server

> git checkout master

> git pull origin master

> git merge develop

> git push origin master

> git checkout develop

That last checkout, is because it’s a bad habit to remain on master. Switching back to develop then means that you’r ready to go at any time that you come back to work with this code.

That brings this kata to an end, and the final master commits are now up to date too.

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.