You are here

SassNumber.php in Sassy 7

File

phamlp/sass/script/literals/SassNumber.php
View 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

Namesort descending 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 =…