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

brick / math / 21924859204

11 Feb 2026 10:03PM UTC coverage: 98.916% (+0.002%) from 98.914%
21924859204

push

github

BenMorel
Update README

[skip CI]

1369 of 1384 relevant lines covered (98.92%)

2305.7 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
     * @pure
37
     */
38
    public static function computeScaleFromReducedFractionDenominator(string $denominator): ?int
39
    {
40
        $calculator = CalculatorRegistry::get();
705✔
41

42
        $d = rtrim($denominator, '0');
705✔
43
        $scale = strlen($denominator) - strlen($d);
705✔
44

45
        foreach ([5, 2] as $prime) {
705✔
46
            for (; ;) {
47
                $lastDigit = (int) $d[-1];
705✔
48

49
                if ($lastDigit % $prime !== 0) {
705✔
50
                    break;
705✔
51
                }
52

53
                $d = $calculator->divQ($d, (string) $prime);
360✔
54
                $scale++;
705✔
55
            }
56
        }
57

58
        return $d === '1' ? $scale : null;
705✔
59
    }
60

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

79
        if ($scaled !== null) {
5,709✔
80
            return $scaled;
2,229✔
81
        }
82

83
        if ($roundingMode === RoundingMode::Unnecessary) {
3,480✔
84
            return null;
51✔
85
        }
86

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

89
        return CalculatorRegistry::get()->divRound($value, $divisor, $roundingMode);
3,429✔
90
    }
91

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

106
        if ($negative) {
6,807✔
107
            $length--;
330✔
108
        }
109

110
        if ($length >= $targetLength) {
6,807✔
111
            return $value;
4,926✔
112
        }
113

114
        if ($negative) {
1,917✔
115
            $value = substr($value, 1);
225✔
116
        }
117

118
        $value = str_pad($value, $targetLength, '0', STR_PAD_LEFT);
1,917✔
119

120
        if ($negative) {
1,917✔
121
            $value = '-' . $value;
225✔
122
        }
123

124
        return $value;
1,917✔
125
    }
126

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

144
        if ($targetScale > $currentScale) {
5,817✔
145
            return $value . str_repeat('0', $targetScale - $currentScale);
12✔
146
        }
147

148
        $negative = ($value[0] === '-');
5,805✔
149
        if ($negative) {
5,805✔
150
            $value = substr($value, 1);
51✔
151
        }
152

153
        $value = self::padUnscaledValue($value, $currentScale);
5,805✔
154
        $discardedDigits = $currentScale - $targetScale;
5,805✔
155

156
        if (substr($value, -$discardedDigits) !== str_repeat('0', $discardedDigits)) {
5,805✔
157
            return null;
3,537✔
158
        }
159

160
        $value = substr($value, 0, -$discardedDigits);
2,268✔
161
        $value = ltrim($value, '0');
2,268✔
162

163
        if ($value === '') {
2,268✔
164
            return '0';
×
165
        }
166

167
        if ($negative) {
2,268✔
168
            $value = '-' . $value;
33✔
169
        }
170

171
        return $value;
2,268✔
172
    }
173
}
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