Skip to content

Commit 372daea

Browse files
authored
Standardize quantity formatting on .NET numeric formats (#1721)
## Motivation UnitsNet v6 has already removed the proprietary `U`, `V`, and `Q` quantity format strings. This completes that cleanup by removing the remaining `A` and `S` formats, so quantity format parameters consistently accept .NET numeric format strings and can be described accurately with `StringSyntaxAttribute.NumericFormat`. The historical rationale supports the cleanup: - `A` only returns unit abbreviations, which are already available explicitly through generated `GetAbbreviation()` APIs and the configurable `UnitAbbreviationsCache`. - #797 intended to remove `S`, and commit 721287d removed its documentation and tests, but the formatter implementation remained in v5. This PR completes that cleanup. The discussion in #1450 also noted that `S` can hide precision and that callers should choose an explicit numeric format. ## Changes - Remove `A`/`An` and `S`/`Sn` formatting from `QuantityFormatter` and `QuantityValue`. - Throw focused `FormatException` messages for those formerly recognized formats with migration guidance. - Keep standard and custom .NET numeric formats, such as `G3`, `F2`, `N2`, `E2`, and `0.##`. - Annotate generated quantity formatting, `QuantityFormatter`, `QuantityValue`, quantity extensions, and type-converter formatting with `StringSyntaxAttribute.NumericFormat`. - Add an internal `StringSyntaxAttribute` compatibility definition for the `netstandard2.0` target. - Regenerate quantities and tests, and update the string-formatting guide and v6 upgrade guide. ## Migration | Removed | Replacement | |---|---| | `A`, `A0`, `A1`, ... | `Length.GetAbbreviation(unit)` or `UnitAbbreviationsCache.GetUnitAbbreviations(unit)` | | `S`, `S2`, ... | An explicit standard/custom numeric format such as `G3`, `F2`, `N2`, `E2`, or `0.##` | | `U` | `quantity.Unit` | | `V` | `quantity.Value` | | `Q` | Static metadata such as `Length.Info.Name` | Currency (`C`) and percent (`P`) formats remain intentionally unsupported for physical quantities. Refs #1200. ## Validation - `generate-code.bat` - `dotnet build UnitsNet/UnitsNet.csproj --no-restore` (`netstandard2.0`, `net8.0`, `net9.0`, `net10.0`) - `dotnet test UnitsNet.Tests --no-restore -f net10.0` (52,506 passed, 16 skipped) - `dotnet test UnitsNet.Tests --no-restore -f net48` (45,321 passed, 16 skipped)
1 parent 82994b0 commit 372daea

272 files changed

Lines changed: 615 additions & 3457 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,9 @@ public override string ToString()
10321032
/// <summary>
10331033
/// Gets the string representation of this instance in the specified format string using the specified format provider, or <see cref=""CultureInfo.CurrentCulture"" /> if null.
10341034
/// </summary>
1035-
public string ToString(string? format, IFormatProvider? provider)
1035+
public string ToString(
1036+
[StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format,
1037+
IFormatProvider? provider)
10361038
{{
10371039
return QuantityFormatter.Default.Format(this, format, provider);
10381040
}}

CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,26 +1255,6 @@ public void ToString_WithSwedishCulture_ReturnsUnitAbbreviationForEnglishCulture
12551255
Writer.WL($@"
12561256
}}
12571257
1258-
[Fact]
1259-
public void ToString_SFormat_FormatsNumberWithGivenDigitsAfterRadixForCurrentCulture()
1260-
{{
1261-
var _ = new CultureScope(CultureInfo.InvariantCulture);
1262-
Assert.Equal(""0.1{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s1""));
1263-
Assert.Equal(""0.12{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s2""));
1264-
Assert.Equal(""0.123{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s3""));
1265-
Assert.Equal(""0.1235{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s4""));
1266-
}}
1267-
1268-
[Fact]
1269-
public void ToString_SFormatAndCulture_FormatsNumberWithGivenDigitsAfterRadixForGivenCulture()
1270-
{{
1271-
var culture = CultureInfo.InvariantCulture;
1272-
Assert.Equal(""0.1{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s1"", culture));
1273-
Assert.Equal(""0.12{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s2"", culture));
1274-
Assert.Equal(""0.123{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s3"", culture));
1275-
Assert.Equal(""0.1235{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s4"", culture));
1276-
}}
1277-
12781258
[Theory]
12791259
[InlineData(null)]
12801260
[InlineData(""en-US"")]

Docs/string-formatting.md

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,65 @@
1-
# String Formatting
1+
# String formatting
22

3-
## Common examples
3+
Quantity formatting applies a .NET numeric format string to the value and appends the localized
4+
abbreviation of the quantity's current unit.
45

5-
Assuming computer running with US English culture.
6-
```c#
7-
var length = Length.FromCentimeters(3.14159265358979);
6+
```csharp
7+
Length length = Length.FromCentimeters(Math.PI);
88

9-
// Typical formats
10-
length.ToString(); // 3.14 cm
11-
length.ToString("s4"); // 3.1416 cm
9+
length.ToString(); // 3.141592653589793 cm
10+
length.ToString("F2", CultureInfo.InvariantCulture); // 3.14 cm
11+
$"Length: {length:N3}"; // Length: 3.142 cm with an en-US current culture
12+
```
13+
14+
The parameterless overload uses the general (`G`) numeric format. Formatting honors the supplied
15+
`IFormatProvider`, or `CultureInfo.CurrentCulture` when none is supplied.
1216

13-
// Localized
14-
length.ToString(new CultureInfo("nb-NO")); // 3,14 cm
15-
length.ToString(new CultureInfo("ru-RU")); // 3,14 sm (Cyrillic)
17+
```csharp
18+
length.ToString(CultureInfo.GetCultureInfo("nb-NO")); // 3,141592653589793 cm
19+
length.ToString(CultureInfo.GetCultureInfo("ru-RU")); // 3,141592653589793 см
20+
```
1621

17-
// Converted
18-
length.As(LengthUnit.Meters).ToString(); // 0.13 m
22+
Convert the quantity before formatting when a different unit is required:
1923

20-
// Use .NET's built-in formatting methods
21-
Console.WriteLine("Length is {0:v} {0:a}", l); // "Length is 3.14159265358979 ft"
22-
string.Format("Length is {0:v} {0:a}", l); // "Length is 3.14159265358979 ft"
23-
$"Length is {l:v} {l:a}"; // "Length is 3.14159265358979 ft"
24+
```csharp
25+
Length meters = length.ToUnit(LengthUnit.Meter);
26+
string text = meters.ToString("G3", CultureInfo.InvariantCulture); // 0.0314 m
2427
```
2528

26-
## Standard Quantity Format Strings
29+
## Numeric formats
2730

28-
| Format specifier | Description | Examples |
29-
|------------------|-------------|---------|
30-
| "g" | General quantity pattern. Equivalent to parameterless `ToString()`. Rounds to 2 significant digits after the radix. | `Length.FromFeet(Math.PI).ToString("g")` -> 3.14 ft |
31-
| `f`, `f2`, ... `e`, `e3`, ... `r` `#.0` `00000.0` | [Standard numeric formatting](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#standard-format-specifiers) of value, with unit appended. | `Length.FromFeet(Math.PI).ToString("f")` -> 3.140 ft, `Length.FromFeet(Math.PI).ToString("f1")` -> 3.1 ft, `Length.FromFeet(Math.PI).ToString("r")` -> 3.141592653589793 ft, `Length.FromFeet(Math.PI).ToString("e2")` -> 3.14e+000 ft, `Length.FromFeet(Math.PI).ToString("#.0")` -> 3.1 ft, `Length.FromFeet(Math.PI).ToString("00.0")` -> 003.1 ft |
32-
| "aXX" | Unit abbreviation pattern. If more than one abbreviation is defined for the unit, then XX specifies the zero-indexed position in the array of abbreviations. XX defaults to 0. If the position is not found, `System.FormatException` is thrown. | `Length.FromFeet(Math.PI).ToString("a")` -> ft, `Length.FromFeet(Math.PI).ToString("a0")` -> ft, `Length.FromFeet(Math.PI).ToString("a1")` -> ', `Length.FromFeet(Math.PI).ToString("a2")` -> prime symbol, `Length.FromFeet(Math.PI).ToString("a3")` -> System.FormatException |
33-
| "q" | Quantity name pattern. Outputs the corresponding QuantityType enum name. | `Length.FromFeet(Math.PI).ToString("q")` -> Length, `Mass.FromTonnes(Math.PI).ToString("u")` -> Mass |
34-
| "u" | Unit name pattern. Each quantity type has a corresponding unit enum, such as `Length` quantity having `LengthUnit` unit enum with values `Meter`, `Centimeter` etc. This pattern outputs the unit enum name. | `Length.FromFeet(Math.PI).ToString("u")` -> Foot, `Mass.FromTonnes(Math.PI).ToString("u")` -> Tonne |
31+
UnitsNet accepts standard and custom .NET numeric formats, including:
3532

36-
There are three different overloads of the ToString() method to provide a string representation of a value and its units.
33+
| Intent | Format | Example |
34+
|---|---|---|
35+
| General notation with a precision | `G3` | `3.14 cm` |
36+
| Fixed decimal places | `F2` | `3.14 cm` |
37+
| Grouped number with fixed decimals | `N2` | `1,234.50 cm` |
38+
| Scientific notation | `E2` | `3.14E+000 cm` |
39+
| Up to two decimal places | `0.##` | `3.14 cm` |
40+
| Grouped with up to two decimal places | `#,##0.##` | `1,234.5 cm` |
3741

38-
## Number Formatting
42+
Currency (`C`) and percent (`P`) formats are rejected because adding currency or percent symbols to
43+
a physical quantity is misleading.
3944

40-
For "g" pattern (or if no pattern is specified), the number will be formatted with scientific notation for very small or very large values to increase readability. We did not find .NET's default behavior to work well for this so we created our own rules.
45+
See [.NET standard numeric format strings](https://learn.microsoft.com/dotnet/standard/base-types/standard-numeric-format-strings)
46+
and [.NET custom numeric format strings](https://learn.microsoft.com/dotnet/standard/base-types/custom-numeric-format-strings)
47+
for the complete syntax.
4148

42-
| Interval | Format | Examples |
43-
|-----------|-----------|-------------|
44-
| `(-inf <= x < 1e-03]` | scientific notation | 1e-04; 2.13e-05 |
45-
| `[1e-03 <= x < 1e+03]` | fixed point notation | 0.001; 0.01; 100 |
46-
| `[1e+03 <= x < 1e+06]` | fixed point notation with digit grouping | 1,000; 10,000; 100,000 |
47-
| `[1e+06 <= x <= +inf)` | scientific notation | 1.1e+06; 3.14e+07 |
49+
## Unit abbreviations
4850

49-
The symbols used for digit grouping and radix point are culture-sensitive. The above examples use `CultureInfo.InvariantCulture`.
51+
Use the generated quantity API to get the primary localized abbreviation explicitly:
5052

51-
For more examples, refer to the unit tests in [UnitsNet/UnitFormatter.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/UnitFormatter.cs).
53+
```csharp
54+
string abbreviation = Length.GetAbbreviation(LengthUnit.Foot);
55+
string localized = Length.GetAbbreviation(LengthUnit.Meter, CultureInfo.GetCultureInfo("ru-RU"));
56+
```
57+
58+
Use the configured abbreviation cache when every accepted abbreviation is needed. This includes
59+
runtime customizations made through `UnitsNetSetup`:
60+
61+
```csharp
62+
IReadOnlyList<string> abbreviations = UnitsNetSetup.Default.UnitAbbreviations
63+
.GetUnitAbbreviations(LengthUnit.Foot, CultureInfo.InvariantCulture);
64+
// "ft", "'", "′"
65+
```

Docs/upgrading-from-5.x-to-6.x.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ as binary floating point lose precision.
1919
- Allow `NaN` and infinity values in quantities #1289
2020
- Exact fractional values, arithmetic and generated unit conversions with `QuantityValue` #1544
2121
- `UnitsNet.Serialization.SystemTextJson`, with configurable quantity, unit and value converters #1544
22+
- Annotate numeric format-string parameters with `StringSyntaxAttribute.NumericFormat` so supported IDEs can provide
23+
syntax assistance
2224

2325
## Breaking changes
2426

@@ -62,6 +64,13 @@ as binary floating point lose precision.
6264

6365
### Behavioral change
6466

67+
- Complete the removal of proprietary quantity format strings. Quantity formatting now accepts .NET numeric format
68+
strings only; see [String formatting](string-formatting.md).
69+
- Replace `A`, `A0`, `A1`, ... with a generated `GetAbbreviation()` method or
70+
`UnitAbbreviationsCache.GetUnitAbbreviations()`.
71+
- Replace `S`, `S2`, ... with an explicit numeric format such as `G3`, `F2`, `N2`, `E2`, or `0.##`.
72+
- Replace `U`, `V`, and `Q` with the `Unit` property, the `Value` property, and static quantity metadata such as
73+
`Length.Info.Name`, respectively.
6574
- Exact rational conversions and arithmetic may produce results that differ from the previous `double` implementation in the least significant digits. Precision can be lost when a `QuantityValue` is converted to `double`; perform that conversion only at boundaries where floating-point behavior is required. #1544
6675
- Calls to `.As()` and `.ToUnit()` through an `IQuantity` or `IQuantity<TUnitType>` reference now use the `QuantityExtensions` methods and `UnitConverter.Default`. They no longer dispatch to type-specific methods defined by a custom quantity. Custom quantities that need these calls to support conversion must register their conversion functions with `UnitConverter.Default`. #1696
6776
- Calling these extension methods with an incompatible unit type now throws `UnitNotFoundException` instead of `ArgumentException`. Code that catches `ArgumentException` around interface-based conversions may need to be updated. #1696

0 commit comments

Comments
 (0)