diff --git a/README.md b/README.md index 82845e1..1035701 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,53 @@ $stringsArray = Wrap::iterable($range(0, 10)) }); ``` +### `NumberValue` + +Object wrapping number (float or int). + +```php +add(20) + ->subtract(1) + ->multiply(2) + ->divide(3) + ->modulo(100) + ->calculate('sqrt') + ->round(2); +// ... + +$formattedNumber = $number->format(2, '.', ','); + +``` + +### `NumbersArray` + +Object wrapping array of numbers wrapped in NumberValue. + +```php +sum()->format(2)->toString(); +$min = $numbers->min()->format(2)->toString(); +$max = $numbers->max()->format(2)->toString(); +$avg = $numbers->average()->format(2)->toString(); + +$customSum = $numbers->reduceNumber( + fn(NumberValue $sum, NumberValue $next): NumberValue => $sum->add($next->round()), + 0 +); + +``` + ## Documentation For full methods reference and more examples see [here](./docs/examples.md). diff --git a/docs/examples.md b/docs/examples.md index ccb4ae6..d6af72c 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -348,6 +348,7 @@ array ( ```php $size * @phpstan-return ArrayValue> */ public function chunk(int $size): ArrayValue; @@ -650,9 +651,9 @@ public function offsetExists($offset): bool; $other - * @param (callable(TValue,TValue):int)|null $comparator + * @param (callable(TValue,TValue):int<-1,1>)|null $comparator * @phpstan-return ArrayValue */ public function diff(ArrayValue $other, ?callable $comparator = null): ArrayValue; @@ -1154,6 +1155,14 @@ array ( public function toStringsArray(): StringsArray; ``` +### ArrayValue::toNumbersArray + +```php +> + * @return ArrayValue> */ public function matchAllPatterns($pattern): ArrayValue; ``` @@ -2782,7 +2791,7 @@ public function offsetExists($offset): bool; /** * @param int $offset */ -public function offsetGet($offset): StringValue; +public function offsetGet($offset): ?StringValue; ``` ### StringsArray::offsetSet @@ -2853,7 +2862,7 @@ public function splice(int $offset, int $length, ?StringsArray $replacement = nu ```php )|null $comparator */ public function diff(StringsArray $other, ?callable $comparator = null): StringsArray; ``` @@ -3657,6 +3666,7 @@ array ( ```php $size * @phpstan-return ArrayValue> */ public function chunk(int $size): ArrayValue; @@ -3715,7 +3725,7 @@ public function positionLast($needle): ?int; > + * @return ArrayValue> */ public function matchAllPatterns($pattern): ArrayValue; ``` @@ -4466,4 +4476,851 @@ array ( *(definition not available)* +## NumberValue + +### NumberValue::compare + +```php + + */ +public function compare(float|int|string|Numberable $other): int; +``` + +### NumberValue::equals + +```php +add(50)->toNumber(); +echo "\n"; + +echo "100 + 11.22 = "; +echo $number->add(new JustFloat(11.22))->toNumber(); +echo "\n"; + +echo "100 + (10 + 20 + 30) = "; +echo $number->add(new Sum(new JustNumbers(10, 20, 30)))->toNumber(); +echo "\n"; +``` + +``` +100 + 50 = 150 +100 + 11.22 = 111.22 +100 + (10 + 20 + 30) = 160 +``` + +### NumberValue::subtract + +```php +subtract(50)->toNumber(); +echo "\n"; + +echo "100 - 11.22 = "; +echo $number->subtract(new JustFloat(11.11))->toNumber(); +echo "\n"; + +echo "100 - (10 + 20 + 30) = "; +echo $number->subtract(new Sum(new JustNumbers(10, 20, 30)))->toNumber(); +echo "\n"; +``` + +``` +100 - 50 = 50 +100 - 11.22 = 88.89 +100 - (10 + 20 + 30) = 40 +``` + +### NumberValue::multiply + +```php +round(1)->toNumber(); +echo "\n"; +``` + +``` +round(22.55, 1) = 22.6 +``` + +### NumberValue::floor + +```php +calculate(fn($number): Numberable => new Multiply(new JustNumber($number), new JustInteger(12))) + ->toNumber(); +echo "\n"; + +echo "cos(100) = "; +echo $number->calculate('cos')->toNumber(); +echo "\n"; + +echo "√100 = "; +echo $number->calculate('sqrt')->toNumber(); +echo "\n"; +``` + +``` +100 * 12 = 1200 +cos(100) = 0.86231887228768 +√100 = 10 +``` + +### NumberValue::isEmpty + +```php +format(2)->toString(); +echo "\n"; + +echo $number->format(3, '.', ' ')->toString(); +echo "\n"; +``` + +``` +1,000.11 +1 000.111 +``` + +### NumberValue::toStringValue + +```php +sum()->toNumber(); +echo "\n"; +``` + +``` +1 + 2.5 + 5 + 10 = 18.5 +``` + +### NumbersArray::average + +```php +average()->toNumber(); +echo "\n"; +``` + +``` +avg(1, 3, 5, 10) = 4.75 +``` + +### NumbersArray::min + +```php +min()->toNumber(); +echo "\n"; +``` + +``` +min(100, 10, 50, 80) = 10 +``` + +### NumbersArray::max + +```php +max()->toNumber(); +echo "\n"; +``` + +``` +max(100, 10, 50, 80) = 100 +``` + +### NumbersArray::each + +```php + */ +public function toNativeNumbers(): array; +``` + +### NumbersArray::filter + +```php + + */ +public function map(callable $transformer): ArrayValue; +``` + +### NumbersArray::flatMap + +```php + $transformer + * @return ArrayValue + */ +public function flatMap(callable $transformer): ArrayValue; +``` + +### NumbersArray::groupBy + +```php +> + */ +public function groupBy(callable $reducer): AssocValue; +``` + +### NumbersArray::sort + +```php + $other + */ +public function join(ArrayValue $other): NumbersArray; +``` + +### NumbersArray::slice + +```php +|null $replacement + */ +public function splice(int $offset, int $length, ?ArrayValue $replacement = null): NumbersArray; +``` + +### NumbersArray::diff + +```php + $other + * @param (callable(NumberValue,NumberValue):int)|null $comparator + */ +public function diff(ArrayValue $other, ?callable $comparator = null): NumbersArray; +``` + +### NumbersArray::intersect + +```php + $other + * @param (callable(NumberValue,NumberValue):int)|null $comparator + */ +public function intersect(ArrayValue $other, ?callable $comparator = null): NumbersArray; +``` + +### NumbersArray::reduce + +```php +reduceNumber( + fn(NumberValue $factorial, NumberValue $next): NumberValue => $factorial->multiply($next), + 1 + ) + ->toNumber(); +echo "\n"; +``` + +``` +5! = 120 +``` + +### NumbersArray::implode + +```php + + */ +public function toAssocValue(): AssocValue; +``` + +### NumbersArray::toStringsArray + +```php + $size + * @phpstan-return ArrayValue> + */ +public function chunk(int $size): ArrayValue; +``` + +### NumbersArray::toNumbersArray + +```php +add(50)->toNumber(); +echo "\n"; + +echo "100 + 11.22 = "; +echo $number->add(new JustFloat(11.22))->toNumber(); +echo "\n"; + +echo "100 + (10 + 20 + 30) = "; +echo $number->add(new Sum(new JustNumbers(10, 20, 30)))->toNumber(); +echo "\n"; diff --git a/examples/example/NumberValue-calculate.php b/examples/example/NumberValue-calculate.php new file mode 100644 index 0000000..16caa26 --- /dev/null +++ b/examples/example/NumberValue-calculate.php @@ -0,0 +1,23 @@ +calculate(fn($number): Numberable => new Multiply(new JustNumber($number), new JustInteger(12))) + ->toNumber(); +echo "\n"; + +echo "cos(100) = "; +echo $number->calculate('cos')->toNumber(); +echo "\n"; + +echo "√100 = "; +echo $number->calculate('sqrt')->toNumber(); +echo "\n"; diff --git a/examples/example/NumberValue-format.php b/examples/example/NumberValue-format.php new file mode 100644 index 0000000..d592395 --- /dev/null +++ b/examples/example/NumberValue-format.php @@ -0,0 +1,11 @@ +format(2)->toString(); +echo "\n"; + +echo $number->format(3, '.', ' ')->toString(); +echo "\n"; diff --git a/examples/example/NumberValue-round.php b/examples/example/NumberValue-round.php new file mode 100644 index 0000000..418e330 --- /dev/null +++ b/examples/example/NumberValue-round.php @@ -0,0 +1,7 @@ +round(1)->toNumber(); +echo "\n"; diff --git a/examples/example/NumberValue-subtract.php b/examples/example/NumberValue-subtract.php new file mode 100644 index 0000000..34de615 --- /dev/null +++ b/examples/example/NumberValue-subtract.php @@ -0,0 +1,20 @@ +subtract(50)->toNumber(); +echo "\n"; + +echo "100 - 11.11 = "; +echo $number->subtract(new JustFloat(11.11))->toNumber(); +echo "\n"; + +echo "100 - (10 + 20 + 30) = "; +echo $number->subtract(new Sum(new JustNumbers(10, 20, 30)))->toNumber(); +echo "\n"; diff --git a/examples/example/NumbersArray-average.php b/examples/example/NumbersArray-average.php new file mode 100644 index 0000000..d9d0a50 --- /dev/null +++ b/examples/example/NumbersArray-average.php @@ -0,0 +1,9 @@ +average()->toNumber(); +echo "\n"; diff --git a/examples/example/NumbersArray-max.php b/examples/example/NumbersArray-max.php new file mode 100644 index 0000000..668fb0f --- /dev/null +++ b/examples/example/NumbersArray-max.php @@ -0,0 +1,9 @@ +max()->toNumber(); +echo "\n"; diff --git a/examples/example/NumbersArray-min.php b/examples/example/NumbersArray-min.php new file mode 100644 index 0000000..4eb29b8 --- /dev/null +++ b/examples/example/NumbersArray-min.php @@ -0,0 +1,9 @@ +min()->toNumber(); +echo "\n"; diff --git a/examples/example/NumbersArray-reduceNumber.php b/examples/example/NumbersArray-reduceNumber.php new file mode 100644 index 0000000..ce6f5a1 --- /dev/null +++ b/examples/example/NumbersArray-reduceNumber.php @@ -0,0 +1,15 @@ +reduceNumber( + fn(NumberValue $factorial, NumberValue $next): NumberValue => $factorial->multiply($next), + 1 + ) + ->toNumber(); +echo "\n"; diff --git a/examples/example/NumbersArray-sum.php b/examples/example/NumbersArray-sum.php new file mode 100644 index 0000000..20efeb9 --- /dev/null +++ b/examples/example/NumbersArray-sum.php @@ -0,0 +1,9 @@ +sum()->toNumber(); +echo "\n"; diff --git a/make-examples.php b/make-examples.php index d599276..109bd7a 100644 --- a/make-examples.php +++ b/make-examples.php @@ -4,6 +4,8 @@ use GW\Value\ArrayValue; use GW\Value\AssocValue; use GW\Value\IterableValue; +use GW\Value\NumbersArray; +use GW\Value\NumberValue; use GW\Value\StringsArray; use GW\Value\StringValue; @@ -15,6 +17,8 @@ StringValue::class, StringsArray::class, IterableValue::class, + NumberValue::class, + NumbersArray::class, ]); file_put_contents('docs/examples.md', $markdown->toString()); diff --git a/spec/PlainArraySpec.php b/spec/PlainArraySpec.php index c0c554c..e223054 100644 --- a/spec/PlainArraySpec.php +++ b/spec/PlainArraySpec.php @@ -3,6 +3,8 @@ namespace spec\GW\Value; use GW\Value\Filters; +use GW\Value\Numberable\JustInteger; +use GW\Value\NumbersArray; use GW\Value\PlainArray; use GW\Value\Sorts; use GW\Value\StringsArray; @@ -694,6 +696,15 @@ function it_can_be_converted_to_StringsArray() $stringsArray->toNativeStrings()->shouldBeLike(['one', 'two', 'three']); } + function it_can_be_converted_to_NumbersArray() + { + $this->beConstructedWith(['1', 2.1, new JustInteger(3)]); + + $numbersArray = $this->toNumbersArray(); + $numbersArray->shouldBeAnInstanceOf(NumbersArray::class); + $numbersArray->toNativeNumbers()->shouldBeLike([1, 2.1, 3]); + } + function it_can_tell_if_has_element_or_not() { $this->beConstructedWith(['one', '2', 'three']); diff --git a/spec/PlainNumberSpec.php b/spec/PlainNumberSpec.php new file mode 100644 index 0000000..ba4c384 --- /dev/null +++ b/spec/PlainNumberSpec.php @@ -0,0 +1,394 @@ +beConstructedWith(new JustInteger(123)); + + $this->toNumber()->shouldBe(123); + $this->toInteger()->shouldBe(123); + $this->toFloat()->shouldBe(123.0); + } + + function it_can_be_created_from_integer() + { + $this->beConstructedThrough('from', [123]); + + $this->toNumber()->shouldBe(123); + } + + function it_can_be_float() + { + $this->beConstructedWith(new JustFloat(123.66)); + + $this->toNumber()->shouldBe(123.66); + $this->toInteger()->shouldBe(123); + $this->toFloat()->shouldBe(123.66); + } + + function it_can_be_created_from_float() + { + $this->beConstructedThrough('from', [123.66]); + + $this->toNumber()->shouldBe(123.66); + } + + function it_compares_int_with_numbers_just_like_scalars() + { + $this->beConstructedWith(new JustInteger(123)); + + $this->compare(new JustFloat(123.00))->shouldBe(0); + $this->compare(123)->shouldBe(0); + $this->compare(123.00)->shouldBe(0); + $this->compare('123.00')->shouldBe(0); + + $this->compare(122)->shouldBe(1); + $this->compare(124)->shouldBe(-1); + $this->compare(122.9)->shouldBe(1); + $this->compare(123.1)->shouldBe(-1); + $this->compare('122.9')->shouldBe(1); + $this->compare('123.1')->shouldBe(-1); + + $this->equals(new JustFloat(123.00))->shouldBe(true); + $this->equals(123)->shouldBe(true); + $this->equals(123.00)->shouldBe(true); + $this->equals('123.00')->shouldBe(true); + } + + function it_adds_integers() + { + $this->beConstructedWith(new JustInteger(123)); + + $this->add(new JustInteger(45))->toNumber()->shouldBe(168); + $this->add(45)->toNumber()->shouldBe(168); + $this->add('45')->toNumber()->shouldBe(168); + } + + function it_adds_integer_and_float() + { + $this->beConstructedWith(new JustInteger(123)); + + $this->add(new JustFloat(.45))->toNumber()->shouldBe(123.45); + $this->add(.45)->toNumber()->shouldBe(123.45); + $this->add('.45')->toNumber()->shouldBe(123.45); + } + + function it_adds_floats() + { + $this->beConstructedWith(new JustFloat(.1)); + + $this->add(new JustFloat(.1))->toNumber()->shouldBe(.2); + $this->add(.1)->toNumber()->shouldBe(.2); + $this->add('.1')->toNumber()->shouldBe(.2); + } + + function it_adds_float_and_integer() + { + $this->beConstructedWith(new JustFloat(.1)); + + $this->add(new JustInteger(123))->toNumber()->shouldBe(123.1); + $this->add(123)->toNumber()->shouldBe(123.1); + $this->add('123')->toNumber()->shouldBe(123.1); + } + + function it_subtracts_integers() + { + $this->beConstructedWith(new JustInteger(123)); + + $this->subtract(new JustInteger(45))->toNumber()->shouldBe(78); + $this->subtract(45)->toNumber()->shouldBe(78); + $this->subtract('45')->toNumber()->shouldBe(78); + } + + function it_subtracts_integer_and_float() + { + $this->beConstructedWith(new JustInteger(123)); + + $this->subtract(new JustFloat(.45))->toNumber()->shouldBe(122.55); + $this->subtract(.45)->toNumber()->shouldBe(122.55); + $this->subtract('.45')->toNumber()->shouldBe(122.55); + } + + function it_subtracts_floats() + { + $this->beConstructedWith(new JustFloat(.2)); + + $this->subtract(new JustFloat(.1))->toNumber()->shouldBe(.1); + $this->subtract(.1)->toNumber()->shouldBe(.1); + $this->subtract('.1')->toNumber()->shouldBe(.1); + } + + function it_subtracts_float_and_integer() + { + $this->beConstructedWith(new JustFloat(123.5)); + + $this->subtract(new JustInteger(23))->toNumber()->shouldBe(100.5); + $this->subtract(23)->toNumber()->shouldBe(100.5); + $this->subtract('23')->toNumber()->shouldBe(100.5); + } + + function it_multiplies_integers() + { + $this->beConstructedWith(new JustInteger(8)); + + $this->multiply(new JustInteger(9))->toNumber()->shouldBe(72); + $this->multiply(9)->toNumber()->shouldBe(72); + $this->multiply('9')->toNumber()->shouldBe(72); + } + + function it_multiplies_integer_and_float() + { + $this->beConstructedWith(new JustInteger(8)); + + $this->multiply(new JustFloat(2.4))->toNumber()->shouldBe(19.2); + $this->multiply(2.4)->toNumber()->shouldBe(19.2); + $this->multiply('2.4')->toNumber()->shouldBe(19.2); + } + + function it_multiplies_floats() + { + $this->beConstructedWith(new JustFloat(.4)); + + $this->multiply(new JustFloat(.5))->toNumber()->shouldBe(.2); + $this->multiply(.5)->toNumber()->shouldBe(.2); + $this->multiply('.5')->toNumber()->shouldBe(.2); + } + + function it_multiplies_float_and_integer() + { + $this->beConstructedWith(new JustFloat(11.2)); + + $this->multiply(new JustInteger(4))->toNumber()->shouldBe(44.8); + $this->multiply(4)->toNumber()->shouldBe(44.8); + $this->multiply('4')->toNumber()->shouldBe(44.8); + } + + function it_divides_integers() + { + $this->beConstructedWith(new JustInteger(12)); + + $this->divide(new JustInteger(4))->toNumber()->shouldBe(3); + $this->divide(4)->toNumber()->shouldBe(3); + $this->divide('4')->toNumber()->shouldBe(3); + } + + function it_divides_integers_returning_float_when_fraction_result() + { + $this->beConstructedWith(new JustInteger(12)); + + $this->divide(new JustInteger(5))->toNumber()->shouldBe(2.4); + $this->divide(5)->toNumber()->shouldBe(2.4); + $this->divide('5')->toNumber()->shouldBe(2.4); + } + + function it_throws_error_when_dividing_by_zero() + { + $this->beConstructedThrough( + fn() => (new PlainNumber(new JustInteger(12)))->divide(new JustInteger(0)) + ); + + $this->shouldThrow(DivisionByZeroError::class)->during('toNumber'); + } + + function it_divides_integer_and_float_returning_float() + { + $this->beConstructedWith(new JustInteger(12)); + + $this->divide(new JustFloat(.5))->toNumber()->shouldBe(24.0); + $this->divide(.5)->toNumber()->shouldBe(24.0); + $this->divide('0.5')->toNumber()->shouldBe(24.0); + } + + function it_divides_floats() + { + $this->beConstructedWith(new JustFloat(.12)); + + $this->divide(new JustFloat(.04))->toNumber()->shouldBe(3.0); + $this->divide(.04)->toNumber()->shouldBe(3.0); + $this->divide('0.04')->toNumber()->shouldBe(3.0); + } + + function it_divides_float_and_integer() + { + $this->beConstructedWith(new JustFloat(12.5)); + + $this->divide(new JustInteger(5))->toNumber()->shouldBe(2.5); + $this->divide(5)->toNumber()->shouldBe(2.5); + $this->divide('5')->toNumber()->shouldBe(2.5); + } + + function it_calculates_modulo_of_integer() + { + $this->beConstructedWith(new JustInteger(12)); + + $this->modulo(new JustInteger(11))->toNumber()->shouldBe(1); + $this->modulo(11)->toNumber()->shouldBe(1); + $this->modulo('11')->toNumber()->shouldBe(1); + $this->modulo(new JustInteger(7))->toNumber()->shouldBe(5); + $this->modulo(7)->toNumber()->shouldBe(5); + $this->modulo('7')->toNumber()->shouldBe(5); + } + + function it_calculates_modulo_of_float_just_like_php_does() + { + $this->beConstructedWith(new JustInteger(12)); + + $this->modulo(new JustFloat(11.9))->toNumber()->shouldBe(1); + $this->modulo(11.9)->toNumber()->shouldBe(1); + $this->modulo('11.9')->toNumber()->shouldBe(1); + } + + function it_throws_error_when_modulo_divider_is_zero() + { + $this->beConstructedThrough( + fn() => (new PlainNumber(new JustInteger(12)))->modulo(new Zero()) + ); + + $this->shouldThrow(DivisionByZeroError::class)->during('toNumber'); + } + + function it_absolutes_positive_integer() + { + $this->beConstructedWith(new JustInteger(2)); + + $this->abs()->toNumber()->shouldBe(2); + } + + function it_absolutes_negative_integer() + { + $this->beConstructedWith(new JustInteger(-2)); + + $this->abs()->toNumber()->shouldBe(2); + } + + function it_absolutes_positive_float() + { + $this->beConstructedWith(new JustFloat(12.3)); + + $this->abs()->toNumber()->shouldBe(12.3); + } + + function it_absolutes_negative_float() + { + $this->beConstructedWith(new JustFloat(-12.3)); + + $this->abs()->toNumber()->shouldBe(12.3); + } + + function it_rounds_integer() + { + $this->beConstructedWith(new JustInteger(123)); + + $this->round(-2)->toNumber()->shouldBe(100.0); + } + + function it_rounds_float() + { + $this->beConstructedWith(new JustFloat(12.3)); + + $this->round()->toNumber()->shouldBe(12.0); + } + + function it_rounds_float_half_up() + { + $this->beConstructedWith(new JustFloat(12.5)); + + $this->round()->toNumber()->shouldBe(13.0); + } + + function it_rounds_float_half_down() + { + $this->beConstructedWith(new JustFloat(12.5)); + + $this->round(0, PHP_ROUND_HALF_DOWN)->toNumber()->shouldBe(12.0); + } + + function it_floors_integer_to_float() + { + $this->beConstructedWith(new JustInteger(2)); + + $this->floor()->toNumber()->shouldBe(2.0); + } + + function it_floors_float() + { + $this->beConstructedWith(new JustFloat(2.9)); + + $this->floor()->toNumber()->shouldBe(2.0); + } + + function it_ceil_integer_to_float() + { + $this->beConstructedWith(new JustInteger(2)); + + $this->ceil()->toNumber()->shouldBe(2.0); + } + + function it_ceil_float() + { + $this->beConstructedWith(new JustFloat(2.1)); + + $this->ceil()->toNumber()->shouldBe(3.0); + } + + function it_is_empty_when_zero_integer() + { + $this->beConstructedWith(new JustInteger(0)); + + $this->isEmpty()->shouldBe(true); + } + + function it_is_empty_when_zero_float() + { + $this->beConstructedWith(new Add(new JustFloat(-1.0), new JustFloat(1.0))); + + $this->isEmpty()->shouldBe(true); + } + + function it_calculates_custom_formula() + { + $this->beConstructedWith(new JustInteger(100)); + + $formula = fn(int $number): Numberable => new Divide( + new Sum(new JustNumbers($number, 300, 400)), + new JustInteger(2) + ); + + $this->calculate($formula)->toNumber()->shouldBe(400); + } + + function it_calculates_math_formulas() + { + $this->beConstructedWith(new JustInteger(90)); + + $this->calculate('cos')->toNumber()->shouldBe(cos(90)); + $this->divide(new JustInteger(100))->calculate('acos')->toNumber()->shouldBe(acos(.90)); + $this->calculate('sin')->toNumber()->shouldBe(sin(90)); + $this->divide(new JustInteger(100))->calculate('asin')->toNumber()->shouldBe(asin(.90)); + $this->calculate('tan')->toNumber()->shouldBe(tan(90)); + $this->divide(new JustInteger(100))->calculate('atan')->toNumber()->shouldBe(atan(.90)); + $this->calculate('exp')->toNumber()->shouldBe(exp(90)); + $this->calculate('sqrt')->toNumber()->shouldBe(sqrt(90)); + } +} diff --git a/spec/PlainNumbersArraySpec.php b/spec/PlainNumbersArraySpec.php new file mode 100644 index 0000000..3773b93 --- /dev/null +++ b/spec/PlainNumbersArraySpec.php @@ -0,0 +1,597 @@ +beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5, 6, 7]); + + $this->sum()->toNumber()->shouldBe(28); + } + + function it_calculates_sum_of_floats() + { + $this->beConstructedThrough('fromNumbers', [.5, 1.0, 1.5, 2.0, 2.5]); + + $this->sum()->toNumber()->shouldBeApproximately(7.5, 1.0e-9); + } + + function it_calculates_sum_of_mixed_numerics() + { + $this->beConstructedThrough('fromNumbers', [.5, 1, '1.5', '2', new JustFloat(2.5), PlainNumber::from(2)]); + + $this->sum()->toNumber()->shouldBeApproximately(9.5, 1.0e-9); + } + + function it_fails_to_create_sum_with_non_numeric() + { + $this->beConstructedThrough('fromNumbers', [.5, 1, 'x']); + + $this->sum()->shouldThrow(LogicException::class)->during('toNumber'); + } + + function it_calculates_average_of_integers() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5, 6, 7]); + + $this->average()->toNumber()->shouldBe(4); + } + + function it_calculates_average_of_floats() + { + $this->beConstructedThrough('fromNumbers', [1.1, 1.2, 1.3, 1.4, 1.5, 2.2]); + + $this->average()->toNumber()->shouldBeApproximately(1.45, 1.0e-9); + } + + function it_calculates_average_of_numerics() + { + $this->beConstructedThrough('fromNumbers', [1.1, '1.2', 1.3, new JustNumber(1.4), PlainNumber::from(1.7), '2']); + + $this->average()->toNumber()->shouldBeApproximately(1.45, 1.0e-9); + } + + function it_cannot_calculate_average_from_empty_set() + { + $this->beConstructedThrough('fromNumbers', []); + + $this->average()->shouldThrow(DivisionByZeroError::class)->during('toNumber'); + } + + function it_returns_min_of_integers() + { + $this->beConstructedThrough('fromNumbers', [6, 2, 1, 7, -2, 3, 4, 5]); + + $this->min()->toNumber()->shouldBe(-2); + } + + function it_returns_min_of_floats() + { + $this->beConstructedThrough('fromNumbers', [.6, .2, .1, .7, -.2, .3, .4, .5]); + + $this->min()->toNumber()->shouldBe(-.2); + } + + function it_returns_min_of_numerics() + { + $this->beConstructedThrough('fromNumbers', [2, .1, '-0.2', new Zero(), '11']); + + $this->min()->toNumber()->shouldBe(-.2); + } + + function it_returns_max_of_integers() + { + $this->beConstructedThrough('fromNumbers', [6, 2, 1, 7, -2, 3, 4, 5]); + + $this->max()->toNumber()->shouldBe(7); + } + + function it_returns_max_of_floats() + { + $this->beConstructedThrough('fromNumbers', [.6, .2, .1, .7, -.2, .3, .4, .5]); + + $this->max()->toNumber()->shouldBe(.7); + } + + function it_returns_max_of_numerics() + { + $this->beConstructedThrough('fromNumbers', ['0.1', '7', -.2, new Zero()]); + + $this->max()->toNumber()->shouldBe(7); + } + + function it_cannot_calculate_min_nor_max_from_empty_set() + { + $this->beConstructedThrough('fromNumbers', []); + + $this->min()->shouldThrow(LogicException::class)->during('toNumber'); + $this->max()->shouldThrow(LogicException::class)->during('toNumber'); + } + + function it_filters_numbers() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5, 6, 7]); + + $even = $this->filter(fn(NumberValue $value): bool => $value->toNumber() % 2 === 0); + $even->shouldBeAnInstanceOf(PlainNumbersArray::class); + $even->toNativeNumbers()->shouldBe([2, 4, 6]); + } + + function it_filters_zeros_as_empty_elements() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 0, 4, 5, .0]); + + $notEmpty = $this->filterEmpty(); + $notEmpty->shouldBeAnInstanceOf(PlainNumbersArray::class); + $notEmpty->toNativeNumbers()->shouldBe([1, 2, 3, 4, 5]); + + $this->notEmpty()->toNativeNumbers()->shouldBe([1, 2, 3, 4, 5]); + } + + function it_maps_to_ArrayValue() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5, 6, 7]); + + $mapped = $this->map(fn(NumberValue $number): string => "#{$number->toNumber()}"); + $mapped->beAnInstanceOf(PlainArray::class); + $mapped->toArray()->shouldBe(['#1', '#2', '#3', '#4', '#5', '#6', '#7']); + } + + function it_maps_flat_to_ArrayValue() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5, 6, 7]); + + $mapped = $this->flatMap( + fn(NumberValue $number): array => ["#{$number->toNumber()}.1", "#{$number->toNumber()}.2"] + ); + $mapped->beAnInstanceOf(PlainArray::class); + $mapped->toArray()->shouldBe( + [ + '#1.1', + '#1.2', + '#2.1', + '#2.2', + '#3.1', + '#3.2', + '#4.1', + '#4.2', + '#5.1', + '#5.2', + '#6.1', + '#6.2', + '#7.1', + '#7.2', + ] + ); + } + + function it_groups_numbers_returning_association_of_numbers_array() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $grouped = $this->groupBy(fn(NumberValue $value) => $value->modulo(new JustInteger(2))->toNumber()); + + $even = $grouped->get(0); + $even->shouldBeAnInstanceOf(PlainNumbersArray::class); + $even->toNativeNumbers()->shouldBe([2, 4]); + + $odd = $grouped->get(1); + $odd->toNativeNumbers()->shouldBe([1, 3, 5]); + } + + function it_chunks_number_values() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $chunked = $this->chunk(2); + $chunked->shouldBeAnInstanceOf(PlainArray::class); + $chunked[0]->shouldBeArray(); + $chunked[0]->shouldHaveCount(2); + $chunked[0][0]->toNumber()->shouldBe(1); + $chunked[0][1]->toNumber()->shouldBe(2); + + $chunked[1]->shouldBeArray(); + $chunked[1]->shouldHaveCount(2); + $chunked[1][0]->toNumber()->shouldBe(3); + $chunked[1][1]->toNumber()->shouldBe(4); + + $chunked[2]->shouldBeArray(); + $chunked[2]->shouldHaveCount(1); + $chunked[2][0]->toNumber()->shouldBe(5); + } + + function it_sorts_numbers() + { + $this->beConstructedThrough('fromNumbers', [6, 2, 1, 7, -2, 3, 4, 5]); + + $asc = $this->sort(Sorts::asc()); + $asc->beAnInstanceOf(PlainNumbersArray::class); + $asc->toNativeNumbers()->shouldBe([-2, 1, 2, 3, 4, 5, 6, 7]); + + $this->sort(Sorts::desc())->toNativeNumbers()->shouldBe([7, 6, 5, 4, 3, 2, 1, -2]); + } + + function it_reverses_numbers() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->reverse()->toNativeNumbers()->shouldBe([5, 4, 3, 2, 1]); + } + + function it_invokes_callback_for_each_number(CallableMock $callback) + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $expect = static fn(int $expected) => Argument::that(fn(NumberValue $n): bool => $n->toNumber() === $expected); + $callback->__invoke($expect(1))->shouldBeCalled(); + $callback->__invoke($expect(2))->shouldBeCalled(); + $callback->__invoke($expect(3))->shouldBeCalled(); + $callback->__invoke($expect(4))->shouldBeCalled(); + $callback->__invoke($expect(5))->shouldBeCalled(); + + $this->each($callback->getWrappedObject()); + } + + function it_joins_numbers_array_value() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->join(PlainNumbersArray::fromNumbers(6, 7, 8)) + ->toNativeNumbers() + ->shouldBe([1, 2, 3, 4, 5, 6, 7, 8]); + + $this->join(Wrap::array([PlainNumber::from(6), PlainNumber::from(7)])) + ->toNativeNumbers() + ->shouldBe([1, 2, 3, 4, 5, 6, 7]); + } + + function it_slices_numbers_array() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->slice(0, 2)->toNativeNumbers()->shouldBe([1, 2]); + $this->take(2)->toNativeNumbers()->shouldBe([1, 2]); + $this->slice(1, 2)->toNativeNumbers()->shouldBe([2, 3]); + $this->slice(-2, 2)->toNativeNumbers()->shouldBe([4, 5]); + $this->slice(2)->toNativeNumbers()->shouldBe([3, 4, 5]); + $this->skip(2)->toNativeNumbers()->shouldBe([3, 4, 5]); + } + + function it_splices_numbers_array() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5, 6, 7]); + + $this->splice(0, 4)->toNativeNumbers()->shouldBe([5, 6, 7]); + $this->splice(2, 3)->toNativeNumbers()->shouldBe([1, 2, 6, 7]); + $this->splice(-4, 3)->toNativeNumbers()->shouldBe([1, 2, 3, 7]); + $this->splice(-4, 3, PlainNumbersArray::fromNumbers(11, 12)) + ->toNativeNumbers() + ->shouldBe([1, 2, 3, 11, 12, 7]); + } + + function it_resolves_unique_numbers() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 2, 3, 3.0, 4, 4, 4, 5]); + + $this->unique()->toNativeNumbers()->shouldBe([1, 2, 3, 3.0, 4, 5]); + } + + function it_resolves_unique_numbers_with_comparator() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 2, 3, 3.0, 4, 4, 4.3, 5, 5.9]); + + $this->unique(CompareAsInt::asc()) + ->toNativeNumbers() + ->shouldBe([1, 2, 3, 4, 5]); + } + + function it_resolves_diff() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 2, 3, 3.0, 4, 4, 4, 5]); + + $this->diff(PlainNumbersArray::fromNumbers(2, 3, 4))->toNativeNumbers()->shouldBe([1, 3.0, 5]); + } + + function it_resolves_diff_by_comparator() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 2, 3, 3.0, 4, 4, 4.6, 5, 5.9]); + + $this->diff(PlainNumbersArray::fromNumbers(2, 3, 4), CompareAsInt::desc())->toNativeNumbers()->shouldBe([1, 5, 5.9]); + } + + function it_resolves_intersect() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 2, 3, 3.0, 4, 4, 4, 5]); + + $this->intersect(PlainNumbersArray::fromNumbers(2, 3, 4))->toNativeNumbers()->shouldBe([2, 2, 3, 4, 4, 4]); + } + + function it_resolves_intersect_by_comparator() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 2, 3, 3.0, 4, 4, 4.6, 5, 5.9]); + + $this->intersect(PlainNumbersArray::fromNumbers(2, 3, 4), CompareAsInt::asc()) + ->toNativeNumbers() + ->shouldBe([2, 2, 3, 3.0, 4, 4, 4.6]); + } + + function it_shuffles_numbers() + { + $numbers = range(1, 1000); + $this->beConstructedThrough('fromNumbers', [...$numbers]); + + $shuffled = $this->shuffle(); + $shuffled->beAnInstanceOf(PlainNumbersArray::class); + $shuffled->toNativeNumbers()->shouldNotBe($numbers); + $shuffled->diff($this)->toNativeNumbers()->shouldBe([]); + } + + function it_unshift_value() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $new = $this->unshift(PlainNumber::from(6)); + $new->toNativeNumbers()->shouldBe([6, 1, 2, 3, 4, 5]); + } + + function it_shifts_value() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $new = $this->getWrappedObject()->shift($value); + + if (!$value instanceof NumberValue || $value->toNumber() !== 1) { + throw new FailureException('Expected to shift 1'); + } + + if ($new->toNativeNumbers() !== [2, 3, 4, 5]) { + throw new FailureException('Expected array 2, 3, 4, 5'); + } + } + + function it_pushes_value() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $new = $this->push(PlainNumber::from(6)); + $new->toNativeNumbers()->shouldBe([1, 2, 3, 4, 5, 6]); + } + + function it_pops_value() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $new = $this->getWrappedObject()->pop($value); + + if (!$value instanceof NumberValue || $value->toNumber() !== 5) { + throw new FailureException('Expected to pop 5'); + } + + if ($new->toNativeNumbers() !== [1, 2, 3, 4]) { + throw new FailureException('Expected array 1, 2, 3, 4'); + } + } + + function it_reduces_numbers() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->reduce(fn(int $sum, NumberValue $number) => $sum + $number->toNumber(), 0) + ->shouldBe(15); + } + + function it_reduces_to_number_value() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this + ->reduceNumber( + fn(NumberValue $sum, NumberValue $number): NumberValue => $sum->add($number), + PlainNumber::from(0) + ) + ->toNumber() + ->shouldBe(15); + } + + function it_returns_first() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->first()->toNumber()->shouldBe(1); + } + + function it_returns_last() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->last()->toNumber()->shouldBe(5); + } + + function it_returns_null_as_first_and_last_when_empty() + { + $this->beConstructedThrough('fromNumbers', []); + + $this->first()->shouldBeNull(); + $this->last()->shouldBeNull(); + } + + function it_finds_first() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->find($this->isEven())->toNumber()->shouldBe(2); + } + + function it_finds_last() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->findLast($this->isEven())->toNumber()->shouldBe(4); + } + + function it_returns_null_when_not_found() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->find($this->isGreater(5))->shouldBeNull(); + $this->findLast($this->isGreater(5))->shouldBeNull(); + } + + function it_resolves_any_and_every_condition() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->any($this->isEven())->shouldBe(true); + $this->any($this->isGreater(4))->shouldBe(true); + $this->any($this->isGreater(5))->shouldBe(false); + + + $this->every($this->isEven())->shouldBe(false); + $this->every($this->isGreater(1))->shouldBe(false); + $this->every($this->isGreater(0))->shouldBe(true); + } + + function it_resolves_that_array_has_element_strict() + { + $two = PlainNumber::from(2); + $numbers = new JustArray([PlainNumber::from(1), $two]); + $this->beConstructedThrough('fromArrayable', [$numbers]); + + $this->hasElement($two)->shouldBe(true); + $this->hasElement(PlainNumber::from(2))->shouldBe(false); + } + + function it_is_countable() + { + $this->beConstructedThrough('fromNumbers', [1, 2, 3, 4, 5]); + + $this->count()->shouldBe(5); + } + + function it_is_arrayable() + { + $one = PlainNumber::from(1); + $two = PlainNumber::from(2); + $numbers = new JustArray([$one, $two]); + $this->beConstructedThrough('fromArrayable', [$numbers]); + + $this->toArray()->shouldBe([$one, $two]); + } + + function it_is_iterable() + { + $one = PlainNumber::from(1); + $two = PlainNumber::from(2); + $numbers = new JustArray([$one, $two]); + $this->beConstructedThrough('fromArrayable', [$numbers]); + + $this->shouldIterateLike([$one, $two]); + } + + function it_has_immutable_array_access() + { + $one = PlainNumber::from(1); + $two = PlainNumber::from(2); + $numbers = new JustArray([$one, $two]); + $this->beConstructedThrough('fromArrayable', [$numbers]); + + $this[0]->shouldBe($one); + $this[1]->shouldBe($two); + $this->shouldHaveOffset(0); + $this->shouldNotHaveOffset(2); + + $this->shouldThrow(BadMethodCallException::class)->during('offsetSet', [1, PlainNumber::from(3)]); + $this->shouldThrow(BadMethodCallException::class)->during('offsetUnset', [1]); + } + + function it_implodes_to_string_value() + { + $this->beConstructedThrough('fromNumbers', [1, 2.5, 3.6, 4, 5]); + + $this->implode(', ')->toString()->shouldBe('1, 2.5, 3.6, 4, 5'); + } + + function it_is_not_empty_when_has_number() + { + $this->beConstructedThrough('fromNumbers', [0]); + + $this->isEmpty()->shouldBe(false); + } + + function it_is_empty_when_no_numbers() + { + $this->beConstructedThrough('fromNumbers', []); + + $this->isEmpty()->shouldBe(true); + } + + function it_can_be_casted_to_association() + { + $one = PlainNumber::from(1); + $two = PlainNumber::from(2); + $three = PlainNumber::from(3); + $four = PlainNumber::from(4); + $numbers = new JustArray([$one, $two, $three, $four]); + $this->beConstructedThrough('fromArrayable', [$numbers]); + + $this->toAssocValue()->filter($this->isEven())->toAssocArray()->shouldBe([1 => $two, 3 => $four]); + } + + function it_can_be_casted_to_strings_array() + { + $this->beConstructedThrough('fromNumbers', [1, 2.5, 3.6, 4, 5]); + + $this->toStringsArray()->toNativeStrings()->shouldBe(['1', '2.5', '3.6', '4', '5']); + } + + function it_returns_self_on_toNumbersArray() + { + $this->beConstructedThrough('fromNumbers', [1, 2.5, 3.6, 4, 5]); + + $this->toNumbersArray()->shouldBe($this); + } + + private function isEven(): Closure + { + return static fn(NumberValue $number) => $number->toNumber() % 2 === 0; + } + + private function isGreater(int $than): Closure + { + return static fn(NumberValue $number) => $number->toNumber() > $than; + } + + public function getMatchers(): array + { + return [ + 'haveOffset' => fn(PlainNumbersArray $subject, int $key): bool => $subject->offsetExists($key), + ]; + } +} diff --git a/spec/PlainStringSpec.php b/spec/PlainStringSpec.php index 9339542..ee0804f 100644 --- a/spec/PlainStringSpec.php +++ b/spec/PlainStringSpec.php @@ -1,4 +1,4 @@ -shouldBeLike(new PlainArray([['Lorem', 'Lorem'], ['dolor', 'dolor']])); } + function it_can_be_converted_to_NumbersArray() + { + $this->beConstructedWithStrings('1', '2.1', '3.0'); + + $this->toNumbersArray() + ->toNativeNumbers() + ->shouldBeLike([1, 2.1, 3.0]); + } + private function beConstructedWithStrings(string ...$strings): void { $this->beConstructedWith(Wrap::array($strings)); diff --git a/spec/WrapSpec.php b/spec/WrapSpec.php index af51670..a035904 100644 --- a/spec/WrapSpec.php +++ b/spec/WrapSpec.php @@ -1,9 +1,15 @@ -beConstructedThrough('stringsArray', [['a', 'b', 'c']]); $this->shouldHaveType(StringsArray::class); } + + function it_should_return_NumberValue_of_scalar_number() + { + $this->beConstructedThrough('number', [123]); + $this->shouldHaveType(NumberValue::class); + } + + function it_should_return_NumberValue_of_Numberable() + { + $this->beConstructedThrough('number', [new JustInteger(123)]); + $this->shouldHaveType(NumberValue::class); + } + + function it_should_return_NumberValue_of_itself() + { + $this->beConstructedThrough('number', [PlainNumber::from(123)]); + $this->shouldHaveType(NumberValue::class); + } + + function it_should_return_NumbersArray_of_scalar_numbers() + { + $this->beConstructedThrough('numbersArray', [[1, 2, 3.0]]); + $this->shouldHaveType(NumbersArray::class); + } + + function it_should_return_NumbersArray_of_numberable() + { + $this->beConstructedThrough('numbersArray', [new JustArray([1, 2, 3.0])]); + $this->shouldHaveType(NumbersArray::class); + } + + function it_should_return_NumbersArray_of_itself() + { + $this->beConstructedThrough('numbersArray', [PlainNumbersArray::fromNumbers(1, 2, 3.0)]); + $this->shouldHaveType(NumbersArray::class); + } } diff --git a/src/ArrayValue.php b/src/ArrayValue.php index 9fb0b02..72c9445 100644 --- a/src/ArrayValue.php +++ b/src/ArrayValue.php @@ -203,4 +203,6 @@ public function notEmpty(): ArrayValue; public function toAssocValue(): AssocValue; public function toStringsArray(): StringsArray; + + public function toNumbersArray(): NumbersArray; } diff --git a/src/NumberValue.php b/src/NumberValue.php new file mode 100644 index 0000000..a85e7b0 --- /dev/null +++ b/src/NumberValue.php @@ -0,0 +1,76 @@ + + */ + public function compare(float|int|string|Numberable $other): int; + + /** + * @param float|int|numeric-string|Numberable $other + */ + public function equals(float|int|string|Numberable $other): bool; + + // basic math + + /** + * @param float|int|numeric-string|Numberable $other + */ + public function add(float|int|string|Numberable $other): NumberValue; + + /** + * @param float|int|numeric-string|Numberable $other + */ + public function subtract(float|int|string|Numberable $other): NumberValue; + + /** + * @param float|int|numeric-string|Numberable $other + */ + public function multiply(float|int|string|Numberable $other): NumberValue; + + /** + * @param float|int|numeric-string|Numberable $other + */ + public function divide(float|int|string|Numberable $other): NumberValue; + + public function abs(): NumberValue; + + /** + * @param float|int|numeric-string|Numberable $divider + */ + public function modulo(float|int|string|Numberable $divider): NumberValue; + + // rounding + + public function round(int $precision = 0, ?int $roundMode = null): NumberValue; + + public function floor(): NumberValue; + + public function ceil(): NumberValue; + + /** @param callable(int|float):(int|float|Numberable) $formula */ + public function calculate(callable $formula): NumberValue; + + // value + + /** @return bool true when 0 or 0.0, false otherwise */ + public function isEmpty(): bool; + + // casting + + public function format(int $decimals = 0, string $separator = '.', string $thousandsSeparator = ','): StringValue; + + public function toStringValue(): StringValue; + + public function toInteger(): int; + + public function toFloat(): float; + + public function __toString(): string; +} diff --git a/src/Numberable.php b/src/Numberable.php new file mode 100644 index 0000000..988e160 --- /dev/null +++ b/src/Numberable.php @@ -0,0 +1,8 @@ +numberable->toNumber()); + } +} diff --git a/src/Numberable/Add.php b/src/Numberable/Add.php new file mode 100644 index 0000000..a3a7200 --- /dev/null +++ b/src/Numberable/Add.php @@ -0,0 +1,19 @@ +leftTerm->toNumber() + $this->rightTerm->toNumber(); + } +} diff --git a/src/Numberable/Average.php b/src/Numberable/Average.php new file mode 100644 index 0000000..475d5ce --- /dev/null +++ b/src/Numberable/Average.php @@ -0,0 +1,28 @@ + */ + private Arrayable $terms, + ) { + } + + public function toNumber(): float|int + { + $terms = $this->terms->toArray(); + $count = count($terms); + if ($count === 0) { + throw new DivisionByZeroError('Cannot calculate avg number from empty set'); + } + + return (new Sum(new JustArray($terms)))->toNumber() / $count; + } +} diff --git a/src/Numberable/Calculate.php b/src/Numberable/Calculate.php new file mode 100644 index 0000000..915232e --- /dev/null +++ b/src/Numberable/Calculate.php @@ -0,0 +1,26 @@ +formula = $formula; + } + + public function toNumber(): float|int + { + $result = ($this->formula)($this->numberable->toNumber()); + + return JustNumber::wrap($result)->toNumber(); + } +} diff --git a/src/Numberable/Ceil.php b/src/Numberable/Ceil.php new file mode 100644 index 0000000..cf0474a --- /dev/null +++ b/src/Numberable/Ceil.php @@ -0,0 +1,19 @@ +numberable->toNumber()); + } +} diff --git a/src/Numberable/CompareAsInt.php b/src/Numberable/CompareAsInt.php new file mode 100644 index 0000000..f8d723d --- /dev/null +++ b/src/Numberable/CompareAsInt.php @@ -0,0 +1,28 @@ +direction * ((int)$left->toNumber() <=> (int)$right->toNumber()); + } +} diff --git a/src/Numberable/Divide.php b/src/Numberable/Divide.php new file mode 100644 index 0000000..f714217 --- /dev/null +++ b/src/Numberable/Divide.php @@ -0,0 +1,27 @@ +divisor->toNumber(); + + if ($divisor === 0) { + // bypass warning triggered by PHP 7.4 before throwing DivisionByZeroError + throw new DivisionByZeroError('Division by zero'); + } + + return $this->dividend->toNumber() / $divisor; + } +} diff --git a/src/Numberable/Floor.php b/src/Numberable/Floor.php new file mode 100644 index 0000000..d5bd66b --- /dev/null +++ b/src/Numberable/Floor.php @@ -0,0 +1,19 @@ +numberable->toNumber()); + } +} diff --git a/src/Numberable/JustFloat.php b/src/Numberable/JustFloat.php new file mode 100644 index 0000000..92b462a --- /dev/null +++ b/src/Numberable/JustFloat.php @@ -0,0 +1,18 @@ +float; + } +} diff --git a/src/Numberable/JustInteger.php b/src/Numberable/JustInteger.php new file mode 100644 index 0000000..a4c16d2 --- /dev/null +++ b/src/Numberable/JustInteger.php @@ -0,0 +1,18 @@ +integer; + } +} diff --git a/src/Numberable/JustNumber.php b/src/Numberable/JustNumber.php new file mode 100644 index 0000000..9a3a3c8 --- /dev/null +++ b/src/Numberable/JustNumber.php @@ -0,0 +1,33 @@ +number; + } +} diff --git a/src/Numberable/JustNumbers.php b/src/Numberable/JustNumbers.php new file mode 100644 index 0000000..8163380 --- /dev/null +++ b/src/Numberable/JustNumbers.php @@ -0,0 +1,31 @@ + + */ +final class JustNumbers implements Arrayable +{ + /** @var Arrayable */ + private Arrayable $numbers; + + /** @param float|int|numeric-string|Numberable ...$numbers */ + public function __construct(float|int|string|Numberable ...$numbers) + { + $this->numbers = new Map(new JustArray($numbers), new ToNumberable()); + } + + /** + * @return Numberable[] + */ + public function toArray(): array + { + return $this->numbers->toArray(); + } +} diff --git a/src/Numberable/Max.php b/src/Numberable/Max.php new file mode 100644 index 0000000..6b8109f --- /dev/null +++ b/src/Numberable/Max.php @@ -0,0 +1,31 @@ + */ + private Arrayable $numbers; + + /** @param Arrayable $numbers */ + public function __construct(Arrayable $numbers) + { + $this->numbers = new Map($numbers, new ToScalarNumber()); + } + + public function toNumber(): float|int + { + $numbers = $this->numbers->toArray(); + if (count($numbers) === 0) { + throw new LogicException('Cannot calculate max number from empty set'); + } + + return max(...$numbers); + } +} diff --git a/src/Numberable/Min.php b/src/Numberable/Min.php new file mode 100644 index 0000000..93bac7d --- /dev/null +++ b/src/Numberable/Min.php @@ -0,0 +1,32 @@ + */ + private Arrayable $numbers; + + /** @param Arrayable $numbers */ + public function __construct(Arrayable $numbers) + { + $this->numbers = new Map($numbers, new ToScalarNumber()); + } + + public function toNumber(): float|int + { + $numbers = $this->numbers->toArray(); + if (count($numbers) === 0) { + throw new LogicException('Cannot calculate min number from empty set'); + } + + return min(...$numbers); + } +} diff --git a/src/Numberable/Modulo.php b/src/Numberable/Modulo.php new file mode 100644 index 0000000..e99fe2c --- /dev/null +++ b/src/Numberable/Modulo.php @@ -0,0 +1,19 @@ +dividend->toNumber() % $this->divisor->toNumber(); + } +} diff --git a/src/Numberable/Multiply.php b/src/Numberable/Multiply.php new file mode 100644 index 0000000..8757a06 --- /dev/null +++ b/src/Numberable/Multiply.php @@ -0,0 +1,19 @@ +left->toNumber() * $this->right->toNumber(); + } +} diff --git a/src/Numberable/NumberValues.php b/src/Numberable/NumberValues.php new file mode 100644 index 0000000..936e736 --- /dev/null +++ b/src/Numberable/NumberValues.php @@ -0,0 +1,32 @@ + + */ +final class NumberValues implements Arrayable +{ + /** @var Arrayable */ + private Arrayable $numbers; + + /** @param float|int|numeric-string|Numberable ...$numbers */ + public function __construct(float|int|string|Numberable ...$numbers) + { + $this->numbers = new Map(new JustArray($numbers), new ToNumberValue()); + } + + /** + * @return NumberValue[] + */ + public function toArray(): array + { + return $this->numbers->toArray(); + } +} diff --git a/src/Numberable/NumericString.php b/src/Numberable/NumericString.php new file mode 100644 index 0000000..698ba35 --- /dev/null +++ b/src/Numberable/NumericString.php @@ -0,0 +1,27 @@ +number = $number + 0; + } + + public function toNumber(): int|float + { + return $this->number; + } +} diff --git a/src/Numberable/Round.php b/src/Numberable/Round.php new file mode 100644 index 0000000..102d0d3 --- /dev/null +++ b/src/Numberable/Round.php @@ -0,0 +1,26 @@ + */ + private int $mode; + + public function __construct( + private Numberable $numberable, + private int $precision, + ?int $mode = null, + ) { + $this->mode = $mode >= 1 && $mode <= 4 ? $mode : PHP_ROUND_HALF_UP; + } + + public function toNumber(): float + { + return round($this->numberable->toNumber(), $this->precision, $this->mode); + } +} diff --git a/src/Numberable/Subtract.php b/src/Numberable/Subtract.php new file mode 100644 index 0000000..951116f --- /dev/null +++ b/src/Numberable/Subtract.php @@ -0,0 +1,19 @@ +minuend->toNumber() - $this->subtrahend->toNumber(); + } +} diff --git a/src/Numberable/Sum.php b/src/Numberable/Sum.php new file mode 100644 index 0000000..04501e0 --- /dev/null +++ b/src/Numberable/Sum.php @@ -0,0 +1,21 @@ + */ + private Arrayable $terms, + ) { + } + + public function toNumber(): float|int + { + return array_reduce($this->terms->toArray(), new SumReducer(), 0); + } +} diff --git a/src/Numberable/SumReducer.php b/src/Numberable/SumReducer.php new file mode 100644 index 0000000..10bcfb6 --- /dev/null +++ b/src/Numberable/SumReducer.php @@ -0,0 +1,13 @@ +toNumber(); + } +} diff --git a/src/Numberable/ToNumberValue.php b/src/Numberable/ToNumberValue.php new file mode 100644 index 0000000..0a6dc5f --- /dev/null +++ b/src/Numberable/ToNumberValue.php @@ -0,0 +1,27 @@ +toNumberable = new ToNumberable(); + } + + public function __invoke(mixed $number): NumberValue + { + $numberable = ($this->toNumberable)($number); + + if ($numberable instanceof NumberValue) { + return $numberable; + } + + return new PlainNumber($numberable); + } +} diff --git a/src/Numberable/ToNumberable.php b/src/Numberable/ToNumberable.php new file mode 100644 index 0000000..b9ef794 --- /dev/null +++ b/src/Numberable/ToNumberable.php @@ -0,0 +1,34 @@ +toNumber(); + } +} diff --git a/src/Numberable/Zero.php b/src/Numberable/Zero.php new file mode 100644 index 0000000..6238dae --- /dev/null +++ b/src/Numberable/Zero.php @@ -0,0 +1,13 @@ + + */ +interface NumbersArray extends ArrayValue +{ + public function sum(): NumberValue; + + public function average(): NumberValue; + + public function min(): NumberValue; + + public function max(): NumberValue; + + // ArrayValue + + /** + * @param callable(NumberValue):void $callback + */ + public function each(callable $callback): NumbersArray; + + /** + * @param (callable(NumberValue $valueA, NumberValue $valueB):int)|null $comparator + */ + public function unique(?callable $comparator = null): NumbersArray; + + /** @return NumberValue[] */ + public function toArray(): array; + + /** @return array */ + public function toNativeNumbers(): array; + + /** + * @param callable(NumberValue):bool $filter + */ + public function filter(callable $filter): NumbersArray; + + public function filterEmpty(): NumbersArray; + + /** + * @template TNewValue + * @param callable(NumberValue):TNewValue $transformer + * @return ArrayValue + */ + public function map(callable $transformer): ArrayValue; + + /** + * @template TNewValue + * @param callable(NumberValue):iterable $transformer + * @return ArrayValue + */ + public function flatMap(callable $transformer): ArrayValue; + + /** + * @template TNewKey of int|string + * @param callable(NumberValue):TNewKey $reducer + * @return AssocValue> + */ + public function groupBy(callable $reducer): AssocValue; + + public function sort(callable $comparator): NumbersArray; + + public function shuffle(): NumbersArray; + + public function reverse(): NumbersArray; + + /** + * @param NumberValue $value + */ + public function unshift($value): NumbersArray; + + /** + * @param NumberValue|null $value + */ + public function shift(&$value = null): NumbersArray; + + /** + * @param NumberValue $value + */ + public function push($value): NumbersArray; + + /** + * @param NumberValue|null $value + */ + public function pop(&$value = null): NumbersArray; + + public function offsetExists($offset): bool; + + public function offsetGet($offset): ?NumberValue; + + public function offsetSet($offset, $value): void; + + public function offsetUnset($offset): void; + + /** + * @param ArrayValue $other + */ + public function join(ArrayValue $other): NumbersArray; + + public function slice(int $offset, ?int $length = null): NumbersArray; + + public function skip(int $length): NumbersArray; + + public function take(int $length): NumbersArray; + + /** + * @param ArrayValue|null $replacement + */ + public function splice(int $offset, int $length, ?ArrayValue $replacement = null): NumbersArray; + + /** + * @param ArrayValue $other + * @param (callable(NumberValue,NumberValue):int)|null $comparator + */ + public function diff(ArrayValue $other, ?callable $comparator = null): NumbersArray; + + /** + * @param ArrayValue $other + * @param (callable(NumberValue,NumberValue):int)|null $comparator + */ + public function intersect(ArrayValue $other, ?callable $comparator = null): NumbersArray; + + /** + * @template TNewValue + * @param callable(TNewValue, NumberValue):TNewValue $transformer + * @param TNewValue $start + * @return TNewValue + */ + public function reduce(callable $transformer, $start); + + /** + * @param callable(NumberValue $reduced, NumberValue $item):NumberValue $transformer + */ + public function reduceNumber(callable $transformer, float|int|Numberable $start): NumberValue; + + public function implode(string $glue): StringValue; + + public function notEmpty(): NumbersArray; + + /** + * @return AssocValue + */ + public function toAssocValue(): AssocValue; + + public function toStringsArray(): StringsArray; + + public function first(): ?NumberValue; + + public function last(): ?NumberValue; + + /** + * @param callable(NumberValue):bool $filter + */ + public function find(callable $filter): ?NumberValue; + + /** + * @param callable(NumberValue):bool $filter + */ + public function findLast(callable $filter): ?NumberValue; + + /** + * @param NumberValue $element + */ + public function hasElement($element): bool; + + /** + * @param callable(NumberValue):bool $filter + */ + public function any(callable $filter): bool; + + /** + * @param callable(NumberValue):bool $filter + */ + public function every(callable $filter): bool; +} diff --git a/src/PlainArray.php b/src/PlainArray.php index 1c32796..5fbe707 100644 --- a/src/PlainArray.php +++ b/src/PlainArray.php @@ -23,6 +23,7 @@ use GW\Value\Arrayable\Splice; use GW\Value\Arrayable\UniqueByComparator; use GW\Value\Arrayable\UniqueByString; +use GW\Value\Numberable\ToNumberValue; use function array_map; use function array_reverse; use function count; @@ -467,4 +468,9 @@ public function toStringsArray(): StringsArray { return Wrap::stringsArray($this->items->toArray()); } + + public function toNumbersArray(): NumbersArray + { + return new PlainNumbersArray($this->map(new ToNumberValue())); + } } diff --git a/src/PlainNumber.php b/src/PlainNumber.php new file mode 100644 index 0000000..06395e6 --- /dev/null +++ b/src/PlainNumber.php @@ -0,0 +1,147 @@ +number = $number; + } + + /** @param float|int|numeric-string|Numberable $number */ + public static function from(float|int|string|Numberable $number): self + { + return new self(JustNumber::wrap($number)); + } + + public function format(int $decimals = 0, string $separator = '.', string $thousandsSeparator = ','): StringValue + { + return Wrap::string(number_format($this->number->toNumber(), $decimals, $separator, $thousandsSeparator)); + } + + /** + * @param float|int|numeric-string|Numberable $other + * @return int<-1,1> + */ + public function compare(float|int|string|Numberable $other): int + { + return $this->toNumber() <=> JustNumber::wrap($other)->toNumber(); + } + + /** @param float|int|numeric-string|Numberable $other */ + public function equals(float|int|string|Numberable $other): bool + { + return $this->compare($other) === 0; + } + + /** @param float|int|numeric-string|Numberable $other */ + public function add(float|int|string|Numberable $other): NumberValue + { + return new self(new Add($this->number, JustNumber::wrap($other))); + } + + /** @param float|int|numeric-string|Numberable $other */ + public function subtract(float|int|string|Numberable $other): NumberValue + { + return new self(new Subtract($this->number, JustNumber::wrap($other))); + } + + /** @param float|int|numeric-string|Numberable $other */ + public function multiply(float|int|string|Numberable $other): NumberValue + { + return new self(new Multiply($this->number, JustNumber::wrap($other))); + } + + /** @param float|int|numeric-string|Numberable $other */ + public function divide(float|int|string|Numberable $other): NumberValue + { + return new self(new Divide($this->number, JustNumber::wrap($other))); + } + + /** @param float|int|numeric-string|Numberable $divider */ + public function modulo(float|int|string|Numberable $divider): NumberValue + { + return new self(new Modulo($this->number, JustNumber::wrap($divider))); + } + + public function abs(): NumberValue + { + return new self(new Absolute($this->number)); + } + + public function round(int $precision = 0, ?int $roundMode = null): NumberValue + { + return new self(new Round($this->number, $precision, $roundMode)); + } + + public function floor(): NumberValue + { + return new self(new Floor($this->number)); + } + + public function ceil(): NumberValue + { + return new self(new Ceil($this->number)); + } + + /** @param callable(int|float):(int|float|Numberable) $formula */ + public function calculate(callable $formula): NumberValue + { + return new self(new Calculate($this->number, $formula)); + } + + public function isEmpty(): bool + { + return $this->toFloat() === 0.0; + } + + public function toNumber(): float|int + { + return $this->number->toNumber(); + } + + public function toInteger(): int + { + return (int)$this->number->toNumber(); + } + + public function toFloat(): float + { + return (float)$this->number->toNumber(); + } + + public function toStringValue(): StringValue + { + $number = $this->number->toNumber(); + if (is_int($number)) { + return Wrap::string((string)$number); + } + + $value = $this->format(PHP_FLOAT_DIG, '.', '')->trimRight('0'); + + return $value->endsWith('.') ? $value->postfix('0') : $value; + } + + public function __toString(): string + { + return $this->toStringValue()->toString(); + } +} diff --git a/src/PlainNumbersArray.php b/src/PlainNumbersArray.php new file mode 100644 index 0000000..27c533b --- /dev/null +++ b/src/PlainNumbersArray.php @@ -0,0 +1,380 @@ + */ + private ArrayValue $numbers, + ) { + } + + /** @param Arrayable $numbers */ + public static function fromArrayable(Arrayable $numbers): self + { + return new self(new PlainArray($numbers)); + } + + /** @param float|int|numeric-string|Numberable ...$numbers */ + public static function fromNumbers(float|int|string|Numberable ...$numbers): self + { + return self::fromArrayable(new NumberValues(...$numbers)); + } + + public function sum(): NumberValue + { + return new PlainNumber(new Sum($this)); + } + + public function average(): NumberValue + { + return new PlainNumber(new Average($this)); + } + + public function min(): NumberValue + { + return new PlainNumber(new Min($this)); + } + + public function max(): NumberValue + { + return new PlainNumber(new Max($this)); + } + + /** @return (int|float)[] */ + public function toNativeNumbers(): array + { + return $this->map(new ToScalarNumber())->toArray(); + } + + public function getIterator(): Traversable + { + return $this->numbers->getIterator(); + } + + public function count(): int + { + return $this->numbers->count(); + } + + /** + * @param callable(NumberValue):void $callback + */ + public function each(callable $callback): NumbersArray + { + return new self($this->numbers->each($callback)); + } + + /** + * @param (callable(NumberValue $valueA, NumberValue $valueB):int)|null $comparator + */ + public function unique(?callable $comparator = null): NumbersArray + { + return new self($this->numbers->unique($comparator)); + } + + /** @return NumberValue[] */ + public function toArray(): array + { + return $this->numbers->toArray(); + } + + /** + * @param callable(NumberValue):bool $filter + */ + public function filter(callable $filter): NumbersArray + { + return new self($this->numbers->filter($filter)); + } + + public function filterEmpty(): NumbersArray + { + return new self($this->numbers->filterEmpty()); + } + + /** + * @template TNewValue + * @param callable(NumberValue):TNewValue $transformer + * @return ArrayValue + */ + public function map(callable $transformer): ArrayValue + { + return $this->numbers->map($transformer); + } + + /** + * @template TNewValue + * @param callable(NumberValue):iterable $transformer + * @return ArrayValue + */ + public function flatMap(callable $transformer): ArrayValue + { + return $this->numbers->flatMap($transformer); + } + + /** + * @template TNewKey + * @param callable(NumberValue):TNewKey $reducer + * @return AssocValue + * @phpstan-ignore-next-line shrug + */ + public function groupBy(callable $reducer): AssocValue + { + // @phpstan-ignore-next-line shrug + return $this->numbers + ->groupBy($reducer) + ->map(static fn(ArrayValue $numbers) => new self($numbers)); + } + + /** + * @return ArrayValue> + */ + public function chunk(int $size): ArrayValue + { + return $this->numbers->chunk($size); + } + + public function sort(callable $comparator): NumbersArray + { + return new self($this->numbers->sort($comparator)); + } + + public function shuffle(): NumbersArray + { + return new self($this->numbers->shuffle()); + } + + public function reverse(): NumbersArray + { + return new self($this->numbers->reverse()); + } + + /** + * @param NumberValue $value + */ + public function unshift($value): NumbersArray + { + return new self($this->numbers->unshift($value)); + } + + /** + * @param NumberValue|null $value + */ + public function shift(&$value = null): NumbersArray + { + return new self($this->numbers->shift($value)); + } + + /** + * @param NumberValue $value + */ + public function push($value): NumbersArray + { + return new self($this->numbers->push($value)); + } + + /** + * @param NumberValue|null $value + */ + public function pop(&$value = null): NumbersArray + { + return new self($this->numbers->pop($value)); + } + + /** + * @param int $offset + */ + public function offsetExists($offset): bool + { + return $this->numbers->offsetExists($offset); + } + + /** + * @param int $offset + */ + public function offsetGet($offset): ?NumberValue + { + return $this->numbers->offsetGet($offset); + } + + /** + * @param int $offset + * @phpstan-param NumberValue $value + * @throws BadMethodCallException For immutable types. + */ + public function offsetSet($offset, $value): void + { + throw new BadMethodCallException('PlainNumbersArray is immutable'); + } + + /** + * @param int $offset + * @throws BadMethodCallException For immutable types. + */ + public function offsetUnset($offset): void + { + throw new BadMethodCallException('PlainNumbersArray is immutable'); + } + + /** + * @param ArrayValue $other + */ + public function join(ArrayValue $other): NumbersArray + { + return new self($this->numbers->join($other)); + } + + public function slice(int $offset, ?int $length = null): NumbersArray + { + return new self($this->numbers->slice($offset, $length)); + } + + public function skip(int $length): NumbersArray + { + return new self($this->numbers->skip($length)); + } + + public function take(int $length): NumbersArray + { + return new self($this->numbers->take($length)); + } + + /** + * @param ArrayValue|null $replacement + */ + public function splice(int $offset, int $length, ?ArrayValue $replacement = null): NumbersArray + { + return new self($this->numbers->splice($offset, $length, $replacement)); + } + + /** + * @param ArrayValue $other + * @param (callable(NumberValue,NumberValue):int<-1,1>)|null $comparator + */ + public function diff(ArrayValue $other, ?callable $comparator = null): NumbersArray + { + return new self($this->numbers->diff($other, $comparator)); + } + + /** + * @param ArrayValue $other + * @param (callable(NumberValue,NumberValue):int)|null $comparator + */ + public function intersect(ArrayValue $other, ?callable $comparator = null): NumbersArray + { + return new self($this->numbers->intersect($other, $comparator)); + } + + /** + * @template TNewValue + * @param callable(TNewValue, NumberValue):TNewValue $transformer + * @param TNewValue $start + * @return TNewValue + */ + public function reduce(callable $transformer, $start) + { + return $this->numbers->reduce($transformer, $start); + } + + /** + * @param callable(NumberValue $reduced, NumberValue $item):NumberValue $transformer + * @param float|int|Numberable $start + */ + public function reduceNumber(callable $transformer, float|int|Numberable $start): NumberValue + { + return $this->numbers->reduce($transformer, Wrap::number($start)); + } + + public function implode(string $glue): StringValue + { + return $this->numbers->implode($glue); + } + + public function notEmpty(): NumbersArray + { + return new self($this->numbers->notEmpty()); + } + + /** + * @return AssocValue + */ + public function toAssocValue(): AssocValue + { + return $this->numbers->toAssocValue(); + } + + public function toStringsArray(): StringsArray + { + return $this->numbers->toStringsArray(); + } + + public function toNumbersArray(): NumbersArray + { + return $this; + } + + public function first(): ?NumberValue + { + return $this->numbers->first(); + } + + public function last(): ?NumberValue + { + return $this->numbers->last(); + } + + /** + * @param callable(NumberValue):bool $filter + */ + public function find(callable $filter): ?NumberValue + { + return $this->numbers->find($filter); + } + + /** + * @param callable(NumberValue):bool $filter + */ + public function findLast(callable $filter): ?NumberValue + { + return $this->numbers->findLast($filter); + } + + /** + * @param NumberValue $element + */ + public function hasElement($element): bool + { + return $this->numbers->hasElement($element); + } + + /** + * @param callable(NumberValue):bool $filter + */ + public function any(callable $filter): bool + { + return $this->numbers->any($filter); + } + + /** + * @param callable(NumberValue):bool $filter + */ + public function every(callable $filter): bool + { + return $this->numbers->every($filter); + } + + public function isEmpty(): bool + { + return $this->numbers->isEmpty(); + } +} diff --git a/src/PlainString.php b/src/PlainString.php index d07d534..ee098ac 100644 --- a/src/PlainString.php +++ b/src/PlainString.php @@ -257,6 +257,7 @@ public function matchAllPatterns($pattern): ArrayValue throw new RuntimeException("Failed to match regexp: {$pattern}"); } + /** @var array> $matches */ return Wrap::array($matches); } diff --git a/src/PlainStringsArray.php b/src/PlainStringsArray.php index 551f6dc..efe777b 100644 --- a/src/PlainStringsArray.php +++ b/src/PlainStringsArray.php @@ -2,6 +2,8 @@ namespace GW\Value; +use GW\Value\Numberable\ToNumberValue; +use GW\Value\Stringable\ToNativeString; use GW\Value\Stringable\ToStringValue; use Traversable; use function in_array; @@ -216,7 +218,7 @@ public function toArray(): array public function toNativeStrings(): array { return $this->strings - ->map(fn(StringValue $item): string => $item->toString()) + ->map(new ToNativeString()) ->toArray(); } @@ -617,4 +619,9 @@ public function toStringsArray(): self { return $this; } + + public function toNumbersArray(): NumbersArray + { + return new PlainNumbersArray($this->strings->map(new ToNativeString())->map(new ToNumberValue())); + } } diff --git a/src/Stringable/ToNativeString.php b/src/Stringable/ToNativeString.php new file mode 100644 index 0000000..2cb7f6f --- /dev/null +++ b/src/Stringable/ToNativeString.php @@ -0,0 +1,13 @@ +toString(); + } +} diff --git a/src/Stringable/ToStringValue.php b/src/Stringable/ToStringValue.php index c0a405a..41b513d 100644 --- a/src/Stringable/ToStringValue.php +++ b/src/Stringable/ToStringValue.php @@ -5,20 +5,22 @@ use GW\Value\StringValue; use GW\Value\Wrap; use InvalidArgumentException; +use function is_object; use function is_scalar; +use function method_exists; final class ToStringValue { public function __invoke(mixed $string): StringValue { - if (is_scalar($string)) { - return Wrap::string((string)$string); - } - if ($string instanceof StringValue) { return $string; } + if (is_scalar($string) || (is_object($string) && method_exists($string, '__toString'))) { + return Wrap::string((string)$string); + } + throw new InvalidArgumentException('StringsValue can contain only StringValue'); } } diff --git a/src/Wrap.php b/src/Wrap.php index 6dfc566..bc62075 100644 --- a/src/Wrap.php +++ b/src/Wrap.php @@ -66,6 +66,34 @@ public static function stringsArray(array $strings = []): StringsArray return new PlainStringsArray(self::array($strings)); } + /** + * @param float|int|numeric-string|Numberable $number + */ + public static function number(float|int|string|Numberable $number): NumberValue + { + if ($number instanceof NumberValue) { + return $number; + } + + return PlainNumber::from($number); + } + + /** + * @param array|Arrayable|NumbersArray $numbers + */ + public static function numbersArray(array|Arrayable|NumbersArray $numbers): NumbersArray + { + if ($numbers instanceof NumbersArray) { + return $numbers; + } + + if ($numbers instanceof Arrayable) { + return PlainNumbersArray::fromArrayable($numbers); + } + + return PlainNumbersArray::fromNumbers(...$numbers); + } + private function __construct() { // prohibits creation objects of this class