Десятичная дробь

Я работаю над системой, которая должна принимать и отображать сложные дроби. Код для приема дробей и преобразования их в double работает, но когда я хочу отобразить это значение, мне нужно преобразовать обратно в дробное представление.

РЕДАКТИРОВАТЬ: я исправил проблему переполнения, но это не решило дроби, такие как 1/3 или 5/6. Поэтому я разработал очень хакерский способ сделать это. У меня есть код, который генерирует десятичное представление каждой дроби от 0->64 до 1->64 и сохраняет наиболее упрощенную форму. Таким образом, я могу пройтись по списку, найти ближайшую дробь и просто отобразить ее. Выложу код, когда он у меня будет.

Теперь у меня есть код, который работает для подавляющего большинства чисел, но иногда я получаю крошечную дробь, например 1/321. Это преобразуется в двойное, но не может быть преобразовано обратно, потому что в моем подходе числитель вызывает целочисленное переполнение.

Вот мой код, мне интересно, есть ли лучший подход или есть способ безопасно преобразовать их в длинные без потери точности, необходимой для правильного результата:

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

person Adam Yost    schedule 05.09.2014    source источник
comment
Помещение десятичного значения в двойное не закончится хорошо. Double — это бинарный тип.   -  person David Heffernan    schedule 06.09.2014
comment
Можно ли создать новый тип Fraction для передачи ваших значений вместо того, чтобы пытаться конвертировать туда и обратно? Как вы ожидаете обрабатывать повторяющиеся десятичные дроби?   -  person Steve Ruble    schedule 06.09.2014
comment
@DavidHeffernan Я не понимаю, почему я не должен использовать двойное число для хранения этих значений. Насколько я понял, этого будет достаточно для целых чисел и простых дробей в большинстве случаев. Я могу закончить тем, что просто блокирую дроби этого комплекса.   -  person Adam Yost    schedule 06.09.2014
comment
Вы не можете представлять десятичные значения в двоичном типе. И в любом случае, вы, вероятно, хотите большего. Как вы собираетесь представлять 1/3. Вам следует изучить вопрос репрезентативности.   -  person David Heffernan    schedule 06.09.2014
comment
Это логично, какая альтернатива лучше? Я не думал о повторении десятичных знаков, и похоже, что мой текущий подход не будет работать с ними (333/1000 не дает gcd 333)   -  person Adam Yost    schedule 06.09.2014
comment
Вам нужно сохранить числитель и знаменатель. И в БД тоже.   -  person David Heffernan    schedule 06.09.2014
comment
Конечно, есть лучший способ, чем переделывать мои таблицы только для того, чтобы разрешить дроби.   -  person Adam Yost    schedule 06.09.2014
comment
Как вы собираетесь хранить 1/3?   -  person David Heffernan    schedule 06.09.2014
comment
@DavidHeffernan Я понимаю, что это сработает и даст 1/3, 1/6 и т. д., но это такая небольшая выгода, которую можно извлечь из огромной перестройки системы, необходимой на данном этапе. Неужели нет способа обмануть преобразование, а не хранилище?   -  person Adam Yost    schedule 06.09.2014
comment
Как? Подскажите, как хранить 1/3 в двоичном формате с плавающей запятой. А затем суметь отличить его от какого-то другого, близкого по рациональности. Я полагаю, вы должны решить, заботитесь ли вы о том, чтобы получить правильный ответ.   -  person David Heffernan    schedule 06.09.2014
comment
Я могу сохранить 1/3 как 0,33333333 и использовать метод, немного более сложный, чем мой текущий, чтобы выяснить, что ближе всего к 1/3. Мне нравится ответ @InBetween, так как он позволяет найти ближайший   -  person Adam Yost    schedule 06.09.2014
comment
удвоение до дроби всегда точно, дробь до удвоения не всегда точно.   -  person leppie    schedule 06.09.2014
comment
Ближайший? Не знаю, что это значит. В любом случае, ближайшее двоичное значение двойной точности к 1/3 равно 0,33333 33333 33333 31482 96162 56247 39099 29394 72198 48632 8125. Как вы собираетесь с этим бороться. И вы не будете хранить 0,33333333, потому что это непредставимо. Вы получите 0,33333 33299 99999 98325 08024 15568 61437 85715 10314 94140 625   -  person David Heffernan    schedule 06.09.2014
comment
Я думаю, вы предполагаете, что мне нужна гораздо большая точность, чем на самом деле. Это для частей измерений для рецептов. Это не обязательно должна быть точность химлаборатории. 0,3333, вероятно, достаточно, если я найду программный способ показать, что 3333/1000 -> 1/(~3)   -  person Adam Yost    schedule 06.09.2014
comment
Возможно, вы могли бы решить, какие фракции вам нужно поддерживать   -  person David Heffernan    schedule 06.09.2014


Ответы (4)


Недавно мне пришлось кодировать аналогичный сценарий. В моем случае преобразование десятичных чисел в рациональные должно было быть немного более правильным с математической точки зрения, поэтому в итоге я реализовал Continued Дробный алгоритм.

Хотя он адаптирован к моей конкретной реализации RationalNumber, вы должны уловить идею. Это относительно простой алгоритм, который достаточно хорошо работает для любого приближения рациональных чисел. Обратите внимание, что реализация даст вам самое близкое приближение с требуемой точностью.

/// <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);
    }     
}

ОБНОВЛЕНИЕ: К сожалению, забыл включить код operator +. Починил это.

person InBetween    schedule 05.09.2014

Вы можете использовать BigRational, который Microsoft выпустила под своим BCL. проект на codeplex. Он поддерживает произвольно большие рациональные числа и фактически хранит их внутри как отношение. Приятно то, что вы можете обращаться с ним в основном как с обычным числовым типом, поскольку все операторы перегружены для вас.

Интересно, что в нем отсутствует способ печати числа в виде десятичной дроби. Однако я написал некоторый код, который сделал это, в моем предыдущем ответе. Однако нет никаких гарантий на его производительность или качество (я едва помню, что писал это).

person Christopher Currens    schedule 05.09.2014

Сохраните число в виде дроби:

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;
     }
} 
person clcto    schedule 05.09.2014
comment
Это похоже на один из немногих случаев, когда использование структуры имело бы больше смысла (очевидно, обновлено для использования свойств только для чтения). - person Steve Ruble; 06.09.2014
comment
Я работаю с этими количествами как с двойным/плавающим числом чаще, чем в дробном формате. Кроме того, они хранятся в базе данных SQL, где гораздо разумнее использовать десятичное представление. - person Adam Yost; 06.09.2014
comment
@Adam Адам, все твои значения имеют вид k/2^n? Вы должны знать, что вопреки вашему мнению, вы не используете десятичное представление. Пока вы не признаете, что вы не можете добиться прогресса. - person David Heffernan; 06.09.2014
comment
Я принимаю это, я даже не думал об этом. Я пытаюсь изменить все ссылки на эти значения, чтобы использовать поплавки сейчас. - person Adam Yost; 06.09.2014

Пожалуйста, проверьте эти 2 метода:

/// <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();
    }
}
person Syed Faizan Ali    schedule 21.01.2016