Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Measure of length #775

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 22 additions & 3 deletions readme.md
Expand Up @@ -894,12 +894,31 @@ Note that only integers smaller than 4000 can be converted to Roman numberals.
Humanizer can change numbers to Metric numerals using the `ToMetric` extension. The numbers 1, 1230 and 0.1 can be expressed in Metric numerals as follows:

```C#
1d.ToMetric() => "1"
1230d.ToMetric() => "1.23k"
1.ToMetric() => "1"
1230.ToMetric() => "1.23k"
0.1d.ToMetric() => "100m"
```

Also the reverse operation using the `FromMetric` extension.
`ToMetric` accepts a several named optional parameters:

Type | Name | Default | Description
------------ | ------------- | ------------- | -------------
bool | hasSpace | false | True adds a space between the number and metric prefix.
bool | useSymbol | true | False adds the full name of the metric prefix.
int? | decimals | null | Max number of decimals in the numeric part of the output.
MetricPrefix | maxPrefix | Undefined | Sets an upper limit to the used metric prefix.
MetricPrefix | minPrefix | Undefined | Sets a lower limit to the used metric prefix.

Examples:
```C#
1000.ToMetric(hasSpace: true) => "1 k"
1000.ToMetric(useSymbol: false) => "1kilo"
1234.ToMetric(decimals: 1) => "1.2k"
1E6.ToMetric(maxPrefix: MetricPrefix.Kilo) => "1000k"
1E-6.ToMetric(minPrefix: MetricPrefix.Milli) => "0.001m"
```

Humanizer also has the reverse operation using the `FromMetric` extension.

```C#
1d.ToMetric() => "1"
Expand Down
41 changes: 26 additions & 15 deletions src/Humanizer.Tests.Shared/MetricNumeralTests.cs
Expand Up @@ -90,24 +90,35 @@ public void TestAllSymbolsAsInt(int exponent)
}

[Theory]
[InlineData("0", 0d, false, true, null)]
[InlineData("123", 123d, false, true, null)]
[InlineData("-123", (-123d), false, true, null)]
[InlineData("1.23k", 1230d, false, true, null)]
[InlineData("1 k", 1000d, true, true, null)]
[InlineData("1 kilo", 1000d, true, false, null)]
[InlineData("1milli", 1E-3, false, false, null)]
[InlineData("1.23milli", 1.234E-3, false, false, 2)]
[InlineData("12.34k", 12345, false, true, 2)]
[InlineData("12k", 12345, false, true, 0)]
[InlineData("-3.9m", -3.91e-3, false, true, 1)]
public void ToMetric(string expected, double input, bool hasSpace, bool useSymbol, int? decimals)
[InlineData("0", 0d, false, true, null, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("123", 123d, false, true, null, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("-123", (-123d), false, true, null, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("1.23k", 1230d, false, true, null, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("1 k", 1000d, true, true, null, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("1 kilo", 1000d, true, false, null, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("1milli", 1E-3, false, false, null, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("1.23milli", 1.234E-3, false, false, 2, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("12.34k", 12345, false, true, 2, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("12k", 12345, false, true, 0, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("-3.9m", -3.91e-3, false, true, 1, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("-3.9m", -3.91e-3, false, true, 1, MetricPrefix.Mega, MetricPrefix.Undefined)]
[InlineData("1.23", 1.23, false, true, 1, MetricPrefix.Mega, MetricPrefix.Undefined)] // Expected behavior or bug?
[InlineData("1M", 1E6, false, true, null, MetricPrefix.Giga, MetricPrefix.Undefined)]
[InlineData("1000k", 1E6, false, true, null, MetricPrefix.Kilo, MetricPrefix.Undefined)]
[InlineData("1234.56k", 1_234_560, false, true, null, MetricPrefix.Kilo, MetricPrefix.Undefined)]
[InlineData("1m", 1E-3, false, true, null, MetricPrefix.Undefined, MetricPrefix.Undefined)]
[InlineData("1000μ", 1E-3, false, true, null, MetricPrefix.Micro, MetricPrefix.Undefined)]
[InlineData("1000", 1000, false, true, null, MetricPrefix.None, MetricPrefix.Undefined)]
[InlineData("0.001m", 1E-6, false, true, null, MetricPrefix.Undefined, MetricPrefix.Milli)]
[InlineData("0m", 1E-6, false, true, 0, MetricPrefix.Undefined, MetricPrefix.Milli)] // Decimals and minPrefix options combined give value 0. Exponent remains.
[InlineData("0.01m", 1.234E-5, false, true, 2, MetricPrefix.Undefined, MetricPrefix.Milli)]
[InlineData("12.34μ", 1.2345E-5, false, true, 2, MetricPrefix.Undefined, MetricPrefix.Micro)]
[InlineData("0.01μ", 1.2345E-8, false, true, 2, MetricPrefix.Undefined, MetricPrefix.Micro)]
public void ToMetric(string expected, double input, bool hasSpace, bool useSymbol, int? decimals, MetricPrefix maxPrefix, MetricPrefix minPrefix)
{
Assert.Equal(expected, input.ToMetric(hasSpace, useSymbol, decimals));
Assert.Equal(expected, input.ToMetric(hasSpace, useSymbol, decimals, maxPrefix, minPrefix));
}



[Theory]
[InlineData(1E+27)]
[InlineData(1E-27)]
Expand Down
Expand Up @@ -412,8 +412,29 @@ namespace Humanizer
public class static MetricNumeralExtensions
{
public static double FromMetric(this string input) { }
public static string ToMetric(this int input, bool hasSpace = False, bool useSymbol = True, System.Nullable<int> decimals = null) { }
public static string ToMetric(this double input, bool hasSpace = False, bool useSymbol = True, System.Nullable<int> decimals = null) { }
public static string ToMetric(this int input, bool hasSpace = False, bool useSymbol = True, System.Nullable<int> decimals = null, Humanizer.MetricPrefix maxPrefix = -100, Humanizer.MetricPrefix minPrefix = -100) { }
public static string ToMetric(this double input, bool hasSpace = False, bool useSymbol = True, System.Nullable<int> decimals = null, Humanizer.MetricPrefix maxPrefix = -100, Humanizer.MetricPrefix minPrefix = -100) { }
}
public enum MetricPrefix
{
Undefined = -100,
Yocto = -24,
Zepto = -21,
Atto = -18,
Femto = -15,
Pico = -12,
Nano = -9,
Micro = -6,
Milli = -3,
None = 0,
Kilo = 3,
Mega = 6,
Giga = 9,
Tera = 12,
Peta = 15,
Exa = 18,
Zetta = 21,
Yotta = 24,
}
public class NoMatchFoundException : System.Exception
{
Expand Down
74 changes: 56 additions & 18 deletions src/Humanizer/MetricNumeralExtensions.cs
@@ -1,8 +1,8 @@
// Wrote by Alois de Gouvello https://github.com/aloisdg
// Written by Alois de Gouvello https://github.com/aloisdg with additions by Jonas Barkå https://github.com/JonasBarka.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copyrights at the top of the file, if any, should be simply Copyright (c) .NET Foundation and Contributors

Alternatively, it can be removed as it's in the project root.


// The MIT License (MIT)

// Copyright (c) 2015 Alois de Gouvello
// Copyright (c) 2015-2019 Alois de Gouvello and each other contributor for their respective work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
Expand Down Expand Up @@ -101,17 +101,19 @@ public static double FromMetric(this string input)
/// <param name="hasSpace">True will split the number and the symbol with a whitespace.</param>
/// <param name="useSymbol">True will use symbol instead of name</param>
/// <param name="decimals">If not null it is the numbers of decimals to round the number to</param>
/// <param name="maxPrefix">Largest metric prefix used in result.</param>
/// <example>
/// <code>
/// 1000.ToMetric() => "1k"
/// 123.ToMetric() => "123"
/// 1E-1.ToMetric() => "100m"
/// 1_000_000.ToMetric(largestPrefix = MetricPrefix.Kilo) => "1000k"
/// </code>
/// </example>
/// <returns>A valid Metric representation</returns>
public static string ToMetric(this int input, bool hasSpace = false, bool useSymbol = true, int? decimals = null)
public static string ToMetric(this int input, bool hasSpace = false, bool useSymbol = true, int? decimals = null, MetricPrefix maxPrefix = MetricPrefix.Undefined, MetricPrefix minPrefix = MetricPrefix.Undefined)
{
return ((double)input).ToMetric(hasSpace, useSymbol, decimals);
return ((double)input).ToMetric(hasSpace, useSymbol, decimals, maxPrefix);
}

/// <summary>
Expand All @@ -125,15 +127,17 @@ public static string ToMetric(this int input, bool hasSpace = false, bool useSym
/// <param name="hasSpace">True will split the number and the symbol with a whitespace.</param>
/// <param name="useSymbol">True will use symbol instead of name</param>
/// <param name="decimals">If not null it is the numbers of decimals to round the number to</param>
/// <param name="maxPrefix">Largest metric prefix used in result.</param>
/// <example>
/// <code>
/// 1000d.ToMetric() => "1k"
/// 123d.ToMetric() => "123"
/// 1E-1.ToMetric() => "100m"
/// 1_000_000.ToMetric(largestPrefix = MetricPrefix.Kilo) => "1000k"
/// </code>
/// </example>
/// <returns>A valid Metric representation</returns>
public static string ToMetric(this double input, bool hasSpace = false, bool useSymbol = true, int? decimals = null)
public static string ToMetric(this double input, bool hasSpace = false, bool useSymbol = true, int? decimals = null, MetricPrefix maxPrefix = MetricPrefix.Undefined, MetricPrefix minPrefix = MetricPrefix.Undefined)
{
if (input.Equals(0))
{
Expand All @@ -145,7 +149,7 @@ public static string ToMetric(this double input, bool hasSpace = false, bool use
throw new ArgumentOutOfRangeException(nameof(input));
}

return BuildRepresentation(input, hasSpace, useSymbol, decimals);
return BuildRepresentation(input, hasSpace, useSymbol, decimals, maxPrefix, minPrefix);
}

/// <summary>
Expand Down Expand Up @@ -217,40 +221,74 @@ private static string ReplaceNameBySymbol(string input)
/// <param name="hasSpace">True will split the number and the symbol with a whitespace.</param>
/// <param name="useSymbol">True will use symbol instead of name</param>
/// <param name="decimals">If not null it is the numbers of decimals to round the number to</param>
/// <param name="maxPrefix">Largest metric prefix used in result.</param>
/// <returns>A number in a Metric representation</returns>
private static string BuildRepresentation(double input, bool hasSpace, bool useSymbol, int? decimals)
private static string BuildRepresentation(double input, bool hasSpace, bool useSymbol, int? decimals, MetricPrefix maxPrefix, MetricPrefix minPrefix)
{
var exponent = (int)Math.Floor(Math.Log10(Math.Abs(input)) / 3);
return exponent.Equals(0)
? input.ToString()
: BuildMetricRepresentation(input, exponent, hasSpace, useSymbol, decimals);
: BuildMetricRepresentation(input, exponent, hasSpace, useSymbol, decimals, maxPrefix, minPrefix);
}

/// <summary>
/// Build a Metric representation of the number.
/// </summary>
/// <param name="input">Number to convert to a Metric representation.</param>
/// <param name="exponent">Exponent of the number in a scientific notation</param>
/// <param name="numericPrefix">Metric prefix expressed as a number.</param>
/// <param name="hasSpace">True will split the number and the symbol with a whitespace.</param>
/// <param name="useSymbol">True will use symbol instead of name</param>
/// <param name="decimals">If not null it is the numbers of decimals to round the number to</param>
/// <param name="maxPrefix">Largest metric prefix used in result.</param>
/// <param name="minPrefix">Smallest metric prefix used in result.</param>
/// <returns>A number in a Metric representation</returns>
private static string BuildMetricRepresentation(double input, int exponent, bool hasSpace, bool useSymbol, int? decimals)
private static string BuildMetricRepresentation(double input, int numericPrefix, bool hasSpace, bool useSymbol, int? decimals, MetricPrefix maxPrefix, MetricPrefix minPrefix)
{
var number = input * Math.Pow(1000, -exponent);
numericPrefix = numericPrefix.Clamp(minPrefix, maxPrefix);

if (numericPrefix == 0)
return input.ToString();

var number = input * Math.Pow(1000, -numericPrefix);

if (decimals.HasValue)
{
number = Math.Round(number, decimals.Value);
}

var symbol = Math.Sign(exponent) == 1
? Symbols[0][exponent - 1]
: Symbols[1][-exponent - 1];
var symbol = Math.Sign(numericPrefix) == 1
? Symbols[0][numericPrefix - 1]
: Symbols[1][-numericPrefix - 1];

return number
+ (hasSpace ? " " : string.Empty)
+ GetUnit(symbol, useSymbol);
}

/// <summary>
/// Clamps a numeric representation of metric prefix.
/// </summary>
/// <param name="numericPrefix">Metric prefix, expressed as a number, to limit.</param>
/// <param name="minPrefix">Lower limit.</param>
/// <param name="maxPrefix">Upper limit.</param>
/// <returns>Clamped numeric prefix representation</returns>
private static int Clamp(this int numericPrefix, MetricPrefix minPrefix, MetricPrefix maxPrefix)
{
if (maxPrefix == MetricPrefix.Undefined && minPrefix == MetricPrefix.Undefined)
return numericPrefix;

if (maxPrefix != MetricPrefix.Undefined && minPrefix != MetricPrefix.Undefined && maxPrefix < minPrefix)
throw new ArgumentException($"{nameof(maxPrefix)} can't be smaller than {nameof(minPrefix)}.");

var maxNumericPrefix = (int)maxPrefix / 3;
var minNumericPrefix = (int)minPrefix / 3;

if (maxPrefix != MetricPrefix.Undefined && maxNumericPrefix < numericPrefix)
return maxNumericPrefix;
else if (minPrefix != MetricPrefix.Undefined && minNumericPrefix > numericPrefix)
return minNumericPrefix;
else
return numericPrefix;
}

/// <summary>
/// Get the unit from a symbol of from the symbol's name.
/// </summary>
Expand All @@ -265,7 +303,7 @@ private static string GetUnit(char symbol, bool useSymbol)
/// <summary>
/// Check if a Metric representation is out of the valid range.
/// </summary>
/// <param name="input">A Metric representation who might be out of the valid range.</param>
/// <param name="input">A Metric representation that may be out of the valid range.</param>
/// <returns>True if input is out of the valid range.</returns>
private static bool IsOutOfRange(this double input)
{
Expand All @@ -283,7 +321,7 @@ private static bool IsOutOfRange(this double input)
/// <remarks>
/// ToDo: Performance: Use (string input, out number) to escape the double use of Parse()
/// </remarks>
/// <param name="input">A string who might contain a invalid Metric representation.</param>
/// <param name="input">A string that may contain an invalid Metric representation.</param>
/// <returns>True if input is not a valid Metric representation.</returns>
private static bool IsInvalidMetricNumeral(this string input)
{
Expand Down
31 changes: 31 additions & 0 deletions src/Humanizer/MetricPrefix.cs
@@ -0,0 +1,31 @@
namespace Humanizer
{
/// <summary>
/// MetricPrefix contains all supported metric prefixes and their corresponding powers of ten as underlying values.
/// </summary>
/// <remarks>
/// "None" represents no prefix, use "Undefined" as null equivalent.
/// Unsupported values: hecto, deca, deci and centi.
/// </remarks>
public enum MetricPrefix
{
Undefined = -100,
Yocto = -24,
Zepto = -21,
Atto = -18,
Femto = -15,
Pico = -12,
Nano = -9,
Micro = -6,
Milli = -3,
None = 0,
Kilo = 3,
Mega = 6,
Giga = 9,
Tera = 12,
Peta = 15,
Exa = 18,
Zetta = 21,
Yotta = 24
}
}