SassNumber.php in Sassy 7
File
phamlp/sass/script/literals/SassNumber.phpView source
<?php
/* SVN FILE: $Id$ */
/**
* SassNumber class file.
* @author Chris Yates <chris.l.yates@gmail.com>
* @copyright Copyright (c) 2010 PBM Web Development
* @license http://phamlp.googlecode.com/files/license.txt
* @package PHamlP
* @subpackage Sass.script.literals
*/
require_once 'SassLiteral.php';
/**
* SassNumber class.
* Provides operations and type testing for Sass numbers.
* Units are of the passed value are converted the those of the class value
* if it has units. e.g. 2cm + 20mm = 4cm while 2 + 20mm = 22mm.
* @package PHamlP
* @subpackage Sass.script.literals
*/
class SassNumber extends SassLiteral {
/**
* Regx for matching and extracting numbers
*/
const MATCH = '/^((?:-)?(?:\\d*\\.)?\\d+)(([a-z%]+)(\\s*[\\*\\/]\\s*[a-z%]+)*)?/i';
const VALUE = 1;
const UNITS = 2;
/**
* The number of decimal digits to round to.
* If the units are pixels the result is always
* rounded down to the nearest integer.
*/
const PRECISION = 4;
/**
* @var array Conversion factors for units using inches as the base unit
* (only because pt and pc are expressed as fraction of an inch, so makes the
* numbers easy to understand).
* Conversions are based on the following
* in: inches — 1 inch = 2.54 centimeters
* cm: centimeters
* mm: millimeters
* pc: picas — 1 pica = 12 points
* pt: points — 1 point = 1/72nd of an inch
*/
private static $unitConversion = array(
'in' => 1,
'cm' => 2.54,
'mm' => 25.4,
'pc' => 6,
'pt' => 72,
);
/**
* @var array numerator units of this number
*/
private $numeratorUnits = array();
/**
* @var array denominator units of this number
*/
private $denominatorUnits = array();
/**
* @var boolean whether this number is in an expression or a literal number
* Used to determine whether division should take place
*/
public $inExpression = true;
/**
* class constructor.
* Sets the value and units of the number.
* @param string number
* @return SassNumber
*/
public function __construct($value) {
preg_match(self::MATCH, $value, $matches);
$this->value = $matches[self::VALUE];
if (!empty($matches[self::UNITS])) {
$units = explode('/', $matches[self::UNITS]);
$numeratorUnits = $denominatorUnits = array();
foreach (explode('*', $units[0]) as $unit) {
$numeratorUnits[] = trim($unit);
}
if (isset($units[1])) {
foreach (explode('*', $units[1]) as $unit) {
$denominatorUnits[] = trim($unit);
}
}
$units = $this
->removeCommonUnits($numeratorUnits, $denominatorUnits);
$this->numeratorUnits = $units[0];
$this->denominatorUnits = $units[1];
}
}
/**
* Adds the value of other to the value of this
* @param mixed SassNumber|SassColour: value to add
* @return mixed SassNumber if other is a SassNumber or
* SassColour if it is a SassColour
*/
public function op_plus($other) {
if ($other instanceof SassColour) {
return $other
->op_plus($this);
}
elseif (!$other instanceof SassNumber) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'number'),
), SassScriptParser::$context->node);
}
else {
$other = $this
->convert($other);
return new SassNumber($this->value + $other->value . $this->units);
}
}
/**
* Unary + operator
* @return SassNumber the value of this number
*/
public function op_unary_plus() {
return $this;
}
/**
* Subtracts the value of other from this value
* @param mixed SassNumber|SassColour: value to subtract
* @return mixed SassNumber if other is a SassNumber or
* SassColour if it is a SassColour
*/
public function op_minus($other) {
if ($other instanceof SassColour) {
return $other
->op_minus($this);
}
elseif (!$other instanceof SassNumber) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'number'),
), SassScriptParser::$context->node);
}
else {
$other = $this
->convert($other);
return new SassNumber($this->value - $other->value . $this->units);
}
}
/**
* Unary - operator
* @return SassNumber the negative value of this number
*/
public function op_unary_minus() {
return new SassNumber($this->value * -1 . $this->units);
}
public function op_unary_concat() {
return $this;
}
/**
* Multiplies this value by the value of other
* @param mixed SassNumber|SassColour: value to multiply by
* @return mixed SassNumber if other is a SassNumber or
* SassColour if it is a SassColour
*/
public function op_times($other) {
if ($other instanceof SassColour) {
return $other
->op_times($this);
}
elseif (!$other instanceof SassNumber) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'number'),
), SassScriptParser::$context->node);
}
else {
return new SassNumber($this->value * $other->value . $this
->unitString(array_merge($this->numeratorUnits, $other->numeratorUnits), array_merge($this->denominatorUnits, $other->denominatorUnits)));
}
}
/**
* Divides this value by the value of other
* @param mixed SassNumber|SassColour: value to divide by
* @return mixed SassNumber if other is a SassNumber or
* SassColour if it is a SassColour
*/
public function op_div($other) {
if ($other instanceof SassColour) {
return $other
->op_div($this);
}
elseif (!$other instanceof SassNumber) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'number'),
), SassScriptParser::$context->node);
}
elseif ($this->inExpression || $other->inExpression) {
return new SassNumber($this->value / $other->value . $this
->unitString(array_merge($this->numeratorUnits, $other->denominatorUnits), array_merge($this->denominatorUnits, $other->numeratorUnits)));
}
else {
return parent::op_div($other);
}
}
/**
* The SassScript == operation.
* @return SassBoolean SassBoolean object with the value true if the values
* of this and other are equal, false if they are not
*/
public function op_eq($other) {
if (!$other instanceof SassNumber) {
return new SassBoolean(false);
}
try {
return new SassBoolean($this->value == $this
->convert($other)->value);
} catch (Exception $e) {
return new SassBoolean(false);
}
}
/**
* The SassScript > operation.
* @param sassLiteral the value to compare to this
* @return SassBoolean SassBoolean object with the value true if the values
* of this is greater than the value of other, false if it is not
*/
public function op_gt($other) {
if (!$other instanceof SassNumber) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'number'),
), SassScriptParser::$context->node);
}
return new SassBoolean($this->value > $this
->convert($other)->value);
}
/**
* The SassScript >= operation.
* @param sassLiteral the value to compare to this
* @return SassBoolean SassBoolean object with the value true if the values
* of this is greater than or equal to the value of other, false if it is not
*/
public function op_gte($other) {
if (!$other instanceof SassNumber) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'number'),
), SassScriptParser::$context->node);
}
return new SassBoolean($this->value >= $this
->convert($other)->value);
}
/**
* The SassScript < operation.
* @param sassLiteral the value to compare to this
* @return SassBoolean SassBoolean object with the value true if the values
* of this is less than the value of other, false if it is not
*/
public function op_lt($other) {
if (!$other instanceof SassNumber) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'number'),
), SassScriptParser::$context->node);
}
return new SassBoolean($this->value < $this
->convert($other)->value);
}
/**
* The SassScript <= operation.
* @param sassLiteral the value to compare to this
* @return SassBoolean SassBoolean object with the value true if the values
* of this is less than or equal to the value of other, false if it is not
*/
public function op_lte($other) {
if (!$other instanceof SassNumber) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'number'),
), SassScriptParser::$context->node);
}
return new SassBoolean($this->value <= $this
->convert($other)->value);
}
/**
* Takes the modulus (remainder) of this value divided by the value of other
* @param string value to divide by
* @return mixed SassNumber if other is a SassNumber or
* SassColour if it is a SassColour
*/
public function op_modulo($other) {
if (!$other instanceof SassNumber || !$other
->isUnitless()) {
throw new SassNumberException('{what} must be a {type}', array(
'{what}' => Phamlp::t('sass', 'Number'),
'{type}' => Phamlp::t('sass', 'unitless number'),
), SassScriptParser::$context->node);
}
$this->value %= $this
->convert($other)->value;
return $this;
}
/**
* Converts values and units.
* If this is a unitless numeber it will take the units of other; if not
* other is coerced to the units of this.
* @param SassNumber the other number
* @return SassNumber the other number with its value and units coerced if neccessary
* @throws SassNumberException if the units are incompatible
*/
private function convert($other) {
if ($this
->isUnitless()) {
$this->numeratorUnits = $other->numeratorUnits;
$this->denominatorUnits = $other->denominatorUnits;
}
else {
$other = $other
->coerce($this->numeratorUnits, $this->denominatorUnits);
}
return $other;
}
/**
* Returns the value of this number converted to other units.
* The conversion takes into account the relationship between e.g. mm and cm,
* as well as between e.g. in and cm.
*
* If this number is unitless, it will simply return itself with the given units.
* @param array $numeratorUnits
* @param array $denominatorUnits
* @return SassNumber
*/
public function coerce($numeratorUnits, $denominatorUnits) {
return new SassNumber(($this
->isUnitless() ? $this->value : $this->value * $this
->coercionFactor($this->numeratorUnits, $numeratorUnits) / $this
->coercionFactor($this->denominatorUnits, $denominatorUnits)) . join(' * ', $numeratorUnits) . (!empty($denominatorUnits) ? ' / ' . join(' * ', $denominatorUnits) : ''));
}
/**
* Calculates the corecion factor to apply to the value
* @param array units being converted from
* @param array units being converted to
* @return float the coercion factor to apply
*/
private function coercionFactor($fromUnits, $toUnits) {
$units = $this
->removeCommonUnits($fromUnits, $toUnits);
$fromUnits = $units[0];
$toUnits = $units[1];
if (sizeof($fromUnits) !== sizeof($toUnits) || !$this
->areConvertable(array_merge($fromUnits, $toUnits))) {
throw new SassNumberException("Incompatible units: '{from}' and '{to}'", array(
'{from}' => join(' * ', $fromUnits),
'{to}' => join(' * ', $toUnits),
), SassScriptParser::$context->node);
}
$coercionFactor = 1;
foreach ($fromUnits as $i => $from) {
if (array_key_exists($from) && array_key_exists($from)) {
$coercionFactor *= self::$unitConversion[$toUnits[$i]] / self::$unitConversion[$from];
}
else {
throw new SassNumberException("Incompatible units: '{from}' and '{to}", array(
'{from}' => join(' * ', $fromUnits),
'{to}' => join(' * ', $toUnits),
), SassScriptParser::$context->node);
}
}
return $coercionFactor;
}
/**
* Returns a value indicating if all the units are capable of being converted
* @param array units to test
* @return boolean true if all units can be converted, false if not
*/
private function areConvertable($units) {
$convertable = array_keys(self::$unitConversion);
foreach ($units as $unit) {
if (!in_array($unit, $convertable)) {
return false;
}
}
return true;
}
/**
* Removes common units from each set.
* We don't use array_diff because we want (for eaxmple) mm*mm/mm*cm to
* end up as mm/cm.
* @param array first set of units
* @param array second set of units
* @return array both sets of units with common units removed
*/
private function removeCommonUnits($u1, $u2) {
$_u1 = array();
while (!empty($u1)) {
$u = array_shift($u1);
$i = array_search($u, $u2);
if ($i !== false) {
unset($u2[$i]);
}
else {
$_u1[] = $u;
}
}
return array(
$_u1,
$u2,
);
}
/**
* Returns a value indicating if this number is unitless.
* @return boolean true if this number is unitless, false if not
*/
public function isUnitless() {
return empty($this->numeratorUnits) && empty($this->denominatorUnits);
}
/**
* Returns a value indicating if this number has units.
* @return boolean true if this number has, false if not
*/
public function hasUnits() {
return !$this
->isUnitless();
}
/**
* Returns a value indicating if this number has units that can be represented
* in CSS.
* @return boolean true if this number has units that can be represented in
* CSS, false if not
*/
public function hasLegalUnits() {
return (empty($this->numeratorUnits) || count($this->numeratorUnits) === 1) && empty($this->denominatorUnits);
}
/**
* Returns a string representation of the units.
* @return string the units
*/
public function unitString($numeratorUnits, $denominatorUnits) {
return join(' * ', $numeratorUnits) . (!empty($denominatorUnits) ? ' / ' . join(' * ', $denominatorUnits) : '');
}
/**
* Returns the units of this number.
* @return string the units of this number
*/
public function getUnits() {
return $this
->unitString($this->numeratorUnits, $this->denominatorUnits);
}
/**
* Returns the denominator units of this number.
* @return string the denominator units of this number
*/
public function getDenominatorUnits() {
return join(' * ', $this->denominatorUnits);
}
/**
* Returns the numerator units of this number.
* @return string the numerator units of this number
*/
public function getNumeratorUnits() {
return join(' * ', $this->numeratorUnits);
}
/**
* Returns a value indicating if this number can be compared to other.
* @return boolean true if this number can be compared to other, false if not
*/
public function isComparableTo($other) {
try {
$this
->op_plus($other);
return true;
} catch (Exception $e) {
return false;
}
}
/**
* Returns a value indicating if this number is an integer.
* @return boolean true if this number is an integer, false if not
*/
public function isInt() {
return $this->value % 1 === 0;
}
/**
* Returns the value of this number.
* @return float the value of this number.
*/
public function getValue() {
return $this->value;
}
/**
* Returns the integer value.
* @return integer the integer value.
* @throws SassNumberException if the number is not an integer
*/
public function toInt() {
if (!$this
->isInt()) {
throw new SassNumberException('Not an integer: {value}', array(
'{value}' => $this->value,
), SassScriptParser::$context->node);
}
return intval($this->value);
}
/**
* Converts the number to a string with it's units if any.
* If the units are px the result is rounded down to the nearest integer,
* otherwise the result is rounded to the specified precision.
* @return string number as a string with it's units if any
*/
public function toString() {
if (!$this
->hasLegalUnits()) {
throw new SassNumberException('Invalid {what}', array(
'{what}' => "CSS units ({$this->units})",
), SassScriptParser::$context->node);
}
return ($this->units == 'px' ? floor($this->value) : round($this->value, self::PRECISION)) . $this->units;
}
/**
* Returns a value indicating if a token of this type can be matched at
* the start of the subject string.
* @param string the subject string
* @return mixed match at the start of the string or false if no match
*/
public static function isa($subject) {
return preg_match(self::MATCH, $subject, $matches) ? $matches[0] : false;
}
}
Classes
Name | Description |
---|---|
SassNumber | SassNumber class. Provides operations and type testing for Sass numbers. Units are of the passed value are converted the those of the class value if it has units. e.g. 2cm + 20mm = 4cm while 2 + 20mm =… |