Completed
Pull Request — master (#9)
by Gabriel
02:39
created

Euro::assertMaximumValueNotExceeded()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare( strict_types = 1 );
4
5
namespace WMDE\Euro;
6
7
use InvalidArgumentException;
8
9
/**
10
 * @licence GNU GPL v2+
11
 * @author Jeroen De Dauw < [email protected] >
12
 */
13
final class Euro {
14
15
	private const DECIMAL_COUNT = 2;
16
	private const CENTS_PER_EURO = 100;
17
18
	private $cents;
19
20
	/**
21
	 * @param int $cents
22
	 * @throws InvalidArgumentException
23
	 */
24 54
	private function __construct( int $cents ) {
25 54
		if ( $cents < 0 ) {
26 5
			throw new InvalidArgumentException( 'Amount needs to be positive' );
27
		}
28
29 49
		$this->cents = $cents;
30 49
	}
31
32
	/**
33
	 * @param int $cents
34
	 * @return self
35
	 * @throws InvalidArgumentException
36
	 */
37 30
	public static function newFromCents( int $cents ): self {
38 30
		return new self( $cents );
39
	}
40
41
	/**
42
	 * Constructs a Euro object from a string representation such as "13.37".
43
	 *
44
	 * This method takes into account the errors that can arise from floating
45
	 * point number usage. Amounts with too many decimals are rounded to the
46
	 * nearest whole euro cent amount.
47
	 *
48
	 * @param string $euroAmount
49
	 * @return self
50
	 * @throws InvalidArgumentException
51
	 */
52 19
	public static function newFromString( string $euroAmount ): self {
53 19
		if ( !is_numeric( $euroAmount ) ) {
54 3
			throw new InvalidArgumentException( 'Not a number' );
55
		}
56
57 16
		if ( self::stringIsTooLong( $euroAmount ) ) {
58 3
			throw new InvalidArgumentException( 'Number is too big' );
59
		}
60
61 13
		$parts = explode( '.', $euroAmount, 2 );
62
63 13
		$euros = (int)$parts[0];
64 13
		$cents = self::centsFromString( $parts[1] ?? '0' );
65
66 13
		return new self( $euros * self::CENTS_PER_EURO + $cents );
67
	}
68
69 16
	private static function stringIsTooLong( string $euroString ): bool {
70 16
		return strlen( $euroString ) + 2 > log10( PHP_INT_MAX );
71
	}
72
73 13
	private static function centsFromString( string $cents ): int {
74 13
		if ( strlen( $cents ) > self::DECIMAL_COUNT ) {
75 2
			return self::roundCentsToInt( $cents );
76
		}
77
78
		// Turn .1 into .10, so it ends up as 10 cents
79 11
		return (int)str_pad( $cents, self::DECIMAL_COUNT, '0' );
80
	}
81
82 2
	private static function roundCentsToInt( string $cents ): int {
83 2
		$centsInt = (int)substr( $cents, 0, self::DECIMAL_COUNT );
84
85 2
		if ( (int)$cents[self::DECIMAL_COUNT] >= 5 ) {
86 1
			$centsInt++;
87
		}
88
89 2
		return $centsInt;
90
	}
91
92
	/**
93
	 * This method takes into account the errors that can arise from floating
94
	 * point number usage. Amounts with too many decimals are rounded to the
95
	 * nearest whole euro cent amount.
96
	 *
97
	 * @param float $euroAmount
98
	 * @return self
99
	 * @throws InvalidArgumentException
100
	 */
101 9
	public static function newFromFloat( float $euroAmount ): self {
102 9
		self::assertMaximumValueNotExceeded( $euroAmount );
103 6
		return new self( intval(
104 6
			round(
105 6
				round( $euroAmount, self::DECIMAL_COUNT ) * self::CENTS_PER_EURO,
106
				0
107
			)
108
		) );
109
	}
110
111
	/**
112
	 * @param int|float $euroAmount
113
	 */
114 18
	private static function assertMaximumValueNotExceeded( $euroAmount ): void {
115
		// When $euroAmount == PHP_INT_MAX / self::CENTS_PER_EURO, multiplying it
116
		// by self::CENTS_PER_EURO still exceeds PHP_INT_MAX, which leads type errors
117
		// due to float conversion. The safest thing to do here is using a safety
118
		// margin of 1 with self::CENTS_PER_EURO
119 18
		if ( $euroAmount > floor( PHP_INT_MAX / ( self::CENTS_PER_EURO + 1 ) ) ) {
120 6
			throw new InvalidArgumentException( 'Number is too big' );
121
		}
122 12
	}
123
124
	/**
125
	 * @param int $euroAmount
126
	 * @return self
127
	 * @throws InvalidArgumentException
128
	 */
129 9
	public static function newFromInt( int $euroAmount ): self {
130 9
		self::assertMaximumValueNotExceeded( $euroAmount );
131 6
		return new self( $euroAmount * self::CENTS_PER_EURO );
132
	}
133
134 33
	public function getEuroCents(): int {
135 33
		return $this->cents;
136
	}
137
138 10
	public function getEuroFloat(): float {
139 10
		return $this->cents / self::CENTS_PER_EURO;
140
	}
141
142
	/**
143
	 * Returns the euro amount as string with two decimals always present in format "42.00".
144
	 */
145 6
	public function getEuroString(): string {
146 6
		return number_format( $this->getEuroFloat(), self::DECIMAL_COUNT, '.', '' );
147
	}
148
149 10
	public function equals( Euro $euro ): bool {
150 10
		return $this->cents === $euro->cents;
151
	}
152
153
}
154