Money.php 13.4 KB
Newer Older
Mathias Verraes's avatar
Mathias Verraes committed
1
2
<?php
/**
Mathias Verraes's avatar
Mathias Verraes committed
3
 * This file is part of the Money library
Mathias Verraes's avatar
Mathias Verraes committed
4
 *
Mathias Verraes's avatar
Mathias Verraes committed
5
 * Copyright (c) 2011-2013 Mathias Verraes
Mathias Verraes's avatar
Mathias Verraes committed
6
7
8
9
10
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

Mathias Verraes's avatar
Mathias Verraes committed
11
namespace Money;
Mathias Verraes's avatar
Mathias Verraes committed
12

13
class Money
Mathias Verraes's avatar
Mathias Verraes committed
14
{
15
16
17
18
19
    const ROUND_HALF_UP = PHP_ROUND_HALF_UP;
    const ROUND_HALF_DOWN = PHP_ROUND_HALF_DOWN;
    const ROUND_HALF_EVEN = PHP_ROUND_HALF_EVEN;
    const ROUND_HALF_ODD = PHP_ROUND_HALF_ODD;

20
21
22
	/** @var Money[] */
	static $instances = array();

23
24
25
    /**
     * @var int
     */
Mathias Verraes's avatar
Mathias Verraes committed
26
    private $amount;
27

Mathias Verraes's avatar
Mathias Verraes committed
28
    /** @var \Money\Currency */
29
30
31
32
    private $currency;

    /**
     * Create a Money instance
Mathias Verraes's avatar
Mathias Verraes committed
33
     * @param  integer $amount    Amount, expressed in the smallest units of $currency (eg cents)
Thorsten Buss's avatar
Thorsten Buss committed
34
     * @param  \Money\Currency|string $currency as Obj or isoString
35
     * @param bool $parseAmountAsMoneyString amount ist unit (not subuit) and in moneyformat (maybe , as dec_mark)
Mathias Verraes's avatar
Mathias Verraes committed
36
     * @throws \Money\InvalidArgumentException
37
	 */
38
	public function __construct($amount, $currency=null, $parseAmountAsMoneyString=false)
39
    {
40
        if (!$parseAmountAsMoneyString && !is_int($amount) && (strval($amount) != strval(intval($amount)))) { // only numbers(int) - as string or int type
Mathias Verraes's avatar
Mathias Verraes committed
41
            throw new InvalidArgumentException("The first parameter of Money must be an integer. It's the amount, expressed in the smallest units of currency (eg cents)");
42
        }
43
44
	    $this->currency = Currency::getInstance($currency);
	    $this->setAmount($amount, $parseAmountAsMoneyString);
45
46
    }

47
    /**
48
	 * return new instance of MoneyObj
49
50
	 * @param null $currency
	 * @param int $amount
51
52
53
54
55
56
57
58
59
60
61
     * @param bool $parseAmountAsMoneyString amount ist unit (not subuit) and in moneyformat (maybe , as dec_mark)
     * @return Money
	 */
	public static function newInstance($currency=null, $amount=0, $parseAmountAsMoneyString = false) {
        return new static($amount, $currency, $parseAmountAsMoneyString);
	}

    /**
	 * return saved instance of a MoneyObj with given currency - for single use to reduce memory usage in loops
     * WARNING: NOT SAFE FOR EXTERNAL USAGE - ONLY INTERN OBJ CACHE
	 * @param null $currency
62
63
	 * @return Money
	 */
64
	public static function getInstance($currency=null) {
65
66
67
		// get isocode from currency, direct or default
		$iso_code = $currency instanceof Currency ? $currency->getIsostring() : ($currency ? : Currency::getDefaultCurrency());
		if (!array_key_exists($iso_code, static::$instances)) {
68
69
70
			static::$instances[$iso_code] = new static(0, $currency);
		}
		return static::$instances[$iso_code];
71
72
	}

73
    /**
74
75
     * Convenience factory method for a Money object
     * @example $fiveDollar = Money::USD(500);
Mathias Verraes's avatar
Mathias Verraes committed
76
77
78
     * @param string $method
     * @param array $arguments
     * @return \Money\Money
79
     */
80
    public static function __callStatic($method, $arguments)
81
    {
82
        return new Money($arguments[0], new Currency($method));
83
84
    }

85
86
87
	/**
	 * change the amount
	 * @param int $amount amount in subunit
88
89
	 * @param bool $parseAmountAsMoneyString amount ist unit (not subuit) and in moneyformat (maybe , as dec_mark)
	 * @return $this
90
	 */
91
92
	public function setAmount($amount, $parseAmountAsMoneyString = false) {
		$this->amount = $parseAmountAsMoneyString ? self::parseMoneyString($amount, $this->getCurrency()) : $amount;
93
94
95
		return $this;
    }

Mathias Verraes's avatar
Mathias Verraes committed
96
97
98
99
    /**
     * @param \Money\Money $other
     * @return bool
     */
100
101
102
103
104
105
    public function isSameCurrency(Money $other)
    {
        return $this->currency->equals($other->currency);
    }

    /**
Mathias Verraes's avatar
Mathias Verraes committed
106
     * @throws \Money\InvalidArgumentException
107
108
109
110
111
112
113
114
     */
    private function assertSameCurrency(Money $other)
    {
        if (!$this->isSameCurrency($other)) {
            throw new InvalidArgumentException('Different currencies');
        }
    }

Mathias Verraes's avatar
Mathias Verraes committed
115
116
117
118
    /**
     * @param \Money\Money $other
     * @return bool
     */
119
120
121
122
    public function equals(Money $other)
    {
        return
            $this->isSameCurrency($other)
Mathias Verraes's avatar
Mathias Verraes committed
123
            && $this->amount == $other->amount;
124
125
    }

Mathias Verraes's avatar
Mathias Verraes committed
126
127
128
129
    /**
     * @param \Money\Money $other
     * @return int
     */
130
131
132
    public function compare(Money $other)
    {
        $this->assertSameCurrency($other);
Mathias Verraes's avatar
Mathias Verraes committed
133
        if ($this->amount < $other->amount) {
134
            return -1;
Mathias Verraes's avatar
Mathias Verraes committed
135
        } elseif ($this->amount == $other->amount) {
136
137
138
139
140
141
            return 0;
        } else {
            return 1;
        }
    }

Mathias Verraes's avatar
Mathias Verraes committed
142
143
144
145
    /**
     * @param \Money\Money $other
     * @return bool
     */
146
147
148
149
150
    public function greaterThan(Money $other)
    {
        return 1 == $this->compare($other);
    }

Mathias Verraes's avatar
Mathias Verraes committed
151
152
153
154
    /**
     * @param \Money\Money $other
     * @return bool
     */
155
156
157
158
159
160
    public function lessThan(Money $other)
    {
        return -1 == $this->compare($other);
    }

    /**
Mathias Verraes's avatar
Mathias Verraes committed
161
     * @deprecated Use getAmount() instead
162
163
164
165
     * @return int
     */
    public function getUnits()
    {
Mathias Verraes's avatar
Mathias Verraes committed
166
167
168
        return $this->amount;
    }

Thorsten Buss's avatar
Thorsten Buss committed
169
170
171
172
173
174
175
176
	/**
	 * @param bool $asUnit - default as (int)cent for internals, with true for exernals(string) (formfields etc) as $, EUR etc.
	 * if subunit == 0 -> strip decimal places
	 * @param array $params
	 *  force_decimal -> add ,00 if cents are empty ("0"-quantitiy from decimalPlaces)
	 * @return int|string
	 */
	public function getAmount($asUnit=false, $params=array())
Mathias Verraes's avatar
Mathias Verraes committed
177
    {
Thorsten Buss's avatar
Thorsten Buss committed
178
179
180
181
182
183
184
185
186
	    if (!$asUnit)
		    return $this->amount;

	    // without decimal if == 0 if params dont force
	    $subunit = (string)floor($this->amount) % $this->currency->getSubunitToUnit();
	    if (($subunit == 0 && (!isset($params['force_decimal']) || !$params['force_decimal'])) || $this->currency->getDecimalPlaces() == 0)
		    return (string)floor($this->amount) / $this->currency->getSubunitToUnit();

	    return (string)number_format($this->amount / $this->currency->getSubunitToUnit(), $this->currency->getDecimalPlaces(), $this->currency->getDecimalMark(), '');
187
188
189
    }

    /**
Mathias Verraes's avatar
Mathias Verraes committed
190
     * @return \Money\Currency
191
192
193
194
195
196
     */
    public function getCurrency()
    {
        return $this->currency;
    }

Mathias Verraes's avatar
Mathias Verraes committed
197
198
199
200
    /**
     * @param \Money\Money $addend
     *@return \Money\Money 
     */
201
202
203
204
    public function add(Money $addend)
    {
        $this->assertSameCurrency($addend);

Mathias Verraes's avatar
Mathias Verraes committed
205
        return new self($this->amount + $addend->amount, $this->currency);
206
207
    }

Mathias Verraes's avatar
Mathias Verraes committed
208
209
210
211
    /**
     * @param \Money\Money $subtrahend
     * @return \Money\Money
     */
212
213
214
215
    public function subtract(Money $subtrahend)
    {
        $this->assertSameCurrency($subtrahend);

Mathias Verraes's avatar
Mathias Verraes committed
216
        return new self($this->amount - $subtrahend->amount, $this->currency);
217
218
219
    }

    /**
Mathias Verraes's avatar
Mathias Verraes committed
220
     * @throws \Money\InvalidArgumentException
221
222
223
224
225
226
227
228
229
     */
    private function assertOperand($operand)
    {
        if (!is_int($operand) && !is_float($operand)) {
            throw new InvalidArgumentException('Operand should be an integer or a float');
        }
    }

    /**
Mathias Verraes's avatar
Mathias Verraes committed
230
     * @throws \Money\InvalidArgumentException
231
232
233
234
235
236
237
238
     */
    private function assertRoundingMode($rounding_mode)
    {
        if (!in_array($rounding_mode, array(self::ROUND_HALF_DOWN, self::ROUND_HALF_EVEN, self::ROUND_HALF_ODD, self::ROUND_HALF_UP))) {
            throw new InvalidArgumentException('Rounding mode should be Money::ROUND_HALF_DOWN | Money::ROUND_HALF_EVEN | Money::ROUND_HALF_ODD | Money::ROUND_HALF_UP');
        }
    }

Mathias Verraes's avatar
Mathias Verraes committed
239
240
241
242
243
    /**
     * @param $multiplier
     * @param int $rounding_mode
     * @return \Money\Money
     */
244
245
246
247
248
    public function multiply($multiplier, $rounding_mode = self::ROUND_HALF_UP)
    {
        $this->assertOperand($multiplier);
        $this->assertRoundingMode($rounding_mode);

Mathias Verraes's avatar
Mathias Verraes committed
249
        $product = (int) round($this->amount * $multiplier, 0, $rounding_mode);
250
251
252
253

        return new Money($product, $this->currency);
    }

Mathias Verraes's avatar
Mathias Verraes committed
254
255
256
257
258
    /**
     * @param $divisor
     * @param int $rounding_mode
     * @return \Money\Money
     */
259
260
261
262
263
    public function divide($divisor, $rounding_mode = self::ROUND_HALF_UP)
    {
        $this->assertOperand($divisor);
        $this->assertRoundingMode($rounding_mode);

Mathias Verraes's avatar
Mathias Verraes committed
264
        $quotient = (int) round($this->amount / $divisor, 0, $rounding_mode);
265
266
267
268
269
270
271

        return new Money($quotient, $this->currency);
    }

    /**
     * Allocate the money according to a list of ratio's
     * @param array $ratios List of ratio's
Mathias Verraes's avatar
Mathias Verraes committed
272
     * @return \Money\Money
273
274
275
     */
    public function allocate(array $ratios)
    {
Mathias Verraes's avatar
Mathias Verraes committed
276
        $remainder = $this->amount;
277
278
279
280
        $results = array();
        $total = array_sum($ratios);

        foreach ($ratios as $ratio) {
Mathias Verraes's avatar
Mathias Verraes committed
281
            $share = (int) floor($this->amount * $ratio / $total);
282
283
284
285
            $results[] = new Money($share, $this->currency);
            $remainder -= $share;
        }
        for ($i = 0; $remainder > 0; $i++) {
Mathias Verraes's avatar
Mathias Verraes committed
286
            $results[$i]->amount++;
287
288
289
290
291
292
293
294
295
            $remainder--;
        }

        return $results;
    }

    /** @return bool */
    public function isZero()
    {
Mathias Verraes's avatar
Mathias Verraes committed
296
        return $this->amount === 0;
297
298
299
300
301
    }

    /** @return bool */
    public function isPositive()
    {
Mathias Verraes's avatar
Mathias Verraes committed
302
        return $this->amount > 0;
303
304
305
306
307
    }

    /** @return bool */
    public function isNegative()
    {
Mathias Verraes's avatar
Mathias Verraes committed
308
        return $this->amount < 0;
309
310
    }

311
312
313
314
315
316
	/**
	 * @see Money::parseMoneyString()
	 */
	public static function stringToUnits($string, $currency=null) {
		return self::parseMoneyString($string, $currency);
	}
Thorsten Buss's avatar
Thorsten Buss committed
317
318

	/**
319
320
321
322
323
	 * parse moneyformated string and returns amount in subunit
	 * @param $string
	 * @param null $currency
	 * @return int subunit from Money-string
	 * @throws InvalidArgumentException
Thorsten Buss's avatar
Thorsten Buss committed
324
	 */
325
326
327
328
329
330
331
332
	public static function parseMoneyString($string, $currency = null) {
		$currency = Currency::getInstance($currency);
		$t = str_replace('.', '\.', $currency->getThousandsSeparator());
		$d = str_replace('.', '\.', $currency->getDecimalMark());
		if (!preg_match("/^([-+])?(?:0|([1-9]\d{0,2})(?:$t?(\d{3}))*)(?:$d(\d+))?$/", $string, $matches)) {
			throw new InvalidArgumentException(sprintf('The string "%s" could not be parsed as money', $string));
		}
		$units = (float)(@$matches[1] . @$matches[2] . @$matches[3] . '.' . @$matches[4]) * 100;
333
		return (int)round($units);
Thorsten Buss's avatar
Thorsten Buss committed
334
335
336
337
338
339
340
341
342
343
344
345
346
	}

	/**
	 * returns the formatted Money Amount
	 * @param array $params
	 *  display_free => shows 'free' if amount == 0
	 *  html => display html formatted
	 *  symbol => user_defined symbol
	 *  no_cents => show no cents
	 *  no_cents_if_zero => show no cents if ==00
	 *  thousands_separator => overwrite the currency thousands_separator
	 *  decimal_mark => overwrite the currency decimal_mark
	 *  with_currency => append currency ISOCODE
347
	 *  no_blank_separator => remove the blank separator between amount and symbol/currency
Thorsten Buss's avatar
Thorsten Buss committed
348
349
350
	 * @return mixed|string
	 */
	public function format($params = array()) {
351
352
		// spearator between amount andsymbol/currency
		$sep = (array_key_exists('no_blank_separator', $params) && $params['no_blank_separator']) ? '' : ' ';
Thorsten Buss's avatar
Thorsten Buss committed
353
354
		// show 'free'
		if ($this->amount === 0) {
355
			if (isset($params['display_free']) && is_string($params['display_free']))
Thorsten Buss's avatar
Thorsten Buss committed
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
				return $params['display_free'];
			elseif (isset($params['display_free']) && $params['display_free'])
				return "free";
		}

		// html symbol
		$symbolValue = $this->currency->getSymbol(isset($params['html']) && $params['html']);

		// userdefined symbol
		if (isset($params['symbol']) && $params['symbol'] !== true) {
			if (!$params['symbol'])
				$symbolValue = '';
			else
				$symbolValue = $params['symbol'];
		}

		// show no_cents
		if (isset($params['no_cents']) && $params['no_cents'] === true) {
			$formatted = (string)floor($this->getAmount(true));
		// no_cents if ==00 -> default behjaviour from getAmount(true)
		} elseif (isset($params['no_cents_if_zero']) && $params['no_cents_if_zero'] === true && $this->amount % $this->currency->getSubunitToUnit() == 0) {
			$formatted = (string)$this->getAmount(true);
		} else {
			$formatted = $this->getAmount(true, array('force_decimal' => true));
		}

		// warp span arrount amount if html
		if (isset($params['html']) && $params['html']) {
			$formatted = '<span class="amount">' . $formatted . '</span>';
		}

		if (isset($params['symbol_position']))
			$symbolPosition = $params['symbol_position'];
		else
			$symbolPosition = $this->currency->getSymbolPosition(true);

		// wrap span arround symbol if html
		if (isset($params['html']) && $params['html']) {
			$symbolValue = '<span class="symbol">' . $symbolValue . '</span>';
		}

		// combine symbol and formatted amount
		if (isset($symbolValue) && !empty($symbolValue)) {
			$formatted = $symbolPosition === 'before'
400
401
					? "$symbolValue$sep$formatted"
					: "$formatted$sep$symbolValue";
Thorsten Buss's avatar
Thorsten Buss committed
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
		}

		if (isset($params['decimal_mark']) && $params['decimal_mark'] && $params['decimal_mark'] !== $this->currency->getDecimalMark()) {
			$tmp = 1; /* Needs to be pass by ref */
			$formatted = str_replace($this->currency->getDecimalMark(), $params['decimal_mark'], $formatted, $tmp);
		}

		$thousandsSeparatorValue = $this->getCurrency()->getThousandsSeparator();
		if (isset($params['thousands_separator'])) {
			if ($params['thousands_separator'] === false || $params['thousands_separator'] === null)
				$thousandsSeparatorValue = '';
			elseif ($params['thousands_separator'])
				$thousandsSeparatorValue = $params['thousands_separator'];
		}

		$formatted = preg_replace('/(\d)(?=(?:\d{3})+(?:[^\d]|$))/', '\1' . $thousandsSeparatorValue, $formatted);

		if (isset($params['with_currency']) && $params['with_currency']) {

			if (isset($params['html']) && $params['html'])
422
				$formatted .= $sep.'<span class="currency">'. $this->currency->__toString(). '</span>';
Thorsten Buss's avatar
Thorsten Buss committed
423
			else
424
				$formatted .= $sep.$this->currency->__toString();
Thorsten Buss's avatar
Thorsten Buss committed
425
426
427
428
429
430
431
432
433
434
435
		}

		return $formatted;
	}


	/**
	 * build string represantiation of the amount without formatting and currency sign
	 * @return string
	 */
	public function __toString() {
436
437
438
439
440
		try {
			return $this->getAmount(true);
		} catch (\Exception $e) {
			return 'ERR';
		}
Thorsten Buss's avatar
Thorsten Buss committed
441
442
	}

443
}