Skip to content

Commit a5f8f34

Browse files
authored
Remove GMP dependency and switch to BC math for arithmetic operations.
* refactor: Remove GMP dependency and switch to BC math for arithmetic operations.
1 parent 3698734 commit a5f8f34

File tree

9 files changed

+152
-84
lines changed

9 files changed

+152
-84
lines changed

.github/workflows/ci.yml

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,44 @@ on:
77
permissions:
88
contents: read
99

10+
env:
11+
PHP_VERSION: '8.3'
12+
1013
jobs:
1114
auto-review:
1215
name: Auto review
1316
runs-on: ubuntu-latest
1417

1518
steps:
1619
- name: Checkout
17-
uses: actions/checkout@v3
20+
uses: actions/checkout@v4
1821

19-
- name: Use PHP 8.2
22+
- name: Configure PHP
2023
uses: shivammathur/setup-php@v2
2124
with:
22-
php-version: '8.2'
25+
php-version: ${{ env.PHP_VERSION }}
2326

2427
- name: Install dependencies
2528
run: composer update --no-progress --optimize-autoloader
2629

27-
- name: Run phpcs
28-
run: composer phpcs
29-
30-
- name: Run phpmd
31-
run: composer phpmd
30+
- name: Run review
31+
run: composer review
3232

3333
tests:
3434
name: Tests
3535
runs-on: ubuntu-latest
3636

3737
steps:
3838
- name: Checkout
39-
uses: actions/checkout@v3
39+
uses: actions/checkout@v4
4040

41-
- name: Use PHP 8.2
41+
- name: Use PHP ${{ env.PHP_VERSION }}
4242
uses: shivammathur/setup-php@v2
4343
with:
44-
php-version: '8.2'
44+
php-version: ${{ env.PHP_VERSION }}
4545

4646
- name: Install dependencies
4747
run: composer update --no-progress --optimize-autoloader
4848

49-
- name: Run unit tests
50-
env:
51-
XDEBUG_MODE: coverage
52-
run: composer test
53-
54-
- name: Run mutation tests
55-
run: composer test-mutation
49+
- name: Run tests
50+
run: composer tests

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.2
1+
DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.3
22

33
.PHONY: configure test test-file test-no-coverage review show-reports clean
44

composer.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,18 @@
4040
}
4141
},
4242
"require": {
43-
"php": "^8.2",
44-
"ext-gmp": "*"
43+
"php": "^8.3",
44+
"ext-bcmath": "*"
4545
},
4646
"require-dev": {
4747
"phpmd/phpmd": "^2.15",
4848
"phpunit/phpunit": "^11",
4949
"phpstan/phpstan": "^1",
5050
"infection/infection": "^0.29",
51-
"squizlabs/php_codesniffer": "^3.10"
51+
"squizlabs/php_codesniffer": "^3.11"
5252
},
5353
"suggest": {
54-
"ext-gmp": "Enables faster math with arbitrary-precision integers using GMP."
54+
"ext-bcmath": "Enables the extension which is an interface to the GNU implementation as a Basic Calculator utility library."
5555
},
5656
"scripts": {
5757
"phpcs": "phpcs --standard=PSR12 --extensions=php ./src",
@@ -60,7 +60,6 @@
6060
"test": "phpunit --log-junit=report/coverage/junit.xml --coverage-xml=report/coverage/coverage-xml --coverage-html=report/coverage/coverage-html tests",
6161
"test-mutation": "infection --only-covered --logger-html=report/coverage/mutation-report.html --coverage=report/coverage --min-msi=100 --min-covered-msi=100 --threads=4",
6262
"test-no-coverage": "phpunit --no-coverage",
63-
"test-mutation-no-coverage": "infection --only-covered --min-msi=100 --threads=4",
6463
"review": [
6564
"@phpcs",
6665
"@phpmd",
@@ -71,8 +70,7 @@
7170
"@test-mutation"
7271
],
7372
"tests-no-coverage": [
74-
"@test-no-coverage",
75-
"@test-mutation-no-coverage"
73+
"@test-no-coverage"
7674
]
7775
}
7876
}

infection.json.dist

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"timeout": 10,
2+
"timeout": 30,
33
"testFramework": "phpunit",
44
"tmpDir": "report/infection/",
55
"source": {
@@ -13,7 +13,12 @@
1313
},
1414
"mutators": {
1515
"@default": true,
16+
"BCMath": false,
17+
"CastInt": false,
18+
"Increment": false,
19+
"GreaterThan": false,
1620
"UnwrapSubstr": false,
21+
"UnwrapStrToLower": false,
1722
"LogicalAndNegation": false,
1823
"LogicalAndAllSubExprNegation": false
1924
},

src/Base62.php

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44

55
namespace TinyBlocks\Encoder;
66

7+
use TinyBlocks\Encoder\Internal\Decimal;
78
use TinyBlocks\Encoder\Internal\Exceptions\InvalidDecoding;
89
use TinyBlocks\Encoder\Internal\Hexadecimal;
910

1011
final readonly class Base62 implements Encoder
1112
{
12-
private const BASE62_RADIX = 62;
13-
private const BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
14-
private const BASE62_CHARACTER_LENGTH = 1;
15-
private const BASE62_HEXADECIMAL_RADIX = 16;
13+
public const int BASE62_RADIX = 62;
14+
private const int BASE62_CHARACTER_LENGTH = 1;
15+
16+
private const string BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
1617

1718
private function __construct(private string $value)
1819
{
@@ -25,18 +26,18 @@ public static function from(string $value): Encoder
2526

2627
public function encode(): string
2728
{
28-
$hexadecimal = Hexadecimal::fromBinary(binary: $this->value);
29-
$bytes = $hexadecimal->removeLeadingZeroBytes();
29+
$hexadecimal = Hexadecimal::fromBinary(binary: $this->value, alphabet: self::BASE62_ALPHABET);
30+
$hexadecimal = $hexadecimal->removeLeadingZeroBytes();
3031

31-
$base62 = str_repeat(self::BASE62_ALPHABET[0], $bytes);
32+
$base62 = str_repeat(self::BASE62_ALPHABET[0], $hexadecimal->getBytes());
3233

3334
if ($hexadecimal->isEmpty()) {
3435
return $base62;
3536
}
3637

37-
$number = $hexadecimal->toGmpInit(base: self::BASE62_HEXADECIMAL_RADIX);
38+
$base62Value = $hexadecimal->toBase(base: self::BASE62_RADIX);
3839

39-
return sprintf('%s%s', $base62, gmp_strval($number, self::BASE62_RADIX));
40+
return sprintf('%s%s', $base62, $base62Value);
4041
}
4142

4243
public function decode(): string
@@ -57,16 +58,13 @@ public function decode(): string
5758
return str_repeat("\x00", $bytes);
5859
}
5960

60-
$number = gmp_init($value, self::BASE62_RADIX);
61-
$hexadecimal = Hexadecimal::fromGmp(number: $number, base: self::BASE62_HEXADECIMAL_RADIX);
62-
$hexadecimal->padLeft();
63-
64-
$binary = hex2bin(sprintf('%s%s', str_repeat('00', $bytes), $hexadecimal->toString()));
61+
$decimal = Decimal::fromBase62(number: $value, alphabet: self::BASE62_ALPHABET);
62+
$hexadecimal = Hexadecimal::from(value: $decimal->toHexadecimal())
63+
->fillWithZeroIfNecessary()
64+
->toString();
6565

66-
if (!is_string($binary)) {
67-
throw new InvalidDecoding(value: $this->value);
68-
}
66+
$binary = hex2bin($hexadecimal);
6967

70-
return $binary;
68+
return sprintf('%s%s', str_repeat("\x00", $bytes), $binary);
7169
}
7270
}

src/Internal/Decimal.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TinyBlocks\Encoder\Internal;
6+
7+
use TinyBlocks\Encoder\Base62;
8+
9+
final readonly class Decimal
10+
{
11+
private function __construct(private string $value)
12+
{
13+
}
14+
15+
public static function fromBase62(string $number, string $alphabet): Decimal
16+
{
17+
$value = '0';
18+
$length = strlen($number);
19+
20+
for ($index = 0; $index < $length; $index++) {
21+
$digit = strpos($alphabet, $number[$index]);
22+
$value = bcmul($value, (string)Base62::BASE62_RADIX);
23+
$value = bcadd($value, (string)$digit);
24+
}
25+
26+
return new Decimal(value: $value);
27+
}
28+
29+
public function toHexadecimal(): string
30+
{
31+
$value = $this->value;
32+
$hexadecimalValue = '';
33+
34+
while (bccomp($value, '0') > 0) {
35+
$remainder = bcmod($value, Hexadecimal::HEXADECIMAL_RADIX);
36+
$hexadecimalValue = sprintf('%s%s', Hexadecimal::HEXADECIMAL_ALPHABET[(int)$remainder], $hexadecimalValue);
37+
$value = bcdiv($value, Hexadecimal::HEXADECIMAL_RADIX);
38+
}
39+
40+
return $hexadecimalValue;
41+
}
42+
}

src/Internal/Exceptions/InvalidDecoding.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ final class InvalidDecoding extends RuntimeException
1111
public function __construct(private readonly string $value)
1212
{
1313
$template = 'The value <%s> could not be decoded.';
14+
1415
parent::__construct(message: sprintf($template, $this->value));
1516
}
1617
}

src/Internal/Hexadecimal.php

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,86 @@
44

55
namespace TinyBlocks\Encoder\Internal;
66

7-
use GMP;
8-
9-
final class Hexadecimal
7+
final readonly class Hexadecimal
108
{
11-
private const HEXADECIMAL_BYTE_LENGTH = 2;
9+
private const int DEFAULT_BYTE_COUNT = 0;
10+
private const int HEXADECIMAL_BYTE_LENGTH = 2;
1211

13-
private string $value;
12+
public const string HEXADECIMAL_RADIX = '16';
13+
public const string HEXADECIMAL_ALPHABET = '0123456789abcdef';
1414

15-
private function __construct(string $value)
16-
{
17-
$this->value = $value;
15+
private function __construct(
16+
private string $value,
17+
private string $alphabet,
18+
private int $bytes = self::DEFAULT_BYTE_COUNT
19+
) {
1820
}
1921

20-
public static function fromGmp(GMP $number, int $base): Hexadecimal
22+
public static function from(string $value): Hexadecimal
2123
{
22-
return new Hexadecimal(value: gmp_strval($number, $base));
24+
return new Hexadecimal(value: $value, alphabet: self::HEXADECIMAL_ALPHABET);
2325
}
2426

25-
public static function fromBinary(string $binary): Hexadecimal
27+
public static function fromBinary(string $binary, string $alphabet): Hexadecimal
2628
{
27-
return new Hexadecimal(value: bin2hex($binary));
29+
return new Hexadecimal(value: bin2hex($binary), alphabet: $alphabet);
2830
}
2931

30-
public function isEmpty(): bool
32+
public function removeLeadingZeroBytes(): Hexadecimal
3133
{
32-
return empty($this->value);
34+
$bytes = 0;
35+
$newValue = $this->value;
36+
37+
while (str_starts_with($newValue, '00')) {
38+
$bytes++;
39+
$newValue = substr($newValue, self::HEXADECIMAL_BYTE_LENGTH);
40+
}
41+
42+
return new Hexadecimal(value: $newValue, alphabet: $this->alphabet, bytes: $bytes);
3343
}
3444

35-
public function padLeft(): void
45+
public function fillWithZeroIfNecessary(): Hexadecimal
3646
{
37-
if (strlen($this->value) % 2 !== 0) {
38-
$this->value = sprintf('0%s', $this->value);
39-
}
47+
$newValue = strlen($this->value) % 2 !== 0 ? sprintf('0%s', $this->value) : $this->value;
48+
49+
return new Hexadecimal(value: $newValue, alphabet: $this->alphabet, bytes: $this->bytes);
4050
}
4151

42-
public function toString(): string
52+
public function getBytes(): int
4353
{
44-
return $this->value;
54+
return $this->bytes;
4555
}
4656

47-
public function toGmpInit(int $base): GMP
57+
public function isEmpty(): bool
4858
{
49-
return gmp_init($this->value, $base);
59+
return empty($this->value);
5060
}
5161

52-
public function removeLeadingZeroBytes(): int
62+
public function toBase(int $base): string
5363
{
54-
$bytes = 0;
64+
$length = strlen($this->value);
65+
$decimalValue = '0';
5566

56-
while (str_starts_with($this->value, '00')) {
57-
$bytes++;
58-
$this->value = substr($this->value, self::HEXADECIMAL_BYTE_LENGTH);
67+
for ($index = 0; $index < $length; $index++) {
68+
$digit = strpos(self::HEXADECIMAL_ALPHABET, strtolower($this->value[$index]));
69+
$decimalValue = bcmul($decimalValue, self::HEXADECIMAL_RADIX);
70+
$decimalValue = bcadd($decimalValue, (string)$digit);
71+
}
72+
73+
$digits = $this->alphabet;
74+
$result = '';
75+
76+
while (bccomp($decimalValue, '0') > 0) {
77+
$remainder = bcmod($decimalValue, (string)$base);
78+
$result = sprintf('%s%s', $digits[(int)$remainder], $result);
79+
$decimalValue = bcdiv($decimalValue, (string)$base);
5980
}
6081

61-
return $bytes;
82+
return $result ?: '0';
83+
}
84+
85+
public function toString(): string
86+
{
87+
return $this->value;
6288
}
6389
}

0 commit comments

Comments
 (0)