Skip to content

Commit 753cc10

Browse files
committed
MathCaster
1 parent 25019bc commit 753cc10

File tree

12 files changed

+415
-36
lines changed

12 files changed

+415
-36
lines changed

.github/workflows/ci.yml

+10-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ name: CI
22
on: [pull_request]
33
jobs:
44
tests:
5-
name: Math CI PHP ${{ matrix.php-versions }}
5+
name: Math (PHP ${{ matrix.php-versions }} / Orchestra ${{ matrix.orchestra-versions }})
66
runs-on: ubuntu-latest
77
strategy:
88
matrix:
99
php-versions: [ '8.2', '8.1' ]
10+
orchestra-versions: [ '8.0', '9.0' ]
11+
exclude:
12+
- php-versions: 8.1
13+
orchestra-versions: 9.0
1014

1115
steps:
1216
- name: Checkout
@@ -26,15 +30,18 @@ jobs:
2630
uses: actions/cache@v3
2731
with:
2832
path: ${{ steps.composer-cache.outputs.dir }}
29-
key: ${{ matrix.php-versions }}-composer-${{ hashFiles('**/composer.json') }}
30-
restore-keys: ${{ matrix.php-versions }}-composer-
33+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
34+
restore-keys: ${{ runner.os }}-composer-
3135

3236
- name: Remove composer.lock
3337
run: rm -f composer.lock
3438

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

42+
- name: Install Orchestra ${{ matrix.orchestra-versions }}
43+
run: composer require "orchestra/testbench:^${{ matrix.orchestra-versions }}" --dev --no-update
44+
3845
- name: Install Composer dependencies
3946
run: composer install --no-progress --prefer-dist --optimize-autoloader
4047

.github/workflows/qa.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ jobs:
2929
uses: actions/cache@v3
3030
with:
3131
path: ${{ steps.composer-cache.outputs.dir }}
32-
key: 8.2-composer-${{ hashFiles('**/composer.json') }}
33-
restore-keys: 8.2-composer-
32+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
33+
restore-keys: ${{ runner.os }}-composer-
3434

3535
- name: Remove composer.lock
3636
run: rm -f composer.lock
@@ -46,6 +46,8 @@ jobs:
4646

4747
- name: Upload coverage to Codecov
4848
uses: codecov/codecov-action@v3
49+
env:
50+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
4951
with:
5052
files: ./coverage.xml
5153
flags: unittests

README.md

+30-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Math
22

3-
[![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)
3+
[![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)
44

55
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?).
66
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.
@@ -19,7 +19,7 @@ composer require "fab2s/math"
1919

2020
## Prerequisites
2121

22-
`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).
22+
`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).
2323

2424
## In practice
2525

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

112112
**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.
113-
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.
113+
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.
114114

115115
## Internal precision
116116

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

129+
## Laravel
130+
131+
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.
132+
133+
````php
134+
use fab2s\Math\Laravel\MathCast;
135+
136+
class MyModel extends Model
137+
{
138+
protected $casts = [
139+
'not_nullable' => MathCast::class,
140+
'nullable' => MathCast::class . ':nullable',
141+
];
142+
}
143+
144+
$model = new MyModel;
145+
146+
$model->not_nullable = 41;
147+
$model->not_nullable->add(1)->eq(42); // true
148+
149+
$model->not_nullable = null; // throw a NotNullableException
150+
151+
$model->nullabe = null; // is ok
152+
153+
````
154+
129155
## Requirements
130156

131-
`Math` is tested against php 8.1 and 8.2
157+
`Math` is tested against php 8.1 and 8.2. Additionally, MathCast is tested against Laravel 10 and 11.
132158

133159
## Contributing
134160

composer.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
],
1919
"require" : {
2020
"php": "^8.1",
21-
"ext-bcmath": "*"
21+
"ext-bcmath": "*",
22+
"fab2s/context-exception": "^2.0|^3.0"
2223
},
2324
"require-dev": {
2425
"phpunit/phpunit": "^10.0",
2526
"laravel/pint": "^1.11",
26-
"orchestra/testbench": "^7.0|^8.0"
27+
"orchestra/testbench": "^8.0|^9.0"
2728
},
2829
"autoload": {
2930
"classmap": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of fab2s/Math.
5+
* (c) Fabrice de Stefanis / https://github.com/fab2s/Math
6+
* This source file is licensed under the MIT license which you will
7+
* find in the LICENSE file or at https://opensource.org/licenses/MIT
8+
*/
9+
10+
namespace fab2s\Math\Laravel\Exception;
11+
12+
use fab2s\ContextException\ContextException;
13+
use Illuminate\Database\Eloquent\Model;
14+
15+
class NotNullableException extends ContextException
16+
{
17+
public static function make(string $field, Model $model): self
18+
{
19+
$modelClass = get_class($model);
20+
21+
return (new self("Field {$field} is not nullable in model {$modelClass}"))
22+
->setContext([
23+
'model' => $modelClass,
24+
'data' => $model->toArray(),
25+
])
26+
;
27+
}
28+
}

src/Laravel/MathCast.php

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of fab2s/Math.
5+
* (c) Fabrice de Stefanis / https://github.com/fab2s/Math
6+
* This source file is licensed under the MIT license which you will
7+
* find in the LICENSE file or at https://opensource.org/licenses/MIT
8+
*/
9+
10+
namespace fab2s\Math\Laravel;
11+
12+
use fab2s\Math\Laravel\Exception\NotNullableException;
13+
use fab2s\Math\Math;
14+
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
15+
use Illuminate\Database\Eloquent\Model;
16+
17+
class MathCast implements CastsAttributes
18+
{
19+
protected bool $isNullable = false;
20+
21+
public function __construct(...$options)
22+
{
23+
$this->isNullable = in_array('nullable', $options);
24+
}
25+
26+
/**
27+
* Cast the given value.
28+
*
29+
* @param Model $model
30+
*
31+
* @throws NotNullableException
32+
*/
33+
public function get($model, string $key, $value, array $attributes): ?Math
34+
{
35+
return Math::isNumber($value) ? Math::number($value) : $this->handleNullable($model, $key);
36+
}
37+
38+
/**
39+
* Prepare the given value for storage.
40+
*
41+
* @param Model $model
42+
*
43+
* @throws NotNullableException
44+
*/
45+
public function set($model, string $key, $value, array $attributes): ?string
46+
{
47+
return Math::isNumber($value) ? (string) Math::number($value) : $this->handleNullable($model, $key);
48+
}
49+
50+
/**
51+
* @throws NotNullableException
52+
*/
53+
protected function handleNullable(Model $model, string $key)
54+
{
55+
return $this->isNullable ? null : throw NotNullableException::make($key, $model);
56+
}
57+
}

src/Math.php

+11-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Math extends MathOpsAbstract implements JsonSerializable, Stringable
2121
public function __construct(string|int|float|Math $number)
2222
{
2323
if (isset(static::$globalPrecision)) {
24+
/* @codeCoverageIgnore */
2425
$this->precision = static::$globalPrecision;
2526
}
2627

@@ -43,26 +44,26 @@ public static function make(string|int|float|Math $number): static
4344
}
4445

4546
/**
46-
* convert any based value bellow or equals to 64 to its decimal value
47+
* convert any based value bellow or equals to 62 to its decimal value
4748
*/
48-
public static function fromBase(string|int $number, int $base): static
49+
public static function fromBase(string $number, int $base): static
4950
{
50-
// trim base 64 padding char, only positive
51-
$number = trim($number, ' =-');
51+
// only positive
52+
$number = trim($number, ' -');
5253
if ($number === '' || str_contains($number, '.')) {
5354
throw new InvalidArgumentException('Argument number is not an integer');
5455
}
5556

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

61-
if (static::$gmpSupport && $base <= 62) {
63+
if (static::$gmpSupport) {
6264
return new static(static::baseConvert($number, $base, 10));
6365
}
6466

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

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

9495
/**
95-
* convert decimal value to any other base bellow or equals to 64
96+
* convert decimal value to any other base bellow or equals to 62
9697
*/
9798
public function toBase(string|int $base): string
9899
{
99100
if ($this->normalize()->hasDecimals()) {
100101
throw new InvalidArgumentException('Argument number is not an integer');
101102
}
102103

104+
static::validateBase($base = (int) static::validatePositiveInteger($base));
105+
103106
// do not mutate, only support positive integers
104107
$number = ltrim((string) $this, '-');
105-
if (static::$gmpSupport && $base <= 62) {
108+
if (static::$gmpSupport) {
106109
return static::baseConvert($number, 10, $base);
107110
}
108111

src/MathBaseAbstract.php

+6-16
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ abstract class MathBaseAbstract
2121
*/
2222
const PRECISION = 9;
2323

24-
/**
25-
* base <= 64 charlist
26-
*/
27-
const BASECHAR_64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
28-
2924
/**
3025
* base <= 62 char list
3126
*/
@@ -39,17 +34,16 @@ abstract class MathBaseAbstract
3934
/**
4035
* highest base supported
4136
*/
42-
const BASE_MAX = 64;
37+
const BASE_MAX = 62;
4338

4439
/**
45-
* base char cache for all supported bases (bellow 64)
40+
* base char cache for all supported bases (up to 62)
4641
*
4742
* @var array<int,string>
4843
*/
4944
protected static array $baseChars = [
5045
36 => self::BASECHAR_36,
5146
62 => self::BASECHAR_62,
52-
64 => self::BASECHAR_64,
5347
];
5448

5549
/**
@@ -130,7 +124,7 @@ public static function isNumber(string|int|float|Math|null $number): bool
130124
/**
131125
* Validation flavour of normalization logic
132126
*/
133-
public static function normalizeNumber(string|int|float|Math $number, string|int|null $default = null): ?string
127+
public static function normalizeNumber(string|int|float|Math|null $number, Math|string|int|float|null $default = null): ?string
134128
{
135129
if (! static::isNumber($number)) {
136130
return $default;
@@ -147,10 +141,6 @@ public static function getBaseChar(string|int $base): string
147141

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

150-
if ($base > 62) {
151-
return static::$baseChars[$base] = substr(static::BASECHAR_64, 0, $base);
152-
}
153-
154144
if ($base > 36) {
155145
return static::$baseChars[$base] = substr(static::BASECHAR_62, 0, $base);
156146
}
@@ -194,12 +184,12 @@ protected static function normalizeReal(string|int $number): string
194184
*/
195185
protected static function validateBase(int $base): void
196186
{
197-
if ($base < 2 || $base > self::BASE_MAX || ! static::gmpSupport() && $base > 62) {
198-
throw new InvalidArgumentException('Argument base is not valid, base 2 to ' . (static::gmpSupport() ? 64 : 62) . ' are supported');
187+
if ($base < 2 || $base > self::BASE_MAX) {
188+
throw new InvalidArgumentException('Argument base is not valid, base 2 to ' . self::BASE_MAX . ' are supported');
199189
}
200190
}
201191

202-
protected static function bcDec2Base(string|int $number, string|int $base, string $baseChar): string
192+
protected static function bcDec2Base(string $number, int $base, string $baseChar): string
203193
{
204194
$result = '';
205195
$numberLen = strlen($number);

tests/Laravel/Artifacts/CastModel.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of fab2s/Math.
5+
* (c) Fabrice de Stefanis / https://github.com/fab2s/Math
6+
* This source file is licensed under the MIT license which you will
7+
* find in the LICENSE file or at https://opensource.org/licenses/MIT
8+
*/
9+
10+
namespace fab2s\Math\Tests\Laravel\Artifacts;
11+
12+
use fab2s\Math\Laravel\MathCast;
13+
use Illuminate\Database\Eloquent\Model;
14+
15+
class CastModel extends Model
16+
{
17+
protected $table = 'table';
18+
protected $guarded = [];
19+
protected $casts = [
20+
'not_nullable' => MathCast::class,
21+
'nullable' => MathCast::class . ':nullable',
22+
];
23+
}

0 commit comments

Comments
 (0)