<?php /* SVN FILE: $Id: SassNumber.php 118 2010-09-21 09:45:11Z chris.l.yates@gmail.com $ */ /**
* 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 */ static private $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); } /** * 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 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); }
}