Skip to content

Commit

Permalink
MathCaster
Browse files Browse the repository at this point in the history
  • Loading branch information
fab2s committed Apr 23, 2024
1 parent 25019bc commit a5c8b7c
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 22 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ name: CI
on: [pull_request]
jobs:
tests:
name: Math CI PHP ${{ matrix.php-versions }}
name: Math (PHP ${{ matrix.php-versions }} / Orchestra ${{ matrix.orchestra-versions }})
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: [ '8.2', '8.1' ]
orchestra-versions: [ '8.0', '9.0' ]
exclude:
- php-versions: 8.1
orchestra-versions: 9.0

steps:
- name: Checkout
Expand All @@ -26,15 +30,18 @@ jobs:
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.php-versions }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ matrix.php-versions }}-composer-
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-

- name: Remove composer.lock
run: rm -f composer.lock

- name: Remove Pint
run: composer remove "laravel/pint" --dev --no-update

- name: Install Orchestra ${{ matrix.orchestra-versions }}
run: composer require "orchestra/testbench:^${{ matrix.orchestra-versions }}" --dev --no-update

- name: Install Composer dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader

Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
],
"require" : {
"php": "^8.1",
"ext-bcmath": "*"
"ext-bcmath": "*",
"fab2s/context-exception": "^2.0|^3.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"laravel/pint": "^1.11",
"orchestra/testbench": "^7.0|^8.0"
"orchestra/testbench": "^8.0|^9.0"
},
"autoload": {
"classmap": [
Expand Down
28 changes: 28 additions & 0 deletions src/Laravel/Exception/NotNullableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of fab2s/Math.
* (c) Fabrice de Stefanis / https://github.com/fab2s/Math
* This source file is licensed under the MIT license which you will
* find in the LICENSE file or at https://opensource.org/licenses/MIT
*/

namespace fab2s\Math\Laravel\Exception;

use fab2s\ContextException\ContextException;
use Illuminate\Database\Eloquent\Model;

class NotNullableException extends ContextException
{
public static function make(string $field, Model $model): self
{
$modelClass = get_class($model);

return (new self("Field {$field} is not nullable in model {$modelClass}"))
->setContext([
'model' => $modelClass,
'data' => $model->toArray(),
])
;
}
}
57 changes: 57 additions & 0 deletions src/Laravel/MathCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/*
* This file is part of fab2s/Math.
* (c) Fabrice de Stefanis / https://github.com/fab2s/Math
* This source file is licensed under the MIT license which you will
* find in the LICENSE file or at https://opensource.org/licenses/MIT
*/

namespace fab2s\Math\Laravel;

use fab2s\Math\Laravel\Exception\NotNullableException;
use fab2s\Math\Math;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class MathCast implements CastsAttributes
{
protected bool $isNullable = false;

public function __construct(...$options)
{
$this->isNullable = in_array('nullable', $options);
}

/**
* Cast the given value.
*
* @param Model $model
*
* @throws NotNullableException
*/
public function get($model, string $key, $value, array $attributes): ?Math
{
return Math::isNumber($value) ? Math::number($value) : $this->handleNullable($model, $key);
}

/**
* Prepare the given value for storage.
*
* @param Model $model
*
* @throws NotNullableException
*/
public function set($model, string $key, $value, array $attributes): ?string
{
return Math::isNumber($value) ? (string) Math::number($value) : $this->handleNullable($model, $key);
}

/**
* @throws NotNullableException
*/
protected function handleNullable(Model $model, string $key)
{
return $this->isNullable ? null : throw NotNullableException::make($key, $model);
}
}
7 changes: 5 additions & 2 deletions src/Math.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Math extends MathOpsAbstract implements JsonSerializable, Stringable
public function __construct(string|int|float|Math $number)
{
if (isset(static::$globalPrecision)) {
/* @codeCoverageIgnore */
$this->precision = static::$globalPrecision;
}

Expand Down Expand Up @@ -92,17 +93,19 @@ public function eq(string|int|float|Math $number): bool
}

/**
* convert decimal value to any other base bellow or equals to 64
* convert decimal value to any other base bellow or equals to 62
*/
public function toBase(string|int $base): string
{
if ($this->normalize()->hasDecimals()) {
throw new InvalidArgumentException('Argument number is not an integer');
}

static::validateBase($base = (int) static::validatePositiveInteger($base));

// do not mutate, only support positive integers
$number = ltrim((string) $this, '-');
if (static::$gmpSupport && $base <= 62) {
if (static::$gmpSupport) {
return static::baseConvert($number, 10, $base);
}

Expand Down
20 changes: 5 additions & 15 deletions src/MathBaseAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ abstract class MathBaseAbstract
*/
const PRECISION = 9;

/**
* base <= 64 charlist
*/
const BASECHAR_64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

/**
* base <= 62 char list
*/
Expand All @@ -39,17 +34,16 @@ abstract class MathBaseAbstract
/**
* highest base supported
*/
const BASE_MAX = 64;
const BASE_MAX = 62;

/**
* base char cache for all supported bases (bellow 64)
* base char cache for all supported bases (up to 62)
*
* @var array<int,string>
*/
protected static array $baseChars = [
36 => self::BASECHAR_36,
62 => self::BASECHAR_62,
64 => self::BASECHAR_64,
];

/**
Expand Down Expand Up @@ -130,7 +124,7 @@ public static function isNumber(string|int|float|Math|null $number): bool
/**
* Validation flavour of normalization logic
*/
public static function normalizeNumber(string|int|float|Math $number, string|int|null $default = null): ?string
public static function normalizeNumber(string|int|float|Math|null $number, Math|string|int|float|null $default = null): ?string
{
if (! static::isNumber($number)) {
return $default;
Expand All @@ -147,10 +141,6 @@ public static function getBaseChar(string|int $base): string

static::validateBase($base = (int) static::validatePositiveInteger($base));

if ($base > 62) {
return static::$baseChars[$base] = substr(static::BASECHAR_64, 0, $base);
}

if ($base > 36) {
return static::$baseChars[$base] = substr(static::BASECHAR_62, 0, $base);
}
Expand Down Expand Up @@ -194,8 +184,8 @@ protected static function normalizeReal(string|int $number): string
*/
protected static function validateBase(int $base): void
{
if ($base < 2 || $base > self::BASE_MAX || ! static::gmpSupport() && $base > 62) {
throw new InvalidArgumentException('Argument base is not valid, base 2 to ' . (static::gmpSupport() ? 64 : 62) . ' are supported');
if ($base < 2 || $base > self::BASE_MAX) {
throw new InvalidArgumentException('Argument base is not valid, base 2 to ' . self::BASE_MAX . ' are supported');
}
}

Expand Down
23 changes: 23 additions & 0 deletions tests/Laravel/Artifacts/CastModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of fab2s/Math.
* (c) Fabrice de Stefanis / https://github.com/fab2s/Math
* This source file is licensed under the MIT license which you will
* find in the LICENSE file or at https://opensource.org/licenses/MIT
*/

namespace fab2s\Math\Tests\Laravel\Artifacts;

use fab2s\Math\Laravel\MathCast;
use Illuminate\Database\Eloquent\Model;

class CastModel extends Model
{
protected $table = 'table';
protected $guarded = [];
protected $casts = [
'not_nullable' => MathCast::class,
'nullable' => MathCast::class . ':nullable',
];
}
116 changes: 116 additions & 0 deletions tests/Laravel/MathCastTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

/*
* This file is part of fab2s/Math.
* (c) Fabrice de Stefanis / https://github.com/fab2s/Math
* This source file is licensed under the MIT license which you will
* find in the LICENSE file or at https://opensource.org/licenses/MIT
*/

namespace fab2s\Math\Tests\Laravel;

use fab2s\Math\Laravel\Exception\NotNullableException;
use fab2s\Math\Laravel\MathCast;
use fab2s\Math\Math;
use fab2s\Math\Tests\Laravel\Artifacts\CastModel;
use Orchestra\Testbench\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class MathCastTest extends TestCase
{
public function setUp(): void
{
// Turn on error reporting
error_reporting(E_ALL);
parent::setUp();
}

/**
* @throws NotNullableException
*/
#[DataProvider('castProvider')]
public function test_math_cast_get(
Math|string|int|float|null $value,
Math|string|null $expected,
array $options = [],
): void {
$cast = new MathCast(...$options);

switch (true) {
case is_object($expected):
$this->assertTrue($expected->eq($cast->get(new CastModel, 'key', $value, [])));
break;
case is_string($expected):
$this->expectException(NotNullableException::class);
$cast->get(new CastModel, 'key', $value, []);
break;
case $expected === null:
$this->assertNull($cast->get(new CastModel, 'key', $value, []));
break;
}
}

/**
* @throws NotNullableException
*/
#[DataProvider('castProvider')]
public function test_math_cast_set(
Math|string|int|float|null $value,
Math|string|null $expected,
array $options = [],
): void {
$cast = new MathCast(...$options);

switch (true) {
case is_object($expected):
$this->assertSame((string) $expected, $cast->set(new CastModel, 'key', $value, []));
break;
case is_string($expected):
$this->expectException(NotNullableException::class);
$cast->set(new CastModel, 'key', $value, []);
break;
case $expected === null:
$this->assertSame(null, $cast->set(new CastModel, 'key', $value, []));
break;
}
}

public static function castProvider(): array
{
return [
[
'value' => null,
'expected' => null,
'options' => ['nullable'],
],
[
'value' => Math::number(42.42),
'expected' => Math::number(42.42),
'options' => ['nullable'],
],
[
'value' => Math::number(42.42),
'expected' => Math::number(42.42),
],
[
'value' => null,
'expected' => NotNullableException::class,
],
[
'value' => '42.4200000',
'expected' => Math::number(42.42),
'options' => ['nullable'],
],
[
'value' => 42.42,
'expected' => Math::number(42.42),
'options' => ['nullable'],
],
[
'value' => 42,
'expected' => Math::number(42),
'options' => ['nullable'],
],
];
}
}
Loading

0 comments on commit a5c8b7c

Please sign in to comment.