diff --git a/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs b/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs
index 4c4855b436..522784ff27 100644
--- a/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs
+++ b/UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs
@@ -1,9 +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 Xunit;
namespace UnitsNet.Tests
{
@@ -20,7 +18,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 +106,79 @@ 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));
+ }
+
+ [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.Tests/CustomCode/LengthTests.cs b/UnitsNet.Tests/CustomCode/LengthTests.cs
index f658ed9d87..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
{
@@ -52,7 +49,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;
@@ -253,6 +250,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 138d9e9f7f..1e228bf68f 100644
--- a/UnitsNet/CustomCode/Quantities/Length.extra.cs
+++ b/UnitsNet/CustomCode/Quantities/Length.extra.cs
@@ -1,17 +1,15 @@
// 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
{
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 +31,7 @@ public FeetInches FeetInches
///
public static Length FromFeetInches(double feet, double inches)
{
- return FromInches(InchesInOneFoot*feet + inches);
+ return FromInches(InchesInOneFoot * feet + inches);
}
///
@@ -48,7 +46,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.
@@ -159,9 +158,37 @@ 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.
- 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
+ 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);
}
///
@@ -189,9 +216,20 @@ public string ToArchitecturalString(int fractionDenominator)
{
throw new ArgumentOutOfRangeException(nameof(fractionDenominator), "Denominator for fractional inch must be greater than zero.");
}
+ var feet = Feet;
+ 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);
- var inchTrunc = (int)Math.Truncate(Inches);
- var numerator = (int)Math.Round((Inches - inchTrunc) * fractionDenominator);
if (numerator == fractionDenominator)
{
@@ -199,6 +237,12 @@ public string ToArchitecturalString(int fractionDenominator)
numerator = 0;
}
+ if (inchTrunc == Length.InchesInOneFoot)
+ {
+ feet++;
+ inchTrunc = 0;
+ }
+
var inchPart = new System.Text.StringBuilder();
if (inchTrunc != 0 || numerator == 0)
@@ -233,12 +277,19 @@ int GreatestCommonDivisor(int a, int b)
inchPart.Append('"');
- if (Feet == 0)
+ if (feet == 0)
{
return inchPart.ToString();
}
- return $"{Feet}' - {inchPart}";
+
+ if (isNegative)
+ {
+ //re-negate feet so the output uses a culture correct negative sign.
+ feet = -feet;
+ }
+
+ return $"{feet}' - {inchPart}";
}
}
}