diff --git a/.travis.yml b/.travis.yml index cc2b635..6a4e6ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,12 @@ php: - 5.3 - 5.4 - 5.5 + - 5.6 + - hhvm before_script: - composer install + +matrix: + allow_failures: + - php: hhvm # this should be the default, eh? \ No newline at end of file diff --git a/README.md b/README.md index 12a8971..a6fa235 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Ruler Ruler is a simple stateless production rules engine for PHP 5.3+. -[![Build Status](https://secure.travis-ci.org/bobthecow/Ruler.png?branch=master)](http://travis-ci.org/bobthecow/Ruler) +[![Package version](http://img.shields.io/packagist/v/ruler/ruler.svg)](https://packagist.org/packages/ruler/ruler) +[![Build status](http://img.shields.io/travis/bobthecow/Ruler/develop.svg)](http://travis-ci.org/bobthecow/Ruler) Ruler has an easy, straightforward DSL @@ -12,8 +13,6 @@ Ruler has an easy, straightforward DSL ... provided by the RuleBuilder: ```php -create( $rb->logicalAnd( @@ -42,8 +41,6 @@ $rule->execute($context); // "Yay!" ... you can use it without a RuleBuilder: ```php -greaterThan($b); // true if $a > $b -$a->greaterThanOrEqualTo($b); // true if $a >= $b -$a->lessThan($b); // true if $a < $b -$a->lessThanOrEqualTo($b); // true if $a <= $b -$a->equalTo($b); // true if $a == $b -$a->notEqualTo($b); // true if $a != $b -$a->contains($b); // true if in_array($b, $a) || strpos($b, $a) !== false -$a->doesNotContain($b); // true if !in_array($b, $a) || strpos($b, $a) === false -$a->sameAs($b); // true if $a === $b -$a->notSameAs($b); // true if $a !== $b +$a->greaterThan($b); // true if $a > $b +$a->greaterThanOrEqualTo($b); // true if $a >= $b +$a->lessThan($b); // true if $a < $b +$a->lessThanOrEqualTo($b); // true if $a <= $b +$a->equalTo($b); // true if $a == $b +$a->notEqualTo($b); // true if $a != $b +$a->stringContains($b); // true if strpos($b, $a) !== false +$a->stringDoesNotContain($b); // true if strpos($b, $a) === false +$a->stringContainsInsensitive($b); // true if stripos($b, $a) !== false +$a->stringDoesNotContainInsensitive($b); // true if stripos($b, $a) === false +$a->startsWith($b); // true if strpos($b, $a) === 0 +$a->startsWithInsensitive($b); // true if stripos($b, $a) === 0 +$a->endsWith($b); // true if strpos($b, $a) === len($a) - len($b) +$a->endsWithInsensitive($b); // true if stripos($b, $a) === len($a) - len($b) +$a->sameAs($b); // true if $a === $b +$a->notSameAs($b); // true if $a !== $b ``` -### Combine things + +### Math even more things ```php -add($rb['shipping']) + ->greaterThanOrEqualTo(50) + +// Of course, there are more. + +$c->add($d); // $c + $d +$c->subtract($d); // $c - $d +$c->multiply($d); // $c * $d +$c->divide($d); // $c / $d +$c->modulo($d); // $c % $d +$c->exponentiate($d); // $c ** $d +$c->negate(); // -$c +$c->ceil(); // ceil($c) +$c->floor(); // floor($c) +``` + + +### Reason about sets + +```php +$e = $rb['e']; // These should both be arrays +$f = $rb['f']; + +// Manipulate sets with set operators + +$e->union($f); +$e->intersect($f); +$e->complement($f); +$e->symmetricDifference($f); +$e->min(); +$e->max(); + +// And use set Propositions to include them in Rules. + +$e->containsSubset($f); +$e->doesNotContainSubset($f); +$e->setContains($a); +$e->setDoesNotContain($a); +``` + +### Combine Rules + +```php // Create a Rule with an $a == $b condition $aEqualsB = $rb->create($a->equalTo($b)); @@ -123,27 +175,27 @@ $context = new Context(array( $eitherOne->evaluate($context); ``` -### Combine more things -```php -logicalNot($aEqualsB); // The same as $aDoesNotEqualB :) $rb->logicalAnd($aEqualsB, $aDoesNotEqualB); // True if both conditions are true $rb->logicalOr($aEqualsB, $aDoesNotEqualB); // True if either condition is true $rb->logicalXor($aEqualsB, $aDoesNotEqualB); // True if only one condition is true ``` + ### `evaluate` and `execute` Rules `evaluate()` a Rule with Context to figure out whether it is true. ```php - function() { + return isset($_SESSION['userName']) ? $_SESSION['userName'] : null; + } +)); $userIsLoggedIn = $rb->create($rb['userName']->notEqualTo(null)); @@ -155,10 +207,7 @@ if ($userIsLoggedIn->evaluate($context)) { If a Rule has an action, you can `execute()` it directly and save yourself a couple of lines of code. - ```php -create( $rb['userName']->equalTo('bobthecow'), function() { @@ -169,6 +218,7 @@ $hiJustin = $rb->create( $hiJustin->execute($context); // "Hi, Justin!" ``` + ### Even `execute` a whole grip of Rules at once ```php @@ -219,8 +269,6 @@ static values, or even code for lazily evaluating the Variables needed by your Rules. ```php - If the current User has placed 5 or more orders, but isn't "really annoying", > give 'em free shipping. +```php +$rb->create( + $rb->logicalAnd( + $rb['orderCount']->greaterThanOrEqualTo(5), + $rb['reallyAnnoyingUsers']->doesNotContain($rb['userName']) + ), + function() use ($shipManager, $context) { + $shipManager->giveFreeShippingTo($context['user']); + } +); +``` + Access variable properties -------------------------- @@ -264,7 +324,6 @@ Context Variable values. This can come in really handy. Say we wanted to log the current user's name if they are an administrator: ```php - // Reusing our $context from the last example... // We'll define a few context variables for determining what roles a user has, @@ -304,7 +363,7 @@ everything we might need to access in a rule, we can use VariableProperties, and their convenient RuleBuilder interface: ```php -// We can skip over the Context Variable building above. We'll simply set our, +// We can skip over the Context Variable building above. We'll simply set our, // default roles on the VariableProperty itself, then go right to writing rules: $rb['user']['roles'] = array('anonymous'); @@ -335,6 +394,48 @@ If none of the above are true, it will return the default value for this VariableProperty. +Add your own Operators +---------------------- + +If none of the default Ruler Operators fit your needs, you can write your own! Just define +additional operators like this: + +```php + +namespace My\Ruler\Operators; + +use Ruler\Context; +use Ruler\Operator\VariableOperator; +use Ruler\Proposition; +use Ruler\Value; + +class ALotGreaterThan extends VariableOperator implements Proposition +{ + public function evaluate(Context $context) + { + list($left, $right) = $this->getOperands(); + $value = $right->prepareValue($context)->getValue() * 10; + + return $left->prepareValue($context)->greaterThan(new Value($value)); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} +``` + +Then you can use them with RuleBuilder like this: + +```php +$rb->registerOperatorNamespace('My\Ruler\Operators'); +$rb->create( + $rb['a']->aLotGreaterThan(10); +); +``` + + But that's not all... --------------------- diff --git a/composer.json b/composer.json index f9601a1..a951f0f 100644 --- a/composer.json +++ b/composer.json @@ -5,14 +5,13 @@ "homepage": "https://github.com/bobthecow/Ruler", "license": "MIT", "require": { - "php": ">=5.3.0", - "pimple/pimple": "1.0.*" + "php": ">=5.3.0" }, "require-dev": { "phpunit/phpunit": "*" }, "autoload": { - "psr-0": { + "psr-0": { "Ruler": "src", "Ruler\\Test": "tests" } diff --git a/src/Ruler/Context.php b/src/Ruler/Context.php index b534b5d..b8cd88f 100644 --- a/src/Ruler/Context.php +++ b/src/Ruler/Context.php @@ -3,10 +3,25 @@ /* * This file is part of the Ruler package, an OpenSky project. * - * (c) 2011 OpenSky Project Inc + * Copyright (c) 2009 Fabien Potencier * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is furnished + * to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. */ namespace Ruler; @@ -16,10 +31,22 @@ * * The Context contains facts with which to evaluate a Rule or other Proposition. * - * @author Justin Hileman + * Derived from Pimple, by Fabien Potencier: + * + * https://github.com/fabpot/Pimple + * + * @author Fabien Potencier + * @author Justin Hileman */ -class Context extends \Pimple +class Context implements \ArrayAccess { + private $keys = array(); + private $values = array(); + private $frozen = array(); + private $raw = array(); + + private $shared; + private $protected; /** * Context constructor. @@ -31,6 +58,185 @@ class Context extends \Pimple */ public function __construct(array $values = array()) { - parent::__construct($values); + $this->shared = new \SplObjectStorage; + $this->protected = new \SplObjectStorage; + + foreach ($values as $key => $value) { + $this->offsetSet($key, $value); + } + } + + /** + * Check if a fact is defined. + * + * @param string $name The unique name for the fact + * + * @return boolean + */ + public function offsetExists($name) + { + return isset($this->keys[$name]); + } + + /** + * Get the value of a fact. + * + * @param string $name The unique name for the fact + * + * @return mixed The resolved value of the fact + * + * @throws InvalidArgumentException if the name is not defined + */ + public function offsetGet($name) + { + if (!$this->offsetExists($name)) { + throw new \InvalidArgumentException(sprintf('Fact "%s" is not defined.', $name)); + } + + $value = $this->values[$name]; + + // If the value is already frozen, or if it's not callable, or if it's protected, return the raw value + if (isset($this->frozen[$name]) || !is_object($value) || $this->protected->contains($value) || !$this->isCallable($value)) { + return $value; + } + + // If this is a shared value, resolve, freeze, and return the result + if ($this->shared->contains($value)) { + $this->frozen[$name] = true; + $this->raw[$name] = $value; + + return $this->values[$name] = $value($this); + } + + // Otherwise, resolve and return the result + return $value($this); + } + + /** + * Set a fact name and value. + * + * A fact will be lazily evaluated if it is a Closure or invokable object. + * To define a fact as a literal callable, use Context::protect. + * + * @param string $name The unique name for the fact + * @param mixed $value The value or a closure to lazily define the value + * + * @throws RuntimeException if a frozen fact overridden + */ + public function offsetSet($name, $value) + { + if (isset($this->frozen[$name])) { + throw new \RuntimeException(sprintf('Cannot override frozen fact "%s".', $name)); + } + + $this->keys[$name] = true; + $this->values[$name] = $value; + } + + /** + * Unset a fact + * + * @param string $name The unique name for the fact + */ + public function offsetUnset($name) + { + if ($this->offsetExists($name)) { + $value = $this->values[$name]; + + if (is_object($value)) { + $this->shared->detach($value); + $this->protected->detach($value); + } + + unset($this->keys[$name], $this->values[$name], $this->frozen[$name], $this->raw[$name]); + } + } + + /** + * Define a fact as "shared". This lazily evaluates and stores the result + * of the callable for the scope of this Context instance. + * + * @param callable $callable A fact callable to share + * + * @return callable The passed callable + * + * @throws InvalidArgumentException if the callable is not a Closure or invokable object + */ + public function share($callable) + { + if (!$this->isCallable($callable)) { + throw new \InvalidArgumentException('Value is not a Closure or invokable object.'); + } + + $this->shared->attach($callable); + + return $callable; + } + + /** + * Protect a callable from being interpreted as a lazy fact definition. + * + * This is useful when you want to store a callable as the literal value of + * a fact. + * + * @param callable $callable A callable to protect from being evaluated + * + * @return callable The passed callable + * + * @throws InvalidArgumentException if the callable is not a Closure or invokable object + */ + public function protect($callable) + { + if (!$this->isCallable($callable)) { + throw new \InvalidArgumentException('Callable is not a Closure or invokable object.'); + } + + $this->protected->attach($callable); + + return $callable; + } + + /** + * Get a fact or the closure defining a fact. + * + * @param string $name The unique name for the fact + * + * @return mixed The value of the fact or the closure defining the fact + * + * @throws InvalidArgumentException if the name is not defined + */ + public function raw($name) + { + if (!$this->offsetExists($name)) { + throw new \InvalidArgumentException(sprintf('Fact "%s" is not defined.', $name)); + } + + if (isset($this->frozen[$name])) { + return $this->raw[$name]; + } + + return $this->values[$name]; + } + + /** + * Get all defined fact names. + * + * @return array An array of fact names + */ + public function keys() + { + return array_keys($this->keys); + } + + /** + * Check whether a value is a Closure or invokable object. + * + * @param mixed $callable + * + * @return boolean + */ + protected function isCallable($callable) + { + return is_object($callable) && is_callable($callable); } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator.php b/src/Ruler/Operator.php new file mode 100644 index 0000000..c7e3ddb --- /dev/null +++ b/src/Ruler/Operator.php @@ -0,0 +1,60 @@ + + */ +abstract class Operator +{ + const UNARY = 'UNARY'; + const BINARY = 'BINARY'; + const MULTIPLE = 'MULTIPLE'; + + protected $operands; + + /** + * @param array $operands + */ + public function __construct() + { + foreach (func_get_args() as $operand) { + $this->addOperand($operand); + } + } + + public function getOperands() + { + switch ($this->getOperandCardinality()) { + case self::UNARY: + if (1 != count($this->operands)) { + throw new \LogicException(get_class($this) . ' takes only 1 operand'); + } + break; + case self::BINARY: + if (2 != count($this->operands)) { + throw new \LogicException(get_class($this) . ' takes 2 operands'); + } + break; + case self::MULTIPLE: + if (0 == count($this->operands)) { + throw new \LogicException(get_class($this) . ' takes at least 1 operand'); + } + break; + } + + return $this->operands; + } + + abstract public function addOperand($operand); + abstract protected function getOperandCardinality(); +} diff --git a/src/Ruler/Operator/Addition.php b/src/Ruler/Operator/Addition.php new file mode 100644 index 0000000..1cccc43 --- /dev/null +++ b/src/Ruler/Operator/Addition.php @@ -0,0 +1,38 @@ + + */ +class Addition extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return new Value($left->prepareValue($context)->add($right->prepareValue($context))); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/Ceil.php b/src/Ruler/Operator/Ceil.php new file mode 100644 index 0000000..061eb93 --- /dev/null +++ b/src/Ruler/Operator/Ceil.php @@ -0,0 +1,37 @@ + + */ +class Ceil extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $operand */ + list($operand) = $this->getOperands(); + + return new Value($operand->prepareValue($context)->ceil()); + } + + protected function getOperandCardinality() + { + return static::UNARY; + } +} diff --git a/src/Ruler/Operator/ComparisonOperator.php b/src/Ruler/Operator/ComparisonOperator.php deleted file mode 100644 index 9a3b676..0000000 --- a/src/Ruler/Operator/ComparisonOperator.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @implements Proposition - */ -abstract class ComparisonOperator implements Proposition -{ - protected $left; - protected $right; - - /** - * Comparison Operator constructor. - * - * @param Variable $left Left side of comparison - * @param Variable $right Right side of comparison - */ - public function __construct(Variable $left, Variable $right) - { - $this->left = $left; - $this->right = $right; - } -} diff --git a/src/Ruler/Operator/Complement.php b/src/Ruler/Operator/Complement.php new file mode 100644 index 0000000..8f69287 --- /dev/null +++ b/src/Ruler/Operator/Complement.php @@ -0,0 +1,45 @@ + + */ +class Complement extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + $complement = null; + /** @var VariableOperand $operand */ + foreach ($this->getOperands() as $operand) { + if (!$complement instanceof Set) { + $complement = $operand->prepareValue($context)->getSet(); + } else { + $set = $operand->prepareValue($context)->getSet(); + $complement = $complement->complement($set); + } + } + + return $complement; + } + + protected function getOperandCardinality() + { + return static::MULTIPLE; + } +} diff --git a/src/Ruler/Operator/Contains.php b/src/Ruler/Operator/Contains.php index 6ef369c..76da4f7 100644 --- a/src/Ruler/Operator/Contains.php +++ b/src/Ruler/Operator/Contains.php @@ -12,24 +12,44 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** * A Contains comparison operator. * - * @author Justin Hileman - * @extends ComparisonOperator + * @deprecated Please use SetContains or StringContains operators instead. + * + * @author Justin Hileman */ -class Contains extends ComparisonOperator +class Contains extends VariableOperator implements Proposition { /** - * Evaluate whether the left variable is contained within the right in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->contains($this->right->prepareValue($context)); + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + $left = $left->prepareValue($context); + + if (is_array($left->getValue())) { + trigger_error('Contains operator is deprecated, please use SetContains', E_USER_DEPRECATED); + + return $left->getSet()->setContains($right->prepareValue($context)); + } else { + trigger_error('Contains operator is deprecated, please use StringContains', E_USER_DEPRECATED); + + return $left->stringContains($right->prepareValue($context)); + } + } + + protected function getOperandCardinality() + { + return static::BINARY; } } diff --git a/src/Ruler/Operator/ContainsSubset.php b/src/Ruler/Operator/ContainsSubset.php new file mode 100644 index 0000000..674dbdc --- /dev/null +++ b/src/Ruler/Operator/ContainsSubset.php @@ -0,0 +1,44 @@ + + */ +class ContainsSubset extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->getSet() + ->containsSubset($right->prepareValue($context)->getSet()); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/Division.php b/src/Ruler/Operator/Division.php new file mode 100644 index 0000000..a90291f --- /dev/null +++ b/src/Ruler/Operator/Division.php @@ -0,0 +1,38 @@ + + */ +class Division extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return new Value($left->prepareValue($context)->divide($right->prepareValue($context))); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/DoesNotContain.php b/src/Ruler/Operator/DoesNotContain.php deleted file mode 100644 index d574d78..0000000 --- a/src/Ruler/Operator/DoesNotContain.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @extends ComparisonOperator - */ -class DoesNotContain extends ComparisonOperator -{ - /** - * Evaluate whether the left variable is not contained within the right in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator - * - * @return boolean - */ - public function evaluate(Context $context) - { - return $this->left->prepareValue($context)->contains($this->right->prepareValue($context)) === false; - } -} diff --git a/src/Ruler/Operator/DoesNotContainSubset.php b/src/Ruler/Operator/DoesNotContainSubset.php new file mode 100644 index 0000000..d0ecacb --- /dev/null +++ b/src/Ruler/Operator/DoesNotContainSubset.php @@ -0,0 +1,44 @@ + + */ +class DoesNotContainSubset extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->getSet() + ->containsSubset($right->prepareValue($context)->getSet()) === false; + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/EndsWith.php b/src/Ruler/Operator/EndsWith.php new file mode 100644 index 0000000..db4552b --- /dev/null +++ b/src/Ruler/Operator/EndsWith.php @@ -0,0 +1,43 @@ + + */ +class EndsWith extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->endsWith($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/EndsWithInsensitive.php b/src/Ruler/Operator/EndsWithInsensitive.php new file mode 100644 index 0000000..5f9fa4a --- /dev/null +++ b/src/Ruler/Operator/EndsWithInsensitive.php @@ -0,0 +1,43 @@ + + */ +class EndsWithInsensitive extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->endsWith($right->prepareValue($context), true); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/EqualTo.php b/src/Ruler/Operator/EqualTo.php index 0529294..d681a96 100644 --- a/src/Ruler/Operator/EqualTo.php +++ b/src/Ruler/Operator/EqualTo.php @@ -12,24 +12,32 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** * An EqualTo comparison operator. * - * @author Justin Hileman - * @extends ComparisonOperator + * @author Justin Hileman */ -class EqualTo extends ComparisonOperator +class EqualTo extends VariableOperator implements Proposition { /** - * Evaluate whether the given variables are equal in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->equalTo($this->right->prepareValue($context)); + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->equalTo($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/Exponentiate.php b/src/Ruler/Operator/Exponentiate.php new file mode 100644 index 0000000..f9d2aef --- /dev/null +++ b/src/Ruler/Operator/Exponentiate.php @@ -0,0 +1,38 @@ + + */ +class Exponentiate extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return new Value($left->prepareValue($context)->exponentiate($right->prepareValue($context))); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/Floor.php b/src/Ruler/Operator/Floor.php new file mode 100644 index 0000000..01a5038 --- /dev/null +++ b/src/Ruler/Operator/Floor.php @@ -0,0 +1,37 @@ + + */ +class Floor extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $operand */ + list($operand) = $this->getOperands(); + + return new Value($operand->prepareValue($context)->floor()); + } + + protected function getOperandCardinality() + { + return static::UNARY; + } +} diff --git a/src/Ruler/Operator/GreaterThan.php b/src/Ruler/Operator/GreaterThan.php index 6d4c454..92f9923 100644 --- a/src/Ruler/Operator/GreaterThan.php +++ b/src/Ruler/Operator/GreaterThan.php @@ -12,24 +12,32 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** * A GreaterThan comparison operator. * - * @author Justin Hileman - * @extends ComparisonOperator + * @author Justin Hileman */ -class GreaterThan extends ComparisonOperator +class GreaterThan extends VariableOperator implements Proposition { /** - * Evaluate whether the left variable is greater than the right in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->greaterThan($this->right->prepareValue($context)); + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->greaterThan($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/GreaterThanOrEqualTo.php b/src/Ruler/Operator/GreaterThanOrEqualTo.php index 81c3418..5625021 100644 --- a/src/Ruler/Operator/GreaterThanOrEqualTo.php +++ b/src/Ruler/Operator/GreaterThanOrEqualTo.php @@ -12,24 +12,32 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** - * A GreaterThan comparison operator. + * A GreaterThanOrEqualTo comparison operator. * - * @author Justin Hileman - * @extends ComparisonOperator + * @author Justin Hileman */ -class GreaterThanOrEqualTo extends ComparisonOperator +class GreaterThanOrEqualTo extends VariableOperator implements Proposition { /** - * Evaluate whether the left variable is greater than or equal to the right in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->lessThan($this->right->prepareValue($context)) === false; + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->lessThan($right->prepareValue($context)) === false; + } + + protected function getOperandCardinality() + { + return static::BINARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/Intersect.php b/src/Ruler/Operator/Intersect.php new file mode 100644 index 0000000..e94a28e --- /dev/null +++ b/src/Ruler/Operator/Intersect.php @@ -0,0 +1,45 @@ + + */ +class Intersect extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + $intersect = null; + /** @var VariableOperand $operand */ + foreach ($this->getOperands() as $operand) { + if (!$intersect instanceof Set) { + $intersect = $operand->prepareValue($context)->getSet(); + } else { + $set = $operand->prepareValue($context)->getSet(); + $intersect = $intersect->intersect($set); + } + } + + return $intersect; + } + + protected function getOperandCardinality() + { + return static::MULTIPLE; + } +} diff --git a/src/Ruler/Operator/LessThan.php b/src/Ruler/Operator/LessThan.php index 04ced37..e214d39 100644 --- a/src/Ruler/Operator/LessThan.php +++ b/src/Ruler/Operator/LessThan.php @@ -12,24 +12,32 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** * A LessThan comparison operator. * - * @author Justin Hileman - * @extends ComparisonOperator + * @author Justin Hileman */ -class LessThan extends ComparisonOperator +class LessThan extends VariableOperator implements Proposition { /** - * Evaluate whether the left variable is less than the right in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->lessThan($this->right->prepareValue($context)); + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->lessThan($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/LessThanOrEqualTo.php b/src/Ruler/Operator/LessThanOrEqualTo.php index 191e2c6..84727f8 100644 --- a/src/Ruler/Operator/LessThanOrEqualTo.php +++ b/src/Ruler/Operator/LessThanOrEqualTo.php @@ -12,24 +12,32 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** * A LessThanOrEqualTo comparison operator. * - * @author Justin Hileman - * @extends ComparisonOperator + * @author Justin Hileman */ -class LessThanOrEqualTo extends ComparisonOperator +class LessThanOrEqualTo extends VariableOperator implements Proposition { /** - * Evaluate whether the left variable is less than or equal to the right in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->greaterThan($this->right->prepareValue($context)) === false; + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->greaterThan($right->prepareValue($context)) === false; + } + + protected function getOperandCardinality() + { + return static::BINARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/LogicalAnd.php b/src/Ruler/Operator/LogicalAnd.php index ee43168..1c2533e 100644 --- a/src/Ruler/Operator/LogicalAnd.php +++ b/src/Ruler/Operator/LogicalAnd.php @@ -12,34 +12,34 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; /** * A logical AND operator. * - * @author Justin Hileman - * @extends LogicalOperator + * @author Justin Hileman */ class LogicalAnd extends LogicalOperator { /** - * Evaluate whether all child Propositions evaluate to true given the current Context. - * - * @param Context $context Context with which to evaluate this LogicalOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - if (empty($this->propositions)) { - throw new \LogicException('Logical And requires at least one proposition'); - } - - foreach ($this->propositions as $prop) { - if ($prop->evaluate($context) === false) { + /** @var Proposition $operand */ + foreach ($this->getOperands() as $operand) { + if ($operand->evaluate($context) === false) { return false; } } return true; } -} \ No newline at end of file + + protected function getOperandCardinality() + { + return static::MULTIPLE; + } +} diff --git a/src/Ruler/Operator/LogicalNot.php b/src/Ruler/Operator/LogicalNot.php index 501c067..e8c3dcd 100644 --- a/src/Ruler/Operator/LogicalNot.php +++ b/src/Ruler/Operator/LogicalNot.php @@ -17,66 +17,25 @@ /** * A logical NOT operator. * - * @author Justin Hileman - * @extends LogicalOperator + * @author Justin Hileman */ class LogicalNot extends LogicalOperator { - protected $proposition; - /** - * Logical NOT constructor - * - * Logical NOT is unable to process multiple child Propositions, so passing an array with - * more than one Proposition will result in a LogicException. - * - * @param array $props Child Proposition (default:null) - * - * @throws LogicException - */ - public function __construct(array $props = null) - { - if ($props !== null) { - if (count($props) != 1) { - throw new \LogicException('Logical Not requires exactly one proposition'); - } - - $this->proposition = array_pop($props); - } - } - - /** - * Set the child Proposition. - * - * Logical NOT is unable to process multiple child Propositions, so calling addProposition - * if a Proposition has already been set will result in a LogicException. + * @param Context $context Context with which to evaluate this Proposition * - * @param Proposition $prop Child Proposition - * - * @throws LogicException + * @return boolean */ - public function addProposition(Proposition $prop) + public function evaluate(Context $context) { - if (isset($this->proposition)) { - throw new \LogicException('Logical Not requires exactly one proposition'); - } + /** @var Proposition $operand */ + list($operand) = $this->getOperands(); - $this->proposition = $prop; + return !$operand->evaluate($context); } - /** - * Evaluate whether the child Proposition evaluates to false given the current Context. - * - * @param Context $context Context with which to evaluate this LogicalOperator - * - * @return boolean - */ - public function evaluate(Context $context) + protected function getOperandCardinality() { - if (!isset($this->proposition)) { - throw new \LogicException('Logical Not requires exactly one proposition'); - } - - return !$this->proposition->evaluate($context); + return static::UNARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/LogicalOperator.php b/src/Ruler/Operator/LogicalOperator.php index 5364560..15ef083 100644 --- a/src/Ruler/Operator/LogicalOperator.php +++ b/src/Ruler/Operator/LogicalOperator.php @@ -14,39 +14,21 @@ use Ruler\Proposition; /** - * Abstract Logical Operator class. + * Logical operator base class * - * Logical Operators represent propositional operations: AND, OR, NOT and XOR. - * - * @abstract - * @author Justin Hileman - * @implements Proposition + * @author Justin Hileman */ -abstract class LogicalOperator implements Proposition +abstract class LogicalOperator extends PropositionOperator implements Proposition { - protected $propositions = array(); - /** - * Logical Operator constructor. + * array of propositions * - * @param array $props Initial Propositions to add to the Operator (default: null) + * @param array $props */ - public function __construct(array $props = null) + public function __construct(array $props = array()) { - if ($props !== null) { - foreach ($props as $prop) { - $this->addProposition($prop); - } + foreach ($props as $operand) { + $this->addOperand($operand); } } - - /** - * Add a Proposition to the Operator. - * - * @param Proposition $prop Proposition to add to this Operator - */ - public function addProposition(Proposition $prop) - { - $this->propositions[] = $prop; - } } diff --git a/src/Ruler/Operator/LogicalOr.php b/src/Ruler/Operator/LogicalOr.php index 60104f0..ada2ace 100644 --- a/src/Ruler/Operator/LogicalOr.php +++ b/src/Ruler/Operator/LogicalOr.php @@ -12,34 +12,34 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; /** * A logical OR operator. * - * @author Justin Hileman - * @extends LogicalOperator + * @author Justin Hileman */ class LogicalOr extends LogicalOperator { /** - * Evaluate whether any child Proposition evaluates to true given the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - if (empty($this->propositions)) { - throw new \LogicException('Logical Or requires at least one proposition'); - } - - foreach ($this->propositions as $prop) { - if ($prop->evaluate($context) === true) { + /** @var Proposition $operand */ + foreach ($this->getOperands() as $operand) { + if ($operand->evaluate($context) === true) { return true; } } return false; } -} \ No newline at end of file + + protected function getOperandCardinality() + { + return static::MULTIPLE; + } +} diff --git a/src/Ruler/Operator/LogicalXor.php b/src/Ruler/Operator/LogicalXor.php index 084827d..b252a08 100644 --- a/src/Ruler/Operator/LogicalXor.php +++ b/src/Ruler/Operator/LogicalXor.php @@ -12,31 +12,26 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; /** * A logical XOR operator. * - * @author Justin Hileman - * @extends LogicalOperator + * @author Justin Hileman */ class LogicalXor extends LogicalOperator { /** - * Evaluate whether exactly one child Proposition evaluates to true given the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - if (empty($this->propositions)) { - throw new \LogicException('Logical Xor requires at least one proposition'); - } - $true = 0; - foreach ($this->propositions as $prop) { - if ($prop->evaluate($context) === true) { + /** @var Proposition $operand */ + foreach ($this->getOperands() as $operand) { + if (true === $operand->evaluate($context)) { if (++$true > 1) { return false; } @@ -45,4 +40,9 @@ public function evaluate(Context $context) return $true === 1; } -} \ No newline at end of file + + protected function getOperandCardinality() + { + return static::MULTIPLE; + } +} diff --git a/src/Ruler/Operator/Max.php b/src/Ruler/Operator/Max.php new file mode 100644 index 0000000..c5773a4 --- /dev/null +++ b/src/Ruler/Operator/Max.php @@ -0,0 +1,37 @@ + + */ +class Max extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $operand */ + list($operand) = $this->getOperands(); + + return new Value($operand->prepareValue($context)->getSet()->max()); + } + + protected function getOperandCardinality() + { + return static::UNARY; + } +} diff --git a/src/Ruler/Operator/Min.php b/src/Ruler/Operator/Min.php new file mode 100644 index 0000000..c833b01 --- /dev/null +++ b/src/Ruler/Operator/Min.php @@ -0,0 +1,37 @@ + + */ +class Min extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $operand */ + list($operand) = $this->getOperands(); + + return new Value($operand->prepareValue($context)->getSet()->min()); + } + + protected function getOperandCardinality() + { + return static::UNARY; + } +} diff --git a/src/Ruler/Operator/Modulo.php b/src/Ruler/Operator/Modulo.php new file mode 100644 index 0000000..5f22d0d --- /dev/null +++ b/src/Ruler/Operator/Modulo.php @@ -0,0 +1,38 @@ + + */ +class Modulo extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return new Value($left->prepareValue($context)->modulo($right->prepareValue($context))); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/Multiplication.php b/src/Ruler/Operator/Multiplication.php new file mode 100644 index 0000000..4c1bc83 --- /dev/null +++ b/src/Ruler/Operator/Multiplication.php @@ -0,0 +1,38 @@ + + */ +class Multiplication extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return new Value($left->prepareValue($context)->multiply($right->prepareValue($context))); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/Negation.php b/src/Ruler/Operator/Negation.php new file mode 100644 index 0000000..9c8ea91 --- /dev/null +++ b/src/Ruler/Operator/Negation.php @@ -0,0 +1,37 @@ + + */ +class Negation extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $operand */ + list($operand) = $this->getOperands(); + + return new Value($operand->prepareValue($context)->negate()); + } + + protected function getOperandCardinality() + { + return static::UNARY; + } +} diff --git a/src/Ruler/Operator/NotEqualTo.php b/src/Ruler/Operator/NotEqualTo.php index 08939e7..b6b80b7 100644 --- a/src/Ruler/Operator/NotEqualTo.php +++ b/src/Ruler/Operator/NotEqualTo.php @@ -12,24 +12,32 @@ namespace Ruler\Operator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** * A NotEqualTo comparison operator. * - * @author Justin Hileman - * @extends ComparisonOperator + * @author Justin Hileman */ -class NotEqualTo extends ComparisonOperator +class NotEqualTo extends VariableOperator implements Proposition { /** - * Evaluate whether the given variables are not equal in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->equalTo($this->right->prepareValue($context)) === false; + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->equalTo($right->prepareValue($context)) === false; + } + + protected function getOperandCardinality() + { + return static::BINARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/NotSameAs.php b/src/Ruler/Operator/NotSameAs.php index 5f7378a..25370af 100644 --- a/src/Ruler/Operator/NotSameAs.php +++ b/src/Ruler/Operator/NotSameAs.php @@ -11,26 +11,33 @@ namespace Ruler\Operator; -use Ruler\Operator\ComparisonOperator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** - * An NotSameAs comparison operator. + * A NotSameAs comparison operator. * * @author Christophe Sicard - * @extends ComparisonOperator */ -class NotSameAs extends ComparisonOperator +class NotSameAs extends VariableOperator implements Proposition { /** - * Evaluate whether the given variables are not identical in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->sameAs($this->right->prepareValue($context)) === false; + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->sameAs($right->prepareValue($context)) === false; + } + + protected function getOperandCardinality() + { + return static::BINARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/PropositionOperator.php b/src/Ruler/Operator/PropositionOperator.php new file mode 100644 index 0000000..ee24ed4 --- /dev/null +++ b/src/Ruler/Operator/PropositionOperator.php @@ -0,0 +1,36 @@ + + */ +abstract class PropositionOperator extends BaseOperator +{ + public function addOperand($operand) + { + $this->addProposition($operand); + } + + public function addProposition(Proposition $operand) + { + if (static::UNARY == $this->getOperandCardinality() + && 0 < count($this->operands) + ) { + throw new \LogicException(get_class($this) . " can only have 1 operand"); + } + $this->operands[] = $operand; + } +} diff --git a/src/Ruler/Operator/SameAs.php b/src/Ruler/Operator/SameAs.php index a4aab98..0d8cb1a 100644 --- a/src/Ruler/Operator/SameAs.php +++ b/src/Ruler/Operator/SameAs.php @@ -11,26 +11,33 @@ namespace Ruler\Operator; -use Ruler\Operator\ComparisonOperator; use Ruler\Context; +use Ruler\Proposition; +use Ruler\VariableOperand; /** - * An SameAs comparison operator. + * A SameAs comparison operator. * * @author Christophe Sicard - * @extends ComparisonOperator */ -class SameAs extends ComparisonOperator +class SameAs extends VariableOperator implements Proposition { /** - * Evaluate whether the given variables are identical in the current Context. - * - * @param Context $context Context with which to evaluate this ComparisonOperator + * @param Context $context Context with which to evaluate this Proposition * * @return boolean */ public function evaluate(Context $context) { - return $this->left->prepareValue($context)->sameAs($this->right->prepareValue($context)); + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->sameAs($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; } -} \ No newline at end of file +} diff --git a/src/Ruler/Operator/SetContains.php b/src/Ruler/Operator/SetContains.php new file mode 100644 index 0000000..c16a156 --- /dev/null +++ b/src/Ruler/Operator/SetContains.php @@ -0,0 +1,43 @@ + + */ +class SetContains extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->getSet()->setContains($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/SetDoesNotContain.php b/src/Ruler/Operator/SetDoesNotContain.php new file mode 100644 index 0000000..dc200ce --- /dev/null +++ b/src/Ruler/Operator/SetDoesNotContain.php @@ -0,0 +1,43 @@ + + */ +class SetDoesNotContain extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->getSet()->setContains($right->prepareValue($context)) === false; + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/StartsWith.php b/src/Ruler/Operator/StartsWith.php new file mode 100644 index 0000000..787f951 --- /dev/null +++ b/src/Ruler/Operator/StartsWith.php @@ -0,0 +1,43 @@ + + */ +class StartsWith extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->startsWith($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/StartsWithInsensitive.php b/src/Ruler/Operator/StartsWithInsensitive.php new file mode 100644 index 0000000..3437191 --- /dev/null +++ b/src/Ruler/Operator/StartsWithInsensitive.php @@ -0,0 +1,43 @@ + + */ +class StartsWithInsensitive extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->startsWith($right->prepareValue($context), true); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/StringContains.php b/src/Ruler/Operator/StringContains.php new file mode 100644 index 0000000..9c6d0d0 --- /dev/null +++ b/src/Ruler/Operator/StringContains.php @@ -0,0 +1,43 @@ + + */ +class StringContains extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->stringContains($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/StringContainsInsensitive.php b/src/Ruler/Operator/StringContainsInsensitive.php new file mode 100644 index 0000000..ea557f9 --- /dev/null +++ b/src/Ruler/Operator/StringContainsInsensitive.php @@ -0,0 +1,43 @@ + + */ +class StringContainsInsensitive extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->stringContainsInsensitive($right->prepareValue($context)); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/StringDoesNotContain.php b/src/Ruler/Operator/StringDoesNotContain.php new file mode 100644 index 0000000..b0d4caa --- /dev/null +++ b/src/Ruler/Operator/StringDoesNotContain.php @@ -0,0 +1,43 @@ + + */ +class StringDoesNotContain extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->stringContains($right->prepareValue($context)) === false; + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/StringDoesNotContainInsensitive.php b/src/Ruler/Operator/StringDoesNotContainInsensitive.php new file mode 100644 index 0000000..c3e84d7 --- /dev/null +++ b/src/Ruler/Operator/StringDoesNotContainInsensitive.php @@ -0,0 +1,43 @@ + + */ +class StringDoesNotContainInsensitive extends VariableOperator implements Proposition +{ + /** + * @param Context $context Context with which to evaluate this Proposition + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->stringContainsInsensitive($right->prepareValue($context)) === false; + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/Subtraction.php b/src/Ruler/Operator/Subtraction.php new file mode 100644 index 0000000..1e9290f --- /dev/null +++ b/src/Ruler/Operator/Subtraction.php @@ -0,0 +1,38 @@ + + */ +class Subtraction extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return new Value($left->prepareValue($context)->subtract($right->prepareValue($context))); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/SymmetricDifference.php b/src/Ruler/Operator/SymmetricDifference.php new file mode 100644 index 0000000..0d54769 --- /dev/null +++ b/src/Ruler/Operator/SymmetricDifference.php @@ -0,0 +1,39 @@ + + */ +class SymmetricDifference extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + + return $left->prepareValue($context)->getSet() + ->symmetricDifference($right->prepareValue($context)->getSet()); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/src/Ruler/Operator/Union.php b/src/Ruler/Operator/Union.php new file mode 100644 index 0000000..5d19798 --- /dev/null +++ b/src/Ruler/Operator/Union.php @@ -0,0 +1,41 @@ + + */ +class Union extends VariableOperator implements VariableOperand +{ + public function prepareValue(Context $context) + { + $union = new Set(array()); + /** @var VariableOperand $operand */ + foreach ($this->getOperands() as $operand) { + $set = $operand->prepareValue($context)->getSet(); + $union = $union->union($set); + } + + return $union; + } + + protected function getOperandCardinality() + { + return static::MULTIPLE; + } +} diff --git a/src/Ruler/Operator/VariableOperator.php b/src/Ruler/Operator/VariableOperator.php new file mode 100644 index 0000000..067188a --- /dev/null +++ b/src/Ruler/Operator/VariableOperator.php @@ -0,0 +1,36 @@ + + */ +abstract class VariableOperator extends BaseOperator +{ + public function addOperand($operand) + { + $this->addVariable($operand); + } + + public function addVariable(VariableOperand $operand) + { + if (static::UNARY == $this->getOperandCardinality() + && 0 < count($this->operands) + ) { + throw new \LogicException(get_class($this) . " can only have 1 operand"); + } + $this->operands[] = $operand; + } +} diff --git a/src/Ruler/Proposition.php b/src/Ruler/Proposition.php index 52d537e..ac03671 100644 --- a/src/Ruler/Proposition.php +++ b/src/Ruler/Proposition.php @@ -11,12 +11,10 @@ namespace Ruler; -use Ruler\Context; - /** * The Proposition interface represents a propositional statement. * - * @author Justin Hileman + * @author Justin Hileman */ interface Proposition { @@ -29,4 +27,4 @@ interface Proposition * @return boolean */ public function evaluate(Context $context); -} \ No newline at end of file +} diff --git a/src/Ruler/Rule.php b/src/Ruler/Rule.php index 140824d..c4644c2 100644 --- a/src/Ruler/Rule.php +++ b/src/Ruler/Rule.php @@ -11,17 +11,13 @@ namespace Ruler; -use Ruler\Proposition; -use Ruler\Context; - /** * Rule class. * * A Rule is a conditional Proposition with an (optional) action which is * executed upon successful evaluation. * - * @author Justin Hileman - * @implements Proposition + * @author Justin Hileman */ class Rule implements Proposition { @@ -58,7 +54,8 @@ public function evaluate(Context $context) * The Rule will be evaluated, and if successful, will execute its * $action callback. * - * @param Context $context Context with which to execute this Rule + * @param Context $context Context with which to execute this Rule + * @throws \LogicException */ public function execute(Context $context) { @@ -70,4 +67,4 @@ public function execute(Context $context) call_user_func($this->action); } } -} \ No newline at end of file +} diff --git a/src/Ruler/RuleBuilder.php b/src/Ruler/RuleBuilder.php index d1cc5c0..d57057b 100644 --- a/src/Ruler/RuleBuilder.php +++ b/src/Ruler/RuleBuilder.php @@ -11,31 +11,18 @@ namespace Ruler; -use Ruler\Operator; -use Ruler\Proposition; -use Ruler\Rule; -use Ruler\RuleBuilder; - /** * RuleBuilder. * * The RuleBuilder provides a DSL and fluent interface for constructing * Rules. * - * @author Justin Hileman - * @implements ArrayAccess + * @author Justin Hileman */ class RuleBuilder implements \ArrayAccess { - private $variables; - - /** - * RuleBuilder constructor. - */ - public function __construct() - { - $this->variables = array(); - } + private $variables = array(); + private $operatorNamespaces = array(); /** * Create a Rule with the given propositional condition. @@ -50,11 +37,33 @@ public function create(Proposition $condition, $action = null) return new Rule($condition, $action); } + /** + * Register an operator namespace. + * + * Note that, depending on your filesystem, operator namespaces are most likely case sensitive. + * + * @throws \InvalidArgumentException + * + * @param string $namespace Operator namespace + * + * @return RuleBuilder + */ + public function registerOperatorNamespace($namespace) + { + if (!is_string($namespace)) { + throw new \InvalidArgumentException('Namespace argument must be a string'); + } + + $this->operatorNamespaces[$namespace] = true; + + return $this; + } + /** * Create a logical AND operator proposition. * - * @param Proposition $prop Initial Proposition - * @param Proposition $prop,... Optional unlimited number of additional Propositions + * @param Proposition $prop Initial Proposition + * @param Proposition $prop2,... Optional unlimited number of additional Propositions * * @return Operator\LogicalAnd */ @@ -66,8 +75,8 @@ public function logicalAnd(Proposition $prop, Proposition $prop2 = null) /** * Create a logical OR operator proposition. * - * @param Proposition $prop Initial Proposition - * @param Proposition $prop,... Optional unlimited number of additional Propositions + * @param Proposition $prop Initial Proposition + * @param Proposition $prop2,... Optional unlimited number of additional Propositions * * @return Operator\LogicalOr */ @@ -85,14 +94,14 @@ public function logicalOr(Proposition $prop, Proposition $prop2 = null) */ public function logicalNot(Proposition $prop) { - return new Operator\LogicalNot(func_get_args()); + return new Operator\LogicalNot(array($prop)); } /** * Create a logical XOR operator proposition. * - * @param Proposition $prop Initial Proposition - * @param Proposition $prop,... Optional unlimited number of additional Propositions + * @param Proposition $prop Initial Proposition + * @param Proposition $prop2,... Optional unlimited number of additional Propositions * * @return Operator\LogicalXor */ @@ -123,7 +132,7 @@ public function offsetExists($name) public function offsetGet($name) { if (!isset($this->variables[$name])) { - $this->variables[$name] = new RuleBuilder\Variable($name); + $this->variables[$name] = new RuleBuilder\Variable($this, $name); } return $this->variables[$name]; @@ -151,4 +160,26 @@ public function offsetUnset($name) { unset($this->variables[$name]); } + + /** + * Find an operator in the registered operator namespaces. + * + * @throws \LogicException If a matching operator is not found. + * + * @param string $name + * + * @return string + */ + public function findOperator($name) + { + $operator = ucfirst($name); + foreach (array_keys($this->operatorNamespaces) as $namespace) { + $class = $namespace . '\\' . $operator; + if (class_exists($class)) { + return $class; + } + } + + throw new \LogicException(sprintf('Unknown operator: "%s"', $name)); + } } diff --git a/src/Ruler/RuleBuilder/Variable.php b/src/Ruler/RuleBuilder/Variable.php index dda624c..76e67c1 100644 --- a/src/Ruler/RuleBuilder/Variable.php +++ b/src/Ruler/RuleBuilder/Variable.php @@ -12,8 +12,10 @@ namespace Ruler\RuleBuilder; use Ruler\Operator; -use Ruler\RuleBuilder\VariableProperty; +use Ruler\Operator\VariableOperator; +use Ruler\RuleBuilder; use Ruler\Variable as BaseVariable; +use Ruler\VariableOperand; /** * A propositional Variable. @@ -26,18 +28,42 @@ * interface for creating VariableProperties, Operators and Rules without all * kinds of awkward object instantiation. * - * @author Justin Hileman + * @author Justin Hileman */ class Variable extends BaseVariable implements \ArrayAccess { + private $ruleBuilder; private $properties = array(); + /** + * RuleBuilder Variable constructor. + * + * @param RuleBuilder $ruleBuilder + * @param string $name Variable name (default: null) + * @param mixed $value Default Variable value (default: null) + */ + public function __construct(RuleBuilder $ruleBuilder, $name = null, $value = null) + { + $this->ruleBuilder = $ruleBuilder; + parent::__construct($name, $value); + } + + /** + * Get the RuleBuilder instance set on this Variable. + * + * @return RuleBuilder + */ + public function getRuleBuilder() + { + return $this->ruleBuilder; + } + /** * Get a VariableProperty for accessing methods, indexes and properties of * the current variable. * - * @param string $name Property name - * @param mixed $value The default VariableProperty value + * @param string $name Property name + * @param mixed $value The default VariableProperty value * * @return VariableProperty */ @@ -53,7 +79,7 @@ public function getProperty($name, $value = null) /** * Fluent interface method for checking whether a VariableProperty has been defined. * - * @param string $name Property name + * @param string $name Property name * * @return bool */ @@ -67,7 +93,7 @@ public function offsetExists($name) * * @see getProperty * - * @param string $name Property name + * @param string $name Property name * * @return VariableProperty */ @@ -100,7 +126,9 @@ public function offsetUnset($name) } /** - * Fluent interface helper to create a contains comparison operator. + * Contains comparison. + * + * @deprecated Use `stringContains` or `setContains` instead. * * @param mixed $variable Right side of comparison operator * @@ -116,11 +144,23 @@ public function contains($variable) * * @param mixed $variable Right side of comparison operator * - * @return Operator\DoesNotContain + * @return Operator\StringContains + */ + public function stringContains($variable) + { + return new Operator\StringContains($this, $this->asVariable($variable)); + } + + /** + * Fluent interface helper to create a contains comparison operator. + * + * @param mixed $variable Right side of comparison operator + * + * @return Operator\StringDoesNotContain */ - public function doesNotContain($variable) + public function stringDoesNotContain($variable) { - return new Operator\DoesNotContain($this, $this->asVariable($variable)); + return new Operator\StringDoesNotContain($this, $this->asVariable($variable)); } /** @@ -198,7 +238,7 @@ public function notEqualTo($variable) /** * Fluent interface helper to create a SameAs comparison operator. * - * @param mixed $variable Right side of comparison operator + * @param mixed $variable Right side of comparison operator * * @return Operator\SameAs */ @@ -210,7 +250,7 @@ public function sameAs($variable) /** * Fluent interface helper to create a NotSameAs comparison operator. * - * @param mixed $variable Right side of comparison operator + * @param mixed $variable Right side of comparison operator * * @return Operator\SameAs */ @@ -219,6 +259,190 @@ public function notSameAs($variable) return new Operator\NotSameAs($this, $this->asVariable($variable)); } + /** + * @param $variable + * + * @return Variable + */ + public function union($variable) + { + return $this->applySetOperator('Union', func_get_args()); + } + + /** + * @param $variable + * + * @return Variable + */ + public function intersect($variable) + { + return $this->applySetOperator('Intersect', func_get_args()); + } + + /** + * @param $variable + * + * @return Variable + */ + public function complement($variable) + { + return $this->applySetOperator('Complement', func_get_args()); + } + + /** + * @param $variable + * + * @return Variable + */ + public function symmetricDifference($variable) + { + return $this->applySetOperator('SymmetricDifference', func_get_args()); + } + + /** + * @return Variable + */ + public function min() + { + return $this->wrap(new Operator\Min($this)); + } + + /** + * @return Variable + */ + public function max() + { + return $this->wrap(new Operator\Max($this)); + } + + /** + * @param $variable + * + * @return Operator\ContainsSubset + */ + public function containsSubset($variable) + { + return new Operator\ContainsSubset($this, $this->asVariable($variable)); + } + + /** + * @param $variable + * + * @return Operator\DoesNotContainSubset + */ + public function doesNotContainSubset($variable) + { + return new Operator\DoesNotContainSubset($this, $this->asVariable($variable)); + } + + /** + * Fluent interface helper to create a contains comparison operator. + * + * @param mixed $variable Right side of comparison operator + * + * @return Operator\SetContains + */ + public function setContains($variable) + { + return new Operator\SetContains($this, $this->asVariable($variable)); + } + + /** + * Fluent interface helper to create a contains comparison operator. + * + * @param mixed $variable Right side of comparison operator + * + * @return Operator\SetDoesNotContain + */ + public function setDoesNotContain($variable) + { + return new Operator\SetDoesNotContain($this, $this->asVariable($variable)); + } + + /** + * @param $variable + * + * @return Operator\Addition + */ + public function add($variable) + { + return $this->wrap(new Operator\Addition($this, $this->asVariable($variable))); + } + + /** + * @param $variable + * + * @return Operator\Division + */ + public function divide($variable) + { + return $this->wrap(new Operator\Division($this, $this->asVariable($variable))); + } + + /** + * @param $variable + * + * @return Operator\Modulo + */ + public function modulo($variable) + { + return $this->wrap(new Operator\Modulo($this, $this->asVariable($variable))); + } + + /** + * @param $variable + * + * @return Operator\Multiplication + */ + public function multiply($variable) + { + return $this->wrap(new Operator\Multiplication($this, $this->asVariable($variable))); + } + + /** + * @param $variable + * + * @return Operator\Subtraction + */ + public function subtract($variable) + { + return $this->wrap(new Operator\Subtraction($this, $this->asVariable($variable))); + } + + /** + * @return Operator\Negation + */ + public function negate() + { + return $this->wrap(new Operator\Negation($this)); + } + + /** + * @return Operator\Ceil + */ + public function ceil() + { + return $this->wrap(new Operator\Ceil($this)); + } + + /** + * @return Operator\Floor + */ + public function floor() + { + return $this->wrap(new Operator\Floor($this)); + } + + /** + * @param $variable + * + * @return Operator\Exponentiate + */ + public function exponentiate($variable) + { + return $this->wrap(new Operator\Exponentiate($this, $this->asVariable($variable))); + } + /** * Private helper to retrieve a Variable instance for the given $variable. * @@ -230,4 +454,107 @@ private function asVariable($variable) { return ($variable instanceof BaseVariable) ? $variable : new BaseVariable(null, $variable); } + + /** + * Private helper to apply a set operator. + * + * @param string $name + * @param array $args + * + * @return Variable + */ + private function applySetOperator($name, array $args) + { + $reflection = new \ReflectionClass('\\Ruler\\Operator\\' . $name); + array_unshift($args, $this); + + return $this->wrap($reflection->newInstanceArgs($args)); + } + + /** + * Private helper to wrap a VariableOperator in a Variable instance. + * + * @param VariableOperator $op + * + * @return Variable + */ + private function wrap(VariableOperator $op) + { + return new self($this->ruleBuilder, null, $op); + } + + /** + * Fluent interface helper to create a endsWith comparison operator. + * + * @param mixed $variable Right side of comparison operator + * + * @return Operator\EndsWith + */ + public function endsWith($variable) + { + return new Operator\EndsWith($this, $this->asVariable($variable)); + } + + /** + * Fluent interface helper to create a endsWith insensitive comparison operator. + * + * @param mixed $variable Right side of comparison operator + * + * @return Operator\EndsWithInsensitive + */ + public function endsWithInsensitive($variable) + { + return new Operator\EndsWithInsensitive($this, $this->asVariable($variable)); + } + + /** + * Fluent interface helper to create a startsWith comparison operator. + * + * @param mixed $variable Right side of comparison operator + * + * @return Operator\StartsWith + */ + public function startsWith($variable) + { + return new Operator\StartsWith($this, $this->asVariable($variable)); + } + + /** + * Fluent interface helper to create a startsWith insensitive comparison operator. + * + * @param mixed $variable Right side of comparison operator + * + * @return Operator\StartsWithInsensitive + */ + public function startsWithInsensitive($variable) + { + return new Operator\StartsWithInsensitive($this, $this->asVariable($variable)); + } + + /** + * Magic method to apply operators registered with RuleBuilder. + * + * @see RuleBuilder::registerOperatorNamespace + * + * @throws \LogicException if operator is not registered. + * + * @param string $name + * @param array $args + * + * @return Operator + */ + public function __call($name, array $args) + { + $reflection = new \ReflectionClass($this->ruleBuilder->findOperator($name)); + $args = array_map(array($this, 'asVariable'), $args); + array_unshift($args, $this); + + $op = $reflection->newInstanceArgs($args); + + if ($op instanceof VariableOperand) { + return $this->wrap($op); + } else { + return $op; + } + } } diff --git a/src/Ruler/RuleBuilder/VariableProperty.php b/src/Ruler/RuleBuilder/VariableProperty.php index 7f872ad..cdd6fac 100644 --- a/src/Ruler/RuleBuilder/VariableProperty.php +++ b/src/Ruler/RuleBuilder/VariableProperty.php @@ -12,7 +12,6 @@ namespace Ruler\RuleBuilder; use Ruler\Context; -use Ruler\RuleBuilder\Variable; use Ruler\Value; use Ruler\Variable as BaseVariable; @@ -34,7 +33,7 @@ * PHP 5.3+. Instead it uses a highly refined "copy and paste" technique, * perfected over years of diligent practice.) * - * @author Justin Hileman + * @author Justin Hileman */ class VariableProperty extends Variable { @@ -50,7 +49,7 @@ class VariableProperty extends Variable public function __construct(BaseVariable $parent, $name, $value = null) { $this->parent = $parent; - parent::__construct($name, $value); + parent::__construct($parent->getRuleBuilder(), $name, $value); } /** diff --git a/src/Ruler/RuleBuilder/VariablePropertyTrait.php b/src/Ruler/RuleBuilder/VariablePropertyTrait.php index 8673935..5ce13d8 100644 --- a/src/Ruler/RuleBuilder/VariablePropertyTrait.php +++ b/src/Ruler/RuleBuilder/VariablePropertyTrait.php @@ -31,7 +31,7 @@ * replaced with terminal Values from properties of their parent Variable, * either from their default Value, or from the current Context. * - * @author Justin Hileman + * @author Justin Hileman */ trait VariablePropertyTrait { diff --git a/src/Ruler/RuleSet.php b/src/Ruler/RuleSet.php index 67d209e..776790d 100644 --- a/src/Ruler/RuleSet.php +++ b/src/Ruler/RuleSet.php @@ -11,12 +11,10 @@ namespace Ruler; -use Ruler\Context; - /** * A Ruler RuleSet. * - * @author Justin Hileman + * @author Justin Hileman */ class RuleSet { @@ -57,4 +55,4 @@ public function executeRules(Context $context) $rule->execute($context); } } -} \ No newline at end of file +} diff --git a/src/Ruler/Set.php b/src/Ruler/Set.php new file mode 100644 index 0000000..4f7f604 --- /dev/null +++ b/src/Ruler/Set.php @@ -0,0 +1,241 @@ + + */ +class Set extends Value +{ + /** + * Set constructor. + * + * A Set object is immutable, and is used by Variables for comparing their + * Default values or facts from the current Context. + * + * @param mixed $set Immutable value represented by this Value object + */ + public function __construct($set) + { + parent::__construct($set); + if (!is_array($this->value)) { + if (is_null($this->value)) { + $this->value = array(); + } else { + $this->value = array($this->value); + } + } + foreach ($this->value as &$value) { + if (is_array($value)) { + $value = new Set($value); + } + } + + $this->value = array_unique($this->value); + } + + /** + * @return string + */ + public function __toString() + { + $returnValue = ''; + foreach ($this->value as $value) { + $returnValue .= (string) $value; + } + + return $returnValue; + } + + /** + * Set Contains comparison. + * + * @param Value $value Value object to compare against + * + * @return boolean + */ + public function setContains(Value $value) + { + if (is_array($value->getValue())) { + foreach ($this->value as $val) { + if ($val instanceof Set + && $val == $value->getSet()) { + return true; + } + } + + return false; + } + + return in_array($value->getValue(), $this->value); + } + + /** + * Set union operator. + * + * Returns a Set which is the union of this Set with all passed Sets. + * + * @param Value $set,... + * + * @return Set + */ + public function union(Value $set) + { + $union = $this->value; + + /** @var Value $arg */ + foreach (func_get_args() as $arg) { + /** @var array $convertedArg */ + $convertedArg = $arg->getSet()->getValue(); + $union = array_merge($union, array_diff($convertedArg, $union)); + } + + return new self($union); + } + + /** + * Set intersection operator. + * + * Returns a Set which is the intersection of this Set with all passed sets. + * + * @param Value $set,... + * + * @return Set + */ + public function intersect(Value $set) + { + $intersect = $this->value; + + /** @var Value $arg */ + foreach (func_get_args() as $arg) { + /** @var array $convertedArg */ + $convertedArg = $arg->getSet()->getValue(); + //array_values is needed to make sure the indexes are ordered from 0 + $intersect = array_values(array_intersect($intersect, $convertedArg)); + } + + return new self($intersect); + } + + /** + * Set complement operator. + * + * Returns a Set which is the complement of this Set with all passed Sets. + * + * @param Value $set,... + * + * @return Set + */ + public function complement(Value $set) + { + $complement = $this->value; + + /** @var Value $arg */ + foreach (func_get_args() as $arg) { + /** @var array $convertedArg */ + $convertedArg = $arg->getSet()->getValue(); + //array_values is needed to make sure the indexes are ordered from 0 + $complement = array_values(array_diff($complement, $convertedArg)); + } + + return new self($complement); + } + + /** + * Set symmetric difference operator. + * + * Returns a Set which is the symmetric difference of this Set with the + * passed Set. + * + * @param Value $set + * + * @return Set + */ + public function symmetricDifference(Value $set) + { + $returnValue = new Set(array()); + + return $returnValue->union( + $this->complement($set), + $set->getSet()->complement($this) + ); + } + + /** + * Numeric minimum value in this Set. + * + * @throws \RuntimeException if this Set contains non-numeric members. + * + * @return mixed + */ + public function min() + { + if (!$this->isValidNumericSet()) { + throw new \RuntimeException('min: all values must be numeric'); + } + if (empty($this->value)) { + return null; + } + + return min($this->value); + } + + /** + * Numeric maximum value in this Set. + * + * @throws \RuntimeException if this Set contains non-numeric members. + * + * @return mixed + */ + public function max() + { + if (!$this->isValidNumericSet()) { + throw new \RuntimeException('max: all values must be numeric'); + } + if (empty($this->value)) { + return null; + } + + return max($this->value); + } + + /** + * Contains Subset comparison. + * + * @param Set $set + * + * @return bool + */ + public function containsSubset(Set $set) + { + if (count($set->getValue()) > count($this->getValue())) { + return false; + } + + return array_intersect($set->getValue(), $this->getValue()) == $set->getValue(); + } + + /** + * Helper function to validate that a set contains only numeric members. + * + * @return bool + */ + protected function isValidNumericSet() + { + return count($this->value) == array_sum(array_map('is_numeric', $this->value)); + } +} diff --git a/src/Ruler/Value.php b/src/Ruler/Value.php index a902f26..eed0974 100644 --- a/src/Ruler/Value.php +++ b/src/Ruler/Value.php @@ -17,7 +17,7 @@ * A Value represents a comparable terminal value. Variables and Comparison Operators * are resolved to Values by applying the current Context and the default Variable value. * - * @author Justin Hileman + * @author Justin Hileman */ class Value { @@ -46,6 +46,16 @@ public function getValue() return $this->value; } + /** + * Get a Set containing this Value. + * + * @return Set + */ + public function getSet() + { + return new Set($this->value); + } + /** * Equal To comparison. * @@ -71,21 +81,27 @@ public function sameAs(Value $value) } /** - * Contains comparison. + * String Contains comparison. * * @param Value $value Value object to compare against * * @return boolean */ - public function contains(Value $value) + public function stringContains(Value $value) { - if (is_array($this->value)) { - return in_array($value->getValue(), $this->value); - } else if (is_string($this->value)) { - return strpos($value->getValue(), $this->value) !== false; - } + return strpos($this->value, $value->getValue()) !== false; + } - return false; + /** + * String Contains case-insensitive comparison. + * + * @param Value $value Value object to compare against + * + * @return boolean + */ + public function stringContainsInsensitive(Value $value) + { + return stripos($this->value, $value->getValue()) !== false; } /** @@ -111,4 +127,131 @@ public function lessThan(Value $value) { return $this->value < $value->getValue(); } + + public function add(Value $value) + { + if (!is_numeric($this->value) || !is_numeric($value->getValue())) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + + return $this->value + $value->getValue(); + } + + public function divide(Value $value) + { + if (!is_numeric($this->value) || !is_numeric($value->getValue())) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + if (0 == $value->getValue()) { + throw new \RuntimeException("Division by zero"); + } + + return $this->value / $value->getValue(); + } + + public function modulo(Value $value) + { + if (!is_numeric($this->value) || !is_numeric($value->getValue())) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + if (0 == $value->getValue()) { + throw new \RuntimeException("Division by zero"); + } + + return $this->value % $value->getValue(); + } + + public function multiply(Value $value) + { + if (!is_numeric($this->value) || !is_numeric($value->getValue())) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + + return $this->value * $value->getValue(); + } + + public function subtract(Value $value) + { + if (!is_numeric($this->value) || !is_numeric($value->getValue())) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + + return $this->value - $value->getValue(); + } + + public function negate() + { + if (!is_numeric($this->value)) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + + return -$this->value; + } + + public function ceil() + { + if (!is_numeric($this->value)) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + + return (int) ceil($this->value); + } + + public function floor() + { + if (!is_numeric($this->value)) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + + return (int) floor($this->value); + } + + public function exponentiate(Value $value) + { + if (!is_numeric($this->value) || !is_numeric($value->getValue())) { + throw new \RuntimeException("Arithmetic: values must be numeric"); + } + + return pow($this->value, $value->getValue()); + } + + /** + * String StartsWith comparison. + * + * @param Value $value Value object to compare against + * @param bool $insensitive Perform a case-insensitive comparison (default: false) + * + * @return boolean + */ + public function startsWith(Value $value, $insensitive = false) + { + $value = $value->getValue(); + $valueLength = strlen($value); + + if (!empty($this->value) && !empty($value) && strlen($this->value) >= $valueLength) { + return substr_compare($this->value, $value, 0, $valueLength, $insensitive) === 0; + } + + return false; + } + + /** + * String EndsWith comparison. + * + * @param Value $value Value object to compare against + * @param bool $insensitive Perform a case-insensitive comparison (default: false) + * + * @return boolean + */ + public function endsWith(Value $value, $insensitive = false) + { + $value = $value->getValue(); + $valueLength = strlen($value); + + if (!empty($this->value) && !empty($value) && strlen($this->value) >= $valueLength) { + return substr_compare($this->value, $value, -$valueLength, $valueLength, $insensitive) === 0; + } + + return false; + } } diff --git a/src/Ruler/Variable.php b/src/Ruler/Variable.php index ad767dd..749a567 100644 --- a/src/Ruler/Variable.php +++ b/src/Ruler/Variable.php @@ -11,9 +11,6 @@ namespace Ruler; -use Ruler\Context; -use Ruler\Operator; - /** * A propositional Variable. * @@ -21,9 +18,9 @@ * evaluation, they are replaced with terminal Values, either from the Variable * default or from the current Context. * - * @author Justin Hileman + * @author Justin Hileman */ -class Variable +class Variable implements VariableOperand { private $name; private $value; @@ -81,6 +78,8 @@ public function prepareValue(Context $context) { if (isset($this->name) && isset($context[$this->name])) { $value = $context[$this->name]; + } elseif ($this->value instanceof VariableOperand) { + $value = $this->value->prepareValue($context); } else { $value = $this->value; } diff --git a/src/Ruler/VariableOperand.php b/src/Ruler/VariableOperand.php new file mode 100644 index 0000000..3b0f652 --- /dev/null +++ b/src/Ruler/VariableOperand.php @@ -0,0 +1,25 @@ + + */ +interface VariableOperand +{ + /** + * @param Context $context + * + * @return Value + */ + public function prepareValue(Context $context); +} diff --git a/src/Ruler/VariableProperty.php b/src/Ruler/VariableProperty.php index 3a654e8..e81b5a3 100644 --- a/src/Ruler/VariableProperty.php +++ b/src/Ruler/VariableProperty.php @@ -11,9 +11,6 @@ namespace Ruler; -use Ruler\Context; -use Ruler\Variable; - /** * A propositional VariableProperty. * @@ -22,7 +19,7 @@ * replaced with terminal Values from properties of their parent Variable, * either from their default Value, or from the current Context. * - * @author Justin Hileman + * @author Justin Hileman */ class VariableProperty extends Variable { @@ -32,8 +29,8 @@ class VariableProperty extends Variable * VariableProperty class constructor. * * @param Variable $parent Parent Variable instance - * @param string $name Property name - * @param mixed $value Default Property value (default: null) + * @param string $name Property name + * @param mixed $value Default Property value (default: null) */ public function __construct(Variable $parent, $name, $value = null) { diff --git a/tests/Ruler/Test/ContextTest.php b/tests/Ruler/Test/ContextTest.php index 355f87b..71c5dca 100644 --- a/tests/Ruler/Test/ContextTest.php +++ b/tests/Ruler/Test/ContextTest.php @@ -1,9 +1,43 @@ + * @author Justin Hileman + */ class ContextTest extends \PHPUnit_Framework_TestCase { public function testConstructor() @@ -11,7 +45,7 @@ public function testConstructor() $facts = array( 'name' => 'Mint Chip', 'type' => 'Ice Cream', - 'delicious' => function() { + 'delicious' => function () { return true; } ); @@ -27,5 +61,241 @@ public function testConstructor() $this->assertTrue(isset($context['delicious'])); $this->assertTrue($context['delicious']); } -} + public function testWithString() + { + $context = new Context(); + $context['param'] = 'value'; + + $this->assertEquals('value', $context['param']); + } + + public function testWithClosure() + { + $context = new Context(); + $context['fact'] = function () { + return new Fact(); + }; + + $this->assertInstanceOf('Ruler\Test\Fixtures\Fact', $context['fact']); + } + + public function testFactsShouldBeDifferent() + { + $context = new Context(); + $context['fact'] = function () { + return new Fact(); + }; + + $factOne = $context['fact']; + $this->assertInstanceOf('Ruler\Test\Fixtures\Fact', $factOne); + + $factTwo = $context['fact']; + $this->assertInstanceOf('Ruler\Test\Fixtures\Fact', $factTwo); + + $this->assertNotSame($factOne, $factTwo); + } + + public function testShouldPassContextAsParameter() + { + $context = new Context(); + $context['fact'] = function () { + return new Fact(); + }; + $context['context'] = function ($context) { + return $context; + }; + + $this->assertNotSame($context, $context['fact']); + $this->assertSame($context, $context['context']); + } + + public function testIsset() + { + $context = new Context(); + $context['param'] = 'value'; + $context['fact'] = function () { + return new Fact(); + }; + + $context['null'] = null; + + $this->assertTrue(isset($context['param'])); + $this->assertTrue(isset($context['fact'])); + $this->assertTrue(isset($context['null'])); + $this->assertFalse(isset($context['non_existent'])); + } + + public function testConstructorInjection() + { + $params = array("param" => "value"); + $context = new Context($params); + + $this->assertSame($params['param'], $context['param']); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Fact "foo" is not defined. + */ + public function testOffsetGetValidatesKeyIsPresent() + { + $context = new Context(); + echo $context['foo']; + } + + public function testOffsetGetHonorsNullValues() + { + $context = new Context(); + $context['foo'] = null; + $this->assertNull($context['foo']); + } + + public function testUnset() + { + $context = new Context(); + $context['param'] = 'value'; + $context['fact'] = function () { + return new Fact(); + }; + + unset($context['param'], $context['fact']); + $this->assertFalse(isset($context['param'])); + $this->assertFalse(isset($context['fact'])); + } + + /** + * @dataProvider factDefinitionProvider + */ + public function testShare($fact) + { + $context = new Context(); + $context['shared_fact'] = $context->share($fact); + + $factOne = $context['shared_fact']; + $this->assertInstanceOf('Ruler\Test\Fixtures\Fact', $factOne); + + $factTwo = $context['shared_fact']; + $this->assertInstanceOf('Ruler\Test\Fixtures\Fact', $factTwo); + + $this->assertSame($factOne, $factTwo); + } + + /** + * @dataProvider factDefinitionProvider + */ + public function testProtect($fact) + { + $context = new Context(); + $context['protected'] = $context->protect($fact); + + $this->assertSame($fact, $context['protected']); + } + + public function testGlobalFunctionNameAsParameterValue() + { + $context = new Context(); + $context['global_function'] = 'strlen'; + $this->assertSame('strlen', $context['global_function']); + } + + public function testRaw() + { + $context = new Context(); + $context['fact'] = $definition = function () { return 'foo'; }; + $this->assertSame($definition, $context->raw('fact')); + } + + public function testRawHonorsNullValues() + { + $context = new Context(); + $context['foo'] = null; + $this->assertNull($context->raw('foo')); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Fact "foo" is not defined. + */ + public function testRawValidatesKeyIsPresent() + { + $context = new Context(); + $context->raw('foo'); + } + + public function testKeys() + { + $context = new Context(); + $context['foo'] = 123; + $context['bar'] = 123; + + $this->assertEquals(array('foo', 'bar'), $context->keys()); + } + + /** @test */ + public function settingAnInvokableObjectShouldTreatItAsFactory() + { + $context = new Context(); + $context['invokable'] = new Invokable(); + + $this->assertInstanceOf('Ruler\Test\Fixtures\Fact', $context['invokable']); + } + + /** @test */ + public function settingNonInvokableObjectShouldTreatItAsParameter() + { + $context = new Context(); + $context['non_invokable'] = new Fact(); + + $this->assertInstanceOf('Ruler\Test\Fixtures\Fact', $context['non_invokable']); + } + + /** + * @dataProvider badFactDefinitionProvider + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Value is not a Closure or invokable object. + */ + public function testShareFailsForInvalidFactDefinitions($fact) + { + $context = new Context(); + $context->share($fact); + } + + /** + * @dataProvider badFactDefinitionProvider + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Callable is not a Closure or invokable object. + */ + public function testProtectFailsForInvalidFactDefinitions($fact) + { + $context = new Context(); + $context->protect($fact); + } + + /** + * Provider for invalid fact definitions + */ + public function badFactDefinitionProvider() + { + return array( + array(123), + array(new Fact()) + ); + } + + /** + * Provider for fact definitions + */ + public function factDefinitionProvider() + { + return array( + array(function ($value) { + $fact = new Fact(); + $fact->value = $value; + + return $fact; + }), + array(new Invokable()) + ); + } +} diff --git a/tests/Ruler/Test/Fixtures/ALotGreaterThan.php b/tests/Ruler/Test/Fixtures/ALotGreaterThan.php new file mode 100644 index 0000000..ec73def --- /dev/null +++ b/tests/Ruler/Test/Fixtures/ALotGreaterThan.php @@ -0,0 +1,48 @@ + + */ +class ALotGreaterThan extends VariableOperator implements Proposition +{ + /** + * Evaluate whether the given variables are equal in the current Context. + * + * @param Context $context Context with which to evaluate this ComparisonOperator + * + * @return boolean + */ + public function evaluate(Context $context) + { + /** @var VariableOperand $left */ + /** @var VariableOperand $right */ + list($left, $right) = $this->getOperands(); + $value = $right->prepareValue($context)->getValue() * 10; + + return $left->prepareValue($context)->greaterThan(new Value($value)); + } + + protected function getOperandCardinality() + { + return static::BINARY; + } +} diff --git a/tests/Ruler/Test/Fixtures/CallbackProposition.php b/tests/Ruler/Test/Fixtures/CallbackProposition.php index 6fdaaac..c3a9d50 100644 --- a/tests/Ruler/Test/Fixtures/CallbackProposition.php +++ b/tests/Ruler/Test/Fixtures/CallbackProposition.php @@ -22,4 +22,4 @@ public function evaluate(Context $context) { return call_user_func($this->callback, $context); } -} \ No newline at end of file +} diff --git a/tests/Ruler/Test/Fixtures/Fact.php b/tests/Ruler/Test/Fixtures/Fact.php new file mode 100644 index 0000000..8a28d5b --- /dev/null +++ b/tests/Ruler/Test/Fixtures/Fact.php @@ -0,0 +1,13 @@ +value = $value; + } + } +} diff --git a/tests/Ruler/Test/Fixtures/FalseProposition.php b/tests/Ruler/Test/Fixtures/FalseProposition.php index 8dcee3e..32e9d36 100644 --- a/tests/Ruler/Test/Fixtures/FalseProposition.php +++ b/tests/Ruler/Test/Fixtures/FalseProposition.php @@ -11,4 +11,4 @@ public function evaluate(Context $context) { return false; } -} \ No newline at end of file +} diff --git a/tests/Ruler/Test/Fixtures/Invokable.php b/tests/Ruler/Test/Fixtures/Invokable.php new file mode 100644 index 0000000..de4626e --- /dev/null +++ b/tests/Ruler/Test/Fixtures/Invokable.php @@ -0,0 +1,13 @@ + 'a', + 'foo' => array('a', 'z'), + 'bar' => array('z', 'b'), + 'baz' => array('a', 'z', 'b', 'q'), + 'bob' => array('a', 'd'), + )); + + $this->assertTrue( + $rb->create( + $rb['foo']->intersect( + $rb['bar']->symmetricDifference($rb['baz']) + )->setContains($rb['expected']) + )->evaluate($context) + ); + + $this->assertTrue( + $rb->create( + $rb['bar']->union( + $rb['bob'] + )->containsSubset($rb['foo']) + )->evaluate($context) + ); + } + + public function setUnion() + { + return array( + array( + array('a', 'b', 'c'), + array(), + array('a', 'b', 'c'), + ), + array( + array(), + array('a', 'b', 'c'), + array('a', 'b', 'c'), + ), + array( + array(), + array(), + array(), + ), + array( + array('a', 'b', 'c'), + array('d', 'e', 'f'), + array('a', 'b', 'c', 'd', 'e', 'f'), + ), + array( + array('a', 'b', 'c'), + array('a', 'b', 'c'), + array('a', 'b', 'c'), + ), + array( + array('a', 'b', 'c'), + array('b', 'c'), + array('a', 'b', 'c'), + ), + array( + array('b', 'c'), + array('b', 'd'), + array('b', 'c', 'd'), + ), + ); + } + + /** + * @dataProvider setUnion + */ + public function testUnion($a, $b, $expected) + { + $rb = new RuleBuilder(); + $context = new Context(compact('a', 'b', 'expected')); + $this->assertTrue( + $rb->create( + $rb['expected']->equalTo( + $rb['a']->union($rb['b']) + ) + )->evaluate($context) + ); + } + + public function setIntersect() + { + return array( + array( + array('a', 'b', 'c'), + array(), + array(), + ), + array( + array(), + array('a', 'b', 'c'), + array(), + ), + array( + array(), + array(), + array(), + ), + array( + array('a', 'b', 'c'), + array('d', 'e', 'f'), + array(), + ), + array( + array('a', 'b', 'c'), + array('a', 'b', 'c'), + array('a', 'b', 'c'), + ), + array( + array('a', 'b', 'c'), + array('b', 'c'), + array('b', 'c'), + ), + array( + array('b', 'c'), + array('b', 'd'), + array('b'), + ), + ); + } + + /** + * @dataProvider setIntersect + */ + public function testIntersect($a, $b, $expected) + { + $rb = new RuleBuilder(); + $context = new Context(compact('a', 'b', 'expected')); + $this->assertTrue( + $rb->create( + $rb['expected']->equalTo( + $rb['a']->intersect($rb['b']) + ) + )->evaluate($context) + ); + } + + public function setComplement() + { + return array( + array( + array('a', 'b', 'c'), + array(), + array('a', 'b', 'c'), + ), + array( + array(), + array('a', 'b', 'c'), + array(), + ), + array( + array(), + array(), + array(), + ), + array( + array('a', 'b', 'c'), + array('d', 'e', 'f'), + array('a', 'b', 'c'), + ), + array( + array('a', 'b', 'c'), + array('a', 'b', 'c'), + array(), + ), + array( + array('a', 'b', 'c'), + array('b', 'c'), + array('a'), + ), + array( + array('b', 'c'), + array('b', 'd'), + array('c'), + ), + ); + } + + /** + * @dataProvider setComplement + */ + public function testComplement($a, $b, $expected) + { + $rb = new RuleBuilder(); + $context = new Context(compact('a', 'b', 'expected')); + $this->assertTrue( + $rb->create( + $rb['expected']->equalTo( + $rb['a']->complement($rb['b']) + ) + )->evaluate($context) + ); + } + + public function setSymmetricDifference() + { + return array( + array( + array('a', 'b', 'c'), + array(), + array('a', 'b', 'c'), + ), + array( + array(), + array('a', 'b', 'c'), + array('a', 'b', 'c'), + ), + array( + array(), + array(), + array(), + ), + array( + array('a', 'b', 'c'), + array('d', 'e', 'f'), + array('a', 'b', 'c', 'd', 'e', 'f'), + ), + array( + array('a', 'b', 'c'), + array('a', 'b', 'c'), + array(), + ), + array( + array('a', 'b', 'c'), + array('b', 'c'), + array('a'), + ), + array( + array('b', 'c'), + array('b', 'd'), + array('c', 'd'), + ), + ); + } + + /** + * @dataProvider setSymmetricDifference + */ + public function testSymmetricDifference($a, $b, $expected) + { + $rb = new RuleBuilder(); + $context = new Context(compact('a', 'b', 'expected')); + $this->assertTrue( + $rb->create( + $rb['expected']->equalTo( + $rb['a']->symmetricDifference($rb['b']) + ) + )->evaluate($context) + ); + } +} diff --git a/tests/Ruler/Test/Operator/AdditionTest.php b/tests/Ruler/Test/Operator/AdditionTest.php new file mode 100644 index 0000000..ac53736 --- /dev/null +++ b/tests/Ruler/Test/Operator/AdditionTest.php @@ -0,0 +1,54 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Addition($varA, $varB); + $op->prepareValue($context); + } + + /** + * @dataProvider additionData + */ + public function testAddition($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Addition($varA, $varB); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function additionData() + { + return array( + array(1, 2, 3), + array(2.5, 3.8, 6.3), + ); + } +} diff --git a/tests/Ruler/Test/Operator/CeilTest.php b/tests/Ruler/Test/Operator/CeilTest.php new file mode 100644 index 0000000..f0d4923 --- /dev/null +++ b/tests/Ruler/Test/Operator/CeilTest.php @@ -0,0 +1,54 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $context = new Context(); + + $op = new Operator\Ceil($varA); + $op->prepareValue($context); + } + + /** + * @dataProvider ceilingData + */ + public function testCeiling($a, $result) + { + $varA = new Variable('a', $a); + $context = new Context(); + + $op = new Operator\Ceil($varA); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function ceilingData() + { + return array( + array(1.2, 2), + array(1.0, 1), + array(1, 1), + array(-0.5, 0), + array(-1.5, -1), + ); + } +} diff --git a/tests/Ruler/Test/Operator/ComplementTest.php b/tests/Ruler/Test/Operator/ComplementTest.php new file mode 100644 index 0000000..79cbf38 --- /dev/null +++ b/tests/Ruler/Test/Operator/ComplementTest.php @@ -0,0 +1,105 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Complement($varA, $varB); + $this->assertEquals( + array('string'), + $op->prepareValue($context)->getValue() + ); + } + + /** + * @dataProvider complementData + */ + public function testComplement($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Complement($varA, $varB); + $this->assertEquals( + $result, + $op->prepareValue($context)->getValue() + ); + } + + public function complementData() + { + return array( + array(6, 2, array(6)), + array( + array('a', 'b', 'c'), + 'a', + array('b', 'c'), + ), + array( + 'a', + array('a', 'b', 'c'), + array(), + ), + array( + 'a', + array('b', 'c'), + array('a'), + ), + array( + array('a', 'b', 'c'), + array(), + array('a', 'b', 'c'), + ), + array( + array(), + array('a', 'b', 'c'), + array(), + ), + array( + array(), + array(), + array(), + ), + array( + array('a', 'b', 'c'), + array('d', 'e', 'f'), + array('a', 'b', 'c'), + ), + array( + array('a', 'b', 'c'), + array('a', 'b', 'c'), + array(), + ), + array( + array('a', 'b', 'c'), + array('b', 'c'), + array('a'), + ), + array( + array('b', 'c'), + array('b', 'd'), + array('c'), + ), + ); + } +} diff --git a/tests/Ruler/Test/Operator/ContainsSubsetTest.php b/tests/Ruler/Test/Operator/ContainsSubsetTest.php new file mode 100644 index 0000000..c2ed6b5 --- /dev/null +++ b/tests/Ruler/Test/Operator/ContainsSubsetTest.php @@ -0,0 +1,64 @@ +assertInstanceOf('Ruler\Proposition', $op); + } + + /** + * @dataProvider containsData + */ + public function testContains($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\ContainsSubset($varA, $varB); + $this->assertEquals($op->evaluate($context), $result); + } + + /** + * @dataProvider containsData + */ + public function testDoesNotContain($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\DoesNotContainSubset($varA, $varB); + $this->assertNotEquals($op->evaluate($context), $result); + } + + public function containsData() + { + return array( + array(array(1), array(1), true), + array(array(1), 1, true), + array(array(1, 2, 3), array(1, 2), true), + array(array(1, 2, 3), array(2, 4), false), + array(array('foo', 'bar', 'baz'), array('pow'), false), + array(array('foo', 'bar', 'baz'), array('bar'), true), + array(array('foo', 'bar', 'baz'), array('bar', 'baz'), true), + array(null, 'bar', false), + array(null, array('bar'), false), + array(null, array('bar', 'baz'), false), + array(null, null, true), + array(array(), array(), true), + array(array(1, 2, 3), array(2), true), + ); + } +} diff --git a/tests/Ruler/Test/Operator/DivisionTest.php b/tests/Ruler/Test/Operator/DivisionTest.php new file mode 100644 index 0000000..15d5309 --- /dev/null +++ b/tests/Ruler/Test/Operator/DivisionTest.php @@ -0,0 +1,68 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Division($varA, $varB); + $op->prepareValue($context); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Division by zero + */ + public function testDivideByZero() + { + $varA = new Variable('a', rand(1, 100)); + $varB = new Variable('b', 0); + $context = new Context(); + + $op = new Operator\Division($varA, $varB); + $op->prepareValue($context); + } + + /** + * @dataProvider divisionData + */ + public function testDivision($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Division($varA, $varB); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function divisionData() + { + return array( + array(6, 2, 3), + array(7.5, 2.5, 3.0), + ); + } +} diff --git a/tests/Ruler/Test/Operator/EndsWithInsensitiveTest.php b/tests/Ruler/Test/Operator/EndsWithInsensitiveTest.php new file mode 100644 index 0000000..23d7d6e --- /dev/null +++ b/tests/Ruler/Test/Operator/EndsWithInsensitiveTest.php @@ -0,0 +1,43 @@ +assertInstanceOf('Ruler\Proposition', $op); + } + + /** + * @dataProvider endsWithData + */ + public function testEndsWithInsensitive($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\EndsWithInsensitive($varA, $varB); + $this->assertEquals($op->evaluate($context), $result); + } + + public function endsWithData() + { + return array( + array('supercalifragilistic', 'supercalifragilistic', true), + array('supercalifragilistic', 'stic', true), + array('supercalifragilistic', 'STIC', true), + array('supercalifragilistic', 'super', false), + array('supercalifragilistic', '', false), + ); + } +} diff --git a/tests/Ruler/Test/Operator/EndsWithTest.php b/tests/Ruler/Test/Operator/EndsWithTest.php new file mode 100644 index 0000000..d54ba33 --- /dev/null +++ b/tests/Ruler/Test/Operator/EndsWithTest.php @@ -0,0 +1,43 @@ +assertInstanceOf('Ruler\Proposition', $op); + } + + /** + * @dataProvider endsWithData + */ + public function testEndsWith($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\EndsWith($varA, $varB); + $this->assertEquals($op->evaluate($context), $result); + } + + public function endsWithData() + { + return array( + array('supercalifragilistic', 'supercalifragilistic', true), + array('supercalifragilistic', 'stic', true), + array('supercalifragilistic', 'STIC', false), + array('supercalifragilistic', 'super', false), + array('supercalifragilistic', '', false), + ); + } +} diff --git a/tests/Ruler/Test/Operator/EqualToTest.php b/tests/Ruler/Test/Operator/EqualToTest.php index 0ad4c4a..c47c83b 100644 --- a/tests/Ruler/Test/Operator/EqualToTest.php +++ b/tests/Ruler/Test/Operator/EqualToTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\EqualTo($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } public function testConstructorAndEvaluation() @@ -31,7 +30,7 @@ public function testConstructorAndEvaluation() $this->assertTrue($op->evaluate($context)); $context['a'] = 3; - $context['b'] = function() { + $context['b'] = function () { return 3; }; $this->assertTrue($op->evaluate($context)); diff --git a/tests/Ruler/Test/Operator/ExponentiateTest.php b/tests/Ruler/Test/Operator/ExponentiateTest.php new file mode 100644 index 0000000..e69e744 --- /dev/null +++ b/tests/Ruler/Test/Operator/ExponentiateTest.php @@ -0,0 +1,54 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Exponentiate($varA, $varB); + $op->prepareValue($context); + } + + /** + * @dataProvider exponentiateData + */ + public function testExponentiate($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Exponentiate($varA, $varB); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function exponentiateData() + { + return array( + array(6, 2, 36), + array(10, -1, 0.1), + ); + } +} diff --git a/tests/Ruler/Test/Operator/ExtraOperatorTest.php b/tests/Ruler/Test/Operator/ExtraOperatorTest.php new file mode 100644 index 0000000..8f2e0c8 --- /dev/null +++ b/tests/Ruler/Test/Operator/ExtraOperatorTest.php @@ -0,0 +1,29 @@ +assertFalse($op->evaluate($context)); + + $context['a'] = 2; + $this->assertFalse($op->evaluate($context)); + + $context['a'] = 3; + $context['b'] = function () { + return 0; + }; + $this->assertTrue($op->evaluate($context)); + } +} diff --git a/tests/Ruler/Test/Operator/FloorTest.php b/tests/Ruler/Test/Operator/FloorTest.php new file mode 100644 index 0000000..57da303 --- /dev/null +++ b/tests/Ruler/Test/Operator/FloorTest.php @@ -0,0 +1,54 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $context = new Context(); + + $op = new Operator\Floor($varA); + $op->prepareValue($context); + } + + /** + * @dataProvider ceilingData + */ + public function testCeiling($a, $result) + { + $varA = new Variable('a', $a); + $context = new Context(); + + $op = new Operator\Floor($varA); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function ceilingData() + { + return array( + array(1.2, 1), + array(1.0, 1), + array(1, 1), + array(-0.5, -1), + array(-1.5, -2), + ); + } +} diff --git a/tests/Ruler/Test/Operator/GreaterThanOrEqualToTest.php b/tests/Ruler/Test/Operator/GreaterThanOrEqualToTest.php index 1dfd049..314c069 100644 --- a/tests/Ruler/Test/Operator/GreaterThanOrEqualToTest.php +++ b/tests/Ruler/Test/Operator/GreaterThanOrEqualToTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\GreaterThanOrEqualTo($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } public function testConstructorAndEvaluation() @@ -31,7 +30,7 @@ public function testConstructorAndEvaluation() $this->assertTrue($op->evaluate($context)); $context['a'] = 3; - $context['b'] = function() { + $context['b'] = function () { return 3; }; $this->assertTrue($op->evaluate($context)); diff --git a/tests/Ruler/Test/Operator/GreaterThanTest.php b/tests/Ruler/Test/Operator/GreaterThanTest.php index 6bc42a5..9b03ab2 100644 --- a/tests/Ruler/Test/Operator/GreaterThanTest.php +++ b/tests/Ruler/Test/Operator/GreaterThanTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\GreaterThan($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } public function testConstructorAndEvaluation() @@ -31,7 +30,7 @@ public function testConstructorAndEvaluation() $this->assertFalse($op->evaluate($context)); $context['a'] = 3; - $context['b'] = function() { + $context['b'] = function () { return 0; }; $this->assertTrue($op->evaluate($context)); diff --git a/tests/Ruler/Test/Operator/IntersectTest.php b/tests/Ruler/Test/Operator/IntersectTest.php new file mode 100644 index 0000000..de38cf2 --- /dev/null +++ b/tests/Ruler/Test/Operator/IntersectTest.php @@ -0,0 +1,95 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Intersect($varA, $varB); + $this->assertEquals( + array(), + $op->prepareValue($context)->getValue() + ); + } + + /** + * @dataProvider intersectData + */ + public function testIntersect($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Intersect($varA, $varB); + $this->assertEquals( + $result, + $op->prepareValue($context)->getValue() + ); + } + + public function intersectData() + { + return array( + array(6, 2, array()), + array( + array('a', 'c'), + 'a', + array('a'), + ), + array( + array('a', 'b', 'c'), + array(), + array(), + ), + array( + array(), + array('a', 'b', 'c'), + array(), + ), + array( + array(), + array(), + array(), + ), + array( + array('a', 'b', 'c'), + array('d', 'e', 'f'), + array(), + ), + array( + array('a', 'b', 'c'), + array('a', 'b', 'c'), + array('a', 'b', 'c'), + ), + array( + array('a', 'b', 'c'), + array('b', 'c'), + array('b', 'c'), + ), + array( + array('b', 'c'), + array('b', 'd'), + array('b'), + ), + ); + } +} diff --git a/tests/Ruler/Test/Operator/LessThanOrEqualToTest.php b/tests/Ruler/Test/Operator/LessThanOrEqualToTest.php index 1c8d9d1..59cc623 100644 --- a/tests/Ruler/Test/Operator/LessThanOrEqualToTest.php +++ b/tests/Ruler/Test/Operator/LessThanOrEqualToTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\GreaterThanOrEqualTo($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } public function testConstructorAndEvaluation() @@ -31,7 +30,7 @@ public function testConstructorAndEvaluation() $this->assertTrue($op->evaluate($context)); $context['a'] = 3; - $context['b'] = function() { + $context['b'] = function () { return 3; }; $this->assertTrue($op->evaluate($context)); diff --git a/tests/Ruler/Test/Operator/LessThanTest.php b/tests/Ruler/Test/Operator/LessThanTest.php index 1ea4545..cabbf8b 100644 --- a/tests/Ruler/Test/Operator/LessThanTest.php +++ b/tests/Ruler/Test/Operator/LessThanTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\LessThan($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } public function testConstructorAndEvaluation() @@ -31,7 +30,7 @@ public function testConstructorAndEvaluation() $this->assertFalse($op->evaluate($context)); $context['a'] = 3; - $context['b'] = function() { + $context['b'] = function () { return 1; }; $this->assertFalse($op->evaluate($context)); diff --git a/tests/Ruler/Test/Operator/LogicalAndTest.php b/tests/Ruler/Test/Operator/LogicalAndTest.php index 93eecdc..cc16610 100644 --- a/tests/Ruler/Test/Operator/LogicalAndTest.php +++ b/tests/Ruler/Test/Operator/LogicalAndTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\LogicalAnd(array($true)); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $op); } public function testConstructor() @@ -39,7 +38,7 @@ public function testAddPropositionAndEvaluate() $op->addProposition($true); $this->assertTrue($op->evaluate($context)); - $op->addProposition($true); + $op->addOperand($true); $this->assertTrue($op->evaluate($context)); $op->addProposition($false); @@ -47,7 +46,7 @@ public function testAddPropositionAndEvaluate() } /** - * @expectedException LogicException + * @expectedException \LogicException */ public function testExecutingALogicalAndWithoutPropositionsThrowsAnException() { diff --git a/tests/Ruler/Test/Operator/LogicalNotTest.php b/tests/Ruler/Test/Operator/LogicalNotTest.php index 02cc5b1..51985ea 100644 --- a/tests/Ruler/Test/Operator/LogicalNotTest.php +++ b/tests/Ruler/Test/Operator/LogicalNotTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\LogicalNot(array($true)); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $op); } public function testConstructor() @@ -33,7 +32,7 @@ public function testAddPropositionAndEvaluate() } /** - * @expectedException LogicException + * @expectedException \LogicException */ public function testExecutingALogicalNotWithoutPropositionsThrowsAnException() { @@ -42,7 +41,7 @@ public function testExecutingALogicalNotWithoutPropositionsThrowsAnException() } /** - * @expectedException LogicException + * @expectedException \LogicException */ public function testInstantiatingALogicalNotWithTooManyArgumentsThrowsAnException() { @@ -50,7 +49,7 @@ public function testInstantiatingALogicalNotWithTooManyArgumentsThrowsAnExceptio } /** - * @expectedException LogicException + * @expectedException \LogicException */ public function testAddingASecondPropositionToLogicalNotThrowsAnException() { diff --git a/tests/Ruler/Test/Operator/LogicalOrTest.php b/tests/Ruler/Test/Operator/LogicalOrTest.php index 0758d39..a769bdd 100644 --- a/tests/Ruler/Test/Operator/LogicalOrTest.php +++ b/tests/Ruler/Test/Operator/LogicalOrTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\LogicalOr(array($true)); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $op); } public function testConstructor() @@ -42,12 +41,12 @@ public function testAddPropositionAndEvaluate() $op->addProposition($false); $this->assertFalse($op->evaluate($context)); - $op->addProposition($true); + $op->addOperand($true); $this->assertTrue($op->evaluate($context)); } /** - * @expectedException LogicException + * @expectedException \LogicException */ public function testExecutingALogicalOrWithoutPropositionsThrowsAnException() { diff --git a/tests/Ruler/Test/Operator/LogicalXorTest.php b/tests/Ruler/Test/Operator/LogicalXorTest.php index a53c976..7ad3b7d 100644 --- a/tests/Ruler/Test/Operator/LogicalXorTest.php +++ b/tests/Ruler/Test/Operator/LogicalXorTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\LogicalXor(array($true)); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $op); } public function testConstructor() @@ -39,18 +38,18 @@ public function testAddPropositionAndEvaluate() $op->addProposition($false); $this->assertFalse($op->evaluate($context)); - $op->addProposition($false); + $op->addOperand($false); $this->assertFalse($op->evaluate($context)); $op->addProposition($true); $this->assertTrue($op->evaluate($context)); - $op->addProposition($true); + $op->addOperand($true); $this->assertFalse($op->evaluate($context)); } /** - * @expectedException LogicException + * @expectedException \LogicException */ public function testExecutingALogicalXorWithoutPropositionsThrowsAnException() { diff --git a/tests/Ruler/Test/Operator/MaxTest.php b/tests/Ruler/Test/Operator/MaxTest.php new file mode 100644 index 0000000..6d81025 --- /dev/null +++ b/tests/Ruler/Test/Operator/MaxTest.php @@ -0,0 +1,68 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @dataProvider invalidData + * @expectedException \RuntimeException + * @expectedExceptionMessage max: all values must be numeric + */ + public function testInvalidData($datum) + { + $var = new Variable('a', $datum); + $context = new Context(); + + $op = new Operator\Max($var); + $op->prepareValue($context); + } + + public function invalidData() + { + return array( + array('string'), + array(array('string')), + array(array(1, 2, 3, 'string')), + array(array('string', 1, 2, 3)), + ); + } + + /** + * @dataProvider maxData + */ + public function testMax($a, $result) + { + $var = new Variable('a', $a); + $context = new Context(); + + $op = new Operator\Max($var); + $this->assertEquals( + $result, + $op->prepareValue($context)->getValue() + ); + } + + public function maxData() + { + return array( + array(5, 5), + array(array(), null), + array(array(5), 5), + array(array(-2, -5, -242), -2), + array(array(2, 5, 242), 242), + ); + } +} diff --git a/tests/Ruler/Test/Operator/MinTest.php b/tests/Ruler/Test/Operator/MinTest.php new file mode 100644 index 0000000..0a2e956 --- /dev/null +++ b/tests/Ruler/Test/Operator/MinTest.php @@ -0,0 +1,68 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @dataProvider invalidData + * @expectedException \RuntimeException + * @expectedExceptionMessage min: all values must be numeric + */ + public function testInvalidData($datum) + { + $var = new Variable('a', $datum); + $context = new Context(); + + $op = new Operator\Min($var); + $op->prepareValue($context); + } + + public function invalidData() + { + return array( + array('string'), + array(array('string')), + array(array(1, 2, 3, 'string')), + array(array('string', 1, 2, 3)), + ); + } + + /** + * @dataProvider minData + */ + public function testMin($a, $result) + { + $var = new Variable('a', $a); + $context = new Context(); + + $op = new Operator\Min($var); + $this->assertEquals( + $result, + $op->prepareValue($context)->getValue() + ); + } + + public function minData() + { + return array( + array(5, 5), + array(array(), null), + array(array(5), 5), + array(array(-2, -5, -242), -242), + array(array(2, 5, 242), 2), + ); + } +} diff --git a/tests/Ruler/Test/Operator/ModuloTest.php b/tests/Ruler/Test/Operator/ModuloTest.php new file mode 100644 index 0000000..de5f76d --- /dev/null +++ b/tests/Ruler/Test/Operator/ModuloTest.php @@ -0,0 +1,68 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Modulo($varA, $varB); + $op->prepareValue($context); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Division by zero + */ + public function testDivideByZero() + { + $varA = new Variable('a', rand(1, 100)); + $varB = new Variable('b', 0); + $context = new Context(); + + $op = new Operator\Modulo($varA, $varB); + $op->prepareValue($context); + } + + /** + * @dataProvider moduloData + */ + public function testModulo($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Modulo($varA, $varB); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function moduloData() + { + return array( + array(6, 2, 0), + array(7, 3, 1), + ); + } +} diff --git a/tests/Ruler/Test/Operator/MultiplicationTest.php b/tests/Ruler/Test/Operator/MultiplicationTest.php new file mode 100644 index 0000000..37d79b0 --- /dev/null +++ b/tests/Ruler/Test/Operator/MultiplicationTest.php @@ -0,0 +1,55 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Multiplication($varA, $varB); + $op->prepareValue($context); + } + + /** + * @dataProvider multiplyData + */ + public function testMultiply($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Multiplication($varA, $varB); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function multiplyData() + { + return array( + array(6, 2, 12), + array(7, 3, 21), + array(2.5, 1.5, 3.75), + ); + } +} diff --git a/tests/Ruler/Test/Operator/NegationTest.php b/tests/Ruler/Test/Operator/NegationTest.php new file mode 100644 index 0000000..411e896 --- /dev/null +++ b/tests/Ruler/Test/Operator/NegationTest.php @@ -0,0 +1,53 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $context = new Context(); + + $op = new Operator\Negation($varA); + $op->prepareValue($context); + } + + /** + * @dataProvider negateData + */ + public function testSubtract($a, $result) + { + $varA = new Variable('a', $a); + $context = new Context(); + + $op = new Operator\Negation($varA); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function negateData() + { + return array( + array(1, -1), + array(0.0, 0.0), + array("0", 0), + array(-62834, 62834), + ); + } +} diff --git a/tests/Ruler/Test/Operator/NotEqualToTest.php b/tests/Ruler/Test/Operator/NotEqualToTest.php index ac6ee74..ba6b3d2 100644 --- a/tests/Ruler/Test/Operator/NotEqualToTest.php +++ b/tests/Ruler/Test/Operator/NotEqualToTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\NotEqualTo($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } public function testConstructorAndEvaluation() @@ -31,7 +30,7 @@ public function testConstructorAndEvaluation() $this->assertFalse($op->evaluate($context)); $context['a'] = 3; - $context['b'] = function() { + $context['b'] = function () { return 3; }; $this->assertFalse($op->evaluate($context)); diff --git a/tests/Ruler/Test/Operator/NotSameAsTest.php b/tests/Ruler/Test/Operator/NotSameAsTest.php index ccff60d..8810796 100644 --- a/tests/Ruler/Test/Operator/NotSameAsTest.php +++ b/tests/Ruler/Test/Operator/NotSameAsTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\NotSameAs($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } public function testConstructorAndEvaluation() @@ -31,7 +30,7 @@ public function testConstructorAndEvaluation() $this->assertFalse($op->evaluate($context)); $context['a'] = 3; - $context['b'] = function() { + $context['b'] = function () { return 3; }; $this->assertFalse($op->evaluate($context)); diff --git a/tests/Ruler/Test/Operator/SameAsTest.php b/tests/Ruler/Test/Operator/SameAsTest.php index 3895543..8b9d11c 100644 --- a/tests/Ruler/Test/Operator/SameAsTest.php +++ b/tests/Ruler/Test/Operator/SameAsTest.php @@ -15,7 +15,6 @@ public function testInterface() $op = new Operator\SameAs($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } public function testConstructorAndEvaluation() @@ -31,7 +30,7 @@ public function testConstructorAndEvaluation() $this->assertTrue($op->evaluate($context)); $context['a'] = 3; - $context['b'] = function() { + $context['b'] = function () { return 3; }; $this->assertTrue($op->evaluate($context)); diff --git a/tests/Ruler/Test/Operator/ContainsTest.php b/tests/Ruler/Test/Operator/SetContainsTest.php similarity index 73% rename from tests/Ruler/Test/Operator/ContainsTest.php rename to tests/Ruler/Test/Operator/SetContainsTest.php index 24a2a64..452b483 100644 --- a/tests/Ruler/Test/Operator/ContainsTest.php +++ b/tests/Ruler/Test/Operator/SetContainsTest.php @@ -6,16 +6,15 @@ use Ruler\Context; use Ruler\Variable; -class ContainsTest extends \PHPUnit_Framework_TestCase +class SetContainsTest extends \PHPUnit_Framework_TestCase { public function testInterface() { $varA = new Variable('a', 1); $varB = new Variable('b', array(2)); - $op = new Operator\Contains($varA, $varB); + $op = new Operator\SetContains($varA, $varB); $this->assertInstanceOf('Ruler\Proposition', $op); - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op); } /** @@ -27,7 +26,7 @@ public function testContains($a, $b, $result) $varB = new Variable('b', $b); $context = new Context(); - $op = new Operator\Contains($varA, $varB); + $op = new Operator\SetContains($varA, $varB); $this->assertEquals($op->evaluate($context), $result); } @@ -40,7 +39,7 @@ public function testDoesNotContain($a, $b, $result) $varB = new Variable('b', $b); $context = new Context(); - $op = new Operator\DoesNotContain($varA, $varB); + $op = new Operator\SetDoesNotContain($varA, $varB); $this->assertNotEquals($op->evaluate($context), $result); } @@ -57,10 +56,6 @@ public function containsData() array(array(1, 2, 3), array(2), false), array(array(1, 2, array('foo')), array('foo'), true), array(array(1), array(1), false), - array('super', 'supercalifragilistic', true), - array('fragil', 'supercalifragilistic', true), - array('a', 'supercalifragilistic', true), - array('stic', 'supercalifragilistic', true) ); } } diff --git a/tests/Ruler/Test/Operator/StartsWithInsensitiveTest.php b/tests/Ruler/Test/Operator/StartsWithInsensitiveTest.php new file mode 100644 index 0000000..8a72373 --- /dev/null +++ b/tests/Ruler/Test/Operator/StartsWithInsensitiveTest.php @@ -0,0 +1,43 @@ +assertInstanceOf('Ruler\Proposition', $op); + } + + /** + * @dataProvider startsWithData + */ + public function testStartsWithInsensitive($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\StartsWithInsensitive($varA, $varB); + $this->assertEquals($op->evaluate($context), $result); + } + + public function startsWithData() + { + return array( + array('supercalifragilistic', 'supercalifragilistic', true), + array('supercalifragilistic','super', true), + array('supercalifragilistic','SUPER', true), + array('supercalifragilistic', 'stic', false), + array('supercalifragilistic', '', false), + ); + } +} diff --git a/tests/Ruler/Test/Operator/StartsWithTest.php b/tests/Ruler/Test/Operator/StartsWithTest.php new file mode 100644 index 0000000..66dd0c2 --- /dev/null +++ b/tests/Ruler/Test/Operator/StartsWithTest.php @@ -0,0 +1,43 @@ +assertInstanceOf('Ruler\Proposition', $op); + } + + /** + * @dataProvider startsWithData + */ + public function testStartsWith($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\StartsWith($varA, $varB); + $this->assertEquals($op->evaluate($context), $result); + } + + public function startsWithData() + { + return array( + array('supercalifragilistic', 'supercalifragilistic', true), + array('supercalifragilistic','super', true), + array('supercalifragilistic','SUPER', false), + array('supercalifragilistic', 'stic', false), + array('supercalifragilistic', '', false), + ); + } +} diff --git a/tests/Ruler/Test/Operator/StringContainsInsensitiveTest.php b/tests/Ruler/Test/Operator/StringContainsInsensitiveTest.php new file mode 100644 index 0000000..e7fb9ba --- /dev/null +++ b/tests/Ruler/Test/Operator/StringContainsInsensitiveTest.php @@ -0,0 +1,65 @@ +assertInstanceOf('Ruler\Proposition', $op); + } + + /** + * @dataProvider containsData + */ + public function testContains($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\StringContainsInsensitive($varA, $varB); + $this->assertEquals($op->evaluate($context), $result); + } + + /** + * @dataProvider containsData + */ + public function testDoesNotContain($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\StringDoesNotContainInsensitive($varA, $varB); + $this->assertNotEquals($op->evaluate($context), $result); + } + + public function containsData() + { + return array( + array('supercalifragilistic', 'super', true), + array('supercalifragilistic', 'fragil', true), + array('supercalifragilistic', 'a', true), + array('supercalifragilistic', 'stic', true), + array('timmy', 'bob', false), + array('timmy', 'tim', true), + array('supercalifragilistic', 'SUPER', true), + array('supercalifragilistic', 'frAgil', true), + array('supercalifragilistic', 'A', true), + array('supercalifragilistic', 'sTiC', true), + array('timmy', 'bob', false), + array('timmy', 'TIM', true), + array('tim', 'TIM', true), + array('tim', 'TiM', true), + ); + } +} diff --git a/tests/Ruler/Test/Operator/StringContainsTest.php b/tests/Ruler/Test/Operator/StringContainsTest.php new file mode 100644 index 0000000..4693062 --- /dev/null +++ b/tests/Ruler/Test/Operator/StringContainsTest.php @@ -0,0 +1,57 @@ +assertInstanceOf('Ruler\Proposition', $op); + } + + /** + * @dataProvider containsData + */ + public function testContains($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\StringContains($varA, $varB); + $this->assertEquals($op->evaluate($context), $result); + } + + /** + * @dataProvider containsData + */ + public function testDoesNotContain($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\StringDoesNotContain($varA, $varB); + $this->assertNotEquals($op->evaluate($context), $result); + } + + public function containsData() + { + return array( + array('supercalifragilistic', 'super', true), + array('supercalifragilistic', 'fragil', true), + array('supercalifragilistic', 'a', true), + array('supercalifragilistic', 'stic', true), + array('timmy', 'bob', false), + array('tim', 'TIM', false), + ); + } +} diff --git a/tests/Ruler/Test/Operator/SubtractionTest.php b/tests/Ruler/Test/Operator/SubtractionTest.php new file mode 100644 index 0000000..9eb5db3 --- /dev/null +++ b/tests/Ruler/Test/Operator/SubtractionTest.php @@ -0,0 +1,55 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Arithmetic: values must be numeric + */ + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Subtraction($varA, $varB); + $op->prepareValue($context); + } + + /** + * @dataProvider subtractData + */ + public function testSubtract($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Subtraction($varA, $varB); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function subtractData() + { + return array( + array(6, 2, 4), + array(7, -3, 10), + array(2.5, 1.4, 1.1), + ); + } +} diff --git a/tests/Ruler/Test/Operator/SymmetricDifferenceTest.php b/tests/Ruler/Test/Operator/SymmetricDifferenceTest.php new file mode 100644 index 0000000..36b2a52 --- /dev/null +++ b/tests/Ruler/Test/Operator/SymmetricDifferenceTest.php @@ -0,0 +1,105 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\SymmetricDifference($varA, $varB); + $this->assertEquals( + array('string', 'blah'), + $op->prepareValue($context)->getValue() + ); + } + + /** + * @dataProvider symmetricDifferenceData + */ + public function testSymmetricDifference($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\SymmetricDifference($varA, $varB); + $this->assertEquals( + $result, + $op->prepareValue($context)->getValue() + ); + } + + public function symmetricDifferenceData() + { + return array( + array(6, 2, array(6, 2)), + array( + array('a', 'b', 'c'), + 'a', + array('b', 'c'), + ), + array( + 'a', + array('a', 'b', 'c'), + array('b', 'c'), + ), + array( + 'a', + array('b', 'c'), + array('a', 'b', 'c'), + ), + array( + array('a', 'b', 'c'), + array(), + array('a', 'b', 'c'), + ), + array( + array(), + array('a', 'b', 'c'), + array('a', 'b', 'c'), + ), + array( + array(), + array(), + array(), + ), + array( + array('a', 'b', 'c'), + array('d', 'e', 'f'), + array('a', 'b', 'c', 'd', 'e', 'f'), + ), + array( + array('a', 'b', 'c'), + array('a', 'b', 'c'), + array(), + ), + array( + array('a', 'b', 'c'), + array('b', 'c'), + array('a'), + ), + array( + array('b', 'c'), + array('b', 'd'), + array('c', 'd'), + ), + ); + } +} diff --git a/tests/Ruler/Test/Operator/UnionTest.php b/tests/Ruler/Test/Operator/UnionTest.php new file mode 100644 index 0000000..f7b96d0 --- /dev/null +++ b/tests/Ruler/Test/Operator/UnionTest.php @@ -0,0 +1,95 @@ +assertInstanceOf('Ruler\\VariableOperand', $op); + } + + public function testInvalidData() + { + $varA = new Variable('a', "string"); + $varB = new Variable('b', "blah"); + $context = new Context(); + + $op = new Operator\Union($varA, $varB); + $this->assertEquals( + $op->prepareValue($context)->getValue(), + array( + 'string', + 'blah' + ) + ); + } + + /** + * @dataProvider unionData + */ + public function testUnion($a, $b, $result) + { + $varA = new Variable('a', $a); + $varB = new Variable('b', $b); + $context = new Context(); + + $op = new Operator\Union($varA, $varB); + $this->assertEquals($op->prepareValue($context)->getValue(), $result); + } + + public function unionData() + { + return array( + array(6, 2, array(6, 2)), + array( + 'a', + array('b', 'c'), + array('a', 'b', 'c'), + ), + array( + array('a', 'b', 'c'), + array(), + array('a', 'b', 'c'), + ), + array( + array(), + array('a', 'b', 'c'), + array('a', 'b', 'c'), + ), + array( + array(), + array(), + array(), + ), + array( + array('a', 'b', 'c'), + array('d', 'e', 'f'), + array('a', 'b', 'c', 'd', 'e', 'f'), + ), + array( + array('a', 'b', 'c'), + array('a', 'b', 'c'), + array('a', 'b', 'c'), + ), + array( + array('a', 'b', 'c'), + array('b', 'c'), + array('a', 'b', 'c'), + ), + array( + array('b', 'c'), + array('b', 'd'), + array('b', 'c', 'd'), + ), + ); + } +} diff --git a/tests/Ruler/Test/RuleBuilder/VariablePropertyTest.php b/tests/Ruler/Test/RuleBuilder/VariablePropertyTest.php index 5de021b..31b0bb6 100644 --- a/tests/Ruler/Test/RuleBuilder/VariablePropertyTest.php +++ b/tests/Ruler/Test/RuleBuilder/VariablePropertyTest.php @@ -4,6 +4,7 @@ use Ruler\Context; use Ruler\Value; +use Ruler\RuleBuilder; use Ruler\RuleBuilder\Variable; use Ruler\RuleBuilder\VariableProperty; @@ -12,7 +13,7 @@ class VariablePropertyTest extends \PHPUnit_Framework_TestCase public function testConstructor() { $name = 'evil'; - $prop = new VariableProperty(new Variable, $name); + $prop = new VariableProperty(new Variable(new RuleBuilder()), $name); $this->assertEquals($name, $prop->getName()); $this->assertNull($prop->getValue()); } @@ -21,7 +22,7 @@ public function testGetSetValue() { $values = explode(', ', 'Plug it, play it, burn it, rip it, drag and drop it, zip, unzip it'); - $prop = new VariableProperty(new Variable, 'technologic'); + $prop = new VariableProperty(new Variable(new RuleBuilder()), 'technologic'); foreach ($values as $valueString) { $prop->setValue($valueString); $this->assertEquals($valueString, $prop->getValue()); @@ -39,7 +40,7 @@ public function testPrepareValue() $context = new Context($values); - $var = new Variable('root'); + $var = new Variable(new RuleBuilder(), 'root'); $propA = new VariableProperty($var, 'undefined', 'default'); $this->assertInstanceOf('Ruler\Value', $propA->prepareValue($context)); @@ -70,17 +71,21 @@ public function testFluentInterfaceHelpersAndAnonymousVariables() 'qux' => 3, ), ), + 'e' => 'string', + 'f' => 'ring', + 'g' => 'stuff' ), )); - $root = new Variable('root'); + $root = new Variable(new RuleBuilder(), 'root'); $varA = $root['a']; $varB = $root['b']; $varC = $root['c']; $varD = $root['d']; - - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $varA->greaterThan(0)); + $varE = $root['e']; + $varF = $root['f']; + $varG = $root['g']; $this->assertInstanceOf('Ruler\Operator\GreaterThan', $varA->greaterThan(0)); $this->assertTrue($varA->greaterThan(0)->evaluate($context)); @@ -113,11 +118,17 @@ public function testFluentInterfaceHelpersAndAnonymousVariables() $this->assertFalse($varA->greaterThan($varB)->evaluate($context)); $this->assertTrue($varA->lessThan($varB)->evaluate($context)); - $this->assertInstanceOf('Ruler\Operator\Contains', $varC->contains(1)); - $this->assertTrue($varC->contains($varA)->evaluate($context)); + $this->assertInstanceOf('Ruler\Operator\StringContains', $varE->stringContains('ring')); + $this->assertTrue($varE->stringContains($varF)->evaluate($context)); + + $this->assertInstanceOf('Ruler\Operator\StringDoesNotContain', $varE->stringDoesNotContain('cheese')); + $this->assertTrue($varE->stringDoesNotContain($varG)->evaluate($context)); + + $this->assertInstanceOf('Ruler\Operator\SetContains', $varC->setContains(1)); + $this->assertTrue($varC->setContains($varA)->evaluate($context)); - $this->assertInstanceOf('Ruler\Operator\DoesNotContain', $varC->doesNotContain(1)); - $this->assertTrue($varC->doesNotContain($varB)->evaluate($context)); + $this->assertInstanceOf('Ruler\Operator\SetDoesNotContain', $varC->setDoesNotContain(1)); + $this->assertTrue($varC->setDoesNotContain($varB)->evaluate($context)); $this->assertInstanceOf('Ruler\RuleBuilder\VariableProperty', $varD['bar']); $this->assertEquals($varD['foo']->getName(), 'foo'); @@ -130,5 +141,47 @@ public function testFluentInterfaceHelpersAndAnonymousVariables() $this->assertInstanceOf('Ruler\RuleBuilder\VariableProperty', $varD['baz']['qux']); $this->assertEquals($varD['baz']['qux']->getName(), 'qux'); $this->assertTrue($varD['baz']['qux']->equalTo(3)->evaluate($context)); + + $this->assertInstanceOf('Ruler\Operator\EndsWith', $varE->endsWith('string')); + $this->assertTrue($varE->endsWith($varE)->evaluate($context)); + + $this->assertInstanceOf('Ruler\Operator\EndsWithInsensitive', $varE->endsWithInsensitive('STRING')); + $this->assertTrue($varE->endsWithInsensitive($varE)->evaluate($context)); + + $this->assertInstanceOf('Ruler\Operator\StartsWith', $varE->startsWith('string')); + $this->assertTrue($varE->startsWith($varE)->evaluate($context)); + + $this->assertInstanceOf('Ruler\Operator\StartsWithInsensitive', $varE->startsWithInsensitive('STRING')); + $this->assertTrue($varE->startsWithInsensitive($varE)->evaluate($context)); + } + + /** + * @expectedException PHPUnit_Framework_Error_Deprecated + * @expectedExceptionMessage Contains operator is deprecated, please use SetContains + */ + public function testDeprecationNoticeForContainsWithSet() + { + $context = new Context(array( + 'var' => array('foo', 'bar', 'baz'), + )); + + $var = new Variable(new RuleBuilder(), 'var'); + + $this->assertTrue($var->contains('bar')->evaluate($context)); + } + + /** + * @expectedException PHPUnit_Framework_Error_Deprecated + * @expectedExceptionMessage Contains operator is deprecated, please use StringContains + */ + public function testDeprecationNoticeForContainsWithString() + { + $context = new Context(array( + 'var' => 'string', + )); + + $var = new Variable(new RuleBuilder(), 'var'); + + $this->assertTrue($var->contains('ring')->evaluate($context)); } } diff --git a/tests/Ruler/Test/RuleBuilder/VariableTest.php b/tests/Ruler/Test/RuleBuilder/VariableTest.php index 832893a..b1e76a5 100644 --- a/tests/Ruler/Test/RuleBuilder/VariableTest.php +++ b/tests/Ruler/Test/RuleBuilder/VariableTest.php @@ -2,6 +2,7 @@ namespace Ruler\Test\RuleBuilder; +use Ruler\RuleBuilder; use Ruler\RuleBuilder\Variable; use Ruler\Context; use Ruler\Value; @@ -11,7 +12,7 @@ class VariableTest extends \PHPUnit_Framework_TestCase public function testConstructor() { $name = 'evil'; - $var = new Variable($name); + $var = new Variable(new RuleBuilder(), $name); $this->assertEquals($name, $var->getName()); $this->assertNull($var->getValue()); } @@ -20,7 +21,7 @@ public function testGetSetValue() { $values = explode(', ', 'Plug it, play it, burn it, rip it, drag and drop it, zip, unzip it'); - $variable = new Variable('technologic'); + $variable = new Variable(new RuleBuilder(), 'technologic'); foreach ($values as $valueString) { $variable->setValue($valueString); $this->assertEquals($valueString, $variable->getValue()); @@ -32,14 +33,15 @@ public function testPrepareValue() $values = array( 'one' => 'Foo', 'two' => 'BAR', - 'three' => function() { + 'three' => function () { return 'baz'; } ); $context = new Context($values); - $varA = new Variable('four', 'qux'); + $rb = new RuleBuilder(); + $varA = new Variable($rb, 'four', 'qux'); $this->assertInstanceOf('Ruler\Value', $varA->prepareValue($context)); $this->assertEquals( 'qux', @@ -47,19 +49,19 @@ public function testPrepareValue() "Variables should return the default value if it's missing from the context." ); - $varB = new Variable('one', 'FAIL'); + $varB = new Variable($rb, 'one', 'FAIL'); $this->assertEquals( 'Foo', $varB->prepareValue($context)->getValue() ); - $varC = new Variable('three', 'FAIL'); + $varC = new Variable($rb, 'three', 'FAIL'); $this->assertEquals( 'baz', $varC->prepareValue($context)->getValue() ); - $varD = new Variable(null, 'qux'); + $varD = new Variable($rb, null, 'qux'); $this->assertInstanceOf('Ruler\Value', $varD->prepareValue($context)); $this->assertEquals( 'qux', @@ -70,6 +72,7 @@ public function testPrepareValue() public function testFluentInterfaceHelpersAndAnonymousVariables() { + $rb = new RuleBuilder(); $context = new Context(array( 'a' => 1, 'b' => 2, @@ -81,14 +84,14 @@ public function testFluentInterfaceHelpersAndAnonymousVariables() 'qux' => 3, ), ), + 'e' => 1.5, )); - $varA = new Variable('a'); - $varB = new Variable('b'); - $varC = new Variable('c'); - $varD = new Variable('d'); - - $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $varA->greaterThan(0)); + $varA = new Variable($rb, 'a'); + $varB = new Variable($rb, 'b'); + $varC = new Variable($rb, 'c'); + $varD = new Variable($rb, 'd'); + $varE = new Variable($rb, 'e'); $this->assertInstanceOf('Ruler\Operator\GreaterThan', $varA->greaterThan(0)); $this->assertTrue($varA->greaterThan(0)->evaluate($context)); @@ -118,14 +121,60 @@ public function testFluentInterfaceHelpersAndAnonymousVariables() $this->assertTrue($varA->notEqualTo(0)->evaluate($context)); $this->assertTrue($varA->notEqualTo(2)->evaluate($context)); + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varA->add(3)); + $this->assertInstanceof('Ruler\Operator\Addition', $varA->add(3)->getValue()); + $this->assertInstanceof('Ruler\Value', $varA->add(3)->prepareValue($context)); + $this->assertEquals(4, $varA->add(3)->prepareValue($context)->getValue()); + $this->assertEquals(0, $varA->add(-1)->prepareValue($context)->getValue()); + + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varE->ceil()); + $this->assertInstanceof('Ruler\Operator\Ceil', $varE->ceil()->getValue()); + $this->assertEquals(2, $varE->ceil()->prepareValue($context)->getValue()); + + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varB->divide(3)); + $this->assertInstanceof('Ruler\Operator\Division', $varB->divide(3)->getValue()); + $this->assertEquals(1, $varB->divide(2)->prepareValue($context)->getValue()); + $this->assertEquals(-2, $varB->divide(-1)->prepareValue($context)->getValue()); + + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varE->floor()); + $this->assertInstanceof('Ruler\Operator\Floor', $varE->floor()->getValue()); + $this->assertEquals(1, $varE->floor()->prepareValue($context)->getValue()); + + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varA->modulo(3)); + $this->assertInstanceof('Ruler\Operator\Modulo', $varA->modulo(3)->getValue()); + $this->assertEquals(1, $varA->modulo(3)->prepareValue($context)->getValue()); + $this->assertEquals(0, $varB->modulo(2)->prepareValue($context)->getValue()); + + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varA->multiply(3)); + $this->assertInstanceof('Ruler\Operator\Multiplication', $varA->multiply(3)->getValue()); + $this->assertEquals(6, $varB->multiply(3)->prepareValue($context)->getValue()); + $this->assertEquals(-2, $varB->multiply(-1)->prepareValue($context)->getValue()); + + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varA->negate()); + $this->assertInstanceof('Ruler\Operator\Negation', $varA->negate()->getValue()); + $this->assertEquals(-1, $varA->negate()->prepareValue($context)->getValue()); + $this->assertEquals(-2, $varB->negate()->prepareValue($context)->getValue()); + + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varA->subtract(3)); + $this->assertInstanceof('Ruler\Operator\Subtraction', $varA->subtract(3)->getValue()); + $this->assertEquals(-2, $varA->subtract(3)->prepareValue($context)->getValue()); + $this->assertEquals(2, $varA->subtract(-1)->prepareValue($context)->getValue()); + + $this->assertInstanceof('Ruler\RuleBuilder\Variable', $varA->exponentiate(3)); + $this->assertInstanceof('Ruler\Operator\Exponentiate', $varA->exponentiate(3)->getValue()); + $this->assertEquals(1, $varA->exponentiate(3)->prepareValue($context)->getValue()); + $this->assertEquals(1, $varA->exponentiate(-1)->prepareValue($context)->getValue()); + $this->assertEquals(8, $varB->exponentiate(3)->prepareValue($context)->getValue()); + $this->assertEquals(0.5, $varB->exponentiate(-1)->prepareValue($context)->getValue()); + $this->assertFalse($varA->greaterThan($varB)->evaluate($context)); $this->assertTrue($varA->lessThan($varB)->evaluate($context)); - $this->assertInstanceOf('Ruler\Operator\Contains', $varC->contains(1)); - $this->assertTrue($varC->contains($varA)->evaluate($context)); + $this->assertInstanceOf('Ruler\Operator\SetContains', $varC->setContains(1)); + $this->assertTrue($varC->setContains($varA)->evaluate($context)); - $this->assertInstanceOf('Ruler\Operator\DoesNotContain', $varC->doesNotContain(1)); - $this->assertTrue($varC->doesNotContain($varB)->evaluate($context)); + $this->assertInstanceOf('Ruler\Operator\SetDoesNotContain', $varC->setDoesNotContain(1)); + $this->assertTrue($varC->setDoesNotContain($varB)->evaluate($context)); $this->assertInstanceOf('Ruler\RuleBuilder\VariableProperty', $varD['bar']); $this->assertEquals($varD['foo']->getName(), 'foo'); @@ -142,7 +191,7 @@ public function testFluentInterfaceHelpersAndAnonymousVariables() public function testArrayAccess() { - $var = new Variable; + $var = new Variable(new RuleBuilder()); $this->assertInstanceOf('ArrayAccess', $var); $foo = $var['foo']; diff --git a/tests/Ruler/Test/RuleBuilderTest.php b/tests/Ruler/Test/RuleBuilderTest.php index 0b85d3f..4d016cf 100644 --- a/tests/Ruler/Test/RuleBuilderTest.php +++ b/tests/Ruler/Test/RuleBuilderTest.php @@ -48,7 +48,6 @@ public function testLogicalOperatorGeneration() $true = new TrueProposition(); $false = new FalseProposition(); - $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $rb->logicalAnd($true, $false)); $this->assertInstanceOf('Ruler\Operator\LogicalAnd', $rb->logicalAnd($true, $false)); $this->assertFalse($rb->logicalAnd($true, $false)->evaluate($context)); @@ -75,7 +74,7 @@ public function testRuleCreation() $this->assertFalse($rb->create($false)->evaluate($context)); $executed = false; - $rule = $rb->create($true, function() use (&$executed) { + $rule = $rb->create($true, function () use (&$executed) { $executed = true; }); @@ -83,4 +82,83 @@ public function testRuleCreation() $rule->execute($context); $this->assertTrue($executed); } + + public function testNotAddEqualTo() + { + $rb = new RuleBuilder(); + $context = new Context(array( + 'A2' => 8, + 'A3' => 4, + 'B2' => 13 + )); + + $rule = $rb->logicalNot( + $rb['A2']->equalTo($rb['B2']) + ); + $this->assertTrue($rule->evaluate($context)); + + $rule = $rb['A2']->add($rb['A3']); + + $rule = $rb->logicalNot( + $rule->equalTo($rb['B2']) + ); + $this->assertTrue($rule->evaluate($context)); + } + + public function testExternalOperators() + { + $rb = new RuleBuilder(); + $rb->registerOperatorNamespace('\Ruler\Test\Fixtures'); + + $context = new Context(array('a' => 100)); + $varA = $rb['a']; + + $this->assertTrue($varA->aLotGreaterThan(1)->evaluate($context)); + + $context['a'] = 9; + $this->assertFalse($varA->aLotGreaterThan(1)->evaluate($context)); + } + + /** + * @dataProvider testLogicExceptionOnRegisteringOperatorNamespaceProvider + * + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Namespace argument must be a string + */ + public function testInvalidArgumentExceptionOnRegisteringOperatorNamespace($input) + { + $rb = new RuleBuilder(); + $rb->registerOperatorNamespace($input); + } + + public function testLogicExceptionOnRegisteringOperatorNamespaceProvider() + { + return array( + array( + array('ExceptionRisen') + ), + array( + new \StdClass() + ), + array( + 0 + ), + array( + null + ) + ); + } + + /** + * @expectedException LogicException + * @expectedExceptionMessage Unknown operator: "aLotBiggerThan" + */ + public function testLogicExceptionOnUnknownOperator() + { + $rb = new RuleBuilder(); + $rb->registerOperatorNamespace('\Ruler\Test\Fixtures'); + $varA = $rb['a']; + + $varA->aLotBiggerThan(1); + } } diff --git a/tests/Ruler/Test/RuleSetTest.php b/tests/Ruler/Test/RuleSetTest.php index 58636e1..ad81d4a 100644 --- a/tests/Ruler/Test/RuleSetTest.php +++ b/tests/Ruler/Test/RuleSetTest.php @@ -15,17 +15,17 @@ public function testRulesetCreationUpdateAndExecution() $true = new TrueProposition(); $executedActionA = false; - $ruleA = new Rule($true, function() use (&$executedActionA) { + $ruleA = new Rule($true, function () use (&$executedActionA) { $executedActionA = true; }); $executedActionB = false; - $ruleB = new Rule($true, function() use (&$executedActionB) { + $ruleB = new Rule($true, function () use (&$executedActionB) { $executedActionB = true; }); $executedActionC = false; - $ruleC = new Rule($true, function() use (&$executedActionC) { + $ruleC = new Rule($true, function () use (&$executedActionC) { $executedActionC = true; }); diff --git a/tests/Ruler/Test/RuleTest.php b/tests/Ruler/Test/RuleTest.php index 1ae7ba8..8f1931e 100644 --- a/tests/Ruler/Test/RuleTest.php +++ b/tests/Ruler/Test/RuleTest.php @@ -27,9 +27,10 @@ public function testConstructorEvaluationAndExecution() new CallbackProposition(function ($c) use ($test, $context, &$executed, &$actionExecuted) { $test->assertSame($c, $context); $executed = true; + return false; }), - function() use ($test, &$actionExecuted) { + function () use ($test, &$actionExecuted) { $actionExecuted = true; } ); @@ -47,9 +48,10 @@ function() use ($test, &$actionExecuted) { new CallbackProposition(function ($c) use ($test, $context, &$executed, &$actionExecuted) { $test->assertSame($c, $context); $executed = true; + return true; }), - function() use ($test, &$actionExecuted) { + function () use ($test, &$actionExecuted) { $actionExecuted = true; } ); @@ -62,7 +64,7 @@ function() use ($test, &$actionExecuted) { } /** - * @expectedException LogicException + * @expectedException \LogicException */ public function testNonCallableActionsWillThrowAnException() { diff --git a/tests/Ruler/Test/ValueTest.php b/tests/Ruler/Test/ValueTest.php index ca8f59c..de74657 100644 --- a/tests/Ruler/Test/ValueTest.php +++ b/tests/Ruler/Test/ValueTest.php @@ -40,4 +40,4 @@ public function getRelativeValues() ), ); } -} \ No newline at end of file +} diff --git a/tests/Ruler/Test/VariableTest.php b/tests/Ruler/Test/VariableTest.php index f039428..d4dcb21 100644 --- a/tests/Ruler/Test/VariableTest.php +++ b/tests/Ruler/Test/VariableTest.php @@ -32,7 +32,7 @@ public function testPrepareValue() $values = array( 'one' => 'Foo', 'two' => 'BAR', - 'three' => function() { + 'three' => function () { return 'baz'; } );