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 c86e3f6
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 28 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
6 changes: 4 additions & 2 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: 8.2-composer-${{ hashFiles('**/composer.json') }}
restore-keys: 8.2-composer-
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-

- name: Remove composer.lock
run: rm -f composer.lock
Expand All @@ -46,6 +46,8 @@ jobs:

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
flags: unittests
Expand Down
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Math

[![CI](https://github.com/fab2s/Math/actions/workflows/ci.yml/badge.svg)](https://github.com/fab2s/Math/actions/workflows/ci.yml) [![QA](https://github.com/fab2s/Math/actions/workflows/qa.yml/badge.svg)](https://github.com/fab2s/Math/actions/workflows/qa.yml) [![Total Downloads](https://poser.pugx.org/fab2s/math/downloads)](//packagist.org/packages/fab2s/math) [![Monthly Downloads](https://poser.pugx.org/fab2s/math/d/monthly)](//packagist.org/packages/fab2s/math) [![Latest Stable Version](https://poser.pugx.org/fab2s/math/v/stable)](https://packagist.org/packages/fab2s/math) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/fab2s/Math/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/fab2s/Math/?branch=master) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) [![License](https://poser.pugx.org/fab2s/math/license)](https://packagist.org/packages/fab2s/math)
[![CI](https://github.com/fab2s/Math/actions/workflows/ci.yml/badge.svg)](https://github.com/fab2s/Math/actions/workflows/ci.yml) [![QA](https://github.com/fab2s/Math/actions/workflows/qa.yml/badge.svg)](https://github.com/fab2s/Math/actions/workflows/qa.yml) [![Total Downloads](https://poser.pugx.org/fab2s/math/downloads)](//packagist.org/packages/fab2s/math) [![Monthly Downloads](https://poser.pugx.org/fab2s/math/d/monthly)](//packagist.org/packages/fab2s/math) [![Latest Stable Version](https://poser.pugx.org/fab2s/math/v/stable)](https://packagist.org/packages/fab2s/math) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) [![License](https://poser.pugx.org/fab2s/math/license)](https://packagist.org/packages/fab2s/math)

A fluent [bcmath](https://php.net/bcmath) based _Helper_ to handle high precision calculus in base 10 with a rather strict approach (want precision for something right?).
It does not try to be smart and just fails without `bcmath`, but it does auto detect [GMP](https://php.net/GMP) for faster base conversions.
Expand All @@ -19,7 +19,7 @@ composer require "fab2s/math"

## Prerequisites

`Math` requires [bcmath](https://php.net/bcmath), [GMP](https://php.net/GMP) is auto detected and used when available for faster base conversions (up to 62).
`Math` requires [bcmath](https://php.net/bcmath), [GMP](https://php.net/GMP) is auto-detected and used when available for faster base conversions (up to 62).

## In practice

Expand Down Expand Up @@ -110,7 +110,7 @@ Doing so is actually faster than casting a pre-existing instance to string becau
Arguments should be string or `Math`, but it is _ok_ to use integers up to `INT_(32|64)`.

**DO NOT** use `floats` as casting them to `string` may result in local dependent format, such as using a coma instead of a dot for decimals or just turn them exponential notation which is not supported by bcmath.
The way floats are handled in general and by PHP in particular is the very the reason why `bcmath` exists, so even if you trust your locale settings, using floats still kinda defeats the purpose of using such lib.
The way floats are handled in general and by PHP in particular is the very reason why `bcmath` exists, so even if you trust your locale settings, using floats still kinda defeats the purpose of using such lib.

## Internal precision

Expand All @@ -126,9 +126,35 @@ $number = (new Math('100'))->div('3'); // uses precision 18
$number->setPrecision(14); // will use precision 14 for any further calculations
```

## Laravel

For those using [Laravel](https://laravel.com/), `Math` comes with a Laravel caster: [MathCaster](./src/Laravel/MathCast.php) which you can use to directly cast your model properties.

````php
use fab2s\Math\Laravel\MathCast;

class MyModel extends Model
{
protected $casts = [
'not_nullable' => MathCast::class,
'nullable' => MathCast::class . ':nullable',
];
}

$model = new MyModel;

$model->not_nullable = 41;
$model->not_nullable->add(1)->eq(42); // true

$model->not_nullable = null; // throw a NotNullableException

$model->nullabe = null; // is ok

````

## Requirements

`Math` is tested against php 8.1 and 8.2
`Math` is tested against php 8.1 and 8.2. Additionally, MathCast is tested against Laravel 10 and 11.

## Contributing

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');

Check warning on line 101 in src/Math.php

View check run for this annotation

Codecov / codecov/patch

src/Math.php#L101

Added line #L101 was not covered by tests
}

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');

Check warning on line 188 in src/MathBaseAbstract.php

View check run for this annotation

Codecov / codecov/patch

src/MathBaseAbstract.php#L188

Added line #L188 was not covered by tests
}
}

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',
];
}
Loading

0 comments on commit c86e3f6

Please sign in to comment.