Code Katas - Notes

If martial artists use kata as a method for exercise and practice, what might be the equivalent for coders like us? Coding katas are short, repeatable programming challenges which are meant to exercise everything from your focus, to your workflow.

Make folder called Katas, inside make directory called src & tests then run

composer require phpunit/phpunit
vendor/bin/phpunit --generate-configuration

Troubleshoot

This test does not have a @covers annotation but is expected to have one
/** 
 * 
 * @test 
 * @covers \\src\\RomanNumerals -> find the one that it refers
	
 */
/**
* @dataProvider checks -> get the function checks
*/

Prime Numbers

<?php
namespace App;

class PrimeFactors {
    public function generate($number) {
        $factors = [];
        for ($divisor = 2; $number > 1;$divisor++ ) {
            for (;$number % $divisor === 0; $number /= $divisor) {
                $factors[] = $divisor;
            }
        }
        return $factors;
    }
}

<?php
use PHPUnit\\Framework\\TestCase;

class PrimeFactorsTest extends TestCase
{
    /** 
     * 
     * @test 
     * @dataProvider factors
     */
    function it_generate_prime_factors_for_1($number, $expected)
    {
        $factors = new \\App\\PrimeFactors;

        $this->assertEquals($expected, $factors->generate($number));
    }

    public function factors() {
        return [
            [1, []],
            [2, [2]],
            [3, [3]],
            [4, [2,2]],
            [5, [5]],
            [6, [2,3]],
            [8, [2,2,2]],
            [100, [2,2,5,5]],
            [999, [3,3,3,37]],
        ];
    }
}

Roman Numerals

<?php
use App\\RomanNumerals;
use PHPUnit\\Framework\\TestCase;

class RomanNumeralsTest extends TestCase
{
    /** 
     * 
     * @test 
     * @covers \\src\\RomanNumerals
     * @dataProvider checks
     */
    function it_generates_the_roman_numeral_for_1($number, $numeral)
    {
        $this->assertEquals($numeral, RomanNumerals::generate($number));
    }

    /** 
     * 
     * @test 
     * @covers \\src\\RomanNumerals
     */
    function it_cannot_generate_a_roman_numeral()
    {
        $this->assertFalse(RomanNumerals::generate(0));
    }

    public function checks() {
        return [
            [1, 'I'],
            [2, 'II'],
            [3, 'III'],
            [4, 'IV'],
            [5, 'V'],
            [6, 'VI'],
            [7, 'VII'],
            [8, 'VIII'],
            [9, 'IX'],
            [10, 'X'],
            [40, 'XL'],
            [50, 'L'],
            [90, 'XC'],
            [100, 'C'],
            [500, 'D'],
            [400, 'CD'],
            [900, 'CM'],
            [1000, 'M'],
        ];
    }
}

<?php

namespace App;

class RomanNumerals {
    const NUMERALS = [
        'M' => 1000,
        'CM' => 900,
        'D' => 500,
        'CD' => 400,
        'C' => 100,
        'XC' => 90,
        'L' => 50,
        'XL' => 40,
        'X' => 10,
        'IX' => 9,
        'V' => 5,
        'IV' => 4,
        'I' => 1
    ];
    public static function generate($number) {
        if ($number <= 0) {
            return false;
        }
        $result = '';
        
        foreach (static::NUMERALS as $numeral => $arabic) {
            for(;$number >= $arabic; $number -= $arabic) {
                $result .= $numeral;
            }
        }
        return $result;
    }
}

BowlingGame

<?php

namespace App;

class GameBowling {
    const FRAMES_PER_GAME = 10;
    protected array $rolls = [];

    public function roll(int $pins) {
        $this->rolls[] = $pins;
    }

    protected function isStrike($roll) {
        return $this->pinCount($roll)==10;
    }
    protected function isSpare($roll) {
        return $this->defaultFrameScore($roll) == 10;
    }
    protected function defaultFrameScore(int $roll): int {
        return $this->pinCount($roll) + $this->pinCount($roll+1);
    }
    protected function strikeBonus(int $roll): int {
        return $this->pinCount($roll + 1) + $this->pinCount($roll + 2);
    }
    protected function spareBonus(int $roll): mixed {
        return $this->pinCount($roll + 2);
    }
    protected function pinCount(int $roll): int {
        return $this->rolls[$roll];
    }

    public function score() {
        $score = 0;
        $roll = 0;
        foreach (range(1, self::FRAMES_PER_GAME) as $frame) {

            if ($this->isStrike($roll)) {
                $score += $this->pinCount($roll) + $this->strikeBonus($roll);
                $roll += 1;
                continue;
            }
            
            $score += $this->defaultFrameScore($roll);

            if ($this->isSpare($roll)) {
                $score += $this->spareBonus($roll);
                $roll += 2;
                continue;
            }
            
            $roll += 2;
            
        }
        return $score;
    }
}

<?php
use App\\GameBowling;
use PHPUnit\\Framework\\TestCase;

class GameBowlingTest extends TestCase
{
    /** 
     * 
     * @test 
     * @covers \\src\\GameBowling
     */
    function it_scores_a_gutter_game_as_zero() {
        $game = new GameBowling();
        foreach (range(1,20) as $roll) {
            $game->roll(0);
        }
        $this->assertSame(0, $game->score());
    }
    /** 
     * 
     * @test 
     * @covers \\src\\GameBowling
     */
    function it_scores_all_ones()
    {
        $game = new GameBowling();

        foreach (range(1, 20) as $roll) {
            $game->roll(1);
        }

        $this->assertSame(20, $game->score());
    }
    /** 
     * 
     * @test 
     * @covers \\src\\GameBowling
     */
    function it_awards_a_one_roll_bonus_for_every_spare() {
        $game = new GameBowling();

        $game->roll(5);
        $game->roll(5);

        $game->roll(8);

        foreach (range(1,17) as $roll) {
            $game->roll(0);
        }
        $this->assertSame(26, $game->score());
    }

    /** 
     * 
     * @test 
     * @covers \\src\\GameBowling
     */
    function it_awards_a_two_roll_bonus_for_every_strike() {
        $game = new GameBowling();

        $game->roll(10);
        $game->roll(5);
        $game->roll(2);

        foreach (range(1,16) as $roll) {
            $game->roll(0);
        }
        $this->assertSame(24, $game->score());
    }

    /** 
     * 
     * @test 
     * @covers \\src\\GameBowling
     */
    function a_spare_on_the_final_frame_grants_one_extra_balls() {
        $game = new GameBowling();

        foreach (range(1,18) as $roll) {
            $game->roll(0);
        }
        $game->roll(5);
        $game->roll(5);
        $game->roll(5);
        $this->assertSame(15, $game->score());
    }

    /** 
     * 
     * @test 
     * @covers \\src\\GameBowling
     */
    function a_strike_on_the_final_frame_grants_two_extra_balls() {
        $game = new GameBowling();

        foreach (range(1,18) as $roll) {
            $game->roll(0);
        }
        $game->roll(10);
        $game->roll(10);
        $game->roll(10);
        $this->assertSame(30, $game->score());
    }

    /** 
     * 
     * @test 
     * @covers \\src\\GameBowling
     */
    function it_scores_a_perfect_game() {
        $game = new GameBowling();

        foreach (range(1,12) as $roll) {
            $game->roll(10);
        }
        $this->assertSame(300, $game->score());
    }
}

String Calculator Kata

<?php
use App\\StringCalculator;
use PHPUnit\\Framework\\TestCase;

class StringCalculatorTest extends TestCase
{
    /** 
     * 
     * @test 
     * @covers \\src\\StringCalculator
     */
    function it_evaluates_an_empty_string_as_0() {
        $calculator = new StringCalculator;
        $this->assertEquals(0, $calculator->add(''));
    }

    /** 
     * 
     * @test 
     * @covers \\src\\StringCalculator
     */
    function it_finds_the_sum_of_a_single_number() {
        $calculator = new StringCalculator;
        $this->assertEquals(5, $calculator->add('5'));
    }
    
    /** 
     * 
     * @test 
     * @covers \\src\\StringCalculator
     */
    function it_finds_the_sum_of_two_numbers() {
        $calculator = new StringCalculator;
        $this->assertEquals(10, $calculator->add('5,5'));
    }

    /** 
     * 
     * @test 
     * @covers \\src\\StringCalculator
     */
    function it_finds_the_sum_of_any_amount_of_numbers() {
        $calculator = new StringCalculator;
        $this->assertEquals(19, $calculator->add('5,5,5,4'));
    }

    /** 
     * 
     * @test 
     * @covers \\src\\StringCalculator
     */
    function it_accept_a_new_line_character_as_a_delimiter_too() {
        $calculator = new StringCalculator;
        $this->assertEquals(10, $calculator->add("5\\n5"));
    }

    /** 
     * 
     * @test 
     * @covers \\src\\StringCalculator
     */
    function negative_numbers_are_not_allowed() {
        $calculator = new StringCalculator;
        $this->expectException(\\Exception::class);
        $this->assertEquals(10, $calculator->add("5,-4"));
    }

    /** 
     * 
     * @test 
     * @covers \\src\\StringCalculator
     */
    function number_greater_than_1000_are_ignored() {
        $calculator = new StringCalculator;
        $this->assertEquals(5, $calculator->add("5,1001"));
    }

    /** 
     * 
     * @test 
     * @covers \\src\\StringCalculator
     */
    function it_supports_custom_delimiters() {
        $calculator = new StringCalculator;
        $this->assertEquals(20, $calculator->add("//:\\n5:4:11"));
    }

}

<?php

namespace App;

class StringCalculator {

    const MAX_NUMBER_ALLOWED = 1000;
    protected string $delimiter = ",|\\n";

    public function add(string $numbers) {
        $this->disallowNegatives($numbers = $this->parseString($numbers));
        return array_sum($this
            ->ignoreGreaterThan1000($numbers)
        );
    }

    protected function parseString(string $numbers) {
        $customDelimiter = '\\/\\/(.)\\n';
        if (preg_match("/{$customDelimiter}/",$numbers, $matches)) {
            $this->delimiter = $matches[1];
            $numbers = str_replace($matches[0], '', $numbers);
        }
        
        return preg_split("/{$this->delimiter}/", $numbers);
    }

    protected function disallowNegatives(array $numbers): void {
        foreach ($numbers as $number) {
            if (!empty($number) && $number < 0) {
                throw new \\Exception('Negative numbers are disallowed');
            }
        }
    }
    
    protected function ignoreGreaterThan1000(array $numbers): array {
        return array_filter(
            $numbers, fn($number) => $number <= self::MAX_NUMBER_ALLOWED
        );
    }

}

Tennis Match

<?php

namespace App;

class TennisMatch {

    protected Player $playerOne;
    protected Player $playerTwo;

    public function __construct(Player $playerOne, Player $playerTwo) {
        $this->playerOne = $playerOne;
        $this->playerTwo = $playerTwo;
    }

    public function score() {

        if ($this->hasWinner()) {
            return 'Winner: '.$this->leader()->name;
        }

        if ($this->hasAdvantage()) {
            return 'Advantage: ' . $this->leader()->name;
        }

        if ($this->isDeuce()) {
            return 'deuce';
        }

        return sprintf(
            '%s-%s',
            $this->playerOne->toTerm(),
            $this->playerTwo->toTerm(),
        );
    }
    
    protected function hasWinner(): bool {
        if (max([$this->playerOne->points, $this->playerTwo->points]) < 4) {
            return false;
        }
        return abs($this->playerOne->points - $this->playerTwo->points) >= 2 ;
    }

    protected function leader(): Player {
        return $this->playerOne->points > $this->playerTwo->points 
            ? $this->playerOne 
            : $this->playerTwo;
    }
    protected function isDeuce(): bool {
        return $this->canBeWon() && $this->playerOne->points === $this->playerTwo->points;
    }

    protected function hasAdvantage(): bool {
        if ($this->canBeWon()) {
            return ! $this->isDeuce();
        }
        return false;
    }

    protected function canBeWon(): bool {
        return $this->playerOne->points >= 3 && $this->playerTwo->points >= 3;
    }
}
<?php

namespace App;

class Player
{
    public string $name;
    public int $points = 0;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function score() {
        $this->points++;
    }

    public function toTerm() {
        switch($this->points) {
            case '0':
                return 'love';
            case '1':
                return 'fifteen';
            case '2':
                return 'thirty';
            case '3':
                return 'forty';
        }
    }
}
<?php

namespace App;

class TennisMatch {

    protected Player $playerOne;
    protected Player $playerTwo;

    public function __construct(Player $playerOne, Player $playerTwo) {
        $this->playerOne = $playerOne;
        $this->playerTwo = $playerTwo;
    }

    public function score() {

        if ($this->hasWinner()) {
            return 'Winner: '.$this->leader()->name;
        }

        if ($this->hasAdvantage()) {
            return 'Advantage: ' . $this->leader()->name;
        }

        if ($this->isDeuce()) {
            return 'deuce';
        }

        return sprintf(
            '%s-%s',
            $this->playerOne->toTerm(),
            $this->playerTwo->toTerm(),
        );
    }
    
    protected function hasWinner(): bool {
        if (max([$this->playerOne->points, $this->playerTwo->points]) < 4) {
            return false;
        }
        return abs($this->playerOne->points - $this->playerTwo->points) >= 2 ;
    }

    protected function leader(): Player {
        return $this->playerOne->points > $this->playerTwo->points 
            ? $this->playerOne 
            : $this->playerTwo;
    }
    protected function isDeuce(): bool {
        return $this->canBeWon() && $this->playerOne->points === $this->playerTwo->points;
    }

    protected function hasAdvantage(): bool {
        if ($this->canBeWon()) {
            return ! $this->isDeuce();
        }
        return false;
    }

    protected function canBeWon(): bool {
        return $this->playerOne->points >= 3 && $this->playerTwo->points >= 3;
    }
}

Gilded rose Kata (best)

https://github.com/laracasts/gilded-rose-with-phpunit

We’ll be refactoring this.

<?php

namespace App;

class GildedRose
{
    public $name;

    public $quality;

    public $sellIn;

    public function __construct($name, $quality, $sellIn)
    {
        $this->name = $name;
        $this->quality = $quality;
        $this->sellIn = $sellIn;
    }

    public static function of($name, $quality, $sellIn)
    {
        return new static($name, $quality, $sellIn);
    }

    public function tick()
    {
        if ($this->name != 'Aged Brie' and $this->name != 'Backstage passes to a TAFKAL80ETC concert') {
            if ($this->quality > 0) {
                if ($this->name != 'Sulfuras, Hand of Ragnaros') {
                    $this->quality = $this->quality - 1;
                }
            }
        } else {
            if ($this->quality < 50) {
                $this->quality = $this->quality + 1;

                if ($this->name == 'Backstage passes to a TAFKAL80ETC concert') {
                    if ($this->sellIn < 11) {
                        if ($this->quality < 50) {
                            $this->quality = $this->quality + 1;
                        }
                    }
                    if ($this->sellIn < 6) {
                        if ($this->quality < 50) {
                            $this->quality = $this->quality + 1;
                        }
                    }
                }
            }
        }

        if ($this->name != 'Sulfuras, Hand of Ragnaros') {
            $this->sellIn = $this->sellIn - 1;
        }

        if ($this->sellIn < 0) {
            if ($this->name != 'Aged Brie') {
                if ($this->name != 'Backstage passes to a TAFKAL80ETC concert') {
                    if ($this->quality > 0) {
                        if ($this->name != 'Sulfuras, Hand of Ragnaros') {
                            $this->quality = $this->quality - 1;
                        }
                    }
                } else {
                    $this->quality = $this->quality - $this->quality;
                }
            } else {
                if ($this->quality < 50) {
                    $this->quality = $this->quality + 1;
                }
            }
        }
    }
}

to

<?php

namespace App;

class GildedRose
{
    private static $items = [
        'normal' => Item::class,
        'Aged Brie' => Brie::class,
        'Sulfuras, Hand of Ragnaros' => Sulfuras::class,
        'Backstage passes to a TAFKAL80ETC concert' => BackstagePasses::class,
        'Conjured Mana Cake' => Conjured::class,
    ];

    public static function of($name, $quality, $sellIn)
    {
        if (! array_key_exists($name, self::$items)) {
            throw new \\InvalidArgumentException('Item type does not exist.');
        }
        $class = self::$items[$name];
        return new $class($quality, $sellIn);
    }
}
<?php

namespace App;

class Item {
    public $sellIn;
    public $quality;

    public function __construct($quality, $sellIn) {
        $this->sellIn = $sellIn;
        $this->quality = $quality;
    }

    public function tick() {
        $this->sellIn-= 1;
        $this->quality-= 1;

        if ($this->sellIn <= 0 && $this->quality > 0) {
            $this->quality -= 1;
        }

        if ($this->quality <= 0) {
            $this->quality = 0;
        }
    }
}
<?php

namespace App;

class Brie extends Item {
    public function tick() {
        $this->sellIn -= 1;
        $this->quality += 1;

        if ($this->sellIn <= 0) {
            $this->quality += 1;
        }
        if ($this->quality > 50) {
            $this->quality = 50;
        }
    }
}
<?php

namespace App;

class Sulfuras extends Item {
    public function tick() {
        
    }
}
<?php

namespace App;

class BackstagePasses extends Item {
    public function tick() {
        $this->quality += 1;

        if ($this->sellIn <= 10) {
            $this->quality += 1;
        }

        if ($this->sellIn <= 5) {
            $this->quality += 1;
        }
        if ($this->sellIn <= 0) {
            $this->quality = 0;
        }

        if ($this->quality > 50) {
            $this->quality = 50;
        }
        $this->sellIn-= 1;
    }
}
<?php

namespace App;

class Conjured extends Item {
    public function tick() {
        
        $this->sellIn-= 1;
        $this->quality-= 2;

        if ($this->sellIn <= 0 && $this->quality > 0) {
            $this->quality -= 2;
        }

        if ($this->quality <= 0) {
            $this->quality = 0;
        }
    }
}

99 Bottles Kata

<?php

namespace App;

class Song {
    public function sing() {
        return $this->verses(99,0);
    }

    public function verses($start, $end) {
        return implode("\\n", array_map(
            fn($number) => $this->verse($number), 
            range($start, $end)
        ));
    }

    public function verse($number) {

        switch ($number) {
            case 2:
                return 
                    "2 bottles of beer on the wall\\n".
                    "2 bottles of beer\\n".
                    "Take one down and pass it around\\n".
                    "1 bottle of beer on the wall\\n";
            case 1:
                return 
                    "1 bottle of beer on the wall\\n".
                    "1 bottle of beer\\n".
                    "Take one down and pass it around\\n".
                    "No more bottles of beer on the wall\\n";
            case 0:
                return 
                    "No more bottles of beer on the wall\\n".
                    "No more bottles of beer\\n".
                    "Go to the store and buy some more\\n".
                    "99 bottles of beer on the wall";
            default:
                return 
                    $number . " bottles of beer on the wall\\n".
                    $number . " bottles of beer\\n".
                    "Take one down and pass it around\\n".
                    $number - 1 . " bottles of beer on the wall\\n";
        }
    }
}
<?php
use App\\Song;
use PHPUnit\\Framework\\TestCase;

class SongTest extends TestCase
{

    /** 
     * 
     * @test 
     * @covers \\src\\Song
     */
    function ninety_nine_bottle_first() {
        $expected = <<<EOT
            99 bottles of beer on the wall
            99 bottles of beer
            Take one down and pass it around
            98 bottles of beer on the wall

            EOT;
        $result = (new Song)->verse(99);
        $this->assertEquals($expected, $result);
    }
     /** 
     * 
     * @test 
     * @covers \\src\\Song
     */
    function ninety_eight_bottle_verse() {
        $expected = <<<EOT
            98 bottles of beer on the wall
            98 bottles of beer
            Take one down and pass it around
            97 bottles of beer on the wall

            EOT;
        $result = (new Song)->verse(98);
        $this->assertEquals($expected, $result);
    }

     /** 
     * 
     * @test 
     * @covers \\src\\Song
     */
    function two_bottle_verse() {
        $expected = <<<EOT
            2 bottles of beer on the wall
            2 bottles of beer
            Take one down and pass it around
            1 bottle of beer on the wall

            EOT;
        $result = (new Song)->verse(2);
        $this->assertEquals($expected, $result);
    }

     /** 
     * 
     * @test 
     * @covers \\src\\Song
     */
    function one_bottle_verse() {
        $expected = <<<EOT
            1 bottle of beer on the wall
            1 bottle of beer
            Take one down and pass it around
            No more bottles of beer on the wall
            
            EOT;
        $result = (new Song)->verse(1);
        $this->assertEquals($expected, $result);
    }
/** 
     * 
     * @test 
     * @covers \\src\\Song
     */
    function no_more_bottle_verse() {
        $expected = <<<EOT
            No more bottles of beer on the wall
            No more bottles of beer
            Go to the store and buy some more
            99 bottles of beer on the wall
            EOT;
        $result = (new Song)->verse(0);
        $this->assertEquals($expected, $result);
    }

    /** 
     * 
     * @test 
     * @covers \\src\\Song
     */
    function it_gets_the_lyrics() {
        //$this->assertEquals(true, true);
        $expected = file_get_contents(__DIR__.'/stubs/lyrics.stub');
        $result = (new Song)->sing();

        $this->assertEquals($expected, $result);
    }
}

GitHub - madindo/laracast-code-katas
Contribute to madindo/laracast-code-katas development by creating an account on GitHub.

Subscribe to You Live What You Learn

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe