2

I am working on a system that needs to accept and display complex fractions. The code for accepting fractions and turning them in to a double works, but when I want to display that value, I need to convert back to a fractional representation.

EDIT: I have fixed the overflow problem, but that didnt solve fractions like 1/3 or 5/6. SO I have devised a very hacky way to do this. I have code which generates the decimal representation of every fraction 0->64 over 1->64, and saves the most simplified form. This way, I can iterate through the list and find the closest fraction, and simply display that. Will post code once I have some.

I have code now that works for the vast majority of numbers, but occasionally I will get a tiny fraction like 1/321. This gets converted to a double, but cannot be converted back, because in my approach, the numerator causes an integer overflow.

Here is my code, I'm wondering if there is a better approach, or if there is someway to safely convert these to longs without losing the precision needed for a correct result:

public static String DecimalToFraction(double dec)
    {
        string str = dec.ToString();
        if (str.Contains('.'))
        {
            String[] parts = str.Split('.');
            long whole = long.Parse(parts[0]);
            long numerator = long.Parse(parts[1]);
            long denominator = (long)Math.Pow(10, parts[1].Length);
            long divisor = GCD(numerator, denominator);
            long num = numerator / divisor;
            long den = denominator / divisor;

            String fraction = num + "/" + den;
            if (whole > 0)
            {
                return whole + " " + fraction;
            }
            else
            {
                return fraction;
            }
        }
        else
        {
            return str;
        }
    }

    public static long GCD(long a, long b)
    {
        return b == 0 ? a : GCD(b, a % b);
    }
Adam Yost
  • 3,616
  • 23
  • 36
  • 4
    Putting a decimal value into a double won't end well. Double is a binary type. – David Heffernan Sep 05 '14 at 21:25
  • Is it possible to create a new `Fraction` type to pass your values around in, rather than trying to convert back and forth? How do you expect to handle repeating decimals? – Steve Ruble Sep 05 '14 at 21:27
  • @DavidHeffernan I am confused as to why I should not use a double to store these values. It was my understanding that it would be sufficient for the whole numbers and simple fractions in the majority of cases. I may end up just blocking fractions this complex. – Adam Yost Sep 05 '14 at 21:34
  • 1
    You cannot represent decimal values in a binary type. And anyway, you presumably want more. How are you going to represent 1/3. You should study the issue of representability. – David Heffernan Sep 05 '14 at 21:36
  • That makes sense, what is a better alternative? I had not thought about repeating decimals, and it looks like my current approach would not work with those (333/1000 does not give a gcd of 333) – Adam Yost Sep 05 '14 at 21:38
  • You need to store numerator and denominator. And in the db too. – David Heffernan Sep 05 '14 at 21:39
  • Surely there is a better way than redesigning my tables just to allow fractions. – Adam Yost Sep 05 '14 at 21:43
  • 1
    How are you going to store 1/3? – David Heffernan Sep 05 '14 at 21:46
  • @DavidHeffernan I understand that would work, and get 1/3, and 1/6, etc, but that's such a small benefit to reap from a huge system redesign needed at this stage. Is there really no way to fudge the conversion, rather than the storage? – Adam Yost Sep 05 '14 at 21:47
  • How? Tell me how to store 1/3 in binary floating point. And then be able to distinguish it from some other close by rational. I suppose you have to decide whether or not you care about getting the answer right. – David Heffernan Sep 05 '14 at 21:50
  • I can store 1/3 as 0.33333333, and use a method slightly more complex than my current to figure out that is closest to 1/3. I like @InBetween's answer as it allows for finding the closest – Adam Yost Sep 05 '14 at 22:12
  • 1
    double to fraction is always exact, fraction to double is not always exact. – leppie Sep 05 '14 at 22:23
  • The closest? Not sure what that means. Anyway, the closest double precision binary value to 1/3 is 0.33333 33333 33333 31482 96162 56247 39099 29394 72198 48632 8125. How are you going to deal with that. And you won't store 0.33333333 because it's not representable. You'd end up with 0.33333 33299 99999 98325 08024 15568 61437 85715 10314 94140 625 – David Heffernan Sep 05 '14 at 22:24
  • I think you are assuming I need far more precision than I actually do. This is for fractions of measurements for recipes, It doesn't need to be chem-lab precision. 0.3333 is probably enough if I can find a programatic way to show that 3333/1000 -> 1/(~3) – Adam Yost Sep 05 '14 at 22:27
  • 1
    Perhaps you could decide on a spec of which fractions you need to support – David Heffernan Sep 06 '14 at 07:39

4 Answers4

5

Recently I had to code a similar scenario. In my case, converting from decimal to rational number had to be a little more mathematically correct so I ended up implementing a Continued Fraction algorithm.

Although it is tailored made to my concrete implementation of RationalNumber, you should get the idea. It's a relatively simple algorithm that works reasonably well for any rational number approximation. Note that the implementation will give you the closest approximation with the required precision.

/// <summary>
/// Represents a rational number with 64-bit signed integer numerator and denominator.
/// </summary>
[Serializable]
public struct RationalNumber : IComparable, IFormattable, IConvertible, IComparable<RationalNumber>, IEquatable<RationalNumber>
{
     private const int MAXITERATIONCOUNT = 20;

     public RationalNumber(long number) {...}
     public RationalNumber(long numerator, long denominator) {...}
     public RationalNumber(RationalNumber numerator, RationalNumer denominator) {...}

     ...
     /// <summary>
     /// Defines an implicit conversion of a 64-bit signed integer to a rational number.
     /// </summary>
     /// <param name="value">The value to convert to a rational number.</param>
     /// <returns>A rational number that contains the value of the value parameter as its numerator and 1 as its denominator.</returns>
     public static implicit operator RationalNumber(long value)
     {
         return new RationalNumber(value);
     }

     /// <summary>
     /// Defines an explicit conversion of a rational number to a double-precision floating-point number.
     /// </summary>
     /// <param name="value">The value to convert to a double-precision floating-point number.</param>
     /// <returns>A double-precision floating-point number that contains the resulting value of dividing the rational number's numerator by it's denominator.</returns>
     public static explicit operator double(RationalNumber value)
     {
         return (double)value.numerator / value.Denominator;
     }

     ...
     /// <summary>
     /// Adds two rational numbers.
     /// </summary>
     /// <param name="left">The first value to add.</param>
     /// <param name="right">The second value to add.</param>
     /// <returns>The sum of left and right.</returns>
     public static RationalNumber operator +(RationalNumber left, RationalNumber right)
     {
         //First we try directly adding in a checked context. If an overflow occurs we use the least common multiple and return the result. If it overflows again, it
         //will be up to the consumer to decide what he will do with it.
         //Cost penalty should be minimal as adding numbers that cause an overflow should be very rare.

         RationalNumber result;

         try
         {
             long numerator = checked(left.numerator * right.Denominator + right.numerator * left.Denominator);
             long denominator = checked(left.Denominator * right.Denominator);
             result = new RationalNumber(numerator,denominator);
         }
         catch (OverflowException)
         {
             long lcm = RationalNumber.getLeastCommonMultiple(left.Denominator, right.Denominator);
             result = new RationalNumber(left.numerator * (lcm / left.Denominator) + right.numerator * (lcm / right.Denominator), lcm);
         }

         return result;
     }

     private static long getGreatestCommonDivisor(long i1, long i2)
     {
        Debug.Assert(i1 != 0 || i2 != 0, "Whoops!. Both arguments are 0, this should not happen.");

        //Division based algorithm
        long i = Math.Abs(i1);
        long j = Math.Abs(i2);
        long t;

        while (j != 0)
        {
            t = j;
            j = i % j;
            i = t;
        }

        return i;
    }

    private static long getLeastCommonMultiple(long i1, long i2)
    {
        if (i1 == 0 && i2 == 0)
            return 0;

        long lcm = i1 / getGreatestCommonDivisor(i1, i2) * i2;
        return lcm < 0 ? -lcm : lcm;
     }
     ...

     /// <summary>
     /// Returns the nearest rational number approximation to a double-precision floating-point number with a specified precision.
     /// </summary>
     /// <param name="target">Target value of the approximation.</param>
     /// <param name="precision">Minimum precision of the approximation.</param>
     /// <returns>Nearest rational number with, at least, the required precision.</returns>
     /// <exception cref="System.ArgumentException">Can not find a rational number approximation with specified precision.</exception>
     /// <exception cref="System.OverflowException">target is larger than Mathematics.RationalNumber.MaxValue or smaller than Mathematics.RationalNumber.MinValue.</exception>
     /// <remarks>It is important to clarify that the method returns the first rational number found that complies with the specified precision. 
     /// The method is not required to return an exact rational number approximation even if such number exists.
     /// The returned rational number will always be in coprime form.</remarks>
     public static RationalNumber GetNearestRationalNumber(double target, double precision)
     {
         //Continued fraction algorithm: http://en.wikipedia.org/wiki/Continued_fraction
         //Implemented recursively. Problem is figuring out when precision is met without unwinding each solution. Haven't figured out how to do that.
         //Current implementation evaluates a Rational approximation for increasing algorithm depths until precision criteria is met or maximum depth is reached (MAXITERATIONCOUNT)
         //Efficiency is probably improvable but this method will not be used in any performance critical code. No use in optimizing it unless there is a good reason.
         //Current implementation works reasonably well.

         RationalNumber nearestRational = RationalNumber.zero;
         int steps = 0;

         while (Math.Abs(target - (double)nearestRational) > precision)
         {
             if (steps > MAXITERATIONCOUNT)
                 throw new ArgumentException(Strings.RationalMaximumIterationsExceptionMessage, "precision");

             nearestRational = getNearestRationalNumber(target, 0, steps++);
         }

         return nearestRational;
     }

    private static RationalNumber getNearestRationalNumber(double number, int currentStep, int maximumSteps)
    {
        long integerPart;
        integerPart = checked((long)number);
        double fractionalPart = number - integerPart;

        while (currentStep < maximumSteps && fractionalPart != 0)
        {
            return integerPart + new RationalNumber(1, getNearestRationalNumber(1 / fractionalPart, ++currentStep, maximumSteps));
        }

        return new RationalNumber(integerPart);
    }     
}

UPDATE: Whoops, forgot to include the operator + code. Fixed it.

InBetween
  • 32,319
  • 3
  • 50
  • 90
2

Keep the number as a fraction:

struct Fraction
{
     private int _numerator;
     private int _denominator;

     public int Numerator { get { return _numerator; } }
     public int Denominator { get { return _denominator; } }
     public double Value { get { return ((double) Numerator)/Denominator; } }

     public Fraction( int n, int d )
     {
         // move negative to numerator.
         if( d < 0 )
         {
            _numerator = -n;
            _denominator = -d;
         }
         else if( d > 0 )
         {
            _numerator = n;
            _denominator = d;
         }
         else
            throw new NumberFormatException( "Denominator cannot be 0" );
    }



     public void ToString()
     {
         string ret = "";
         int whole = Numerator / Denominator;
         if( whole != 0 )
             ret += whole + " ";
         ret += Math.Abs(Numerator % Denominator) + "/" + Denominator;
         return ret;
     }
} 
clcto
  • 9,530
  • 20
  • 42
  • 1
    This seems like one of the few cases where using a struct would make more sense (updated to use readonly properties, obviously). – Steve Ruble Sep 05 '14 at 21:32
  • I am operating on these quantities as a double/float more frequently than I need them in fraction format. In addition, they are stored in a SQL db, where it makes much more sense to use decimal representation. – Adam Yost Sep 05 '14 at 21:36
  • 1
    @Adam are all your values of the form k/2^n? You should be aware that contrary to your belief, you are not using a decimal representation. Until you accept that you cannot make progress. – David Heffernan Sep 05 '14 at 21:37
  • I accept that, I hadn't even thought about it. I am attempting to change all the references to those values to use floats now. – Adam Yost Sep 05 '14 at 21:39
2

You could use BigRational, which Microsoft released under their BCL project on codeplex. It supports arbitrarily large rational numbers, and actually stores it internally as a ratio. The nice thing is that you can treat it largely as a normal numeric type, since all of the operators are overloaded for you.

Interestingly, it lacks a way to print the number as a decimal. I wrote some code that did this, though, in a previous answer of mine. However, there are no guarantees on its performance or quality (I barely remember writing it).

Community
  • 1
  • 1
Christopher Currens
  • 29,917
  • 5
  • 57
  • 77
-1

Please check these 2 methods:

/// <summary>
    /// Converts Decimals into Fractions.
    /// </summary>
    /// <param name="value">Decimal value</param>
    /// <returns>Fraction in string type</returns>
    public string DecimalToFraction(double value)
    {
        string result;
        double numerator, realValue = value;
        int num, den, decimals, length;
        num = (int)value;
        value = value - num;
        value = Math.Round(value, 5);
        length = value.ToString().Length;
        decimals = length - 2;
        numerator = value;
        for (int i = 0; i < decimals; i++)
        {
            if (realValue < 1)
            {
                numerator = numerator * 10;
            }
            else
            {
                realValue = realValue * 10;
                numerator = realValue;
            }
        }
        den = length - 2;
        string ten = "1";
        for (int i = 0; i < den; i++)
        {
            ten = ten + "0";
        }
        den = int.Parse(ten);
        num = (int)numerator;
        result = SimplifiedFractions(num, den);
        return result;
    }

    /// <summary>
    /// Converts Fractions into Simplest form.
    /// </summary>
    /// <param name="num">Numerator</param>
    /// <param name="den">Denominator</param>
    /// <returns>Simplest Fractions in string type</returns>
    string SimplifiedFractions(int num, int den)
    {
        int remNum, remDen, counter;
        if (num > den)
        {
            counter = den;
        }
        else
        {
            counter = num;
        }
        for (int i = 2; i <= counter; i++)
        {
            remNum = num % i;
            if (remNum == 0)
            {
                remDen = den % i;
                if (remDen == 0)
                {
                    num = num / i;
                    den = den / i;
                    i--;
                }
            }
        }
        return num.ToString() + "/" + den.ToString();
    }
}