Code kata time - Diamond with Jest+

The diamond kata is where you give it a letter, and it creates a diamond up to that letter.

  A
 B B
C   C
 B B
  A

github

Create a github repository for it, then from a command prompt, clone that repository

> git clone https://github.com/pmw57/kata-diamond-2017-12-31 diamond-2017-12-31

Go in to that diamond-2017-12-31 directory and init a new project, using jest --watch` for the test command

init

> npm init

Then install the various modules that we’ll be using, which are jest, watchify, watch-http-server, and npm-run-all
They can be installed all at the same time:

> npm install --save-dev jest watchify watch-http-server npm-run-all

scripts

Then edit package.json to add the build, start, and watch scripts based on the examples at the watchify, watch-http-server, and npm-run-all pages:

    "build": "watchify index.js -o bundle.js",
    "start": "watch-http-server -s",
    "watch": "run-p test build start"

base files

The index.html file is a basic standard HTML page.
In my code editor most of this is automatically generated with: htmlTab

<!DOCTYPE html>
<html>
<head>
    <title>Diamond kata - 2017-12-31</title>
</head>
<body>
<h1>Diamond kata - 2017-12-31</h1>
<script src="bundle.js"></script>
</body>
</html>

The index.js file gets whatever the diamond creates, and shows it in the results section.

/*jslint browser */
var diamond = require("./diamond.js");
var results = document.querySelector(".results");
results.innerHTML = diamond.create("C").join("<br>");

watchers

We can now start the watchers with:

> npm run watch

and force the window to stay on top with Turbotop so that we can have it always visible when writing our code.

commit & first push

We can now make our first commit:

> git status

> git add package.json index.html bundle.js

> git commit -m "Configured for jest, watchify, and watch-http-server"

With the first push it’s important to use -u to set the upstream, so that we in the future only need to use git push and git pull`

> git push -u origin develop

first test

As for the diamond.js file, we’ll start off with a test.

diamond.test.js

var diamond = require("./diamond.js");
test("diamond exists", function () {
    expect(diamond)create.toBeDefined();
});

This test fails because it cannot find diamond.js, so create the file.

Then, the test fails because it cannot find a module, so add one. The simplest way to make this test pass is with:

diamond.js

module.exports.create = null;

So the test needs to be more specific, testing that it is a function instead.

test("diamond exists", function () {
    expect(diamond.create).toBeInstanceOf(Function);
});

We can now make this pass in diamond.js with:

module.exports = {
    create: function () {
        "use strict";
        return;
    }
};

The index page now has an error about .join because it expects to get an array from the diamond.create() function, so we add a new test:

test("diamond gives an array", function () {
    expect(diamond.create()).toEqual([]);
});

commit the first test

Now that everything is setup and working, we can commit our first set of code.

> git status

> git add *.js

> git commit -m "The diamond.create() function exists"
Edit:

npm-run-all doesn’t run the watchers in parallel, it must be run-p that’s used instead

1 Like

A change of server

I’m having trouble with watchify and watch-http-server because things don’t seem to be building.
I’ve since learned that it was because I was using npm-run-all when I should’ve been using run-p

Despite that, it’s time to replace watchify and watch-http-server with browserify and budo instead.

> npm uninstall --save-dev watchify watch-http-server

> npm install --save-dev browserify budo

and replace the build/start/watch scripts with:

    "start": "budo index.js:bundle.js --live",
    "watch": "run-p test start"

We can now run the live testing and bundling server

> npm run watch

and load the test page at localhost:9966

The first test is fairly easy, being for an A diamond.

    test("An A diamond is just the letter A", function () {
        expect(diamond.create("A")).toEqual(["A"]);
    });

In the code we’ll check for an empty parameter, and if it’s not empty we’ll give “A” in an array.

    create: function (char) {
        if (!char) {
            return [];
        }
        return ["A"];
    }

Commit that code

> npm status

> npm add *.js

> npm commit -m "Diamond A"

The next test is for a B diamond, which is a bit more complex.

    test("The B diamond is a bit more complex", function () {
        expect(diamond.create("B")).toEqual([" A ", "B B", " A "]);
    });

As usual, we use simple techniques to make it pass, before refactoring to better code.

        if (char === "A") {
            return ["A"];
        }
        return [
            " A ",
            "B B",
            " A "
        ];

This passes the test, but the output on the screen needs working on.

Putting it in a <pre> tag helps to preserve the spaces

<pre class="results"></pre>

and I’ll update index.js so that it tries to show us a diamond of size Z

results.innerHTML = diamond.create("Z").join("<br>");

Commit this code.

> git status

> git add index.html *.js

> git commit -m "Diamond B"

Before moving on to the next test, we’d better see how we can improve on the current code. Let’s start by defining an array to store the diamond.

    create: function (char) {
        var diamond = [];
        if (!char) {
            return diamond;
        }
        if (char === "A") {
            diamond = ["A"];
            return diamond;
        }
        diamond = [
            " A ",
            "B B",
            " A "
        ];
        return diamond;

Instead of adding A, I want to add the char that’s passed to the function.

        if (char === "A") {
            diamond = [char];
            return diamond;
        }
        diamond = [
            " A ",
            char + " " + char,
            " A "
        ];

I’m not sure how to simplify things further, so will use Diamond C to help figure that out.

    test("The C diamond", function () {
        expect(diamond.create("C")).toEqual(["  A  ", " B B ", "C   C", " B B ", "  A  "]);
    });

The code to make this pass isn’t pretty, but should help to provide a good direction for refactoring:

        if (char === "B") {
            diamond = [
                " A ",
                char + " " + char,
                " A "
            ];
            return diamond;
        }
        diamond = [
            "  A  ",
            " B B ",
            char + "   " + char,
            " B B ",
            "  A  "
        ];
        return diamond;
> git status

> git add index.html *.js

> git commit -m "Diamond C"

I’m seeing a pattern develop, where we can start from the middle and then add on lines to the begin and end.

        diamond.push(char + "   " + char);
        diamond.unshift(" B B ");
        diamond.push(" B B ");
        diamond.unshift("  A  ");
        diamond.push("  A  ");

I want to use a separate function called charLine that creates a line for each character, but to use another function I’ll need to reorganise the current code.

module.exports = (function iife() {
    "use strict";
    function create(char) {
        var diamond = [];
        ...
        return diamond;
    }
    return {
        create
    };
}());

I can now add other functions and easily use them.

    function charLine(char) {
        return char + " " + char;
    }
    ...
    diamond.push(charLine(char));

Now for each character, it will have a different number of spaces between them.
A has no spaces and only 1 A
B has 1 space between
C has 3 spaces
D has 5 spaces

So if n=0 for A, 1 for B, 2 for C, and 3 for D, the number of spaces between is 2n-1

    function charLine(char) {
        var codeForA = String("A").charCodeAt(0);
        var n = char.charCodeAt(0) - codeForA;
        return char + " ".repeat(2 * n - 1) + char;
    }
    ...
        diamond.push(charLine(char));
        diamond.unshift(" " + charLine("B") + " ");
        diamond.push(" " + charLine("B") + " ");
        diamond.unshift("  " + "A" + "  ");
        diamond.push("  " + "A" + "  ");
        return diamond;

With a prevChar() function, the B and C code are getting closer to being identical:

    function prevChar(char) {
        var charCode = char.charCodeAt(0);
        if (char === "A") {
            return;
        }
        return String.fromCharCode(charCode - 1);
    }
...
        if (char === "B") {
            diamond.push(charLine(char));
            char = prevChar(char);
            diamond.unshift(" " + charLine(char) + " ");
            diamond.push(" " + charLine(char) + " ");
            return diamond;
        }
        diamond.push(charLine(char));
        char = prevChar(char);
        diamond.unshift(" " + charLine(char) + " ");
        diamond.push(" " + charLine(char) + " ");
        char = prevChar(char);
        diamond.unshift("  " + charLine(char) + "  ");
        diamond.push("  " + charLine(char) + "  ");
        return diamond;

I’d better commit at this stage:

> git status

> git add *.js

> git commit -m "Using charLine() and prevChar() functions"

The spaces are the next thing to simplify. What if we just use an empty string for the spaces, and add a space to it each time?

        var diamond = [];
        var spaces = "";
...
        if (char === "B") {
            diamond.push(charLine(char));
            char = prevChar(char);
            spaces += " ";
            diamond.unshift(spaces + charLine(char) + spaces);
            diamond.push(spaces + charLine(char) + spaces);
            return diamond;
        }
        diamond.push(charLine(char));
        char = prevChar(char);
        spaces += " ";
        diamond.unshift(spaces + charLine(char) + spaces);
        diamond.push(spaces + charLine(char) + spaces);
        char = prevChar(char);
        spaces += " ";
        diamond.unshift(spaces + charLine(char) + spaces);
        diamond.push(spaces + charLine(char) + spaces);
        return diamond;

We can now see that a while loop will easily take care of the code:

        if (char === "B") {
            diamond.push(charLine(char));
            while (char !== "A") {
                char = prevChar(char);
                spaces += " ";
                diamond.unshift(spaces + charLine(char) + spaces);
                diamond.push(spaces + charLine(char) + spaces);
            }
            return diamond;
        }
        diamond.push(charLine(char));
        while (char !== "A") {
            char = prevChar(char);
            spaces += " ";
            diamond.unshift(spaces + charLine(char) + spaces);
            diamond.push(spaces + charLine(char) + spaces);
        }
        return diamond;

The B and C sections of code are now identical, so the B section of code can now be deleted:

        // if (char === "B") {
        //     diamond.push(charLine(char));
        //     while (char !== "A") {
        //         char = prevChar(char);
        //         spaces += " ";
        //         diamond.unshift(spaces + charLine(char) + spaces);
        //         diamond.push(spaces + charLine(char) + spaces);
        //     }
        //     return diamond;
        // }
        diamond.push(charLine(char));
        while (char !== "A") {
            char = prevChar(char);
            spaces += " ";
            diamond.unshift(spaces + charLine(char) + spaces);
            diamond.push(spaces + charLine(char) + spaces);
        }
        return diamond;

The “A” if statement can also now be removed:

        // if (char === "A") {
        //     diamond.push(char);
        //     return diamond;
        // }
        diamond.push(charLine(char));
        while (char !== "A") {
            char = prevChar(char);
            spaces += " ";
            diamond.unshift(spaces + charLine(char) + spaces);
            diamond.push(spaces + charLine(char) + spaces);
        }
        return diamond;

And commit this code:

> git status

> git add *.js

> git commit -m "Refactoring to a while loop with an expanding space"

And the webpage now shows the full Z-sized diamond.

Diamond kata - 2017-12-31

                         A                         
                        B B                        
                       C   C                       
                      D     D                      
                     E       E                     
                    F         F                    
                   G           G                   
                  H             H                  
                 I               I                 
                J                 J                
               K                   K               
              L                     L              
             M                       M             
            N                         N            
           O                           O           
          P                             P          
         Q                               Q         
        R                                 R        
       S                                   S       
      T                                     T      
     U                                       U     
    V                                         V    
   W                                           W   
  X                                             X  
 Y                                               Y 
Z                                                 Z
 Y                                               Y 
  X                                             X  
   W                                           W   
    V                                         V    
     U                                       U     
      T                                     T      
       S                                   S       
        R                                 R        
         Q                               Q         
          P                             P          
           O                           O           
            N                         N            
             M                       M             
              L                     L              
               K                   K               
                J                 J                
                 I               I                 
                  H             H                  
                   G           G                   
                    F         F                    
                     E       E                     
                      D     D                      
                       C   C                       
                        B B                        
                         A      

Push the code, and that brings an end to this kata

> git push
1 Like

Hi there Paul_Wilkins,

I had nothing better to do today, so I thought
that I would have a go at your kata. :winky:

<!DOCTYPE HTML>
<html lang="en">
<head>
<!--<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self';script-src 'self'; style-src 'self'">-->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1">

<title>untitled document</title>

<link rel="stylesheet" href="screen.css" media="screen">

<style media="screen">

body {
    background-color: #f0f0f0;
    font: 1em/160% verdana, arial, helvetica, sans-serif;
    text-align: center;
 }

h1 {
    font-size: 1.25em;
 } 

#container {
    display: inline-block;
    padding: 1em;
    border: 0.062em solid #999;
    background-color: #fff;
    background-image: linear-gradient( to bottom, #fff, #fcc );
    box-shadow: 0.4em 0.4em 0.4em rgba( 0, 0, 0, 0.3 );
    font-family: 'courier new',  monospace; 
    font-size: 0.9em;
    font-weight: bold;
    line-height: 86%;
    color: #611;
 }

select {
    margin-bottom: 1em;
 }
</style>

</head>
<body> 

 <h1>Javascript needs to be enabled to view this page</h1>
 <div id="container"></div>

<script>
( function( d ) {
   'use strict';

   d.getElementsByTagName( 'h1' )[ 0 ].textContent = 'Diamond kata';

   var alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M',
                   'N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
                  ];

   var obj = d.getElementById( 'container' );
   var c; 
   var opt;
   var sel = d.createElement( 'select' );  /* create the select element */
       sel.setAttribute( 'id', 'letters' );

       opt = d.createElement( 'option' ); /* create the initial option element */
       opt.setAttribute( 'value', '' );
       opt.appendChild( d.createTextNode( 'choose a letter' ) ); /* set the initial option value and text */

       sel.appendChild( opt );
       obj.appendChild( sel );

   for  ( c = 0;  c < alphabet.length; c ++ ) {
          opt[ c ] = d.createElement( 'option' ); /* create the alphabet option elements */
          opt[ c ].setAttribute( 'value',  c + 1 );
          opt[ c ].appendChild( d.createTextNode( alphabet[ c ] ) );
          sel.appendChild( opt[ c ] );
      }
       obj.appendChild( sel );
       d.getElementById( 'letters' ).addEventListener( 'change', creatDiamond, false );

  function creatDiamond( ) {
   while ( obj.children.length > 1 ) {  /* remove previous diamond */
           obj.removeChild( obj.lastChild );
       }
   var n = d.getElementById( 'letters' ).value;  /* get the select value */
   var num = n * 2 - 1;
   var line = [];
   var letter = [];
   var temp = '';
   for  ( c = 0;  c < num; c ++ ) {
        line[ c ] = d.createElement( 'div' ); /* create the appropriate divs */
   if ( c === 0 ) {
        letter[ c]  = d.createTextNode( alphabet[ c ]   ); /* set the first "A" div */
        line[ c ].appendChild( letter[ c ] );
    }
   else {
   if ( ( c > 0 ) && ( c < n ) ) {
        temp += '\u00a0\u00a0\u00a0';
        letter[ c]  = d.createTextNode( alphabet[ c ] + temp + alphabet[ c ] ); /* set the outward  alphabet  divs */
        line[ c ].appendChild( letter[ c ] );
    }
   else {
   if ( c < n * 2-2 ) { 
        temp = temp.replace( /\u00a0\u00a0\u00a0/, '' );
        letter[ c] = d.createTextNode( alphabet[ num - ( c + 1 ) ] + temp + 
                     alphabet[ num - ( c + 1 ) ] ); /* set the inward  alphabet  divs */
        line[ c ].appendChild( letter[ c ] );
    }
   else {
        letter[ c] = d.createTextNode( alphabet[ num - ( c + 1 ) ] ); /* set the final "A" div */
        line[ c ].appendChild( letter[ c ] );
      }
     }
    }
        obj.appendChild( line[ c ] );       
  }
 }
}( document ) );
</script>

</body>
</html>

1 Like

Yes that is certainly an interesting approach, but you haven’t used any tests to drive the development of your program.

The point and purpose of a kata is to be the mechanism around which you practice your test-driven development skills.
It is through repeatedly doing the same kata as a programming warmup discipline each day, that you get to explore different ways and techniques, which allows you to examine the minutia of decisions that are made throughout the process.

I would like to see you use tests to help you drive towards more complex behaviour. For example there’s FooBarQix kata with several extra requirements where 105 gives you FooBarQix*Bar for example.
Attempting to achieve all of those requirements without tests can be tricky. With tests though it can be quite a simple process.

Tests are no panacea though for you can use them inappropriately to make things more difficult for yourself. So long as you use the tests to help you drive your code to where you want it to be, that seems to be the sweet spot.

[edit]Note: I messed up in the above kata because all my commits were going to the Master branch.
I should have created a separate branch on which to do the work, and then only merge it when it was done.[/edit]

Hi there Paul,

That may well be true, but, at my age, today may possibly
be my last before shaking off this mortal coil. :wonky:

If I awake tomorrow morning and find that I am still alive, I
might possibly look at the code that I produced the day
before, but then most probably I will not. :unhappy:

Instead, as is my want, I will sit down quietly, smile benignly,
and then meditate upon something completely different. :winky:

p.s.
By the way, apart from your image, I could not recognise or
understand any of your code apart from “git” which I am
often referred to ,prefixed with silly old". :eek:

coothead

If you’re after some practice material, there’s a good range of kata’s available. And possibly for others (although I do like the design), here’s a gentle introduction to JavaScript test-driven development.

Hi there Paul,

I clicked your link and was unable to make head or tail of it. :rolleyes:

It seems that I need to get Codepen, a Mocha framework
and then Node js quickly followed by GitHub before I can
start to solve some unspecified problem.

My personal preference, though, for problem solving is
Notepad++, a few grey cells and a current browser.

When it comes to coding, due to my advanced age and a
rather delicate constitution, the only flavour that I am able
to actually stomach is, of course, unalloyed “Vanilla”. :biggrin:

coothead

Vanilla is perfectly acceptable, you can start with your own custom expect function:

function expect(expected, actual) {
    if (expected === actual) {
        return;
    }
    console.warn("Expected " + expected + " but got " + actual);
}
1 Like

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