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

brick / math / 21909383256

11 Feb 2026 02:36PM UTC coverage: 98.912% (-0.001%) from 98.913%
21909383256

push

github

BenMorel
Improve documentation

1 of 1 new or added line in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

1364 of 1379 relevant lines covered (98.91%)

2318.5 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
    {
UNCOV
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();
702✔
41

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

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

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

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

58
        return $d === '1' ? $scale : null;
702✔
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,669✔
103
        $negative = ($value[0] === '-');
6,669✔
104
        $length = strlen($value);
6,669✔
105

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

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

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

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

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

124
        return $value;
1,857✔
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
    private static function tryScaleExactly(string $value, int $currentScale, int $targetScale): ?string
139
    {
140
        if ($value === '0') {
5,709✔
141
            return $value;
9✔
142
        }
143

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

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

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

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

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

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

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

171
        return $value;
2,208✔
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