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}"; } } }