Fix ToMetric() rounding boundary issue for doubles #1695#1696
Conversation
fde266f to
64050e9
Compare
git push --force origin fix/tometric-rounding-1695# modified: tests/Humanizer.Tests/MetricNumeralTests.cs
64050e9 to
a8079ca
Compare
There was a problem hiding this comment.
Pull request overview
Fixes an edge case in ToMetric(double, ...) where rounding can push values across a 1000^n boundary (e.g., 999.9 → 1000) but the representation logic previously stayed on the non-metric formatting path.
Changes:
- Adjust
BuildRepresentation(double, ...)to re-evaluate the metric exponent after rounding when the initial exponent is0. - Add theory cases covering boundary-rounding scenarios (positive/negative, different decimal settings) to prevent regressions.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/Humanizer/MetricNumeralExtensions.cs |
Re-checks exponent based on the rounded value to correctly switch to metric formatting when rounding crosses a boundary. |
tests/Humanizer.Tests/MetricNumeralTests.cs |
Adds coverage for rounding-to-boundary cases like 999.9 → 1k and related edge conditions. |
Comments suppressed due to low confidence (1)
src/Humanizer/MetricNumeralExtensions.cs:407
- In the
exponent == 0+decimals.HasValuepath,inputis rounded intorounded, but thenMath.Round(input, decimals.Value)is executed again when buildingrepresentation. Consider reusing the already-computedroundedfor the plain-number fallback to avoid duplicated work and to ensure the same rounded value is used consistently across both the boundary check and the final formatting.
if (decimals.HasValue)
{
var rounded = Math.Round(input, decimals.Value);
if (!rounded.Equals(0d))
{
var roundedExponent = (int)Math.Floor(Math.Log10(Math.Abs(rounded)) / 3);
if (!roundedExponent.Equals(0))
return BuildMetricRepresentation(rounded, roundedExponent, formats, decimals);
}
}
var representation = decimals.HasValue
? Math
.Round(input, decimals.Value)
.ToString()
|
This implementation rounds the number a second time needlessly, and repeats the scale calculation logic for what is conceptually a much simpler check: Proposed alternative: In this version:
This code is conceptually simpler and easier to maintain. I also removed extra braces around single-line blocks, because I noticed that this style is now explicitly called out in the PR checklist. |
Fixes #1695
Problem
Calling
ToMetric()on adoublethat rounds up to a power-of-1000boundary displays the raw number instead of the metric representation.
For example:
999.9.ToMetric(decimals: 0)returns"1000"instead of"1k"999.99.ToMetric(decimals: 1)returns"1000"instead of"1k"Root Cause
In
BuildRepresentation(double, ...), the exponent is calculated fromthe original input before rounding. For
999.9, the exponent evaluatesto
0, so the method skips the metric path entirely and falls throughto plain number formatting. The subsequent rounding then produces
1000with no metric suffix.
Fix
After detecting
exponent == 0, re-check the exponent on the roundedvalue before falling through to plain formatting. If rounding pushed the
number across a boundary, dispatch to
BuildMetricRepresentationinstead.Tests
Added inline test cases to the existing
ToMetrictheory inMetricNumeralTests.cscovering the reported case and edge cases.