diff --git a/src/Humanizer.Tests.Shared/MetricNumeralTests.cs b/src/Humanizer.Tests.Shared/MetricNumeralTests.cs index 0f1892018..5ad53ed81 100644 --- a/src/Humanizer.Tests.Shared/MetricNumeralTests.cs +++ b/src/Humanizer.Tests.Shared/MetricNumeralTests.cs @@ -103,12 +103,100 @@ public void TestAllSymbolsAsInt(int exponent) [InlineData("-3.9m", -3.91e-3, false, true, 1)] [InlineData("10 ", 10, true, false, 0)] [InlineData("1.2", 1.23, false, false, 1)] - public void ToMetric(string expected, double input, bool hasSpace, bool useSymbol, int? decimals) - { + public void ToMetricObsolete(string expected, double input, bool hasSpace, bool useSymbol, int? decimals) + { +#pragma warning disable CS0618 // Type or member is obsolete Assert.Equal(expected, input.ToMetric(hasSpace, useSymbol, decimals)); +#pragma warning restore CS0618 // Type or member is obsolete } - + [Theory] + [InlineData("1.3M", 1300000, null, null)] + [InlineData("1.3million", 1300000, MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1.3 million", 1300000, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1.3 million", 1300000, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("0", 0d, null, null)] + [InlineData("123", 123d, null, null)] + [InlineData("-123", -123d, null, null)] + [InlineData("1.23k", 1230d, null, null)] + [InlineData("1 k", 1000d, MetricNumeralFormats.WithSpace, null)] + [InlineData("1milli", 1E-3, MetricNumeralFormats.UseName, null)] + [InlineData("1.23milli", 1.234E-3, MetricNumeralFormats.UseName, 2)] + [InlineData("12.34k", 12345, null, 2)] + [InlineData("12k", 12345, null, 0)] + [InlineData("-3.9m", -3.91e-3, null, 1)] + [InlineData("10 ", 10, MetricNumeralFormats.WithSpace, 0)] + [InlineData("1.2", 1.23, null, 1)] + [InlineData("1thousand", 1000d, MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1.23 thousand", 1230d, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1Y", 1E24, null, null)] + [InlineData("1 yotta", 1E24, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 septillion", 1E24, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 quadrillion", 1E24, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1Z", 1E21, null, null)] + [InlineData("1 zetta", 1E21, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 sextillion", 1E21, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 trilliard", 1E21, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1E", 1E18, null, null)] + [InlineData("1 exa", 1E18, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 quintillion", 1E18, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 trillion", 1E18, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1P", 1E15, null, null)] + [InlineData("1 peta", 1E15, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 quadrillion", 1E15, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 billiard", 1E15, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1T", 1E12, null, null)] + [InlineData("1 tera", 1E12, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 trillion", 1E12, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 billion", 1E12, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1G", 1E9, null, null)] + [InlineData("1 giga", 1E9, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 billion", 1E9, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 milliard", 1E9, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1M", 1E6, null, null)] + [InlineData("1 mega", 1E6, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 million", 1E6, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 million", 1E6, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1k", 1E3, null, null)] + [InlineData("1 kilo", 1E3, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 thousand", 1E3, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 thousand", 1E3, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1y", 1E-24, null, null)] + [InlineData("1 yocto", 1E-24, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 septillionth", 1E-24, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 quadrillionth", 1E-24, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1z", 1E-21, null, null)] + [InlineData("1 zepto", 1E-21, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 sextillionth", 1E-21, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 trilliardth", 1E-21, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1a", 1E-18, null, null)] + [InlineData("1 atto", 1E-18, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 quintillionth", 1E-18, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 trillionth", 1E-18, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1f", 1E-15, null, null)] + [InlineData("1 femto", 1E-15, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 quadrillionth", 1E-15, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 billiardth", 1E-15, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1p", 1E-12, null, null)] + [InlineData("1 pico", 1E-12, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 trillionth", 1E-12, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 billionth", 1E-12, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1n", 1E-9, null, null)] + [InlineData("1 nano", 1E-9, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 billionth", 1E-9, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 milliardth", 1E-9, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1μ", 1E-6, null, null)] + [InlineData("1 micro", 1E-6, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 millionth", 1E-6, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 millionth", 1E-6, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + [InlineData("1m", 1E-3, null, null)] + [InlineData("1 milli", 1E-3, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseName, null)] + [InlineData("1 thousandth", 1E-3, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseShortScaleWord, null)] + [InlineData("1 thousandth", 1E-3, MetricNumeralFormats.WithSpace | MetricNumeralFormats.UseLongScaleWord, null)] + public void ToMetric(string expected, double input, MetricNumeralFormats? format, int? decimals) + { + Assert.Equal(expected, input.ToMetric(format, decimals)); + } [Theory] [InlineData(1E+27)] diff --git a/src/Humanizer/MetricNumeralExtensions.cs b/src/Humanizer/MetricNumeralExtensions.cs index 271f6b45d..76d1a9f9a 100644 --- a/src/Humanizer/MetricNumeralExtensions.cs +++ b/src/Humanizer/MetricNumeralExtensions.cs @@ -53,7 +53,7 @@ static MetricNumeralExtensions() }; /// - /// Names link a Metric symbol (as key) to its name (as value). + /// UnitPrefixes link a Metric symbol (as key) to its prefix (as value). /// /// /// We dont support : @@ -62,11 +62,26 @@ static MetricNumeralExtensions() /// {'d', "deci" }, /// {'c', "centi"}, /// - private static readonly Dictionary Names = new Dictionary - { - {'Y', "yotta" }, {'Z', "zetta" }, {'E', "exa" }, {'P', "peta" }, {'T', "tera" }, {'G', "giga" }, {'M', "mega" }, {'k', "kilo" }, - {'m', "milli" }, {'μ', "micro" }, {'n', "nano" }, {'p', "pico" }, {'f', "femto" }, {'a', "atto" }, {'z', "zepto" }, {'y', "yocto" } - }; + private static readonly Dictionary UnitPrefixes = new Dictionary + { + {'Y', new UnitPrefix("yotta", "septillion", "quadrillion")}, + {'Z', new UnitPrefix("zetta", "sextillion", "trilliard")}, + {'E', new UnitPrefix("exa", "quintillion", "trillion")}, + {'P', new UnitPrefix("peta", "quadrillion", "billiard")}, + {'T', new UnitPrefix("tera", "trillion", "billion")}, + {'G', new UnitPrefix("giga", "billion", "milliard")}, + {'M', new UnitPrefix("mega", "million")}, + {'k', new UnitPrefix("kilo", "thousand")}, + + {'m', new UnitPrefix("milli", "thousandth")}, + {'μ', new UnitPrefix("micro", "millionth")}, + {'n', new UnitPrefix("nano", "billionth", "milliardth")}, + {'p', new UnitPrefix("pico", "trillionth", "billionth")}, + {'f', new UnitPrefix("femto", "quadrillionth", "billiardth")}, + {'a', new UnitPrefix("atto", "quintillionth", "trillionth")}, + {'z', new UnitPrefix("zepto", "sextillionth", "trilliardth")}, + {'y', new UnitPrefix("yocto", "septillionth", "quadrillionth")} + }; /// /// Converts a Metric representation into a number. @@ -109,11 +124,36 @@ public static double FromMetric(this string input) /// /// /// A valid Metric representation - public static string ToMetric(this int input, bool hasSpace = false, bool useSymbol = true, int? decimals = null) + [Obsolete("Please use overload with MetricNumeralFormats")] + public static string ToMetric(this int input, bool hasSpace, bool useSymbol = true, int? decimals = null) { return ((double)input).ToMetric(hasSpace, useSymbol, decimals); } + + /// + /// Converts a number into a valid and Human-readable Metric representation. + /// + /// + /// Inspired by a snippet from Thom Smith. + /// See this link for more. + /// + /// Number to convert to a Metric representation. + /// A bitwise combination of enumeration values that format the metric representation. + /// If not null it is the numbers of decimals to round the number to + /// + /// + /// 1000.ToMetric() => "1k" + /// 123.ToMetric() => "123" + /// 1E-1.ToMetric() => "100m" + /// + /// + /// A valid Metric representation + public static string ToMetric(this int input, MetricNumeralFormats? formats = null, int? decimals = null) + { + return ((double)input).ToMetric(formats, decimals); + } + /// /// Converts a number into a valid and Human-readable Metric representation. /// @@ -133,7 +173,41 @@ public static string ToMetric(this int input, bool hasSpace = false, bool useSym /// /// /// A valid Metric representation - public static string ToMetric(this double input, bool hasSpace = false, bool useSymbol = true, int? decimals = null) + [Obsolete("Please use overload with MetricNumeralFormats")] + public static string ToMetric(this double input, bool hasSpace, bool useSymbol = true, int? decimals = null) + { + var formats = (MetricNumeralFormats?)null; + if (hasSpace) + { + formats = MetricNumeralFormats.WithSpace; + } + if (!useSymbol) + { + formats = formats.HasValue ? formats | MetricNumeralFormats.UseName + : MetricNumeralFormats.UseName; + } + return ToMetric(input, formats, decimals); + } + + /// + /// Converts a number into a valid and Human-readable Metric representation. + /// + /// + /// Inspired by a snippet from Thom Smith. + /// See this link for more. + /// + /// Number to convert to a Metric representation. + /// A bitwise combination of enumeration values that format the metric representation. + /// If not null it is the numbers of decimals to round the number to + /// + /// + /// 1000d.ToMetric() => "1k" + /// 123d.ToMetric() => "123" + /// 1E-1.ToMetric() => "100m" + /// + /// + /// A valid Metric representation + public static string ToMetric(this double input, MetricNumeralFormats? formats = null, int? decimals = null) { if (input.Equals(0)) { @@ -145,7 +219,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, formats, decimals); } /// @@ -206,25 +280,24 @@ private static double BuildMetricNumber(string input, char last) /// A metric representation with a symbol private static string ReplaceNameBySymbol(string input) { - return Names.Aggregate(input, (current, name) => - current.Replace(name.Value, name.Key.ToString())); + return UnitPrefixes.Aggregate(input, (current, unitPrefix) => + current.Replace(unitPrefix.Value.Name, unitPrefix.Key.ToString())); } /// /// Build a Metric representation of the number. /// /// Number to convert to a Metric representation. - /// True will split the number and the symbol with a whitespace. - /// True will use symbol instead of name + /// A bitwise combination of enumeration values that format the metric representation. /// If not null it is the numbers of decimals to round the number to /// A number in a Metric representation - private static string BuildRepresentation(double input, bool hasSpace, bool useSymbol, int? decimals) + private static string BuildRepresentation(double input, MetricNumeralFormats? formats, int? decimals) { var exponent = (int)Math.Floor(Math.Log10(Math.Abs(input)) / 3); - if (!exponent.Equals(0)) return BuildMetricRepresentation(input, exponent, hasSpace, useSymbol, decimals); + if (!exponent.Equals(0)) return BuildMetricRepresentation(input, exponent, formats, decimals); var representation = decimals.HasValue ? Math.Round(input, decimals.Value).ToString() : input.ToString(); - if (hasSpace) + if ((formats & MetricNumeralFormats.WithSpace) == MetricNumeralFormats.WithSpace) { representation += " "; } @@ -236,11 +309,10 @@ private static string BuildRepresentation(double input, bool hasSpace, bool useS /// /// Number to convert to a Metric representation. /// Exponent of the number in a scientific notation - /// True will split the number and the symbol with a whitespace. - /// True will use symbol instead of name + /// A bitwise combination of enumeration values that format the metric representation. /// If not null it is the numbers of decimals to round the number to /// A number in a Metric representation - private static string BuildMetricRepresentation(double input, int exponent, bool hasSpace, bool useSymbol, int? decimals) + private static string BuildMetricRepresentation(double input, int exponent, MetricNumeralFormats? formats, int? decimals) { var number = input * Math.Pow(1000, -exponent); if (decimals.HasValue) @@ -252,19 +324,34 @@ private static string BuildMetricRepresentation(double input, int exponent, bool ? Symbols[0][exponent - 1] : Symbols[1][-exponent - 1]; return number - + (hasSpace ? " " : string.Empty) - + GetUnit(symbol, useSymbol); + + (formats.HasValue && formats.Value.HasFlag(MetricNumeralFormats.WithSpace) ? " " : string.Empty) + + GetUnitText(symbol, formats); } /// /// Get the unit from a symbol of from the symbol's name. /// /// The symbol linked to the unit - /// True will use symbol instead of name - /// A symbol or a symbol's name - private static string GetUnit(char symbol, bool useSymbol) + /// A bitwise combination of enumeration values that format the metric representation. + /// A symbol, a symbol's name, a symbol's short scale word or a symbol's long scale word + private static string GetUnitText(char symbol, MetricNumeralFormats? formats) { - return useSymbol ? symbol.ToString() : Names[symbol]; + if (formats.HasValue + && formats.Value.HasFlag(MetricNumeralFormats.UseName)) + { + return UnitPrefixes[symbol].Name; + } + if (formats.HasValue + && formats.Value.HasFlag(MetricNumeralFormats.UseShortScaleWord)) + { + return UnitPrefixes[symbol].ShortScaleWord; + } + if (formats.HasValue + && formats.Value.HasFlag(MetricNumeralFormats.UseLongScaleWord)) + { + return UnitPrefixes[symbol].LongScaleWord; + } + return symbol.ToString(); } /// @@ -297,5 +384,21 @@ private static bool IsInvalidMetricNumeral(this string input) var isSymbol = Symbols[0].Contains(last) || Symbols[1].Contains(last); return !double.TryParse(isSymbol ? input.Remove(index) : input, out var number); } + + private struct UnitPrefix + { + private readonly string _longScaleWord; + + public string Name { get; } + public string ShortScaleWord { get; } + public string LongScaleWord => _longScaleWord ?? ShortScaleWord; + + public UnitPrefix(string name, string shortScaleWord, string longScaleWord = null) + { + Name = name; + ShortScaleWord = shortScaleWord; + _longScaleWord = longScaleWord; + } + } } } diff --git a/src/Humanizer/MetricNumeralFormats.cs b/src/Humanizer/MetricNumeralFormats.cs new file mode 100644 index 000000000..a15dcba88 --- /dev/null +++ b/src/Humanizer/MetricNumeralFormats.cs @@ -0,0 +1,31 @@ +using System; + +namespace Humanizer +{ + /// + /// Flags for formatting the metric representation of numerals. + /// + [Flags] + public enum MetricNumeralFormats + { + /// + /// Use the metric prefix long scale word. + /// + UseLongScaleWord = 1, + + /// + /// Use the metric prefix name instead of the symbol. + /// + UseName = 2, + + /// + /// Use the metric prefix short scale word. + /// + UseShortScaleWord = 4, + + /// + /// Include a space after the numeral. + /// + WithSpace = 8 + } +}