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 51ef54a
Show file tree
Hide file tree
Showing 12 changed files with 436 additions and 41 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
17 changes: 10 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,36 @@
"name" : "Fabrice de Stefanis"
}],
"keywords" : [
"math",
"PHP",
"Simple",
"Base10",
"decimal",
"BcMath",
"HighPrecision"
"HighPrecision",
"laravel"
],
"license" : [
"MIT"
],
"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": [
"src"
]
"psr-4": {
"fab2s\\Math\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"fab2s\\Math\\Tests\\": "tests/"
"fab2s\\Math\\Tests\\": "tests"
}
},
"scripts": {
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);
}
}
19 changes: 11 additions & 8 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 All @@ -43,26 +44,26 @@ public static function make(string|int|float|Math $number): static
}

/**
* convert any based value bellow or equals to 64 to its decimal value
* convert any based value bellow or equals to 62 to its decimal value
*/
public static function fromBase(string|int $number, int $base): static
public static function fromBase(string $number, int $base): static
{
// trim base 64 padding char, only positive
$number = trim($number, ' =-');
// only positive
$number = trim($number, ' -');
if ($number === '' || str_contains($number, '.')) {
throw new InvalidArgumentException('Argument number is not an integer');
}

$baseChar = static::getBaseChar($base);
// By now we know we have a correct base and number
if (trim($number, $baseChar[0]) === '') {
return new static('0');
}

if (static::$gmpSupport && $base <= 62) {
if (static::$gmpSupport) {
return new static(static::baseConvert($number, $base, 10));
}

// By now we know we have a correct base and number
return new static(static::bcDec2Base($number, $base, $baseChar));
}

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
22 changes: 6 additions & 16 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,12 +184,12 @@ 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');
}
}

protected static function bcDec2Base(string|int $number, string|int $base, string $baseChar): string
protected static function bcDec2Base(string $number, int $base, string $baseChar): string
{
$result = '';
$numberLen = strlen($number);
Expand Down
Loading

0 comments on commit 51ef54a

Please sign in to comment.