Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce conversion methods between Angle and Ratio #1342

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

mariuszhermansdorfer
Copy link

@angularsen, following your suggestions from #1337, here is an updated PR adding the conversion methods between Angle and Slope.

How do you suggest we proceed with creating strings like "1 in 2, 1 in 5, 1:2, 1:5" etc. ?

@mariuszhermansdorfer
Copy link
Author

mariuszhermansdorfer commented Dec 22, 2023

I could simply add the following method to Ratio.extra.cs:

        /// <summary>
        /// Converts the ratio to a string representing slope in a more detailed fraction format.
        /// </summary>
        /// <param name="cultureInfo">The culture info to format the string. If null, the current culture is used.</param>
        /// <returns>A string representing the slope in a detailed fraction format like "3 in 4" or "3 : 4".</returns>
        public string ToDetailedSlopeString(CultureInfo cultureInfo)
        {
            cultureInfo = cultureInfo ?? CultureInfo.CurrentCulture;

            // Find the closest fraction to represent the slope
            (int numerator, int denominator) = ConvertToFraction(this.DecimalFractions);

            string slopeFormat = cultureInfo.Name == "en-US" ? "{0} in {1}" : "{0} : {1}";

            return string.Format(cultureInfo, slopeFormat, numerator, denominator);
        }

        private (int, int) ConvertToFraction(double decimalFraction, int maxDenominator = 100)
        {
            if (decimalFraction == 0)
            {
                return (0, 1);
            }

            int sign = Math.Sign(decimalFraction);
            decimalFraction = Math.Abs(decimalFraction);

            int wholePart = (int)decimalFraction;
            decimalFraction -= wholePart;

            int lowerNumerator = 0, lowerDenominator = 1, upperNumerator = 1, upperDenominator = 1;

            while (lowerDenominator <= maxDenominator && upperDenominator <= maxDenominator)
            {
                int middleDenominator = lowerDenominator + upperDenominator;
                int middleNumerator = lowerNumerator + upperNumerator;

                if (middleDenominator > maxDenominator) break;

                double middleValue = (double)middleNumerator / middleDenominator;

                if (decimalFraction < middleValue)
                {
                    upperNumerator = middleNumerator;
                    upperDenominator = middleDenominator;
                }
                else if (decimalFraction > middleValue)
                {
                    lowerNumerator = middleNumerator;
                    lowerDenominator = middleDenominator;
                }
                else
                {
                    lowerNumerator = upperNumerator = middleNumerator;
                    lowerDenominator = upperDenominator = middleDenominator;
                    break;
                }
            }

            // Choose the fraction that is closer to the original decimalFraction
            double lowerDiff = decimalFraction - (double)lowerNumerator / lowerDenominator;
            double upperDiff = (double)upperNumerator / upperDenominator - decimalFraction;

            int finalNumerator, finalDenominator;

            if (lowerDiff < upperDiff)
            {
                finalNumerator = wholePart * lowerDenominator + lowerNumerator;
                finalDenominator = lowerDenominator;
            }
            else
            {
                finalNumerator = wholePart * upperDenominator + upperNumerator;
                finalDenominator = upperDenominator;
            }

            return (finalNumerator * sign, finalDenominator);
        }

@angularsen
Copy link
Owner

angularsen commented Dec 22, 2023

I don't know what the correct term is, but I'd expect something like ToRatioString() to output on the format 1:3 for 1 to 3 ratio.

This could be a method just like you proposed above.
Then create some unit tests to verify the behavior for a range of values.
For example, how does it work when the value is almost but not exactly 1:3, like 1:2.999 ? I assume it would be represented as 1000:2999? Or should there be some option to perform rounding? I'm not sure.

@angularsen
Copy link
Owner

angularsen commented Dec 22, 2023

There is some similar work here, for FeetInches.ToArchitecturalString():

/// <summary>
/// Outputs feet and inches on the format: {feetValue}' - {inchesValueIntegral}[ / {inchesValueFractional}]"
/// The inches are rounded to the nearest fraction of the fractionDenominator argument and reduced over the greatest common divisor.
/// The fractional inch value is omitted if the numerator is 0 after rounding, or if the provided denominator is 1.
/// </summary>
/// <param name="fractionDenominator">The maximum precision to express the rounded inch fraction part. Use 1 to round to nearest integer, with no fraction.</param>
/// <example>
/// <code>
/// var length = Length.FromFeetInches(3, 2.6);
/// length.ToArchitecturalString(1) => 3' - 3"
/// length.ToArchitecturalString(2) => 3' - 2 1/2"
/// length.ToArchitecturalString(4) => 3' - 2 1/2"
/// length.ToArchitecturalString(8) => 3' - 2 5/8"
/// length.ToArchitecturalString(16) => 3' - 2 5/8"
/// length.ToArchitecturalString(32) => 3' - 2 19/32"
/// length.ToArchitecturalString(128) => 3' - 2 77/128"
/// </code>
/// </example>
/// <exception cref="ArgumentOutOfRangeException">Denominator for fractional inch must be greater than zero.</exception>
public string ToArchitecturalString(int fractionDenominator)
{
if (fractionDenominator < 1)
{
throw new ArgumentOutOfRangeException(nameof(fractionDenominator), "Denominator for fractional inch must be greater than zero.");
}
var inchTrunc = (int)Math.Truncate(Inches);
var numerator = (int)Math.Round((Inches - inchTrunc) * fractionDenominator);
if (numerator == fractionDenominator)
{
inchTrunc++;
numerator = 0;
}
var inchPart = new System.Text.StringBuilder();
if (inchTrunc != 0 || numerator == 0)
{
inchPart.Append(inchTrunc);
}
if (numerator > 0)
{
int GreatestCommonDivisor(int a, int b)
{
while (a != 0 && b != 0)
{
if (a > b)
a %= b;
else
b %= a;
}
return a | b;
}
int gcd = GreatestCommonDivisor(numerator, fractionDenominator);
if (inchPart.Length > 0)
{
inchPart.Append(' ');
}
inchPart.Append($"{numerator / gcd}/{fractionDenominator / gcd}");
}
inchPart.Append('"');
if (Feet == 0)
{
return inchPart.ToString();
}
return $"{Feet}' - {inchPart}";
}
}

Maybe it can be reused or take inspiration from.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants