Skip to content

Commit 8e9cbd1

Browse files
olszamichael-rubel
andauthored
Add float support in Number (#34)
* Number VO takes a float * SanitizesNumbers trait takes a float * give tests for leading float small numbers after decimal point * add new tests for number can change decimals as a string input * check precessions for float or send exception * changed to more specific tests * add test for float big input - draft * WIP - float tests * correct the test title * WIP tests * WIP is_good_float * WIP progress * wip tests * wip sanitize * refactoring test 'number can change decimals as a float input' * add new test to 'number can change decimals as a float input' * refactoring * remove "wip" * remove "wip" - refactoring * corrections * Swapped the range of numbers for the 'decimal object equality works as expected' test * add a new test item in 'decimal object equality works as expected' test - draft * new test - draft * new test * rename tests * wip SanitizesNumbers * add new tests * refactoring SanitizesNumbers * add new 'numeric integer in float' test * refactoring * refactoring * new tests * refactoring * refactoring - fix for PHPstan * refactoring - after pint * refactoring - length() -> str()->length() * refactoring * refactoring + ignore to phpStan * refactoring - strict types float * refactoring in tests * refactoring - rename var * fix * rename method * refactoring * refactoring * add ignore to phpStan * fix test for infection * test number with 15 char * refactoring - sum char * Missing docblock types 📚 * Refactoring 🛠 * Make method `protected` 🛡 Co-authored-by: Michael Rubél <contact@observer.name>
1 parent fa09fa8 commit 8e9cbd1

File tree

5 files changed

+132
-17
lines changed

5 files changed

+132
-17
lines changed

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ parameters:
2121
- '#Method MichaelRubel\\ValueObjects\\Collection\\Complex\\FullName\:\:firstName\(\) should return string but returns string\|null\.#'
2222
- '#Method MichaelRubel\\ValueObjects\\Collection\\Complex\\FullName\:\:lastName\(\) should return string but returns string\|null\.#'
2323
- '#Parameter \#1 \$value of class Illuminate\\Support\\Stringable constructor expects string, int\|string given\.#'
24+
- '#Parameter \#1 \$string of function str expects string\|null, float given\.#'
25+
- '#Parameter \#1 \$string of function str expects string\|null, float\|int\|string\|null given\.#'
2426

2527
checkMissingIterableValueType: false
2628

src/Collection/Primitive/Number.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
* @template TKey of array-key
2626
* @template TValue
2727
*
28-
* @method static static make(int|string $number, int $scale = 2)
29-
* @method static static from(int|string $number, int $scale = 2)
30-
* @method static static makeOrNull(int|string|null $number, int $scale = 2)
28+
* @method static static make(int|string|float $number, int $scale = 2)
29+
* @method static static from(int|string|float $number, int $scale = 2)
30+
* @method static static makeOrNull(int|string|float|null $number, int $scale = 2)
3131
*
3232
* @method string abs()
3333
* @method string add(float|int|string|BigNumber $value)
@@ -54,10 +54,10 @@ class Number extends ValueObject
5454
/**
5555
* Create a new instance of the value object.
5656
*
57-
* @param int|string $number
57+
* @param int|string|float $number
5858
* @param int $scale
5959
*/
60-
public function __construct(int|string $number, protected int $scale = 2)
60+
public function __construct(int|string|float $number, protected int $scale = 2)
6161
{
6262
if (isset($this->bigNumber)) {
6363
throw new InvalidArgumentException(static::IMMUTABLE_MESSAGE);

src/Concerns/SanitizesNumbers.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44

55
namespace MichaelRubel\ValueObjects\Concerns;
66

7+
use LengthException;
8+
79
trait SanitizesNumbers
810
{
911
/**
10-
* @param int|string|null $number
12+
* @param int|string|float|null $number
1113
*
1214
* @return string
1315
*/
14-
protected function sanitize(int|string|null $number): string
16+
protected function sanitize(int|string|float|null $number): string
1517
{
18+
if (is_float($number) && ! $this->isPrecise($number)) {
19+
throw new LengthException('Float precision loss detected.');
20+
}
21+
1622
$number = str($number)->replace(',', '.');
1723

1824
$dots = $number->substrCount('.');
@@ -28,4 +34,30 @@ protected function sanitize(int|string|null $number): string
2834
->replaceMatches('/[^0-9.]/', '')
2935
->toString();
3036
}
37+
38+
/**
39+
* Determine whether the passed floating point number is precise.
40+
*
41+
* @param float $number
42+
*
43+
* @return bool
44+
*/
45+
protected function isPrecise(float $number): bool
46+
{
47+
$numberAsString = str($number);
48+
49+
$afterFloatingPoint = $numberAsString
50+
->explode('.')
51+
->get(1, '');
52+
53+
$precisionPosition = str($afterFloatingPoint)->length();
54+
55+
$roundedNumber = round($number, $precisionPosition);
56+
57+
if ($roundedNumber === $number && $numberAsString->length() <= PHP_FLOAT_DIG) {
58+
return true;
59+
}
60+
61+
return false;
62+
}
3163
}

tests/Unit/Primitive/NumberTest.php

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
new Number(null);
124124
});
125125

126-
test('number can change decimals', function ($input, $scale, $result) {
126+
test('number can change decimals as a string input', function ($input, $scale, $result) {
127127
$valueObject = new Number($input, $scale);
128128
$this->assertSame($result, $valueObject->value());
129129
})->with([
@@ -155,6 +155,87 @@
155155
['777177711191777.99977777777777777777', 20, '777177711191777.99977777777777777777'],
156156
]);
157157

158+
test('number can change decimals as a float input up to 14 characters/digits', function ($input, $scale, $result) {
159+
$valueObject = new Number($input, $scale);
160+
$this->assertSame($result, $valueObject->value());
161+
})->with([
162+
[111777999.97, 2, '111777999.97'],
163+
164+
[7.1, 0, '7'],
165+
[7.1, 1, '7.1'],
166+
[7.11, 2, '7.11'],
167+
[7.99, 3, '7.990'],
168+
[70.1, 4, '70.1000'],
169+
[71.1, 5, '71.10000'],
170+
[17.9, 6, '17.900000'],
171+
[11.1, 7, '11.1000000'],
172+
[11.7, 8, '11.70000000'],
173+
[77.77, 9, '77.770000000'],
174+
[777.7, 10, '777.7000000000'],
175+
[777.7, 11, '777.70000000000'],
176+
[777.77, 12, '777.770000000000'],
177+
[777.777, 13, '777.7770000000000'],
178+
[7771.777, 14, '7771.77700000000000'],
179+
[7771.7771, 15, '7771.777100000000000'],
180+
[7771.77711, 16, '7771.7771100000000000'],
181+
[7771.777111, 17, '7771.77711100000000000'],
182+
[7771.7771119, 18, '7771.777111900000000000'],
183+
[7771.77711199, 19, '7771.7771119900000000000'],
184+
[3210987654321.0, 2, '3210987654321.00'],
185+
[290987654321.78, 2, '290987654321.78'],
186+
[92233720368.987, 2, '92233720368.98'],
187+
[9223372036.8547, 2, '9223372036.85'],
188+
[1.999999999999, 12, '1.999999999999'],
189+
[2.9999999999999, 12, '2.999999999999'],
190+
[290987654321.78, 3, '290987654321.780'],
191+
[92233720368.987, 3, '92233720368.987'],
192+
[9223372036.8547, 3, '9223372036.854'],
193+
[7771.0777110012, 3, '7771.077'],
194+
[9876.100077799, 3, '9876.100'],
195+
[1.543210987671, 3, '1.543'],
196+
[00002.5432109876712, 3, '2.543'],
197+
[3.5432109876789, 3, '3.543'],
198+
[11.543210987671, 3, '11.543'],
199+
[10987654321.789, 3, '10987654321.789'],
200+
[3210987654321.7, 3, '3210987654321.700'],
201+
[44210987654321.0, 3, '44210987654321.000'],
202+
[92233720368.547, 3, '92233720368.547'],
203+
]);
204+
205+
test('numeric integer in float form input up to 14 characters/digitss', function ($input, $scale, $result) {
206+
$valueObject = new Number($input, $scale);
207+
$this->assertSame($result, $valueObject->value());
208+
})->with([
209+
[1.0, 3, '1.000'],
210+
[2.0000, 3, '2.000'],
211+
[1234567890.0000, 3, '1234567890.000'],
212+
[12345678901234.0000, 3, '12345678901234.000'],
213+
]);
214+
215+
test('no conversion of decimal numbers as float input above 14 characters/digits', function ($number) {
216+
try {
217+
new Number($number, 20);
218+
$this->assertFalse(true);
219+
} catch (LengthException $e) {
220+
$this->assertSame(0, $e->getCode());
221+
$this->assertSame('Float precision loss detected.', $e->getMessage());
222+
}
223+
})->with([
224+
11.5432109876731,
225+
6667777.1234567890123456789,
226+
5556666.2345678901234567891,
227+
4445555.3456789012345678912,
228+
3334444.4567890123456789123,
229+
2223333.5678901234567891234,
230+
1112222.6789012345678912345,
231+
9991111.7890123456789123456,
232+
8880000.8901234567891234567,
233+
8889999.9012345678912345678,
234+
7778888.0123456789123456789,
235+
9553543210987654321.77711199,
236+
777177711191777.99977777777777777777,
237+
]);
238+
158239
test('number can handle huge numbers', function ($input, $scale, $result) {
159240
$valueObject = new Number($input, $scale);
160241
$this->assertSame($result, $valueObject->value());
@@ -250,17 +331,16 @@ class_uses_recursive(Number::class)
250331
test('can extend protected methods in number', function () {
251332
$number = new TestNumber('1 230,00');
252333
$this->assertSame('1230.00', $number->value());
334+
$number = new TestNumber(1230.12);
335+
$this->assertSame('1230.12', $number->value());
253336
});
254337

255338
class TestNumber extends Number
256339
{
257-
public function __construct(int|string $number, protected int $scale = 2)
340+
public function __construct(int|string|float $number, protected int $scale = 2)
258341
{
259-
$this->bigNumber = new BigNumber($this->sanitize($number), $this->scale);
260-
}
342+
parent::isPrecise((float) $number);
261343

262-
protected function sanitize(int|string|null $number): string
263-
{
264-
return parent::sanitize($number);
344+
$this->bigNumber = new BigNumber($this->sanitize($number), $this->scale);
265345
}
266346
}

tests/Unit/ValueObjectEqualityTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@
3232
});
3333

3434
test('decimal object equality works as expected', function () {
35-
$vo1 = new Number(777177711191777.97999999999998, scale: 2);
36-
$vo2 = new Number('777177711191777.97999999999998', scale: 3);
37-
$vo3 = new Number('777177711191777.97999999999998', scale: 2);
35+
$vo1 = new Number(123456789.1234, scale: 2);
36+
$vo2 = new Number('123456789.1234', scale: 3);
37+
$vo3 = new Number('123456789.1234', scale: 2);
3838
$vo4 = clone $vo1;
3939

4040
$this->assertFalse($vo1->equals($vo2));
4141
$this->assertTrue($vo1->notEquals($vo2));
4242
$this->assertTrue($vo3->equals($vo1));
4343
$this->assertTrue($vo4->equals($vo1));
44+
$this->assertFalse($vo2->equals($vo3));
4445
});

0 commit comments

Comments
 (0)