Skip to content

Commit 38d8724

Browse files
committed
Added significant figure formatting.
1 parent d7c5f8d commit 38d8724

File tree

4 files changed

+143
-11
lines changed

4 files changed

+143
-11
lines changed

.github/workflows/Tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: Tests
33
on:
44
push:
55
pull_request:
6+
workflow_dispatch:
67
schedule:
78
- cron: 0 6 * * *
89

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ Increasing the default precision with `setPrecision()` allows the specified numb
4545
```
4646
> 512.55 KiB
4747
48-
Increasing the precision will increase the maximum digits allowed but the formatter will only display as many as
49-
needed.
48+
Increasing the precision will increase the maximum digits allowed, but the formatter will only display as many as needed.
5049

5150
```php
5251
(new ByteFormatter)->setPrecision(2)->format(0x80200);
@@ -67,6 +66,28 @@ The default precision can be overridden by passing the second argument to `forma
6766
```
6867
> 512.5498 KiB
6968
69+
Significant figures
70+
-------------------
71+
72+
Formatting by the specified number of significant figures by calling `setSignificantFigures()`. This is mutually exclusive with precision scaling such that whichever method is called last will be used.
73+
74+
```php
75+
(new ByteFormatter)->setBase(Base::DECIMAL)->setSignificantFigures(2)->format(123);
76+
```
77+
> 120
78+
79+
```php
80+
(new ByteFormatter)->setBase(Base::DECIMAL)->setSignificantFigures(2)->format(1234);
81+
```
82+
> 1.2K
83+
84+
```php
85+
(new ByteFormatter)->setBase(Base::DECIMAL)->setSignificantFigures(3)->format(1234);
86+
```
87+
> 1.23K
88+
89+
This is particularly useful for keeping the display width of formatted numbers predicable.
90+
7091
Output format
7192
-------------
7293

src/Byte/ByteFormatter.php

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class ByteFormatter
2121

2222
private ?int $exponent = null;
2323

24+
private ?int $significantFigures = null;
25+
2426
private UnitDecorator $unitDecorator;
2527

2628
/**
@@ -52,26 +54,49 @@ public function format(int|float $bytes, ?int $precision = null): string
5254

5355
$log = log($bytes, $this->getBase());
5456
$exponent = $this->hasFixedExponent() ? $this->getFixedExponent() : max(0, (int)$log);
55-
$value = round($this->getBase() ** ($log - $exponent), $precision);
57+
$rawValue = $this->getBase() ** ($log - $exponent);
58+
$value = $this->significantFigures ? $this->roundToSignificantFigures($rawValue) : round($rawValue, $precision);
5659
$units = $this->getUnitDecorator()->decorate($exponent, $this->getBase(), $value);
5760

5861
return trim(sprintf($this->sprintfFormat, $this->formatValue($value, $precision), $units));
5962
}
6063

6164
/**
62-
* Formats the specified number with the specified precision.
65+
* Rounds the specified value to the number of significant figures defined for this instance.
66+
*
67+
* @param float $value Value.
68+
*
69+
* @return float Rounded value.
70+
*/
71+
private function roundToSignificantFigures(float $value): float
72+
{
73+
if ($value === 0.) {
74+
return 0.;
75+
}
76+
77+
$factor = 10 ** ($this->significantFigures - floor(log10(abs($value))) - 1);
78+
79+
return round($value * $factor) / $factor;
80+
}
81+
82+
/**
83+
* Formats the specified number with the specified precision or significant figures.
6384
*
64-
* If precision scaling is enabled the precision may be reduced when it
65-
* contains insignificant digits. If the fractional part is zero it will
85+
* If precision scaling is enabled, the precision may be reduced when it
86+
* contains insignificant digits. If the fractional part is zero, it will
6687
* be completely removed.
6788
*
6889
* @param float $value Number.
69-
* @param int $precision Number of fractional digits.
90+
* @param int $precision Number of fractional digits. Ignored when significant figures are set.
7091
*
7192
* @return string Formatted number.
7293
*/
7394
private function formatValue(float $value, int $precision): string
7495
{
96+
if ($this->significantFigures !== null) {
97+
return (string)$value;
98+
}
99+
75100
$formatted = sprintf("%0.{$precision}F", $value);
76101

77102
if ($this->hasAutomaticPrecision()) {
@@ -115,7 +140,7 @@ public function getBase(): int
115140
}
116141

117142
/**
118-
* Sets the exponentiation base which should usually be a Base constant.
143+
* Sets the exponentiation base, which should usually be a Base constant.
119144
*
120145
* @param int $base Exponentiation base.
121146
*
@@ -166,13 +191,14 @@ public function getPrecision(): int
166191
/**
167192
* Sets the maximum number of fractional digits.
168193
*
169-
* @param int $precision
194+
* @param int $precision Fractional digits.
170195
*
171196
* @return $this
172197
*/
173198
public function setPrecision(int $precision): self
174199
{
175200
$this->precision = $precision;
201+
$this->significantFigures = null;
176202

177203
return $this;
178204
}
@@ -204,8 +230,7 @@ public function disableAutomaticPrecision(): self
204230
/**
205231
* Gets a value indicating whether precision will be scaled automatically.
206232
*
207-
* @return bool True if precision will be scaled automatically, otherwise
208-
* false.
233+
* @return bool True if precision is scaled automatically, otherwise false.
209234
*/
210235
public function hasAutomaticPrecision(): bool
211236
{
@@ -258,6 +283,18 @@ public function hasFixedExponent(): bool
258283
return $this->exponent !== null;
259284
}
260285

286+
/**
287+
* Sets the number of significant figures.
288+
*
289+
* @param int $significantFigures Number of significant figures.
290+
*/
291+
public function setSignificantFigures(int $significantFigures): self
292+
{
293+
$this->significantFigures = $significantFigures;
294+
295+
return $this;
296+
}
297+
261298
/**
262299
* Gets the unit decorator.
263300
*

test/Integration/Byte/ByteFormatterTest.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,77 @@ public function testCustomUnitSequence(): void
177177

178178
self::assertSame('1 foo', $formatter->format(1));
179179
}
180+
181+
/** @dataProvider provideSignificantFigures */
182+
public function testSignificantFigures(int $significantFigures, int $in, string $out): void
183+
{
184+
$formatter = $this->formatter->setBase(Base::DECIMAL)->setSignificantFigures($significantFigures);
185+
186+
self::assertSame($out, $formatter->format($in));
187+
}
188+
189+
public static function provideSignificantFigures(): iterable
190+
{
191+
return [
192+
// Small numbers.
193+
[1, 1, '1'],
194+
[2, 1, '1'],
195+
[1, 12, '10'],
196+
[2, 12, '12'],
197+
[3, 12, '12'],
198+
[1, 123, '100'],
199+
[2, 123, '120'],
200+
[3, 123, '123'],
201+
202+
// Thousands.
203+
[1, 1_234, '1K'],
204+
[2, 1_234, '1.2K'],
205+
[3, 1_234, '1.23K'],
206+
[4, 1_234, '1.234K'],
207+
208+
[1, 12_345, '10K'],
209+
[2, 12_345, '12K'],
210+
[3, 12_345, '12.3K'],
211+
[4, 12_345, '12.35K'],
212+
[5, 12_345, '12.345K'],
213+
214+
// Millions.
215+
[1, 1_234_567, '1M'],
216+
[2, 1_234_567, '1.2M'],
217+
[3, 1_234_567, '1.23M'],
218+
[4, 1_234_567, '1.235M'],
219+
[7, 1_234_567, '1.234567M'],
220+
221+
// Billions.
222+
[1, 12_345_678_901, '10G'],
223+
[2, 12_345_678_901, '12G'],
224+
[3, 12_345_678_901, '12.3G'],
225+
[4, 12_345_678_901, '12.35G'],
226+
227+
// Trillions.
228+
[1, 1_234_567_890_123, '1T'],
229+
[2, 1_234_567_890_123, '1.2T'],
230+
[3, 1_234_567_890_123, '1.23T'],
231+
[4, 1_234_567_890_123, '1.235T'],
232+
233+
// Quadrillions.
234+
[1, 1_234_567_890_123_456, '1P'],
235+
[2, 1_234_567_890_123_456, '1.2P'],
236+
[3, 1_234_567_890_123_456, '1.23P'],
237+
[4, 1_234_567_890_123_456, '1.235P'],
238+
239+
// PHP_INT_MAX boundary case.
240+
[1, 9_223_372_036_854_775_807, '9E'],
241+
[2, 9_223_372_036_854_775_807, '9.2E'],
242+
[3, 9_223_372_036_854_775_807, '9.22E'],
243+
[4, 9_223_372_036_854_775_807, '9.223E'],
244+
];
245+
}
246+
247+
public function testClearingSigFigFallsBackToPrecision(): void
248+
{
249+
$formatter = $this->formatter->setBase(Base::DECIMAL)->setSignificantFigures(3)->setPrecision(2);
250+
251+
self::assertSame('908.61K', $formatter->format(908_614));
252+
}
180253
}

0 commit comments

Comments
 (0)