Skip to content

Commit 53d49e2

Browse files
authored
Add long.ToMetric and associated tests (#1596)
1 parent 734eb9d commit 53d49e2

6 files changed

+187
-1
lines changed

readme.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,14 @@ Also, the reverse operation using the `FromMetric` extension.
994994
"100m".FromMetric() => 0.1
995995
```
996996

997+
The `int` and `long` data types are also supported:
998+
999+
```C#
1000+
((int)456789).ToMetric(decimals: 2) => "456.79k"
1001+
((int)456789).ToMetric(decimals: 20) => "456.78900000000000000000k"
1002+
long.MaxValue.ToMetric(decimals: 12) => "9.223372036855E"
1003+
```
1004+
9971005

9981006
### ByteSize
9991007

src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,7 @@ namespace Humanizer
800800
public static double FromMetric(this string input) { }
801801
public static string ToMetric(this double input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
802802
public static string ToMetric(this int input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
803+
public static string ToMetric(this long input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
803804
}
804805
[System.Flags]
805806
public enum MetricNumeralFormats

src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ namespace Humanizer
799799
public static double FromMetric(this string input) { }
800800
public static string ToMetric(this double input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
801801
public static string ToMetric(this int input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
802+
public static string ToMetric(this long input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
802803
}
803804
[System.Flags]
804805
public enum MetricNumeralFormats

src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,7 @@ namespace Humanizer
580580
public static double FromMetric(this string input) { }
581581
public static string ToMetric(this double input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
582582
public static string ToMetric(this int input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
583+
public static string ToMetric(this long input, Humanizer.MetricNumeralFormats? formats = default, int? decimals = default) { }
583584
}
584585
[System.Flags]
585586
public enum MetricNumeralFormats

src/Humanizer.Tests/MetricNumeralTests.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[UseCulture("en-US")]
1+
[UseCulture("en-US")]
22
public class MetricNumeralTests
33
{
44
// Return a sequence of -24 -> 26
@@ -75,6 +75,49 @@ public void TestAllSymbolsAsInt(int exponent)
7575
Assert.True(isEquals);
7676
}
7777

78+
[Theory]
79+
[InlineData(0, 0, "0")]
80+
[InlineData(0, 1, "0.0")]
81+
[InlineData(0, 3, "0.000")]
82+
[InlineData(0, 20, "0.00000000000000000000")]
83+
[InlineData(123, 0, "123")]
84+
[InlineData(123, 1, "123.0")]
85+
[InlineData(123, 3, "123.000")]
86+
[InlineData(123, 20, "123.00000000000000000000")]
87+
[InlineData(123456, null, "123.456k")]
88+
[InlineData(123456, 0, "123k")]
89+
[InlineData(123456, 1, "123.5k")]
90+
[InlineData(123456, 2, "123.46k")]
91+
[InlineData(123456, 3, "123.456k")]
92+
[InlineData(123456, 20, "123.45600000000000000000k")]
93+
[InlineData(123456789, null, "123.456789M")]
94+
[InlineData(123456789, 0, "123M")]
95+
[InlineData(123456789, 1, "123.5M")]
96+
[InlineData(123456789, 2, "123.46M")]
97+
[InlineData(123456789, 3, "123.457M")]
98+
[InlineData(123456789, 5, "123.45679M")]
99+
[InlineData(123456789, 20, "123.45678900000000000000M")]
100+
[InlineData(123456789987, 5, "123.45679G")]
101+
[InlineData(123456789987, 20, "123.45678998700000000000G")]
102+
[InlineData(123456789987654, 5, "123.45679T")]
103+
[InlineData(123456789987654, 20, "123.45678998765400000000T")]
104+
[InlineData(123456789987654321, 5, "123.45679P")]
105+
[InlineData(123456789987654321, 20, "123.45678998765432100000P")]
106+
[InlineData(9223372036854775807, null, "9.223372036854775807E")]
107+
[InlineData(9223372036854775807, 0, "9E")]
108+
[InlineData(9223372036854775807, 3, "9.223E")]
109+
[InlineData(9223372036854775807, 20, "9.22337203685477580700E")]
110+
[InlineData(-1, null, "-1")]
111+
[InlineData(-123, null, "-123")]
112+
[InlineData(-123456, null, "-123.456k")]
113+
[InlineData(-123456789, null, "-123.456789M")]
114+
[InlineData(-9223372036854775808, null, "-9.223372036854775808E")]
115+
[InlineData(-9223372036854775808, 0, "-9E")]
116+
[InlineData(-9223372036854775808, 3, "-9.223E")]
117+
[InlineData(-9223372036854775808, 20, "-9.22337203685477580800E")]
118+
public void TestAllSymbolsAsLong(long subject, int? decimals, string expected) =>
119+
Assert.Equal(expected, subject.ToMetric(decimals: decimals));
120+
78121
[Theory]
79122
[InlineData("1.3M", 1300000, null, null)]
80123
[InlineData("1.3million", 1300000, MetricNumeralFormats.UseShortScaleWord, null)]

src/Humanizer/MetricNumeralExtensions.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,34 @@ public static double FromMetric(this string input)
149149
public static string ToMetric(this int input, MetricNumeralFormats? formats = null, int? decimals = null) =>
150150
((double) input).ToMetric(formats, decimals);
151151

152+
/// <summary>
153+
/// Converts a number into a valid and Human-readable Metric representation.
154+
/// </summary>
155+
/// <remarks>
156+
/// Inspired by a snippet from Thom Smith.
157+
/// See <a href="http://stackoverflow.com/questions/12181024/formatting-a-number-with-a-metric-prefix">this link</a> for more.
158+
/// </remarks>
159+
/// <param name="input">Number to convert to a Metric representation.</param>
160+
/// <param name="formats">A bitwise combination of <see cref="MetricNumeralFormats"/> enumeration values that format the metric representation.</param>
161+
/// <param name="decimals">If not null it is the numbers of decimals to round the number to</param>
162+
/// <example>
163+
/// <code>
164+
/// 1000.ToMetric() => "1k"
165+
/// 123.ToMetric() => "123"
166+
/// 1E-1.ToMetric() => "100m"
167+
/// </code>
168+
/// </example>
169+
/// <returns>A valid Metric representation</returns>
170+
public static string ToMetric(this long input, MetricNumeralFormats? formats = null, int? decimals = null)
171+
{
172+
if (input.Equals(0) && (!decimals.HasValue || (decimals == 0)))
173+
{
174+
return input.ToString();
175+
}
176+
177+
return BuildRepresentation(input, formats, decimals);
178+
}
179+
152180
/// <summary>
153181
/// Converts a number into a valid and Human-readable Metric representation.
154182
/// </summary>
@@ -241,6 +269,110 @@ static string ReplaceNameBySymbol(string input) =>
241269
UnitPrefixes.Aggregate(input, (current, unitPrefix) =>
242270
current.Replace(unitPrefix.Value.Name, unitPrefix.Key.ToString()));
243271

272+
/// <summary>
273+
/// Build a Metric representation of the number.
274+
/// </summary>
275+
/// <param name="input">Number to convert to a Metric representation.</param>
276+
/// <param name="formats">A bitwise combination of <see cref="MetricNumeralFormats"/> enumeration values that format the metric representation.</param>
277+
/// <param name="decimals">If not null it is the numbers of decimals to round the number to</param>
278+
/// <returns>A number in a Metric representation</returns>
279+
static string BuildRepresentation(long input, MetricNumeralFormats? formats, int? decimals)
280+
{
281+
var number = Math.Abs(input / 10);
282+
var exponent = 0;
283+
284+
while (number > 0)
285+
{
286+
exponent++;
287+
number /= 10;
288+
}
289+
290+
var scale = exponent / 3;
291+
292+
if (!scale.Equals(0)) return BuildMetricRepresentation(input, scale, formats, decimals);
293+
294+
var representation = decimals > 0
295+
? $"{input}.{new string('0', decimals.Value)}"
296+
: input.ToString();
297+
if ((formats & MetricNumeralFormats.WithSpace) == MetricNumeralFormats.WithSpace)
298+
{
299+
representation += " ";
300+
}
301+
302+
return representation;
303+
}
304+
305+
/// <summary>
306+
/// Build a Metric representation of the number.
307+
/// </summary>
308+
/// <param name="input">Number to convert to a Metric representation.</param>
309+
/// <param name="scale">Number of times number should be divided by 1000.</param>
310+
/// <param name="formats">A bitwise combination of <see cref="MetricNumeralFormats"/> enumeration values that format the metric representation.</param>
311+
/// <param name="decimals">If not null it is the numbers of decimals to round the number to</param>
312+
/// <returns>A number in a Metric representation</returns>
313+
static string BuildMetricRepresentation(long input, int scale, MetricNumeralFormats? formats, int? decimals)
314+
{
315+
// Convert back to actual exponent (number of 10s places)
316+
var exponent = scale * 3;
317+
318+
var divisor = 1L;
319+
320+
for (var i=0; i < scale; i++)
321+
{
322+
divisor *= 1000;
323+
}
324+
325+
var number = input / divisor;
326+
var fractionalPart = Math.Abs(input % divisor); // input could be negative
327+
328+
if (Math.Abs(number) >= 1000 && exponent < Symbols[0].Count * 3)
329+
{
330+
exponent += 3;
331+
scale++;
332+
divisor *= 1000;
333+
334+
number = input / divisor;
335+
fractionalPart = Math.Abs(input % divisor);
336+
}
337+
338+
if (decimals.HasValue)
339+
{
340+
for (var i = decimals.Value; i < exponent; i++)
341+
{
342+
var roundUp = (i + 1 == exponent);
343+
344+
fractionalPart = (fractionalPart + (roundUp ? 5 : 0)) / 10;
345+
}
346+
}
347+
else
348+
{
349+
decimals = exponent;
350+
}
351+
352+
var symbol = Math.Sign(scale) == 1
353+
? Symbols[0][scale - 1]
354+
: Symbols[1][-scale - 1];
355+
356+
if (decimals == 0)
357+
{
358+
return number
359+
+ (formats.HasValue && formats.Value.HasFlag(MetricNumeralFormats.WithSpace) ? " " : string.Empty)
360+
+ GetUnitText(symbol, formats);
361+
}
362+
else
363+
{
364+
var decimalPlaces = Math.Min(decimals.Value, exponent);
365+
var extraZeroes = (decimals.Value - decimalPlaces);
366+
367+
return number
368+
+ CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator
369+
+ fractionalPart.ToString("d" + decimalPlaces)
370+
+ (extraZeroes <= 0 ? "" : new string('0', extraZeroes))
371+
+ (formats.HasValue && formats.Value.HasFlag(MetricNumeralFormats.WithSpace) ? " " : string.Empty)
372+
+ GetUnitText(symbol, formats);
373+
}
374+
}
375+
244376
/// <summary>
245377
/// Build a Metric representation of the number.
246378
/// </summary>

0 commit comments

Comments
 (0)