From 456153e7f518e7a40a2029ddd790e56e575a59e5 Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp Date: Fri, 22 May 2020 09:39:19 -0400 Subject: [PATCH 1/5] Add support for standard double numeric format strings. Also allow upper case --- UnitsNet.Tests/QuantityFormatterTests.cs | 113 +++++++++++++++++++++++ UnitsNet/QuantityFormatter.cs | 102 +++++++++++++++----- 2 files changed, 190 insertions(+), 25 deletions(-) create mode 100644 UnitsNet.Tests/QuantityFormatterTests.cs diff --git a/UnitsNet.Tests/QuantityFormatterTests.cs b/UnitsNet.Tests/QuantityFormatterTests.cs new file mode 100644 index 0000000000..be099e0614 --- /dev/null +++ b/UnitsNet.Tests/QuantityFormatterTests.cs @@ -0,0 +1,113 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using Xunit; + +namespace UnitsNet.Tests +{ + public class QuantityFormatterTests + { + [Theory] + [InlineData("C")] + [InlineData("C0")] + [InlineData("C1")] + [InlineData("C2")] + [InlineData("C3")] + [InlineData("C4")] + [InlineData("C5")] + [InlineData("C6")] + [InlineData("c")] + [InlineData("c0")] + [InlineData("c1")] + [InlineData("c2")] + [InlineData("c3")] + [InlineData("c4")] + [InlineData("c5")] + [InlineData("c6")] + [InlineData("E")] + [InlineData("E0")] + [InlineData("E1")] + [InlineData("E2")] + [InlineData("E3")] + [InlineData("E4")] + [InlineData("E5")] + [InlineData("E6")] + [InlineData("e")] + [InlineData("e0")] + [InlineData("e1")] + [InlineData("e2")] + [InlineData("e3")] + [InlineData("e4")] + [InlineData("e5")] + [InlineData("e6")] + [InlineData("F")] + [InlineData("F0")] + [InlineData("F1")] + [InlineData("F2")] + [InlineData("F3")] + [InlineData("F4")] + [InlineData("F5")] + [InlineData("F6")] + [InlineData("f")] + [InlineData("f0")] + [InlineData("f1")] + [InlineData("f2")] + [InlineData("f3")] + [InlineData("f4")] + [InlineData("f5")] + [InlineData("f6")] + [InlineData("N")] + [InlineData("N0")] + [InlineData("N1")] + [InlineData("N2")] + [InlineData("N3")] + [InlineData("N4")] + [InlineData("N5")] + [InlineData("N6")] + [InlineData("n")] + [InlineData("n0")] + [InlineData("n1")] + [InlineData("n2")] + [InlineData("n3")] + [InlineData("n4")] + [InlineData("n5")] + [InlineData("n6")] + [InlineData("P")] + [InlineData("P0")] + [InlineData("P1")] + [InlineData("P2")] + [InlineData("P3")] + [InlineData("P4")] + [InlineData("P5")] + [InlineData("P6")] + [InlineData("p")] + [InlineData("p0")] + [InlineData("p1")] + [InlineData("p2")] + [InlineData("p3")] + [InlineData("p4")] + [InlineData("p5")] + [InlineData("p6")] + [InlineData("R")] + [InlineData("R0")] + [InlineData("R1")] + [InlineData("R2")] + [InlineData("R3")] + [InlineData("R4")] + [InlineData("R5")] + [InlineData("R6")] + [InlineData("r")] + [InlineData("r0")] + [InlineData("r1")] + [InlineData("r2")] + [InlineData("r3")] + [InlineData("r4")] + [InlineData("r5")] + [InlineData("r6")] + public static void StandardNumericFormatStrings_EqualsValueWithFormatString(string formatString) + { + var length = Length.FromMeters(1234.56789); + Assert.Equal(length.Value.ToString(formatString), QuantityFormatter.Format(length, formatString)); + } + } +} diff --git a/UnitsNet/QuantityFormatter.cs b/UnitsNet/QuantityFormatter.cs index 782c2de23c..7450f19ff9 100644 --- a/UnitsNet/QuantityFormatter.cs +++ b/UnitsNet/QuantityFormatter.cs @@ -13,6 +13,32 @@ namespace UnitsNet /// public class QuantityFormatter { + /// + /// Formats the given quantity using the given format string. Uses the . + /// + /// The quantity's unit type, for example . + /// The quantity to format. + /// The format string. + /// + /// The valid format strings are as follows: + /// Any of the standard numeric format strings for double values except for "G" or "g" ("C" or "c", "E" or "e", "F" or "f", "N" or "n", "P" or "p", "R" or "r"). + /// "G" or "g": The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m". + /// "A" or "a": The default unit abbreviation for , such as "m". + /// "a0", "a1", ..., "aN": The Nth unit abbreviation for . "a0" is the same as "a". + /// A will be thrown if the requested abbreviation index does not exist. + /// "V" or "v": String representation of . + /// "U" or "u": The enum name of , such as "Meter". + /// "Q" or "q": The quantity name, such as "Length". + /// "s1", "s2", ..., "sN": The value with N significant digits after the radix followed by the unit abbreviation. For example, + /// "s4" would return "1.2345 m" if is 1.2345678. Trailing zeros are omitted. + /// + /// The string representation. + public static string Format(IQuantity quantity, string format) + where TUnitType : Enum + { + return Format(quantity, format, CultureInfo.CurrentUICulture); + } + /// /// Formats the given quantity using the given format string and format provider. /// @@ -23,13 +49,14 @@ public class QuantityFormatter /// if null. /// /// The valid format strings are as follows: - /// "g": The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m". - /// "a": The default unit abbreviation for , such as "m". + /// Any of the standard numeric format strings for double values except for "G" or "g" ("C" or "c", "E" or "e", "F" or "f", "N" or "n", "P" or "p", "R" or "r"). + /// "G" or "g": The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m". + /// "A" or "a": The default unit abbreviation for , such as "m". /// "a0", "a1", ..., "aN": The Nth unit abbreviation for . "a0" is the same as "a". /// A will be thrown if the requested abbreviation index does not exist. - /// "v": String representation of . - /// "u": The enum name of , such as "Meter". - /// "q": The quantity name, such as "Length". + /// "V" or "v": String representation of . + /// "U" or "u": The enum name of , such as "Meter". + /// "Q" or "q": The quantity name, such as "Length". /// "s1", "s2", ..., "sN": The value with N significant digits after the radix followed by the unit abbreviation. For example, /// "s4" would return "1.2345 m" if is 1.2345678. Trailing zeros are omitted. /// @@ -37,41 +64,66 @@ public class QuantityFormatter public static string Format(IQuantity quantity, string format, IFormatProvider formatProvider) where TUnitType : Enum { - formatProvider = formatProvider ?? CultureInfo.CurrentUICulture; + formatProvider ??= CultureInfo.CurrentUICulture; - var number = 0; - var formatString = format; + if(string.IsNullOrEmpty(format)) + format = "g"; - if(string.IsNullOrEmpty(formatString)) - formatString = "g"; + int precisionSpecifier = 0; + char formatSpecifier = format[0]; - if(formatString.StartsWith("a") || formatString.StartsWith("s")) + switch(formatSpecifier) { - if(formatString.Length > 1 && !int.TryParse(formatString.Substring(1), out number)) - throw new FormatException($"The {format} format string is not supported."); - - formatString = formatString.Substring(0, 1); + case 'A': + case 'a': + case 'S': + case 's': + if(format.Length > 1 && !int.TryParse(format.Substring(1), out precisionSpecifier)) + throw new FormatException($"The {format} format string is not supported."); + break; } - switch(formatString) + switch(formatSpecifier) { - case "g": + // Standard numeric format specifiers + case 'C': + case 'c': + case 'E': + case 'e': + case 'F': + case 'f': + case 'N': + case 'n': + case 'P': + case 'p': + case 'R': + case 'r': + return quantity.Value.ToString(format, formatProvider); + + // UnitsNet custom format specifiers + case 'G': + case 'g': return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, 2); - case "a": + case 'A': + case 'a': var abbreviations = UnitAbbreviationsCache.Default.GetUnitAbbreviations(quantity.Unit, formatProvider); - if(number >= abbreviations.Length) + if(precisionSpecifier >= abbreviations.Length) throw new FormatException($"The {format} format string is invalid because the abbreviation index does not exist."); - return abbreviations[number]; - case "v": + return abbreviations[precisionSpecifier]; + case 'V': + case 'v': return quantity.Value.ToString(formatProvider); - case "u": + case 'U': + case 'u': return quantity.Unit.ToString(); - case "q": + case 'Q': + case 'q': return quantity.QuantityInfo.Name; - case "s": - return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, number); + case 'S': + case 's': + return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, precisionSpecifier); default: throw new FormatException($"The {format} format string is not supported."); } From dd720fccddc8e8b365ad0766960cd44c4bc1e53a Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp Date: Fri, 22 May 2020 09:45:22 -0400 Subject: [PATCH 2/5] Getting too fancy --- UnitsNet/QuantityFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitsNet/QuantityFormatter.cs b/UnitsNet/QuantityFormatter.cs index 7450f19ff9..493e892769 100644 --- a/UnitsNet/QuantityFormatter.cs +++ b/UnitsNet/QuantityFormatter.cs @@ -64,7 +64,7 @@ public static string Format(IQuantity quantity, string for public static string Format(IQuantity quantity, string format, IFormatProvider formatProvider) where TUnitType : Enum { - formatProvider ??= CultureInfo.CurrentUICulture; + formatProvider = formatProvider ?? CultureInfo.CurrentUICulture; if(string.IsNullOrEmpty(format)) format = "g"; From 043795a30b3db10273d675a201719f80550d7386 Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp Date: Fri, 22 May 2020 16:32:14 -0400 Subject: [PATCH 3/5] Add abbreviation suffix. Add tests for custom format strings --- UnitsNet.Tests/QuantityFormatterTests.cs | 32 ++++++- UnitsNet/QuantityFormatter.cs | 105 ++++++++++++----------- 2 files changed, 82 insertions(+), 55 deletions(-) diff --git a/UnitsNet.Tests/QuantityFormatterTests.cs b/UnitsNet.Tests/QuantityFormatterTests.cs index be099e0614..9a2a948ca1 100644 --- a/UnitsNet.Tests/QuantityFormatterTests.cs +++ b/UnitsNet.Tests/QuantityFormatterTests.cs @@ -104,10 +104,36 @@ public class QuantityFormatterTests [InlineData("r4")] [InlineData("r5")] [InlineData("r6")] - public static void StandardNumericFormatStrings_EqualsValueWithFormatString(string formatString) + public static void StandardNumericFormatStrings_Equals_ValueWithFormatStringAndAbbreviation(string format) { - var length = Length.FromMeters(1234.56789); - Assert.Equal(length.Value.ToString(formatString), QuantityFormatter.Format(length, formatString)); + var length = Length.FromMeters(123456789.987654321); + + var expected = string.Format($"{{0:{format}}} {{1:a}}", length.Value, length); + Assert.Equal(expected, QuantityFormatter.Format(length, format)); + } + + [Theory] + [InlineData("000")] + [InlineData("0.00")] + [InlineData("#####")] + [InlineData("#.##")] + [InlineData("##,#")] + [InlineData("#,#,,")] + [InlineData("%#0.00")] + [InlineData("##.0 %")] + [InlineData("#0.00‰")] + [InlineData("#0.0e0")] + [InlineData("0.0##e+00")] + [InlineData("0.0e+00")] + [InlineData(@"\###00\#")] + [InlineData("#0.0#;(#0.0#);-\0-")] + [InlineData("#0.0#;(#0.0#)")] + public static void CustomNumericFormatStrings_Equals_ValueWithFormatStringAndAbbreviation(string format) + { + var length = Length.FromMeters(123456789.987654321); + + var expected = string.Format($"{{0:{format}}} {{1:a}}", length.Value, length); + Assert.Equal(expected, QuantityFormatter.Format(length, format)); } } } diff --git a/UnitsNet/QuantityFormatter.cs b/UnitsNet/QuantityFormatter.cs index 493e892769..b5ed816caf 100644 --- a/UnitsNet/QuantityFormatter.cs +++ b/UnitsNet/QuantityFormatter.cs @@ -13,6 +13,11 @@ namespace UnitsNet /// public class QuantityFormatter { + /// + /// The available UnitsNet custom format specifiers. + /// + private static readonly char[] UnitsNetFormatSpecifiers = { 'A', 'a', 'G', 'g', 'Q', 'q', 'S', 's', 'U', 'u', 'V', 'v' }; + /// /// Formats the given quantity using the given format string. Uses the . /// @@ -69,63 +74,59 @@ public static string Format(IQuantity quantity, string for if(string.IsNullOrEmpty(format)) format = "g"; - int precisionSpecifier = 0; - char formatSpecifier = format[0]; - - switch(formatSpecifier) + if(UnitsNetFormatSpecifiers.Any(c => c == format[0])) { - case 'A': - case 'a': - case 'S': - case 's': - if(format.Length > 1 && !int.TryParse(format.Substring(1), out precisionSpecifier)) - throw new FormatException($"The {format} format string is not supported."); - break; - } + // UnitsNet custom format string - switch(formatSpecifier) - { - // Standard numeric format specifiers - case 'C': - case 'c': - case 'E': - case 'e': - case 'F': - case 'f': - case 'N': - case 'n': - case 'P': - case 'p': - case 'R': - case 'r': - return quantity.Value.ToString(format, formatProvider); + int precisionSpecifier = 0; + char formatSpecifier = format[0]; + + switch(formatSpecifier) + { + case 'A': + case 'a': + case 'S': + case 's': + if(format.Length > 1 && !int.TryParse(format.Substring(1), out precisionSpecifier)) + throw new FormatException($"The {format} format string is not supported."); + break; + } + + switch(formatSpecifier) + { + case 'G': + case 'g': + return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, 2); + case 'A': + case 'a': + var abbreviations = UnitAbbreviationsCache.Default.GetUnitAbbreviations(quantity.Unit, formatProvider); - // UnitsNet custom format specifiers - case 'G': - case 'g': - return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, 2); - case 'A': - case 'a': - var abbreviations = UnitAbbreviationsCache.Default.GetUnitAbbreviations(quantity.Unit, formatProvider); + if(precisionSpecifier >= abbreviations.Length) + throw new FormatException($"The {format} format string is invalid because the abbreviation index does not exist."); - if(precisionSpecifier >= abbreviations.Length) - throw new FormatException($"The {format} format string is invalid because the abbreviation index does not exist."); + return abbreviations[precisionSpecifier]; + case 'V': + case 'v': + return quantity.Value.ToString(formatProvider); + case 'U': + case 'u': + return quantity.Unit.ToString(); + case 'Q': + case 'q': + return quantity.QuantityInfo.Name; + case 'S': + case 's': + return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, precisionSpecifier); + default: + throw new FormatException($"The {format} format string is not supported."); + } + } + else + { + // Anything else is a standard numeric format string with default unit abbreviation postfix. - return abbreviations[precisionSpecifier]; - case 'V': - case 'v': - return quantity.Value.ToString(formatProvider); - case 'U': - case 'u': - return quantity.Unit.ToString(); - case 'Q': - case 'q': - return quantity.QuantityInfo.Name; - case 'S': - case 's': - return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, precisionSpecifier); - default: - throw new FormatException($"The {format} format string is not supported."); + var abbreviations = UnitAbbreviationsCache.Default.GetUnitAbbreviations(quantity.Unit, formatProvider); + return string.Format(formatProvider, $"{{0:{format}}} {{1}}", quantity.Value, abbreviations.First()); } } From 35e846239aec422d7abb5d6946abfb32fa94f371 Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp Date: Wed, 27 May 2020 21:43:50 -0400 Subject: [PATCH 4/5] PR requests. Better variable names. --- UnitsNet/QuantityFormatter.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/UnitsNet/QuantityFormatter.cs b/UnitsNet/QuantityFormatter.cs index 60c913e140..609d4974ee 100644 --- a/UnitsNet/QuantityFormatter.cs +++ b/UnitsNet/QuantityFormatter.cs @@ -69,17 +69,18 @@ public static string Format(IQuantity quantity, string for public static string Format(IQuantity quantity, string format, IFormatProvider? formatProvider) where TUnitType : Enum { - formatProvider = formatProvider ?? CultureInfo.CurrentUICulture; + formatProvider ??= CultureInfo.CurrentUICulture; - if(string.IsNullOrEmpty(format)) + if(string.IsNullOrWhiteSpace(format)) format = "g"; - if(UnitsNetFormatSpecifiers.Any(c => c == format[0])) + char formatSpecifier = format[0]; + + if(UnitsNetFormatSpecifiers.Any(unitsNetFormatSpecifier => unitsNetFormatSpecifier == formatSpecifier)) { // UnitsNet custom format string int precisionSpecifier = 0; - char formatSpecifier = format[0]; switch(formatSpecifier) { From c5c20ef5c47b62cbc85a6b3c61ebadf7ba324d97 Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp Date: Thu, 28 May 2020 11:05:57 -0400 Subject: [PATCH 5/5] Add list for valid strings. Update some doc. --- UnitsNet/QuantityFormatter.cs | 100 ++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 22 deletions(-) diff --git a/UnitsNet/QuantityFormatter.cs b/UnitsNet/QuantityFormatter.cs index 609d4974ee..3a774b4427 100644 --- a/UnitsNet/QuantityFormatter.cs +++ b/UnitsNet/QuantityFormatter.cs @@ -19,23 +19,51 @@ public class QuantityFormatter private static readonly char[] UnitsNetFormatSpecifiers = { 'A', 'a', 'G', 'g', 'Q', 'q', 'S', 's', 'U', 'u', 'V', 'v' }; /// - /// Formats the given quantity using the given format string. Uses the . + /// Formats a quantity using the given format string and format provider. /// /// The quantity's unit type, for example . /// The quantity to format. /// The format string. /// /// The valid format strings are as follows: - /// Any of the standard numeric format strings for double values except for "G" or "g" ("C" or "c", "E" or "e", "F" or "f", "N" or "n", "P" or "p", "R" or "r"). - /// "G" or "g": The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m". - /// "A" or "a": The default unit abbreviation for , such as "m". - /// "a0", "a1", ..., "aN": The Nth unit abbreviation for . "a0" is the same as "a". - /// A will be thrown if the requested abbreviation index does not exist. - /// "V" or "v": String representation of . - /// "U" or "u": The enum name of , such as "Meter". - /// "Q" or "q": The quantity name, such as "Length". - /// "s1", "s2", ..., "sN": The value with N significant digits after the radix followed by the unit abbreviation. For example, - /// "s4" would return "1.2345 m" if is 1.2345678. Trailing zeros are omitted. + /// + /// + /// A standard numeric format string. + /// Any of the standard numeric format for except for "G" or "g". + /// "C" or "c", "E" or "e", "F" or "f", "N" or "n", "P" or "p", "R" or "r" are all accepted. + /// + /// + /// + /// "G" or "g". + /// The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m". + /// + /// + /// "A" or "a". + /// The default unit abbreviation for , such as "m". + /// + /// + /// "A0", "A1", ..., "An" or "a0", "a1", ..., "an". + /// The n-th unit abbreviation for . "a0" is the same as "a". + /// A will be thrown if the requested abbreviation index does not exist. + /// + /// + /// "V" or "v". + /// The string representation of using the default ToString method. + /// + /// + /// "U" or "u". + /// The enum name of , such as "Meter". + /// + /// + /// "Q" or "q". + /// The quantity name, such as "Length". + /// + /// + /// "S1", "S2", ..., "Sn" or "s1", "s2", ..., "sn". + /// The value with n significant digits after the radix followed by the unit abbreviation. For example, + /// "s4" would return "1.2345 m" if is 1.2345678. Trailing zeros are omitted. + /// + /// /// /// The string representation. public static string Format(IQuantity quantity, string format) @@ -45,7 +73,7 @@ public static string Format(IQuantity quantity, string for } /// - /// Formats the given quantity using the given format string and format provider. + /// Formats a quantity using the given format string and format provider. /// /// The quantity's unit type, for example . /// The quantity to format. @@ -54,16 +82,44 @@ public static string Format(IQuantity quantity, string for /// if null. /// /// The valid format strings are as follows: - /// Any of the standard numeric format strings for double values except for "G" or "g" ("C" or "c", "E" or "e", "F" or "f", "N" or "n", "P" or "p", "R" or "r"). - /// "G" or "g": The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m". - /// "A" or "a": The default unit abbreviation for , such as "m". - /// "a0", "a1", ..., "aN": The Nth unit abbreviation for . "a0" is the same as "a". - /// A will be thrown if the requested abbreviation index does not exist. - /// "V" or "v": String representation of . - /// "U" or "u": The enum name of , such as "Meter". - /// "Q" or "q": The quantity name, such as "Length". - /// "s1", "s2", ..., "sN": The value with N significant digits after the radix followed by the unit abbreviation. For example, - /// "s4" would return "1.2345 m" if is 1.2345678. Trailing zeros are omitted. + /// + /// + /// A standard numeric format string. + /// Any of the standard numeric format for except for "G" or "g". + /// "C" or "c", "E" or "e", "F" or "f", "N" or "n", "P" or "p", "R" or "r" are all accepted. + /// + /// + /// + /// "G" or "g". + /// The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m". + /// + /// + /// "A" or "a". + /// The default unit abbreviation for , such as "m". + /// + /// + /// "A0", "A1", ..., "An" or "a0", "a1", ..., "an". + /// The n-th unit abbreviation for . "a0" is the same as "a". + /// A will be thrown if the requested abbreviation index does not exist. + /// + /// + /// "V" or "v". + /// The string representation of using the default ToString method. + /// + /// + /// "U" or "u". + /// The enum name of , such as "Meter". + /// + /// + /// "Q" or "q". + /// The quantity name, such as "Length". + /// + /// + /// "S1", "S2", ..., "Sn" or "s1", "s2", ..., "sn". + /// The value with n significant digits after the radix followed by the unit abbreviation. For example, + /// "s4" would return "1.2345 m" if is 1.2345678. Trailing zeros are omitted. + /// + /// /// /// The string representation. public static string Format(IQuantity quantity, string format, IFormatProvider? formatProvider)