Skip to content

Commit c2ebeb9

Browse files
authored
Add support for standard numeric format strings (#788)
1 parent aed403a commit c2ebeb9

File tree

2 files changed

+288
-39
lines changed

2 files changed

+288
-39
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
2+
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
3+
4+
using Xunit;
5+
6+
namespace UnitsNet.Tests
7+
{
8+
public class QuantityFormatterTests
9+
{
10+
[Theory]
11+
[InlineData("C")]
12+
[InlineData("C0")]
13+
[InlineData("C1")]
14+
[InlineData("C2")]
15+
[InlineData("C3")]
16+
[InlineData("C4")]
17+
[InlineData("C5")]
18+
[InlineData("C6")]
19+
[InlineData("c")]
20+
[InlineData("c0")]
21+
[InlineData("c1")]
22+
[InlineData("c2")]
23+
[InlineData("c3")]
24+
[InlineData("c4")]
25+
[InlineData("c5")]
26+
[InlineData("c6")]
27+
[InlineData("E")]
28+
[InlineData("E0")]
29+
[InlineData("E1")]
30+
[InlineData("E2")]
31+
[InlineData("E3")]
32+
[InlineData("E4")]
33+
[InlineData("E5")]
34+
[InlineData("E6")]
35+
[InlineData("e")]
36+
[InlineData("e0")]
37+
[InlineData("e1")]
38+
[InlineData("e2")]
39+
[InlineData("e3")]
40+
[InlineData("e4")]
41+
[InlineData("e5")]
42+
[InlineData("e6")]
43+
[InlineData("F")]
44+
[InlineData("F0")]
45+
[InlineData("F1")]
46+
[InlineData("F2")]
47+
[InlineData("F3")]
48+
[InlineData("F4")]
49+
[InlineData("F5")]
50+
[InlineData("F6")]
51+
[InlineData("f")]
52+
[InlineData("f0")]
53+
[InlineData("f1")]
54+
[InlineData("f2")]
55+
[InlineData("f3")]
56+
[InlineData("f4")]
57+
[InlineData("f5")]
58+
[InlineData("f6")]
59+
[InlineData("N")]
60+
[InlineData("N0")]
61+
[InlineData("N1")]
62+
[InlineData("N2")]
63+
[InlineData("N3")]
64+
[InlineData("N4")]
65+
[InlineData("N5")]
66+
[InlineData("N6")]
67+
[InlineData("n")]
68+
[InlineData("n0")]
69+
[InlineData("n1")]
70+
[InlineData("n2")]
71+
[InlineData("n3")]
72+
[InlineData("n4")]
73+
[InlineData("n5")]
74+
[InlineData("n6")]
75+
[InlineData("P")]
76+
[InlineData("P0")]
77+
[InlineData("P1")]
78+
[InlineData("P2")]
79+
[InlineData("P3")]
80+
[InlineData("P4")]
81+
[InlineData("P5")]
82+
[InlineData("P6")]
83+
[InlineData("p")]
84+
[InlineData("p0")]
85+
[InlineData("p1")]
86+
[InlineData("p2")]
87+
[InlineData("p3")]
88+
[InlineData("p4")]
89+
[InlineData("p5")]
90+
[InlineData("p6")]
91+
[InlineData("R")]
92+
[InlineData("R0")]
93+
[InlineData("R1")]
94+
[InlineData("R2")]
95+
[InlineData("R3")]
96+
[InlineData("R4")]
97+
[InlineData("R5")]
98+
[InlineData("R6")]
99+
[InlineData("r")]
100+
[InlineData("r0")]
101+
[InlineData("r1")]
102+
[InlineData("r2")]
103+
[InlineData("r3")]
104+
[InlineData("r4")]
105+
[InlineData("r5")]
106+
[InlineData("r6")]
107+
public static void StandardNumericFormatStrings_Equals_ValueWithFormatStringAndAbbreviation(string format)
108+
{
109+
var length = Length.FromMeters(123456789.987654321);
110+
111+
var expected = string.Format($"{{0:{format}}} {{1:a}}", length.Value, length);
112+
Assert.Equal(expected, QuantityFormatter.Format(length, format));
113+
}
114+
115+
[Theory]
116+
[InlineData("000")]
117+
[InlineData("0.00")]
118+
[InlineData("#####")]
119+
[InlineData("#.##")]
120+
[InlineData("##,#")]
121+
[InlineData("#,#,,")]
122+
[InlineData("%#0.00")]
123+
[InlineData("##.0 %")]
124+
[InlineData("#0.00‰")]
125+
[InlineData("#0.0e0")]
126+
[InlineData("0.0##e+00")]
127+
[InlineData("0.0e+00")]
128+
[InlineData(@"\###00\#")]
129+
[InlineData("#0.0#;(#0.0#);-\0-")]
130+
[InlineData("#0.0#;(#0.0#)")]
131+
public static void CustomNumericFormatStrings_Equals_ValueWithFormatStringAndAbbreviation(string format)
132+
{
133+
var length = Length.FromMeters(123456789.987654321);
134+
135+
var expected = string.Format($"{{0:{format}}} {{1:a}}", length.Value, length);
136+
Assert.Equal(expected, QuantityFormatter.Format(length, format));
137+
}
138+
}
139+
}

UnitsNet/QuantityFormatter.cs

Lines changed: 149 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,66 @@ namespace UnitsNet
1414
public class QuantityFormatter
1515
{
1616
/// <summary>
17-
/// Formats the given quantity using the given format string and format provider.
17+
/// The available UnitsNet custom format specifiers.
18+
/// </summary>
19+
private static readonly char[] UnitsNetFormatSpecifiers = { 'A', 'a', 'G', 'g', 'Q', 'q', 'S', 's', 'U', 'u', 'V', 'v' };
20+
21+
/// <summary>
22+
/// Formats a quantity using the given format string and format provider.
23+
/// </summary>
24+
/// <typeparam name="TUnitType">The quantity's unit type, for example <see cref="LengthUnit"/>.</typeparam>
25+
/// <param name="quantity">The quantity to format.</param>
26+
/// <param name="format">The format string.</param>
27+
/// <remarks>
28+
/// The valid format strings are as follows:
29+
/// <list type="bullet">
30+
/// <item>
31+
/// <term>A standard numeric format string.</term>
32+
/// <description>Any of the standard numeric format for <see cref="IQuantity.Value" /> except for "G" or "g".
33+
/// "C" or "c", "E" or "e", "F" or "f", "N" or "n", "P" or "p", "R" or "r" are all accepted.
34+
/// </description>
35+
/// </item>
36+
/// <item>
37+
/// <term>"G" or "g".</term>
38+
/// <description>The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m".</description>
39+
/// </item>
40+
/// <item>
41+
/// <term>"A" or "a".</term>
42+
/// <description>The default unit abbreviation for <see cref="IQuantity{TUnitType}.Unit" />, such as "m".</description>
43+
/// </item>
44+
/// <item>
45+
/// <term>"A0", "A1", ..., "An" or "a0", "a1", ..., "an".</term>
46+
/// <description>The n-th unit abbreviation for <see cref="IQuantity{TUnitType}.Unit" />. "a0" is the same as "a".
47+
/// A <see cref="FormatException"/> will be thrown if the requested abbreviation index does not exist.</description>
48+
/// </item>
49+
/// <item>
50+
/// <term>"V" or "v".</term>
51+
/// <description>The string representation of <see cref="IQuantity.Value" /> using the default ToString method.</description>
52+
/// </item>
53+
/// <item>
54+
/// <term>"U" or "u".</term>
55+
/// <description>The enum name of <see cref="IQuantity{TUnitType}.Unit" />, such as "Meter".</description>
56+
/// </item>
57+
/// <item>
58+
/// <term>"Q" or "q".</term>
59+
/// <description>The quantity name, such as "Length".</description>
60+
/// </item>
61+
/// <item>
62+
/// <term>"S1", "S2", ..., "Sn" or "s1", "s2", ..., "sn".</term>
63+
/// <description>The value with n significant digits after the radix followed by the unit abbreviation. For example,
64+
/// "s4" would return "1.2345 m" if <see cref="IQuantity.Value" /> is 1.2345678. Trailing zeros are omitted.</description>
65+
/// </item>
66+
/// </list>
67+
/// </remarks>
68+
/// <returns>The string representation.</returns>
69+
public static string Format<TUnitType>(IQuantity<TUnitType> quantity, string format)
70+
where TUnitType : Enum
71+
{
72+
return Format(quantity, format, CultureInfo.CurrentUICulture);
73+
}
74+
75+
/// <summary>
76+
/// Formats a quantity using the given format string and format provider.
1877
/// </summary>
1978
/// <typeparam name="TUnitType">The quantity's unit type, for example <see cref="LengthUnit"/>.</typeparam>
2079
/// <param name="quantity">The quantity to format.</param>
@@ -23,57 +82,108 @@ public class QuantityFormatter
2382
/// <see cref="CultureInfo.CurrentUICulture" /> if null.</param>
2483
/// <remarks>
2584
/// The valid format strings are as follows:
26-
/// "g": The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m".
27-
/// "a": The default unit abbreviation for <see cref="IQuantity{TUnitType}.Unit" />, such as "m".
28-
/// "a0", "a1", ..., "aN": The Nth unit abbreviation for <see cref="IQuantity{TUnitType}.Unit" />. "a0" is the same as "a".
29-
/// A <see cref="FormatException"/> will be thrown if the requested abbreviation index does not exist.
30-
/// "v": String representation of <see cref="IQuantity.Value" />.
31-
/// "u": The enum name of <see cref="IQuantity{TUnitType}.Unit" />, such as "Meter".
32-
/// "q": The quantity name, such as "Length".
33-
/// "s1", "s2", ..., "sN": The value with N significant digits after the radix followed by the unit abbreviation. For example,
34-
/// "s4" would return "1.2345 m" if <see cref="IQuantity.Value" /> is 1.2345678. Trailing zeros are omitted.
85+
/// <list type="bullet">
86+
/// <item>
87+
/// <term>A standard numeric format string.</term>
88+
/// <description>Any of the standard numeric format for <see cref="IQuantity.Value" /> except for "G" or "g".
89+
/// "C" or "c", "E" or "e", "F" or "f", "N" or "n", "P" or "p", "R" or "r" are all accepted.
90+
/// </description>
91+
/// </item>
92+
/// <item>
93+
/// <term>"G" or "g".</term>
94+
/// <description>The value with 2 significant digits after the radix followed by the unit abbreviation, such as "1.23 m".</description>
95+
/// </item>
96+
/// <item>
97+
/// <term>"A" or "a".</term>
98+
/// <description>The default unit abbreviation for <see cref="IQuantity{TUnitType}.Unit" />, such as "m".</description>
99+
/// </item>
100+
/// <item>
101+
/// <term>"A0", "A1", ..., "An" or "a0", "a1", ..., "an".</term>
102+
/// <description>The n-th unit abbreviation for <see cref="IQuantity{TUnitType}.Unit" />. "a0" is the same as "a".
103+
/// A <see cref="FormatException"/> will be thrown if the requested abbreviation index does not exist.</description>
104+
/// </item>
105+
/// <item>
106+
/// <term>"V" or "v".</term>
107+
/// <description>The string representation of <see cref="IQuantity.Value" /> using the default ToString method.</description>
108+
/// </item>
109+
/// <item>
110+
/// <term>"U" or "u".</term>
111+
/// <description>The enum name of <see cref="IQuantity{TUnitType}.Unit" />, such as "Meter".</description>
112+
/// </item>
113+
/// <item>
114+
/// <term>"Q" or "q".</term>
115+
/// <description>The quantity name, such as "Length".</description>
116+
/// </item>
117+
/// <item>
118+
/// <term>"S1", "S2", ..., "Sn" or "s1", "s2", ..., "sn".</term>
119+
/// <description>The value with n significant digits after the radix followed by the unit abbreviation. For example,
120+
/// "s4" would return "1.2345 m" if <see cref="IQuantity.Value" /> is 1.2345678. Trailing zeros are omitted.</description>
121+
/// </item>
122+
/// </list>
35123
/// </remarks>
36124
/// <returns>The string representation.</returns>
37125
public static string Format<TUnitType>(IQuantity<TUnitType> quantity, string format, IFormatProvider? formatProvider)
38126
where TUnitType : Enum
39127
{
40-
formatProvider = formatProvider ?? CultureInfo.CurrentUICulture;
128+
formatProvider ??= CultureInfo.CurrentUICulture;
41129

42-
var number = 0;
43-
var formatString = format;
130+
if(string.IsNullOrWhiteSpace(format))
131+
format = "g";
44132

45-
if(string.IsNullOrEmpty(formatString))
46-
formatString = "g";
133+
char formatSpecifier = format[0];
47134

48-
if(formatString.StartsWith("a") || formatString.StartsWith("s"))
135+
if(UnitsNetFormatSpecifiers.Any(unitsNetFormatSpecifier => unitsNetFormatSpecifier == formatSpecifier))
49136
{
50-
if(formatString.Length > 1 && !int.TryParse(formatString.Substring(1), out number))
51-
throw new FormatException($"The {format} format string is not supported.");
137+
// UnitsNet custom format string
52138

53-
formatString = formatString.Substring(0, 1);
54-
}
139+
int precisionSpecifier = 0;
55140

56-
switch(formatString)
57-
{
58-
case "g":
59-
return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, 2);
60-
case "a":
61-
var abbreviations = UnitAbbreviationsCache.Default.GetUnitAbbreviations(quantity.Unit, formatProvider);
141+
switch(formatSpecifier)
142+
{
143+
case 'A':
144+
case 'a':
145+
case 'S':
146+
case 's':
147+
if(format.Length > 1 && !int.TryParse(format.Substring(1), out precisionSpecifier))
148+
throw new FormatException($"The {format} format string is not supported.");
149+
break;
150+
}
151+
152+
switch(formatSpecifier)
153+
{
154+
case 'G':
155+
case 'g':
156+
return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, 2);
157+
case 'A':
158+
case 'a':
159+
var abbreviations = UnitAbbreviationsCache.Default.GetUnitAbbreviations(quantity.Unit, formatProvider);
160+
161+
if(precisionSpecifier >= abbreviations.Length)
162+
throw new FormatException($"The {format} format string is invalid because the abbreviation index does not exist.");
62163

63-
if(number >= abbreviations.Length)
64-
throw new FormatException($"The {format} format string is invalid because the abbreviation index does not exist.");
164+
return abbreviations[precisionSpecifier];
165+
case 'V':
166+
case 'v':
167+
return quantity.Value.ToString(formatProvider);
168+
case 'U':
169+
case 'u':
170+
return quantity.Unit.ToString();
171+
case 'Q':
172+
case 'q':
173+
return quantity.QuantityInfo.Name;
174+
case 'S':
175+
case 's':
176+
return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, precisionSpecifier);
177+
default:
178+
throw new FormatException($"The {format} format string is not supported.");
179+
}
180+
}
181+
else
182+
{
183+
// Anything else is a standard numeric format string with default unit abbreviation postfix.
65184

66-
return abbreviations[number];
67-
case "v":
68-
return quantity.Value.ToString(formatProvider);
69-
case "u":
70-
return quantity.Unit.ToString();
71-
case "q":
72-
return quantity.QuantityInfo.Name;
73-
case "s":
74-
return ToStringWithSignificantDigitsAfterRadix(quantity, formatProvider, number);
75-
default:
76-
throw new FormatException($"The {format} format string is not supported.");
185+
var abbreviations = UnitAbbreviationsCache.Default.GetUnitAbbreviations(quantity.Unit, formatProvider);
186+
return string.Format(formatProvider, $"{{0:{format}}} {{1}}", quantity.Value, abbreviations.First());
77187
}
78188
}
79189

0 commit comments

Comments
 (0)