Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

четверг, 26 июля 2012 г.

TDD на PHP используя Zend Studio - собственно, калькулятор

Краткое содержание прошлых серий:
- подружили Zend Studio и Zend Server
- подружили Zend Studio и GitHub

Дальше у нас чистой воды TDD. Буду выкладывать код по очереди [тест], [фикс] [тест][фикс]. Всю хистори можно посмотреть тут (файлы Calculator.php и CalculatorTest.php).

[тест]
    public function testShould2When2Plus4() {
        $calculator = new Calculator();
        $actual =  $calculator->calculate('2+2');
        $this->assertEquals('4', $actual);
    }
[фикс]
class Calculator {
    public function calculate($expression) {
        return 4; // это самое простое, что заставит калькулятор работать!
    }
}
[коммит] 2+2 =4 !!!
 [тест]
    public function testShould3When4Plus7() {
        $calculator = new Calculator();
        $actual =  $calculator->calculate('3+4');
        $this->assertEquals('7', $actual);
    }
[фикс]
class Calculator {
    public function calculate($expression) {
        return substr($expression, 0, 1) + substr($expression, 1, 2); // теперь односимвольные числа умеем суммировать 
    }
}
[коммит] Теперь калькулятор может суммировать два однозначных числа

[рефакторинг] Выделил setup в тестах
class CalculatorTest extends PHPUnit_Framework_TestCase {

    private $Calculator; // поле :)                       

    protected function setUp() { // сетап вызовется перед каждым тестом                  
        parent::setUp ();                          
        $this->Calculator = new Calculator(); // и создаст калькулятор
    }                                              

    protected function tearDown() { // а это чудо восле каждого теста                 
        $this->Calculator = null; // обнулит ссылку                 
        parent::tearDown ();                       
    }                                              

    public function testShould4When2Plus2() {
        $actual = $this->Calculator->calculate('2+2'); // теперь можно пользоваться полем напрямую
        $this->assertEquals('4', $actual);
    }     

    public function testShould7When3Plus4() {
        $actual = $this->Calculator->calculate('3+4');
        $this->assertEquals('7', $actual);
    }

}
[коммит]

[тест]
    public function testShould33When11Plus22() { // новый тест
        $actual = $this->Calculator->calculate('11+22');
        $this->assertEquals('33', $actual);
    }
[фикс]
class Calculator {     

    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);  // усложняем логику
        $sum = (int)($out[0][0]) + (int)($out[0][1]);        

        return $sum;
    }
}
[коммит] Теперь калькулятор умеет суммировать многозначные числа

[рефакторинг] Выделил data provider в тестах
class CalculatorTest extends PHPUnit_Framework_TestCase {

    ...

    public static function provider() { // поставщик данных теста экономит место
        return array(
            array(2, 2, 4),
            array(3, 4, 7),
            array(11, 22, 33),
        );
    }
    
    // эта аннотация важна
    /**
     * @dataProvider provider 
     */
    public function testShouldSumWhenXPlusY($x, $y, $expected) { // тест-шаблон наполнится данными
        $actual = $this->Calculator->calculate($x.'+'.$y);
        $this->assertEquals($expected, $actual);
    }
}
[коммит]

[тест]
    // эта аннотация говорит, что ловим исключение
    /**
     * @expectedException RuntimeException
     */
    public function testShouldExceptionWhenInvalidExpression() {
        $this->Calculator->calculate('1');
    }
[фикс]
    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2) {  // вот и проверочка                             
            throw new RuntimeException('Invalid expression format'); 
        }                                                            

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        return $sum;
    }
[коммит] Есть простая валидация выражения
[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenMoreThanOnePlus() {
         $this->Calculator->calculate('1++3');
    }
[фикс]
    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) { // добавили подусловие
            throw new RuntimeException('Invalid expression format');
        }

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        return $sum;
    }
[коммит] Валидция выражения на количество знаков + (должно быть 1)

[рефакторинг] Добавил новый параметр методу - $base - основание системы счисления
class CalculatorTest extends PHPUnit_Framework_TestCase {

   ...

    public static function provider() {
        return array(
            array(2, 2, 10, 4), // передаю во всех тестах, для 10ричной системы
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
        );
    }

    /**
     * @dataProvider provider
     */
    public function testShouldSumWhenXPlusY($x, $y, $base, $expected) { // расширил метод-шаблон
        $actual = $this->Calculator->calculate($x.'+'.$y, $base);
        $this->assertEquals($expected, $actual);
    }

    ...

}
[фикс]
class Calculator {     
        public function calculate($expression, $base) { // и добавил в сигнатуре метода
            ...
[коммит] Первый шаг в сторону 16-ричного калькулятора - введение основы системы счисления как параметр

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A),  // новый тест   
        );
    }
[фикс]
    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        if ($sum == 10 && $base == 16) { // добавили проверочку (ужас, но работает!)
            return "A";                  
        }                                

        return $sum;
    }
[коммит] В 16ричной 9 + 1 = А

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F), // еще тест    
        );
    }
[фикс]
    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        if ($base == 16) {
            if ($sum == 10) {
                return "A";
            } else if ($sum == 15) { // проверка усложнилась
                return "F";
            }
        }                                

        return $sum;
    }
[коммит] В 16ричной 6 + 9 = F

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E), // еще тест
        );
    }
[фикс]
class Calculator {

    private $Digits = "0123456789ABCDEF";  // выделили все буковки

    public function calculate($expression) {
        preg_match_all("/[0-9]+/", $expression, $out);

        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }

        $sum = (int)($out[0][0]) + (int)($out[0][1]);     

        if ($base == 16) {
            if ($sum > 9) { // и тут сделали сразу для всех остальных буковок
                return $this->Digits[$sum];
            }
        }                               

        return $sum;
    }
}
[коммит] В 16ричной 6 + 8 = E

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E),
            array(A, 5, 16, F), // еще тест  
        );
    }
[фикс]
class Calculator {     
     
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          preg_match_all("/[0-9A]+/", $expression, $out); // тут добавили буковку
          
          if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
               throw new RuntimeException('Invalid expression format');
          }
               
          $sum = $this->toInt($out[0][0]) + // небольшой фикс 
                  (int)($out[0][1]);     

          if ($base == 16) {
               if ($sum > 9) {
                    return $this->Digits[$sum];
               } 
               
          }
          
          return $sum;
     }
     
     private function toInt($hex) { // новый метод
          if (is_numeric($hex)) return $hex; 
          if ($hex == "A") return 10;
     }
}
[коммит] В 16ричной A + 5 = F

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E),
            array(A, 5, 16, F), 
            array(1, E, 16, F), // еще тест
        );
    }
[фикс]
class Calculator {     
     
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          preg_match_all('/['.$this->Digits.']+/', $expression, $out); // сделали вообще универсальную регекспу
          
          if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
               throw new RuntimeException('Invalid expression format');
          }
               
          $sum = $this->toInt($out[0][0]) + 
                  $this->toInt($out[0][1]);  // тут исправили   

          if ($base == 16) {
               if ($sum > 9) {
                    return $this->Digits[$sum];
               } 
               
          }
          
          return $sum;
     }
     
     private function toInt($hex) {
          if (is_numeric($hex)) return $hex; 
          if ($hex == "A") return 10;
          if ($hex == "E") return 14; // тут добавили
     }
}
[коммит] В 16ричной 1 + E = F

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E),
            array(A, 5, 16, F), 
            array(1, E, 16, F),  
            array(2, B, 16, D), // еще тест
        );
    }
[фикс]
     private function toInt($hex) {
          if (is_numeric($hex)) return $hex; 
          return strpos($this->Digits, $hex); // сделали универсальнее
     }
[коммит] В 16ричной 2 + B = D

[тест]
    public static function provider() {
        return array(
            array(2, 2, 10, 4),
            array(3, 4, 10, 7),
            array(11, 22, 10, 33),
            array(9, 1, 16, A), 
            array(6, 9, 16, F),  
            array(6, 8, 16, E),
            array(A, 5, 16, F), 
            array(1, E, 16, F),  
            array(2, B, 16, D),
            array(B, E, 16, 19), // еще тест
        );
    }
[фикс]
class Calculator {     
     
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          ...    

          if ($base == 16) {                              
               if ($sum > 16) { // добавили подифчик
                    $sum = 10 + $sum - 16;
               } else if ($sum > 9) {
                    return $this->Digits[$sum];
               }
                
               
          }
          
          return $sum;
     }
     
     ...
}
[коммит] В 16ричной B + E = 19

[рефакторинг] Немного переделал логику
class Calculator {    
    
    private $Digits = "0123456789ABCDEF";
    
    public function calculate($expression, $base) {
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
            
        $sum = $this->toInt($out[0][0]) + 
               $this->toInt($out[0][1]);    

        if ($base == 16) {                        
            if ($sum > 16) {
                return '1'.($sum - 16); // тут
            } else if ($sum > 9) {
                return $this->Digits[$sum];
            }                     
        }
        
        return (string)$sum; // и тут
    }
    ...
[коммит]

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'), // еще тест
          );
     }
[фикс]
class Calculator {     
     
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          ...     

          if ($base == 16) {                              
               if ($sum > 16) {
                    return '1'.$this->toHex($sum - 16); // исправили
               } else if ($sum > 9) {
                    return $this->Digits[$sum];
               }                          
          }
          
          return (string)$sum;
     }
     
     private function toHex($int) { // новый метод
          if ($int == 11) return "B";
          return $int;
     }
     
     ...
}
[коммит] В 16ричной C + F = 1B

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'), // еще тест
          );
     }
[фикс]
     private function toHex($int) {
          return $this->Digits[$int]; // сделали универсальным
     }
[коммит] В 16ричной F + F = 1E

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'), // еще тест
          );
     }
[фикс]
class Calculator {     
     
     private $Base; // теперь нам основание нужно в другом методе
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          $this->Base = $base; // потому сохраняем его в поле
          preg_match_all('/['.$this->Digits.']+/', $expression, $out);
          
          if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
               throw new RuntimeException('Invalid expression format');
          }
               
          $sum = $this->toInt($out[0][0]) + 
                  $this->toInt($out[0][1]);     

          if ($base == 16) {                              
               if ($sum > $base) { // тут чуть порефакторили
                    return '1'.$this->toHex($sum - 16);
               } else if ($sum > 9) {
                    return $this->Digits[$sum];
               }                          
          }
                    
          return (string)$sum;
     }
     
     private function toHex($int) {
          return $this->Digits[$int];
     }
     
     private function toInt($hex) {          
          if (strlen($hex) == 2) { // и тутки добавили целый if
               return $this->Base*$this->toInt(substr($hex, 0, 1)) + $this->toInt(substr($hex, 1, 1));
          }
          
          if (is_numeric($hex)) return $hex; 
          return strpos($this->Digits, $hex);
     }
[коммит] В 16ричной 1C + 1 = 1D

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'), // еще тест
          );
     }
[фикс]
class Calculator {     
     
     private $Base; 
     private $Digits = "0123456789ABCDEF";
     
     public function calculate($expression, $base) {
          ...    

          $sum = $this->toInt($out[0][0]) + 
                  $this->toInt($out[0][1]); 
         
          if ($base == 16) {                              
               if ($sum >= $base*2) { // добавили еще один if
                    return '2'.$this->toHex($sum - 32);
               } else if ($sum > $base) { // дальше без изменений
                    return '1'.$this->toHex($sum - 16);
               } else if ($sum > 9) {
                    return $this->Digits[$sum];
               }                          
          }                                            
                    
          return (string)$sum;
     }
     
     ...
[коммит] В 16ричной 11 + 11 = 22

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'),
              array('22', '22', 16, '44'), // еще тест
          );
     }
[фикс]
class Calculator {     
     
     private $Base;
     private $Digits = "0123456789ABCDEFG";
     
     public function calculate($expression, $base) {
          ...
               
          $sum = $this->toInt($out[0][0]) + 
                  $this->toInt($out[0][1]);     

          $big = (int)($sum / $base); // тут чуть поменяли логику
          if ($big <> 0) {
               return $this->toHex($big).$this->toHex($sum % $base);
          }
                    
          return $this->toHex($sum); // и тут :)
     }
     
     ...
}
[коммит] В 16ричной 22 + 22 = 44

[рефакторинг] Выделил новый метод из тела calculate
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
            
        $sum = $this->toInt($out[0][0]) + 
               $this->toInt($out[0][1]); ...    

        return $this->intToHex ($sum); // выделяли отсюда
    }

    private function intToHex($int) { // новый метод
        $big = (int)($int / $this->Base);
        if ($big <> 0) {
            return $this->toHex($big).$this->toHex($int % $this->Base);
        }
                
        return $this->toHex($int);
    }
    ... 
[коммит]

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'),
              array('22', '22', 16, '44'),
              array('99', '99', 16, '132'), // еще тест
          );
     }
[фикс]
     private function intToHex($int) {
          $big = (int)($int / $this->Base);
          if ($big <> 0) {
               return $this->intToHex($big).$this->intToHex($int % $this->Base); // тут сделали рекурсию
          }
                    
          return $this->toHex($int);
     }
[коммит] В 16ричной 99 + 99 = 132

[рефакторинг] Выделил еще один махонький метод и переименовал (Внимание!) toInt в hexToInt.
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
            
        $sum = $this->hexToInt($out[0][0]) + // переименовали
               $this->hexToInt($out[0][1]);    

        return $this->intToHex ($sum);
    }

    private function intToHex($int) { 
        $big = (int)($int / $this->Base);
        if ($big <> 0) {
            return $this->intToHex($big).$this->intToHex($int % $this->Base);
        }
                
        return $this->toHex($int);
    }

    
    private function toHex($int) { 
        return $this->Digits[$int];
    }
    
    private function hexToInt($hex) { // переименовали       
        if (strlen($hex) == 2) {
            return $this->Base*$this->hexToInt(substr($hex, 0, 1)) + $this->hexToInt(substr($hex, 1, 1));
        }
        
        return $this->toInt ($hex);

    }
    
    private function toInt($hex) { // выделили новый 
        if (is_numeric($hex)) return $hex; 
        return strpos($this->Digits, $hex);
    }

} 
[коммит]

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'),
              array('22', '22', 16, '44'),
              array('99', '99', 16, '132'),
              array('100', '1', 16, '101'), // еще тест
          );
     }
[фикс]
    private function hexToInt($hex) {  // полностью переписали      
        $sum = 0;
        for ($index = 0; $index < strlen($hex); $index++) {
            $sum = $this->Base*$sum + $this->toInt(substr($hex, $index, 1));
        }
        return $sum;

    }
[коммит] В 16ричной 100 + 1 = 101

[тест]
     public static function provider() {
          return array(
              array('2', '2', 10, '4'),
              array('3', '4', 10, '7'),
              array('11', '22', 10, '33'),
              array('9', '1', 16, 'A'),
              array('6', '9', 16, 'F'),
              array('6', '8', 16, 'E'),
              array('A', '5', 16, 'F'),
              array('1', 'E', 16, 'F'),
              array('2', 'B', 16, 'D'),
              array('B', 'E', 16, '19'),
              array('C', 'F', 16, '1B'),
              array('F', 'F', 16, '1E'),
              array('1C', '1', 16, '1D'),
              array('11', '11', 16, '22'),
              array('22', '22', 16, '44'),
              array('99', '99', 16, '132'),
              array('100', '1', 16, '101'),
              array('D', '3', 17, 'G'),  // добавили этот тест и все что ниже
              array('D', '2', 17, 'F'),
              array('D', '4', 17, '10'),
              array('G', '1', 17, '10'),
              array('G', 'G', 17, '1F'),
              array('1G', '1G', 17, '3F'),
          );
     }
[фикс]
class Calculator {    
    
    private $Digits = "0123456789ABCDEFG"; // фикс всего одна новая буковка
[коммит] Теперь калькулятор умеет считать и 17-ричные числа

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenIUseNotExistsSymbols2() { // этот тест на всякий для личного успокоения, каюсь :)
        $this->Calculator->calculate('10300+1', '2');
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenIUseNotExistsSymbols3() { // этот тест рабочий
        $this->Calculator->calculate('1+1A1', '4');
    }
[фикс]
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
        
        if ($this->isInvalidNumber($out[0][0])) { // добавили новую проверку
            throw new RuntimeException('Invalid expression format');
        }
            
        $sum = $this->hexToInt($out[0][0]) + 
               $this->hexToInt($out[0][1]);    

        return $this->intToHex ($sum);
    }

    private function isInvalidNumber($hex) { // и метод проверки
        $is_invalid = false;
        for ($index = 0; $index < strlen($hex); $index++) {
            $is_invalid |= $this->toInt($hex[$index]) >= $this->Base;                
        }            
        return $is_invalid;        
    }
    
   ...

}
[коммит] Валидация первого слагаемого, чтобы каждая цифра не выходила за пределы основания

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenIUseNotExistsSymbols() {
        $actual =  $this->Calculator->calculate('G+1', '16'); // буквы G быть не может
    }
[фикс]
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }
        
        if ($this->isInvalidNumber($out[0][0]) || $this->isInvalidNumber($out[0][1])) { // добавли еще одно условие
            throw new RuntimeException('Invalid expression format');
        }

        ...
[коммит] Валидация второго слагаемого чтобы не выходил за пределы основания

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenIUseNotExistsSymbols4() {
        $this->Calculator->calculate('QWE+ASD', '17');
    }
[фикс]
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;

        if ($this->isContainsInvalidNumber($expression)) { // проверку перенесли чуть выше и для всего эксепешена сразу
            throw new RuntimeException('Invalid number');
        }
        
        preg_match_all('/['.$this->Digits.']+/', $expression, $out);
        
        if (count($out[0]) < 2 || substr_count($expression, '+') != 1) {
            throw new RuntimeException('Invalid expression format');
        }        
            
        $sum = $this->hexToInt($out[0][0]) + 
               $this->hexToInt($out[0][1]);    

        return $this->intToHex ($sum);
    }

    private function isContainsInvalidNumber($expression) { // метод переименовали и чуть переписали
        $is_invalid = false;
        for ($index = 0; $index < strlen($expression); $index++) {
            if ($expression[$index] == '+') {
                continue;
            }
            
            $int = $this->toInt($expression[$index]);
            $is_invalid |= ($int === false) || $int >= $this->Base;                
        }            
        return $is_invalid;        
    }
    ...
[коммит] Теперь проводится валидация на всякие левые символы

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenBaseIsMoreThan17() {
        $this->Calculator->calculate('1+1', '18');
    }
[фикс]
class Calculator {    
    
    private $Base;
    private $Digits = "0123456789ABCDEFG";
    
    public function calculate($expression, $base) {
        $this->Base = $base;

        if ($this->isContainsInvalidNumber($expression)) {
            throw new RuntimeException('Invalid number');
        }
        
        if ($base > strlen($this->Digits)) { // добавили еще один ифчик
            throw new RuntimeException('Invalid base');
        }
        ...
[коммит] Теперь и основание системы счисления валидаируется, не больше 17

[тест]
    /**
     * @expectedException InvalidArgumentException
     */
    public function testShouldExceptionWhenBaseIsLessThan2() {
        $this->Calculator->calculate('0+0', '1');
    }
[фикс]
        if ($base > strlen($this->Digits) || $base <= 1) { // добавили подусловие
            throw new RuntimeException('Invalid base');
        }
[коммит] Добавил проверку что основание не может быть меньше 2

[рефакторинг] немного упростил
    private function intToHex($int) {
        if ($int / $this->Base < 1) {
            return $this->toHex($int);
        }
                
        $high = (int)($int / $this->Base);
        return $this->intToHex($high).$this->toHex($int % $this->Base); // тут вызвал toHex, а то некошерно как-то intToHex для мелких чисел
    }
[коммит] Небольшой рефакторинг

[рефакторинг] избавился от рекурсии
    private function intToHex($int) {
        $result = '';
        $low = $int;
        do {
            $high = $low % $this->Base;
            $low = (int)$low / $this->Base;
            $result = $this->toHex($high).$result;
        } while ($low >= 1);
        
        return $result;
    }
[коммит] Вот оно преимущество хорошего покрытия тестами - рефакторинг в удовольствие.

Комментариев нет:

Отправить комментарий