From 29da41ffb45106dbf4f16207ca8327f09dadb9fd Mon Sep 17 00:00:00 2001 From: Andrew Moskevitz <49752377+Applesauce314@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:11:40 -0400 Subject: [PATCH 1/8] add handling for FeetInches output where rounding makes inches =12 --- UnitsNet/CustomCode/Quantities/Length.extra.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index 138d9e9f7f..54a3df7286 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -161,7 +161,17 @@ public string ToString(IFormatProvider? cultureInfo) // Note that it isn't customary to use fractions - one wouldn't say "I am 5 feet and 4.5 inches". // So inches are rounded when converting from base units to feet/inches. - return string.Format(cultureInfo, "{0:n0} {1} {2:n0} {3}", Feet, footUnit, Math.Round(Inches), inchUnit); + // When we do this we check if we rounded inches to 12(InchesInOneFoot). + // If it does feet/inches are fixed something like 4 ft 0 in is displayed instead of 3ft 12 in for things very close to 4 e.g. 3.9999 ft + var feet = Feet; + var inches = Math.Round(Inches); + if(inches == InchesInOneFoot) + { + feet++; + inches = 0; + } + + return string.Format(cultureInfo, "{0:n0} {1} {2:n0} {3}", feet, footUnit, inches, inchUnit); } /// From b0e7699d99ce2d1930a7f563a1db9654a96c89a7 Mon Sep 17 00:00:00 2001 From: Andrew Moskevitz <49752377+Applesauce314@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:31:16 -0400 Subject: [PATCH 2/8] update ToArchitecturalString to not output 12 in the inches position --- UnitsNet/CustomCode/Quantities/Length.extra.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index 54a3df7286..d2a05b0fac 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -200,6 +200,7 @@ public string ToArchitecturalString(int fractionDenominator) throw new ArgumentOutOfRangeException(nameof(fractionDenominator), "Denominator for fractional inch must be greater than zero."); } + var feet = Feet; var inchTrunc = (int)Math.Truncate(Inches); var numerator = (int)Math.Round((Inches - inchTrunc) * fractionDenominator); @@ -209,6 +210,12 @@ public string ToArchitecturalString(int fractionDenominator) numerator = 0; } + if (inchTrunc == InchesInOneFoot) + { + feet++; + inchTrunc = 0; + } + var inchPart = new System.Text.StringBuilder(); if (inchTrunc != 0 || numerator == 0) @@ -248,7 +255,7 @@ int GreatestCommonDivisor(int a, int b) return inchPart.ToString(); } - return $"{Feet}' - {inchPart}"; + return $"{feet}' - {inchPart}"; } } } From 6137cdccf59e65d170c181380f7dd48e0673d2d3 Mon Sep 17 00:00:00 2001 From: Andrew Moskevitz <49752377+Applesauce314@users.noreply.github.com> Date: Sat, 2 May 2026 13:38:50 -0400 Subject: [PATCH 3/8] Update Length.extra.cs fixed using `Feet` instead of local copy `feet` --- UnitsNet/CustomCode/Quantities/Length.extra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index d2a05b0fac..20091b22a9 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -250,7 +250,7 @@ int GreatestCommonDivisor(int a, int b) inchPart.Append('"'); - if (Feet == 0) + if (feet == 0) { return inchPart.ToString(); } From 7b8d1d0918bfea3c5a70a83875e49446bca52cfc Mon Sep 17 00:00:00 2001 From: Andrew Moskevitz <49752377+Applesauce314@users.noreply.github.com> Date: Sat, 2 May 2026 13:48:06 -0400 Subject: [PATCH 4/8] Update Length.extra.cs fix formatting --- UnitsNet/CustomCode/Quantities/Length.extra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index 20091b22a9..a52510a3cb 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -165,7 +165,7 @@ public string ToString(IFormatProvider? cultureInfo) // If it does feet/inches are fixed something like 4 ft 0 in is displayed instead of 3ft 12 in for things very close to 4 e.g. 3.9999 ft var feet = Feet; var inches = Math.Round(Inches); - if(inches == InchesInOneFoot) + if (inches == InchesInOneFoot) { feet++; inches = 0; From 63d8cdef45624748177810c6a7207dd50759f121 Mon Sep 17 00:00:00 2001 From: apmoskevitz Date: Thu, 11 Jun 2026 10:12:37 -0400 Subject: [PATCH 5/8] wrote tests for fixed feet inches code and corrected procedure to correct failures --- .../CustomCode/LengthTests.FeetInches.cs | 48 ++++++++++++++++++- UnitsNet.Tests/CustomCode/LengthTests.cs | 6 ++- .../CustomCode/Quantities/Length.extra.cs | 38 +++++++++++---- 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs b/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs index 4c4855b436..66e641bf6f 100644 --- a/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs +++ b/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs @@ -20,7 +20,7 @@ public class FeetInchesTests public void FeetInchesFrom() { Length meter = Length.FromFeetInches(2, 3); - double expectedMeters = 2/FeetInOneMeter + 3/InchesInOneMeter; + double expectedMeters = 2 / FeetInOneMeter + 3 / InchesInOneMeter; AssertEx.EqualTolerance(expectedMeters, meter.Meters, FeetTolerance); } @@ -108,5 +108,51 @@ public void TryParseFeetInches_GivenInvalidString_ReturnsFalseAndZeroOut(string Assert.False(Length.TryParseFeetInches(str, out Length result, formatProvider)); Assert.Equal(Length.Zero, result); } + + [Theory] + [InlineData(-11.9999, 0, -11.9999)] + [InlineData(-23.98, -1, -11.98)] + [InlineData(-13, -1, -1)] + [InlineData(-38.563, -3, -2.563)] + + public static void NegativeFeetInchesIsAsExpected(double inch, double expectedFeet, double expectedInches) + { + var length = Length.FromInches(inch); + + Assert.Equal(expectedFeet, length.FeetInches.Feet, tolerance: 0.000000000001d); + Assert.Equal(expectedInches, length.FeetInches.Inches, tolerance: 0.000000000001d); + + } + + [Theory] + [InlineData(1, -11, 0, 1)] + [InlineData(-2, 2, -1, -10)] + [InlineData(-1, 32, 1, 8)] + + public static void MixedPositiveNegativeFeetInchesIsAsExpected(double feet, double inch, double expectedFeet, double expectedInches) + { + var length = Length.FromFeetInches(feet, inch); + + Assert.Equal(expectedFeet, length.FeetInches.Feet); + Assert.Equal(expectedInches, length.FeetInches.Inches); + + } + + [Theory] + [InlineData(11.9999, 16, "1' - 0\"")] + [InlineData(-11.9999, 16, "-1' - 0\"")] + [InlineData(23.98, 32, "1' - 11 31/32\"")] + [InlineData(-23.98, 32, "-1' - 11 31/32\"")] + [InlineData(13, 32, "1' - 1\"")] + [InlineData(-13, 32, "-1' - 1\"")] + [InlineData(38.563, 32, "3' - 2 9/16\"")] + [InlineData(-38.563, 32, "-3' - 2 9/16\"")] + + public static void NegativeToArchitecturalString_ReturnsFormatted(double inch, int fractionDenominator, string expected) + { + var length = Length.FromInches(inch); + + Assert.Equal(expected, length.FeetInches.ToArchitecturalString(fractionDenominator)); + } } } diff --git a/UnitsNet.Tests/CustomCode/LengthTests.cs b/UnitsNet.Tests/CustomCode/LengthTests.cs index f658ed9d87..9de0563572 100644 --- a/UnitsNet.Tests/CustomCode/LengthTests.cs +++ b/UnitsNet.Tests/CustomCode/LengthTests.cs @@ -52,7 +52,7 @@ public class LengthTests : LengthTestsBase protected override double ShacklesInOneMeter => 0.0364538; - protected override double NauticalMilesInOneMeter => 1.0/1852.0; + protected override double NauticalMilesInOneMeter => 1.0 / 1852.0; protected override double HandsInOneMeter => 9.8425196850393701; @@ -240,6 +240,9 @@ public static void InverseReturnsReciprocalLength(double value, double expected) Assert.Equal(expected, inverseLength.InverseMeters); } + + + [Theory] [InlineData(3, 2.563, 16, "3' - 2 9/16\"")] [InlineData(3, 2.563, 32, "3' - 2 9/16\"")] @@ -253,6 +256,7 @@ public static void InverseReturnsReciprocalLength(double value, double expected) [InlineData(3, 2.6, 16, "3' - 2 5/8\"")] [InlineData(3, 2.6, 32, "3' - 2 19/32\"")] [InlineData(3, 2.6, 128, "3' - 2 77/128\"")] + [InlineData(3, 11.9988, 128, "4' - 0\"")] public static void ToArchitecturalString_ReturnsFormatted(double ft, double inch, int fractionDenominator, string expected) { var length = Length.FromFeetInches(ft, inch); diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index a52510a3cb..f1952b642b 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -11,7 +11,7 @@ namespace UnitsNet { public partial struct Length { - private const double InchesInOneFoot = 12; + internal const double InchesInOneFoot = 12; /// /// Converts the length to a customary feet/inches combination. @@ -33,7 +33,7 @@ public FeetInches FeetInches /// public static Length FromFeetInches(double feet, double inches) { - return FromInches(InchesInOneFoot*feet + inches); + return FromInches(InchesInOneFoot * feet + inches); } /// @@ -48,7 +48,8 @@ public static Length FromFeetInches(double feet, double inches) /// Parsed length. public static Length ParseFeetInches(string str, IFormatProvider? formatProvider = null) { - if (str == null) throw new ArgumentNullException(nameof(str)); + if (str == null) + throw new ArgumentNullException(nameof(str)); if (!TryParseFeetInches(str, out Length result, formatProvider)) { // A bit lazy, but I didn't want to duplicate this edge case implementation just to get more narrow exception descriptions. @@ -165,12 +166,12 @@ public string ToString(IFormatProvider? cultureInfo) // If it does feet/inches are fixed something like 4 ft 0 in is displayed instead of 3ft 12 in for things very close to 4 e.g. 3.9999 ft var feet = Feet; var inches = Math.Round(Inches); - if (inches == InchesInOneFoot) + if (inches == Length.InchesInOneFoot) { feet++; inches = 0; } - + return string.Format(cultureInfo, "{0:n0} {1} {2:n0} {3}", feet, footUnit, inches, inchUnit); } @@ -199,10 +200,20 @@ public string ToArchitecturalString(int fractionDenominator) { throw new ArgumentOutOfRangeException(nameof(fractionDenominator), "Denominator for fractional inch must be greater than zero."); } - var feet = Feet; - var inchTrunc = (int)Math.Truncate(Inches); - var numerator = (int)Math.Round((Inches - inchTrunc) * fractionDenominator); + var inches = Inches; + //if negative value we record this and invert the values, at the end we add a negative sign as necessary, but all the calculations are done positive so rounding behavior is the same. + var isNegative = Feet < 0 || Inches < 0; + if (isNegative) + { + feet = -feet; + inches = -inches; + } + + + var inchTrunc = (int)Math.Truncate(inches); + var numerator = (int)Math.Round((inches - inchTrunc) * fractionDenominator); + if (numerator == fractionDenominator) { @@ -210,7 +221,7 @@ public string ToArchitecturalString(int fractionDenominator) numerator = 0; } - if (inchTrunc == InchesInOneFoot) + if (inchTrunc == Length.InchesInOneFoot) { feet++; inchTrunc = 0; @@ -255,7 +266,14 @@ int GreatestCommonDivisor(int a, int b) return inchPart.ToString(); } - return $"{feet}' - {inchPart}"; + //add the sign to the beginning if negative + string? sign = null; + if (isNegative) + { + sign = "-"; + } + + return $"{sign}{feet}' - {inchPart}"; } } } From 9976c797f2d7ab97b00babb7cf1b6181999c470a Mon Sep 17 00:00:00 2001 From: apmoskevitz Date: Thu, 11 Jun 2026 10:54:05 -0400 Subject: [PATCH 6/8] fixed feetinches.toString rounding negative lengths weirdly and added unit tests for that. --- .../CustomCode/LengthTests.FeetInches.cs | 30 +++++++++++++++++++ .../CustomCode/Quantities/Length.extra.cs | 22 ++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs b/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs index 66e641bf6f..e4f5402264 100644 --- a/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs +++ b/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Globalization; +using System.Text; +using Microsoft.VisualStudio.TestPlatform.Common.Utilities; using Xunit; namespace UnitsNet.Tests @@ -154,5 +156,33 @@ public static void NegativeToArchitecturalString_ReturnsFormatted(double inch, i Assert.Equal(expected, length.FeetInches.ToArchitecturalString(fractionDenominator)); } + + [Theory] + [InlineData(11.9999, "1 ft 0 in")] + [InlineData(-11.9999, "-1 ft 0 in")] + [InlineData(23.98, "2 ft 0 in")] + [InlineData(-23.98, "-2 ft 0 in")] + [InlineData(13, "1 ft 1 in")] + [InlineData(-13, "-1 ft 1 in")] + [InlineData(38.563, "3 ft 3 in")] + [InlineData(-38.563, "-3 ft 3 in")] + [InlineData(50.2, "4 ft 2 in")] + [InlineData(-50.2, "-4 ft 2 in")] + [InlineData(-50.2, "-4 фут 2 дюйм", "ru-RU")]//ensure we are using alternate units + [InlineData(-50.2, "\u22124 ft 2 in", "nb-NO")]// nb-NO does not have alternate abbreviations defined in length.json but does use a different negative symbol + public static void FeetInches_ToStringFormatsCorrectly(double inch, string expected, string? cultureString = null) + { + var length = Length.FromInches(inch); + CultureInfo culture; + if (cultureString == null) + { + culture = CultureInfo.InvariantCulture; + } + else + { + culture = new CultureInfo(cultureString, useUserOverride: false); + } + Assert.Equal(expected, length.FeetInches.ToString(culture)); + } } } diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index f1952b642b..c4f790a054 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -160,18 +160,36 @@ public string ToString(IFormatProvider? cultureInfo) var footUnit = Length.GetAbbreviation(LengthUnit.Foot, cultureInfo); var inchUnit = Length.GetAbbreviation(LengthUnit.Inch, cultureInfo); + // Note that it isn't customary to use fractions - one wouldn't say "I am 5 feet and 4.5 inches". // So inches are rounded when converting from base units to feet/inches. // When we do this we check if we rounded inches to 12(InchesInOneFoot). // If it does feet/inches are fixed something like 4 ft 0 in is displayed instead of 3ft 12 in for things very close to 4 e.g. 3.9999 ft - var feet = Feet; - var inches = Math.Round(Inches); + double feet; + double inches; + bool isNegative = Feet < 0 || Inches < 0; + if (isNegative) + { + feet = -Feet; + inches = Math.Round(-Inches); + } + else + { + feet = Feet; + inches = Math.Round(Inches); + } + if (inches == Length.InchesInOneFoot) { feet++; inches = 0; } + if (isNegative) + { + //we re-negate feet here so the negative will be handled by the built in formatter + feet = -feet; + } return string.Format(cultureInfo, "{0:n0} {1} {2:n0} {3}", feet, footUnit, inches, inchUnit); } From 012a632d6437f4fd5ab46f954ba9aad26415e38e Mon Sep 17 00:00:00 2001 From: apmoskevitz Date: Thu, 11 Jun 2026 10:58:28 -0400 Subject: [PATCH 7/8] fix using culture correct negative sign in ToArchitecturalString (maybe not important but made consistant with the ToString method --- UnitsNet/CustomCode/Quantities/Length.extra.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index c4f790a054..338a4bf039 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -284,14 +284,14 @@ int GreatestCommonDivisor(int a, int b) return inchPart.ToString(); } - //add the sign to the beginning if negative - string? sign = null; + if (isNegative) { - sign = "-"; + //re-negate feet so the output uses a culture correct negative sign. + feet = -feet; } - return $"{sign}{feet}' - {inchPart}"; + return $"{feet}' - {inchPart}"; } } } From c13213fdf9a6c90ae4691a0540d8c9edb6355c3c Mon Sep 17 00:00:00 2001 From: apmoskevitz Date: Thu, 11 Jun 2026 11:06:18 -0400 Subject: [PATCH 8/8] fix usings and formatting --- UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs | 4 ---- UnitsNet.Tests/CustomCode/LengthTests.cs | 6 ------ UnitsNet/CustomCode/Quantities/Length.extra.cs | 2 -- 3 files changed, 12 deletions(-) diff --git a/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs b/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs index e4f5402264..522784ff27 100644 --- a/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs +++ b/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs @@ -1,11 +1,7 @@ // 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 System.Collections.Generic; using System.Globalization; -using System.Text; -using Microsoft.VisualStudio.TestPlatform.Common.Utilities; -using Xunit; namespace UnitsNet.Tests { diff --git a/UnitsNet.Tests/CustomCode/LengthTests.cs b/UnitsNet.Tests/CustomCode/LengthTests.cs index 9de0563572..36a611bcf0 100644 --- a/UnitsNet.Tests/CustomCode/LengthTests.cs +++ b/UnitsNet.Tests/CustomCode/LengthTests.cs @@ -1,10 +1,7 @@ // 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 System; using System.Globalization; -using UnitsNet.Units; -using Xunit; namespace UnitsNet.Tests { @@ -240,9 +237,6 @@ public static void InverseReturnsReciprocalLength(double value, double expected) Assert.Equal(expected, inverseLength.InverseMeters); } - - - [Theory] [InlineData(3, 2.563, 16, "3' - 2 9/16\"")] [InlineData(3, 2.563, 32, "3' - 2 9/16\"")] diff --git a/UnitsNet/CustomCode/Quantities/Length.extra.cs b/UnitsNet/CustomCode/Quantities/Length.extra.cs index 338a4bf039..1e228bf68f 100644 --- a/UnitsNet/CustomCode/Quantities/Length.extra.cs +++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs @@ -1,11 +1,9 @@ // 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 System; using System.Globalization; using System.Text.RegularExpressions; using System.Threading; -using UnitsNet.Units; namespace UnitsNet {