• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brick / math / 22119510950

17 Feb 2026 11:13PM UTC coverage: 98.99%. Remained the same
22119510950

push

github

BenMorel
Update README

[skip CI]

1372 of 1386 relevant lines covered (98.99%)

2345.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.92
/src/Internal/DecimalHelper.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Math\Internal;
6

7
use Brick\Math\RoundingMode;
8

9
use function ltrim;
10
use function rtrim;
11
use function str_pad;
12
use function str_repeat;
13
use function strlen;
14
use function substr;
15

16
use const STR_PAD_LEFT;
17

18
/**
19
 * Shared helper for decimal operations.
20
 *
21
 * @internal
22
 */
23
final class DecimalHelper
24
{
25
    private function __construct()
26
    {
27
    }
×
28

29
    /**
30
     * Computes the scale needed to represent the exact decimal result of a reduced fraction.
31
     *
32
     * Returns null if the denominator has prime factors other than 2 or 5.
33
     *
34
     * @param string $denominator The denominator of the reduced fraction. Must be strictly positive.
35
     *
36
     * @return non-negative-int|null
37
     *
38
     * @pure
39
     */
40
    public static function computeScaleFromReducedFractionDenominator(string $denominator): ?int
41
    {
42
        $calculator = CalculatorRegistry::get();
705✔
43

44
        $d = rtrim($denominator, '0');
705✔
45

46
        /** @var non-negative-int $scale rtrim can only shorten a string */
47
        $scale = strlen($denominator) - strlen($d);
705✔
48

49
        foreach ([5, 2] as $prime) {
705✔
50
            for (; ;) {
51
                $lastDigit = (int) $d[-1];
705✔
52

53
                if ($lastDigit % $prime !== 0) {
705✔
54
                    break;
705✔
55
                }
56

57
                $d = $calculator->divQ($d, (string) $prime);
360✔
58
                $scale++;
705✔
59
            }
60
        }
61

62
        return $d === '1' ? $scale : null;
705✔
63
    }
64

65
    /**
66
     * Scales an unscaled decimal value to the requested scale.
67
     *
68
     * Returns null when rounding is necessary and the rounding mode is Unnecessary.
69
     *
70
     * @param string       $value        The unscaled value.
71
     * @param int          $currentScale The current scale.
72
     * @param int          $targetScale  The target scale.
73
     * @param RoundingMode $roundingMode The rounding mode.
74
     *
75
     * @return string|null The unscaled value at the target scale, or null if RoundingMode::Unnecessary is used and rounding is necessary.
76
     *
77
     * @pure
78
     */
79
    public static function scale(string $value, int $currentScale, int $targetScale, RoundingMode $roundingMode): ?string
80
    {
81
        $scaled = self::tryScaleExactly($value, $currentScale, $targetScale);
5,709✔
82

83
        if ($scaled !== null) {
5,709✔
84
            return $scaled;
2,229✔
85
        }
86

87
        if ($roundingMode === RoundingMode::Unnecessary) {
3,480✔
88
            return null;
51✔
89
        }
90

91
        $divisor = '1' . str_repeat('0', $currentScale - $targetScale);
3,429✔
92

93
        return CalculatorRegistry::get()->divRound($value, $divisor, $roundingMode);
3,429✔
94
    }
95

96
    /**
97
     * Adds leading zeros if necessary to represent the full decimal number.
98
     *
99
     * @param string $value The unscaled value.
100
     * @param int    $scale The current scale.
101
     *
102
     * @pure
103
     */
104
    public static function padUnscaledValue(string $value, int $scale): string
105
    {
106
        $targetLength = $scale + 1;
9,114✔
107
        $negative = ($value[0] === '-');
9,114✔
108
        $length = strlen($value);
9,114✔
109

110
        if ($negative) {
9,114✔
111
            $length--;
1,185✔
112
        }
113

114
        if ($length >= $targetLength) {
9,114✔
115
            return $value;
6,492✔
116
        }
117

118
        if ($negative) {
2,658✔
119
            $value = substr($value, 1);
372✔
120
        }
121

122
        $value = str_pad($value, $targetLength, '0', STR_PAD_LEFT);
2,658✔
123

124
        if ($negative) {
2,658✔
125
            $value = '-' . $value;
372✔
126
        }
127

128
        return $value;
2,658✔
129
    }
130

131
    /**
132
     * Tries to scale exactly without rounding, returning null when rounding would be required.
133
     *
134
     * @param string $value        The unscaled value.
135
     * @param int    $currentScale The current scale.
136
     * @param int    $targetScale  The target scale.
137
     *
138
     * @return string|null The unscaled value at the target scale, or null if rounding would be required.
139
     *
140
     * @pure
141
     */
142
    public static function tryScaleExactly(string $value, int $currentScale, int $targetScale): ?string
143
    {
144
        if ($value === '0' || $targetScale === $currentScale) {
5,967✔
145
            return $value;
168✔
146
        }
147

148
        if ($targetScale > $currentScale) {
5,817✔
149
            return $value . str_repeat('0', $targetScale - $currentScale);
12✔
150
        }
151

152
        $negative = ($value[0] === '-');
5,805✔
153
        if ($negative) {
5,805✔
154
            $value = substr($value, 1);
51✔
155
        }
156

157
        $value = self::padUnscaledValue($value, $currentScale);
5,805✔
158
        $discardedDigits = $currentScale - $targetScale;
5,805✔
159

160
        if (substr($value, -$discardedDigits) !== str_repeat('0', $discardedDigits)) {
5,805✔
161
            return null;
3,537✔
162
        }
163

164
        $value = substr($value, 0, -$discardedDigits);
2,268✔
165
        $value = ltrim($value, '0');
2,268✔
166

167
        if ($value === '') {
2,268✔
168
            return '0';
×
169
        }
170

171
        if ($negative) {
2,268✔
172
            $value = '-' . $value;
33✔
173
        }
174

175
        return $value;
2,268✔
176
    }
177
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc