I had a job interview recently and I was asked to do FizzBuzz.
The code is very simple to write, however I wanted to see if I could make it easily accept any series of numbers and easily alter its output.
The code I came up within an hour or so is below, but its use comes down to the simple one liners;
// Simple demo of numbers 0-5, default behaviour, no args
foreach(NumbersDirector::create() AS $number) echo "$number,";
Output: 0, 1, 2, 3, 4, 5
// Fizz Buzz from 1-100 using fizzbuzz factory builder
foreach(NumbersDirector::create(new NumericalSeries(array('start'=>1,'end'=>100)),new FizzBuzzBuilder()) AS $number) echo "$number,";
Output: 1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,Buzz,Fizz,22,23,Fizz,Buzz,26,Fizz,28,29,FizzBuzz,………………..
// Fibo from 0-2000
foreach(NumbersDirector::create(new FibbonacciSeries(array('end'=>2000))) AS $number) echo "$number,";
Output: 1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,
// Fibo from 2000-50000000 using fizzbuzz factory builder
foreach(NumbersDirector::create(new FibbonacciSeries(array('start'=>2000,'end'=>50000000)),new FizzBuzzBuilder()) AS $number) echo "$number,";
Output: Fizz,5998,9997,Buzz,Fizz,41987,67979,109966,FizzBuzz,287911,465856,753767,Fizz,Buzz,3193013,5166403,Fizz,13525819,Buzz,35411054,Fizz,
As you can see the last one is passing in a custom series and modifying the output to FizzBuzz.
It uses several patterns, ReadOnly, Factory, Director, Builder and returns an iterable instance, just a coding exercise really.
Full code below if anyone would care to see:
_position = 0;
}
public function current() {
if ($this->valid()){
$data = $this->getData();
return $data[$this->_position];
}
}
public function key() {
return $this->_position;
}
public function next() {
++$this->_position;
}
public function valid() {
$data = $this->getData();
return isset($data[$this->_position]);
}
protected function setData(array $data = null){
$this->_data = $data;
$this->rewind();
return $this;
}
public function getData(){
if (!isset($this->_data)){
$this->setData();
}
return $this->_data;
}
public function getStart(){
if (isset($this->_data[0])){
return $this->_data[0];
}
return null;
}
public function getEnd(){
$count = count($this->_data)-1;
if ($count>0){
return $this->_data[$count];
}
return null;
}
/*
* Required by countable
*/
public function count(){
return count($this->_data);
}
}
trait Readonly{
public function __construct(array $options = null)
{
if (is_array($options)) {
$this->setOptions($options);
}
}
public function __set($name, $value)
{
$method = 'set' . $name;
if (method_exists($this, $method)) {
$this->$method($value);
return $this;
}
throw new \Exception('"'.$name.'" is an invalid property of '.__CLASS__.' assignment of "'.$value.'" failed!');
}
public function __get($name)
{
$method = 'get' . $name;
if (method_exists($this, $method)) {
return $this->$method();
}
throw new \Exception('Invalid '.__CLASS__.' property '.$name);
}
public function setOptions(array $options)
{
$methods = get_class_methods($this);
foreach ($options as $key => $value) {
$method = 'set' . ucfirst($key);
if (in_array($method, $methods)) {
$this->$method($value);
}
}
return $this;
}
}
/*
Interfaces
*/
Interface SeriesInterface {
public function generate();
}
Interface BuildersInterface {
public function build($entry);
}
Interface ValueInterface {
public function getValue();
public function setValue();
public function __toString();
}
/*
Abstract classes
*/
abstract class Series implements SeriesInterface {
use Readonly;
protected $_start;
protected $_end;
public function setStart($start = 0){
$this->_start = $start;
return $this;
}
public function getStart(){
if (!isset($this->_start)){
$this->setStart();
}
return $this->_start;
}
public function setEnd($end = 5){
$this->_end = $end;
return $this;
}
public function getEnd(){
if (!isset($this->_end)){
$this->setEnd();
}
return $this->_end;
}
abstract function generate();
}
abstract class Value implements ValueInterface {
use Readonly;
protected $_val;
public function getValue(){
if (!isset($this->_val)){
$this->setValue();
}
return $this->_val;
}
public function setValue($val = 0){
if (!is_int($val)){
throw new Exception('Expected integer as value got '.getType($val)); // would normally be the signature
}
$this->_val = $val;
return $this;
}
abstract public function __toString();
}
/*
Classes
*/
class NumbersDirector implements Iterator{
use Iterable;
protected function __construct(){}
public function create(SeriesInterface $series = null, BuildersInterface $builder = null){
if (is_null($series)){
$series = new NumericalSeries();
}
if (is_null($builder)){
$builder = new NumericalBuilder();
}
$stack = array();
foreach($series->generate() AS $entry){
$stack[]= $builder->build($entry);
}
$me = new self();
$me->setData($stack);
return $me;
}
}
class NumericalSeries extends Series {
public function generate(){
$series = array();
for ($x=$this->start; $x<($this->end + 1); $x++){
$series[] = $x;
}
return $series;
}
}
class FibbonacciSeries extends Series {
/**
* Fibonacci using Binet's formula
* @link http://mathworld.wolfram.com/BinetsFibonacciNumberFormula.html
*/
public function binet($n)
{
$phi = (1 + sqrt(5)) / 2;
return (pow($phi, $n) - pow(1 - $phi, $n)) / sqrt(5);
}
public function setStart($start = 1){
$this->_start = $start;
return $this;
}
public function setEnd($end = 55){
$this->_end = $end;
return $this;
}
public function generate(){
$series = array();
$fib = [$this->start,$this->start - 1];
$next = 0;
while($next < $this->end) {
$next = array_sum($fib);
array_shift($fib);
array_push($fib,$next);
$series[]=$next;
}
return $series;
}
}
class NumericalBuilder implements BuildersInterface {
public function Build($entry){
$number = new Number();
$number->value = $entry;
return $number;
}
}
class FizzBuzzBuilder implements BuildersInterface {
public function Build($entry){
/*
Factory method
*/
$class = "";
if ($entry%3==0){
$class.= 'Fizz';
}
if ($entry%5==0){
$class.= 'Buzz';
}
if (!class_exists($class) || !isset(class_implements($class)['ValueInterface'])){
$class = "Number";
}
$number = new $class();
$number->value = $entry;
return $number;
}
}
class Number extends Value {
public function __toString(){
return (string) $this->getValue();
}
}
class Buzz extends Value {
public function __toString(){
return (string) 'Buzz';
}
}
class Fizz extends Value {
public function __toString(){
return (string) 'Fizz';
}
}
class FizzBuzz extends Value {
public function __toString(){
return (string) 'FizzBuzz';
}
}
// Simple demo of numbers 0-5, default behaviour, no args
foreach(NumbersDirector::create() AS $number) echo "$number,";
echo "
";
// Fizz Buzz from 1-100 using fizzbuzz factory builder
foreach(NumbersDirector::create(new NumericalSeries(array('start'=>1,'end'=>100)),new FizzBuzzBuilder()) AS $number) echo "$number,";
echo "
";
// Fibo from 0-2000
foreach(NumbersDirector::create(new FibbonacciSeries(array('end'=>2000))) AS $number) echo "$number,";
echo "
";
// Fibo from 2000-50000000 using fizzbuzz factory builder
foreach(NumbersDirector::create(new FibbonacciSeries(array('start'=>2000,'end'=>50000000)),new FizzBuzzBuilder()) AS $number) echo "$number,";