diff --git a/readme.md b/readme.md index 135aae71c..bea3af076 100644 --- a/readme.md +++ b/readme.md @@ -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" diff --git a/src/Humanizer.Tests.Shared/MetricNumeralTests.cs b/src/Humanizer.Tests.Shared/MetricNumeralTests.cs index 252a753d2..69bf9237f 100644 --- a/src/Humanizer.Tests.Shared/MetricNumeralTests.cs +++ b/src/Humanizer.Tests.Shared/MetricNumeralTests.cs @@ -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)] diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt index a94665fa7..a34c8bf5a 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt @@ -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 decimals = null) { } - public static string ToMetric(this double input, bool hasSpace = False, bool useSymbol = True, System.Nullable decimals = null) { } + public static string ToMetric(this int input, bool hasSpace = False, bool useSymbol = True, System.Nullable 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 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 { diff --git a/src/Humanizer/MetricNumeralExtensions.cs b/src/Humanizer/MetricNumeralExtensions.cs index 5ce9a3e2d..bab387b4c 100644 --- a/src/Humanizer/MetricNumeralExtensions.cs +++ b/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. // 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. // 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 @@ -101,17 +101,19 @@ public static double FromMetric(this string input) /// True will split the number and the symbol with a whitespace. /// True will use symbol instead of name /// If not null it is the numbers of decimals to round the number to + /// Largest metric prefix used in result. /// /// /// 1000.ToMetric() => "1k" /// 123.ToMetric() => "123" /// 1E-1.ToMetric() => "100m" + /// 1_000_000.ToMetric(largestPrefix = MetricPrefix.Kilo) => "1000k" /// /// /// A valid Metric representation - 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); } /// @@ -125,15 +127,17 @@ public static string ToMetric(this int input, bool hasSpace = false, bool useSym /// True will split the number and the symbol with a whitespace. /// True will use symbol instead of name /// If not null it is the numbers of decimals to round the number to + /// Largest metric prefix used in result. /// /// /// 1000d.ToMetric() => "1k" /// 123d.ToMetric() => "123" /// 1E-1.ToMetric() => "100m" + /// 1_000_000.ToMetric(largestPrefix = MetricPrefix.Kilo) => "1000k" /// /// /// A valid Metric representation - 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)) { @@ -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); } /// @@ -217,40 +221,74 @@ private static string ReplaceNameBySymbol(string input) /// True will split the number and the symbol with a whitespace. /// True will use symbol instead of name /// If not null it is the numbers of decimals to round the number to + /// Largest metric prefix used in result. /// A number in a Metric representation - 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); } /// /// Build a Metric representation of the number. /// /// Number to convert to a Metric representation. - /// Exponent of the number in a scientific notation + /// Metric prefix expressed as a number. /// True will split the number and the symbol with a whitespace. /// True will use symbol instead of name /// If not null it is the numbers of decimals to round the number to + /// Largest metric prefix used in result. + /// Smallest metric prefix used in result. /// 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 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); } + /// + /// Clamps a numeric representation of metric prefix. + /// + /// Metric prefix, expressed as a number, to limit. + /// Lower limit. + /// Upper limit. + /// Clamped numeric prefix representation + 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; + } + /// /// Get the unit from a symbol of from the symbol's name. /// @@ -265,7 +303,7 @@ private static string GetUnit(char symbol, bool useSymbol) /// /// Check if a Metric representation is out of the valid range. /// - /// A Metric representation who might be out of the valid range. + /// A Metric representation that may be out of the valid range. /// True if input is out of the valid range. private static bool IsOutOfRange(this double input) { @@ -283,7 +321,7 @@ private static bool IsOutOfRange(this double input) /// /// ToDo: Performance: Use (string input, out number) to escape the double use of Parse() /// - /// A string who might contain a invalid Metric representation. + /// A string that may contain an invalid Metric representation. /// True if input is not a valid Metric representation. private static bool IsInvalidMetricNumeral(this string input) { diff --git a/src/Humanizer/MetricPrefix.cs b/src/Humanizer/MetricPrefix.cs new file mode 100644 index 000000000..37ca9b9f6 --- /dev/null +++ b/src/Humanizer/MetricPrefix.cs @@ -0,0 +1,31 @@ +namespace Humanizer +{ + /// + /// MetricPrefix contains all supported metric prefixes and their corresponding powers of ten as underlying values. + /// + /// + /// "None" represents no prefix, use "Undefined" as null equivalent. + /// Unsupported values: hecto, deca, deci and centi. + /// + 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 + } +}