Euro   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 155
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 0

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 21
lcom 2
cbo 0
dl 0
loc 155
ccs 51
cts 51
cp 1
rs 10
c 0
b 0
f 0

15 Methods

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