Skip to content
New issue

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

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

Already on GitHub? # to your account

Introduce conversion methods between Angle and Ratio #1342

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.

Copy link

github-actions bot commented Jul 8, 2024

This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.

Copy link

This PR was automatically closed due to inactivity.

@github-actions github-actions bot closed this Jul 16, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants