Skip to content
78 changes: 75 additions & 3 deletions UnitsNet.Tests/CustomCode/LengthTests.FeetInches.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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);
}

Expand Down Expand Up @@ -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));
}
}
}
6 changes: 2 additions & 4 deletions UnitsNet.Tests/CustomCode/LengthTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
71 changes: 61 additions & 10 deletions UnitsNet/CustomCode/Quantities/Length.extra.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Converts the length to a customary feet/inches combination.
Expand All @@ -33,7 +31,7 @@ public FeetInches FeetInches
/// </summary>
public static Length FromFeetInches(double feet, double inches)
{
return FromInches(InchesInOneFoot*feet + inches);
return FromInches(InchesInOneFoot * feet + inches);
}

/// <summary>
Expand All @@ -48,7 +46,8 @@ public static Length FromFeetInches(double feet, double inches)
/// <returns>Parsed length.</returns>
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.
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -189,16 +216,33 @@ 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)
{
inchTrunc++;
numerator = 0;
}

if (inchTrunc == Length.InchesInOneFoot)
{
feet++;
inchTrunc = 0;
}

var inchPart = new System.Text.StringBuilder();

if (inchTrunc != 0 || numerator == 0)
Expand Down Expand Up @@ -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}";
}
}
}