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);
}
}