Seditio Source
Root |
./othercms/ips_4.3.4/system/Math/Number.php
<?php
/**
 * @brief        Number Class for precise math
 * @author        <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) Invision Power Services, Inc.
 * @license        https://www.invisioncommunity.com/legal/standards/
 * @package        Invision Community
 * @since        15 Deb 2016
 */

namespace IPS\Math;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

/**
 * Number Class for precise math
 */
class _Number implements \JsonSerializable
{
   
/* !Bootstrap */
   
    /**
     * @brief    Positive
     */
   
protected $positive = TRUE;
   
   
/**
     * @brief    Number before decimal point
     */
   
protected $beforeDecimalPoint = 0;
   
   
/**
     * @brief    Number of decimal places
     */
   
protected $numberOfDecimalPlaces = 0;
   
   
/**
     * @brief    After decimal point
     */
   
protected $afterDecimalPoint = 0;
   
   
/**
     * Constructor
     *
     * @string    $number    The number, as a string, using "." for decimal points
     * @return    void
     * @throws    \InvalidArgumentException
     */
   
public function __construct( $number )
    {
       
/* Check it's valid */
       
if ( !is_string( $number ) )
        {
            throw new \
InvalidArgumentException('NOT_A_STRING');
        }
        if ( !
preg_match( '/^([+-])?(\d*)(\.(\d*))?$/', $number, $matches ) )
        {
            throw new \
InvalidArgumentException('NOT_VALID_NUMBER');
        }
       
       
/* Set properties */
       
if ( isset( $matches[1] ) and $matches[1] === '-' )
        {
           
$this->positive = FALSE;
        }        
       
$this->beforeDecimalPoint = isset( $matches[2] ) ? intval( $matches[2] ) : 0;
       
$this->numberOfDecimalPlaces = isset( $matches[4] ) ? mb_strlen( $matches[4] ) : 0;
       
$this->afterDecimalPoint = isset( $matches[4] ) ? intval( $matches[4] ) : 0;
       
       
/* If we have x.y00, simplify to x.y */
       
$this->_simplifyDecimalPlaces();
    }
   
   
/**
     * Number is positive?
     *
     * @return    bool
     */
   
public function isPositive()
    {    
        return
$this->positive;
    }
   
   
/**
     * Number is zero?
     *
     * @return    bool
     */
   
public function isZero()
    {
        return ( !
$this->beforeDecimalPoint and !$this->afterDecimalPoint );
    }
   
   
/**
     * Number is greater than zero?
     *
     * @return    bool
     */
   
public function isGreaterThanZero()
    {
        return
$this->isPositive() and ( $this->beforeDecimalPoint or $this->afterDecimalPoint );
    }
   
   
/* !Basic Math */
   
    /**
     * Add
     *
     * @param    \IPS\Math\Number    $number    Number to add
     * @return    \IPS\Math\Number
     */
   
public function add( Number $number )
    {            
       
/* This method handles adding two positive numbers, if either (or both) are negative, pass off to subtract() as appropriate */
       
if ( !$this->positive )
        {
            if ( !
$number->positive )
            {
               
/* Both numbers are negative, return -(abs($x)+abs($y)) */
               
return $this->multiply( new Number( '-1' ) )->add( $number->multiply( new Number( '-1' ) ) )->multiply( new Number( '-1' ) );
            }
            else
            {
               
/* This number is negative, but the number we're adding is positive, return $y-abs($x) */
               
return $number->subtract( $this->multiply( new Number( '-1' ) ) );
            }
        }
        elseif ( !
$number->positive )
        {
           
/* This number is positive, but the number we're adding is negative, return $x-abs($y) */
           
return $this->subtract( $number->multiply( new Number( '-1' ) ) );
        }
       
       
/* Work out which number has more numbers are the decimal */
       
$biggerDecimalPlaces = max( array( $this->numberOfDecimalPlaces, $number->numberOfDecimalPlaces ) );
       
       
/* If these are two integers, just do it natively */
       
if ( !$biggerDecimalPlaces )
        {
            return new
Number( (string) ( $this->beforeDecimalPoint + $number->beforeDecimalPoint ) );
        }
       
       
/* Otherwise, add the two absolute values together */
       
$paddedThis = intval( "{$this->beforeDecimalPoint}" . str_pad( str_pad( $this->afterDecimalPoint, $this->numberOfDecimalPlaces, '0', STR_PAD_LEFT ), $biggerDecimalPlaces, '0' ) );
       
$paddedNumber = intval( "{$number->beforeDecimalPoint}" . str_pad( str_pad( $number->afterDecimalPoint, $number->numberOfDecimalPlaces, '0', STR_PAD_LEFT ), $biggerDecimalPlaces, '0' ) );
       
$absoluteResult = $paddedThis + $paddedNumber;

       
/* ... and then add the decimal back in the correct place */
       
$return = new Number( mb_substr( $absoluteResult, 0, -$biggerDecimalPlaces ) . '.' . str_pad( mb_substr( $absoluteResult, -$biggerDecimalPlaces ), $biggerDecimalPlaces, '0', STR_PAD_LEFT ) );
       
       
/* Simplify and return */
       
$return->_simplifyDecimalPlaces();
        return
$return;
    }
   
   
/**
     * Subtract
     *
     * @param    \IPS\Math\Number    $number    Number to subtract
     * @return    \IPS\Math\Number
     */
   
public function subtract( $number )
    {
       
/* This method handles subtracting two positive numbers, if either (or both) are negative, pass off to add() as appropriate */
       
if ( !$this->positive )
        {
            if ( !
$number->positive )
            {
               
/* Both numbers are negative, return abs(abs($x)-abs($y)) */
               
return $this->multiply( new Number( '-1' ) )->subtract( $number->multiply( new Number( '-1' ) ) )->multiply( new Number( '-1' ) );
            }
            else
            {
               
/* This number is negative, but the number we're adding is positive, return -$y+abs($x) */
               
return $number->add( $this->multiply( new Number( '-1' ) ) )->multiply( new Number( '-1' ) );
            }
        }
        elseif ( !
$number->positive )
        {
           
/* This number is positive, but the number we're adding is negative, return $x+abs($y) */
           
return $this->add( $number->multiply( new Number( '-1' ) ) );
        }
       
       
/* Work out which number has more numbers are the decimal */
       
$biggerDecimalPlaces = max( array( $this->numberOfDecimalPlaces, $number->numberOfDecimalPlaces ) );
               
       
/* If these are two integers, just do it natively */
       
if ( !$biggerDecimalPlaces )
        {
            return new
Number( (string) ( $this->beforeDecimalPoint - $number->beforeDecimalPoint ) );
        }
       
       
/* Otherwise, add the two absolute values together */
       
$paddedThis = intval( "{$this->beforeDecimalPoint}" . str_pad( str_pad( $this->afterDecimalPoint, $this->numberOfDecimalPlaces, '0', STR_PAD_LEFT ), $biggerDecimalPlaces, '0' ) );
       
$paddedNumber = intval( "{$number->beforeDecimalPoint}" . str_pad( str_pad( $number->afterDecimalPoint, $number->numberOfDecimalPlaces, '0', STR_PAD_LEFT ), $biggerDecimalPlaces, '0' ) );
       
$absoluteResult = $paddedThis - $paddedNumber;
               
       
/* ... and then add the decimal back in the correct place */
       
if ( $absoluteResult < 0 )
        {
           
$absoluteResult *= -1;
           
$return = new Number( '-' . mb_substr( $absoluteResult, 0, -$biggerDecimalPlaces ) . '.' . str_pad( mb_substr( $absoluteResult, -$biggerDecimalPlaces ), $biggerDecimalPlaces, '0', STR_PAD_LEFT ) );
        }
        else
        {
           
$return = new Number( mb_substr( $absoluteResult, 0, -$biggerDecimalPlaces ) . '.' . str_pad( mb_substr( $absoluteResult, -$biggerDecimalPlaces ), $biggerDecimalPlaces, '0', STR_PAD_LEFT ) );
        }
       
       
/* Simplify and return */
       
$return->_simplifyDecimalPlaces();
        return
$return;
    }
   
   
/**
     * Multiple
     *
     * @param    \IPS\Math\Number    $number    Number to multiply by
     * @return    \IPS\Math\Number
     */
   
public function multiply( $number )
    {
       
/* Multiply the absolute numbers */
       
$absoluteResult = intval( "{$this->beforeDecimalPoint}" . ( $this->afterDecimalPoint ? str_pad( $this->afterDecimalPoint, $this->numberOfDecimalPlaces, '0', STR_PAD_LEFT ) : '' ) ) * intval( "{$number->beforeDecimalPoint}" . ( $number->afterDecimalPoint ? str_pad( $number->afterDecimalPoint, $number->numberOfDecimalPlaces, '0', STR_PAD_LEFT ) : '' ) );

       
/* Work out where the decimal point goes */
       
$numberOfDecimalPlaces = $this->numberOfDecimalPlaces + $number->numberOfDecimalPlaces;
       
$absoluteResult = str_pad( $absoluteResult, $numberOfDecimalPlaces, '0', STR_PAD_LEFT );

       
$result = $numberOfDecimalPlaces ? ( mb_substr( $absoluteResult, 0, -$numberOfDecimalPlaces ) . '.' . mb_substr( $absoluteResult, mb_strlen( $absoluteResult ) - $numberOfDecimalPlaces ) ) : "{$absoluteResult}";
           
       
/* Work out if the result is positive or negative */
       
$positiveOrNegative = '';
        if (
$this->positive xor $number->positive )
        {
           
$positiveOrNegative = '-';
        }
       
       
/* Return */
       
return new Number( "{$positiveOrNegative}{$result}" );
    }
   
   
/**
     * @brief    Truncates the result and does not round
     */
   
const ROUND_TRUNCATE = 0;
   
   
/**
     * @brief    Uses normal rules for rounding (>=0.5 rounds up, <0.5 rounds down)
     */
   
const ROUND_NORMAL = 1;
   
   
/**
     * @brief    Rounds any decimal up
     */
   
const ROUND_UP = 2;
   
   
/**
     * Divide
     *
     * @param    \IPS\Math\Number    $number            Number to divide by
     * @param    int                    $precision        The precision to work to
     * @param    int|NULL            $round            Handles how to round the result. See ROUND_* constants
     * @return    \IPS\Math\Number
     * @throws    \RuntimeException
     */
   
public function divide( $number, $precision = 3, $round = \IPS\Math\Number::ROUND_TRUNCATE )
    {
       
/* Can't divide by 0 */
       
if ( $number->compare( new static('0') ) === 0 )
        {
            throw new \
RuntimeException('DIVIDE_BY_ZERO');
        }
       
       
/* Divide by 1 does nothing */
       
if ( $number->compare( new static('1') ) === 0 )
        {
            return clone
$this;
        }
       
       
/* If we want to round, go up a precision */
       
if ( $round !== static::ROUND_TRUNCATE )
        {
           
$precision++;
        }
       
       
/* Work out the remainder from ( $x * ( 10 ^ $precision ) / $number ) */
       
$result = 0;
       
$this->multiply( new Number( (string) pow( 10, $precision ) ) )->modulus( $number, $result );
       
       
/* Round it */
       
if ( $round !== static::ROUND_TRUNCATE )
        {
           
$lastDigit = intval( mb_substr( $result, -1 ) );
           
$result = mb_substr( $result, 0, -1 );

            if (
$lastDigit !== 0 )
            {
                if (
$round === static::ROUND_NORMAL )
                {
                    if (
$lastDigit >= 5 )
                    {
                       
$result++;
                    }
                    else
                    {
                       
$result--;
                    }
                }
                elseif (
$round === static::ROUND_UP )
                {
                   
$result++;
                }
            }
           
           
$precision--;
        }
       
       
/* Work out if the result is positive or negative */
       
$positiveOrNegative = '';
        if (
$this->positive xor $number->positive )
        {
           
$positiveOrNegative = '-';
        }
       
       
/* Put the decimal back in the correct place */
       
$result = str_pad( $result, $precision, '0', STR_PAD_LEFT );
       
$return = new Number( $positiveOrNegative . mb_substr( $result, 0, -$precision ) . '.' . mb_substr( $result, -$precision ) );
       
       
/* Simplify and return */
       
$return->_simplifyDecimalPlaces();
        return
$return;
    }
   
   
/**
     * Modulus
     *
     * @param    \IPS\Math\Number    $number            Number to get modulus of
     * @param    int                    $precision        
     * @return    \IPS\Math\Number
     */
   
public function modulus( $number, &$divides = 0 )
    {
       
/* Start with positiver numbers */
       
$remainder = clone $this;
        if ( !
$remainder->positive )
        {
           
$remainder = $remainder->multiply( new Number('-1') );
        }
       
$negative = FALSE;
        if ( !
$number->positive )
        {
           
$negative = TRUE;
           
$number = $number->multiply( new Number('-1') );
        }
       
       
/* While the number is greater than this number... */
       
while ( $remainder->compare( $number ) === 1 )
        {
           
/* Add one to the amount we can divide by */
           
$divides++;
           
           
/* And then subtract the number from what's remaining */
           
$remainder = $remainder->subtract( $number );
        }
       
       
/* If the number is now equal to this number, add one more one and specify the remainder as 0 */
       
if ( $remainder->compare( $number ) === 0 )
        {
           
$divides++;
           
$remainder = new Number('0');
        }
       
       
/* Return */
       
return $negative ? $remainder->multiply( new Number('-1') ) : $remainder;
    }
   
   
/**
     * Compare
     *
     * @param    \IPS\Math\Number    $number            Number to divide by
     * @return    int                    Returns 0 if the two numbers are equal, 1 if this number is larger than the $number, -1 otherwise
     */
   
public function compare( $number )
    {
        list(
$number1, $number2 ) = static::_normaliseTwoNumbers( $this, $number );
       
        if (
$number1->positive and !$number2->positive )
        {
            return
1;
        }
        elseif ( !
$number1->positive and $number2->positive )
        {
            return -
1;
        }
       
        if (
$number1->beforeDecimalPoint === $number2->beforeDecimalPoint )
        {
            if (
$number1->afterDecimalPoint === $number2->afterDecimalPoint )
            {
                return
0;
            }
            elseif (
$number1->afterDecimalPoint > $number2->afterDecimalPoint )
            {
                return
1;
            }
            else
            {
                return -
1;
            }
        }
        elseif (
$number1->beforeDecimalPoint > $number2->beforeDecimalPoint )
        {
            return
1;
        }
        else
        {
            return -
1;
        }
    }
   
   
/**
     * Round to a number of decimal places
     *
     * @param    int        $decimalPlaces    The number of decimal places to round to
     * @param    int        $round            Handles how to round. See ROUND_* constants
     * @return    \IPS\Math\Number
     */
   
public function round( $decimalPlaces, $round = \IPS\Math\Number::ROUND_NORMAL )
    {        
       
/* If it's already precise enough, we don't need to do anything */
       
if ( $decimalPlaces >= $this->numberOfDecimalPlaces )
        {
            return clone
$this;
        }
       
       
/* If we don't know if we're going up or down, figure that out */
       
if ( $round === static::ROUND_NORMAL )
        {    
           
$numberToExamine = intval( \substr( str_pad( $this->afterDecimalPoint, $this->numberOfDecimalPlaces, '0', STR_PAD_LEFT ), $decimalPlaces, 1 ) );
            if (
$numberToExamine >= 5 )
            {
               
$round = static::ROUND_UP;
            }
            else
            {
               
$round = static::ROUND_TRUNCATE;
            }
        }    

       
/* Start with a number without the decimal point, truncated where we need it */
       
$abs = ( $this->positive === FALSE ? "-" : '' ) . "{$this->beforeDecimalPoint}" . str_pad( \substr( str_pad( $this->afterDecimalPoint, $this->numberOfDecimalPlaces, '0', STR_PAD_LEFT ), 0, $decimalPlaces ), $decimalPlaces, '0', STR_PAD_LEFT );
       
       
/* If we're going up, add one on */
       
if ( $round === static::ROUND_UP )
        {
            if (
$this->positive )
            {
               
$abs = intval( $abs ) + 1;
            }
            else
            {
               
$abs = intval( $abs ) - 1;
            }
        }
                       
       
/* Put the decimal back */
       
if ( $decimalPlaces )
        {
            return new static( \
substr( $abs, 0, -$decimalPlaces ) . '.' . str_pad( \substr( $abs, -$decimalPlaces ), $decimalPlaces, '0', STR_PAD_LEFT ) );
        }
        else
        {
            return new static(
"{$abs}" );
        }
    }
   
   
/**
     * Calculate percentage
     *
     * @param    int|\IPS\Math\Number    $percentage    The percentage
     * @return    \IPS\Math\Number
     */
   
public function percentage( $percentage )
    {
        if ( !(
$percentage instanceof static ) )
        {
           
$percentage = new static( "{$percentage}" );
        }
       
        return
$this->multiply( $percentage->divide( new static( '100' ) ) );
    }
   
   
/* !Array math */
   
    /**
     * Get sum of numbers
     *
     * @param    array    $numbers    array of \IPS\Math\Number objects
     * @return    \IPS\Math\Number
     */
   
public static function sum( array $numbers )
    {
       
$result = new static('0');
        foreach (
$numbers as $number )
        {
           
$result = $result->add( $number );
        }
        return
$result;
    }
   
   
/**
     * Get product of numbers
     *
     * @param    array    $numbers    array of \IPS\Math\Number objects
     * @return    \IPS\Math\Number
     */
   
public static function product( array $numbers )
    {
       
$result = new static('0');
        foreach (
$numbers as $number )
        {
           
$result = $result->multiply( $number );
        }
        return
$result;
    }
   
   
/* !Utility Methods */
   
    /**
     * Normalise two numbers
     * Makes the number of decimal places for both numbers equal so that math can be done on them
     *
     * @param    \IPS\Math\Number    $number1    Number 1
     * @param    \IPS\Math\Number    $number2    Number 2
     * @return    array
     */
   
protected static function _normaliseTwoNumbers( Number $number1, Number $number2 )
    {
       
$number1 = clone $number1;
       
$number2 = clone $number2;
               
        if (
$number1->numberOfDecimalPlaces != $number2->numberOfDecimalPlaces )
        {
            if (
$number1->numberOfDecimalPlaces > $number2->numberOfDecimalPlaces )
            {
               
$number2->_normaliseDecimalPlaces( $number1->numberOfDecimalPlaces );
            }
            else
            {
               
$number1->_normaliseDecimalPlaces( $number2->numberOfDecimalPlaces );
            }
        }
       
        return array(
$number1, $number2 );
    }
   
   
/**
     * Makes the number of decimal places a specific number
     *
     * @param    int    $to    The number of decimal places
     * @return    void
     */
   
protected function _normaliseDecimalPlaces( $to )
    {
       
$this->afterDecimalPoint *= ( ( $to - $this->numberOfDecimalPlaces ) * 10 );
       
$this->numberOfDecimalPlaces = $to;
    }
   
   
/**
     * Simplifies the number of decimal places to the lowest number necessary
     *
     * @return    void
     */
   
protected function _simplifyDecimalPlaces()
    {
        if (
$this->afterDecimalPoint === 0 )
        {
           
$this->numberOfDecimalPlaces = 0;
           
            if (
$this->beforeDecimalPoint === 0 )
            {
               
$this->positive = TRUE;
            }
            return;
        }
       
        while ( (
$this->afterDecimalPoint % 10 ) === 0 )
        {
           
$this->numberOfDecimalPlaces--;
           
$this->afterDecimalPoint /= 10;
        }
    }
   
   
/* !Output */
   
    /**
     * Convert to string
     * Not locale aware - uses '.' for decimal points and no thousand separator
     *
     * @return    string
     */
   
public function __toString()
    {
        return (
$this->positive ? '' : '-' ) . $this->beforeDecimalPoint . ( $this->numberOfDecimalPlaces ? ( '.' . str_pad( $this->afterDecimalPoint, $this->numberOfDecimalPlaces, '0', STR_PAD_LEFT ) ) : '' );
    }
   
   
/**
     * Value for json_encode
     *
     * @return    string
     */
   
public function jsonSerialize()
    {
        return (string)
$this;
    }
}