Можно ли удалить ошибки с плавающей запятой, не прибегая к произвольным типам данных?

Мне было интересно, можно ли при определенных условиях удалить ошибки с плавающей запятой, не обращаясь к типам данных произвольной точности.

Проблема обычна. Язык Ruby, но он держится на любом языке:

f = 1829.82
=> 1829.82

f / 12.0
=> 152.485

(f / 12.0).round(2)
=> 152.48

Почему бы не 152.49? Потому что из-за конечной точности плавающих точек:

format("%18.14f", f)
=> "1829.81999999999994"

format("%18.14f", f / 12.0)
=> "152.48499999999999"

Итак, округление правильное. Теперь мой вопрос: есть ли способ получить ответ, который я хочу в любом случае, учитывая следующие обстоятельства: существуют сильные ограничения на (число) операций, выполняемых с использованием float, точная точность ограничена двумя десятичными знаками (максимум 8 цифр) в целом), и небольшое количество оставшихся "ошибочных" округленных ответов приемлемо?

Ситуация такова, что пользователи могут вводить правильные строки Ruby, например:

"foo / 12.0"

где foo - число, предоставленное в контексте, в котором выполняется строка, но где '12.0 '- это то, что вводит пользователь. Представьте себе таблицу с некоторыми свободными полями формул. Строки просто оцениваются как Ruby, поэтому 12.0 становится Float. Я мог бы использовать ruby_parser + ruby2ruby gems для создания дерева синтаксического анализа, кастовать тип данных в Bignum, Rational, что-то из библиотеки Flt, десятичных представлений с плавающей запятой или того, что есть, но это сложно, поскольку фактические строки могут стать несколько сложнее, поэтому я предпочитаю не идти этим путем. Я поеду туда, если ничего не будет возможно, но этот вопрос специально здесь, чтобы увидеть, могу ли я избежать этого пути. Таким образом, тип данных 12.0 строго Float и результат строго Float, и единственное, что я могу сделать, это интерпретировать окончательный ответ фрагмента и пытаться "исправить" его, если он округляет "неправильный" способ.

Единственные расчеты, которые пользователи используют, включают числа с точностью до двух десятичных цифр (и не более 8 цифр). С "простым" я имею в виду, что ошибки с плавающей запятой не имеют возможности накапливаться: я могу добавить два из этих чисел и разделить их на целое число, но затем вычисление выполняется, результат округляется и сохраняется, а любой последующий расчет основано на этом округленном числе. Обычно будет задействована только одна ошибка с плавающей запятой, но я думаю, что проблема не будет существенно изменяться, если два могут накапливаться, хотя остаточная частота ошибок может быть больше по определению.

То, что может сначала прийти на ум, сначала округляется до трех десятичных цифр, а затем до 2. Однако это не работает. Это приведет к

152.48499999999999 => 152.485 => 152.49

но также

152.4846 => 152.485 => 152.49

который вам не нужен.

В следующий раз я подумал о добавлении наименьшего возможного приращения (которое, как указывали люди, зависит от рассматриваемого значения с плавающей запятой) с поплавком, если это подталкивает его к границе .5. Мне в основном интересно, как часто это может привести к "ложному положительному": числу, к которому добавляется наименьший приращение, хотя тот факт, что он был чуть ниже границы .5, был связан не с ошибкой с плавающей запятой, а с потому что это был просто результат расчета?

Второй вариант: всегда добавляйте наименьший приращение к числам, так как область .5 - это единственная, где это имеет значение.

Изменить: Я просто переписал вопрос, чтобы включить часть моих ответов в комментарии, как предложил cdiggins. Я наградил Ирак Бакстер щедростью за активное участие в обсуждении, хотя я еще не уверен, что он прав: Марк Рэнсом и Эмилио М Бумачар, похоже, поддерживают мою идею о том, что возможна коррекция, которая на практике может быть в относительно большом большинстве случаев, получается "правильный" результат.

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

Ответ 1

Похоже, что вы хотите десятичные числа с фиксированной точностью. Хорошая библиотека, реализующая их, будет более надежной, чем взломать что-то вместе.

Для Ruby ознакомьтесь с библиотекой Flt.

Ответ 2

"можно удалить ошибки с плавающей запятой, не прибегая к бесконечным типам данных точности."?

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

Это звучало педантично, что не было моим намерением. Я пытаюсь указать на то, что существует большая концептуальная проблема под вашим, по-видимому, техническим вопросом. Вы не можете правильно округлить неправильные номера в зависимости от того, какими будут их правильные значения, если вы не знаете их правильные значения (т.е. значения бесконечной точности или другую форму переноса этой информации).

Ваша идея добавить небольшое число может работать статистически, но только статистически. Я предлагаю вам написать script для проверки большого количества случаев со случайными числами не более двух знаков после запятой. (script нужно будет также выполнять математику с бесконечной точностью, чтобы знать правильный ответ для сравнения) Таким образом, вы можете измерить поправки и ложные срабатывания.

Ответ 3

Если вы можете контролировать количество арифметических (особенно умножает и делит), вы можете попробовать просто масштабировать все ваши значения с плавающей запятой с помощью некоторой шкалы мощности в десять (скажем, шкала = 4). Вам нужно будет изменить код ввода, вывода и умножения и деления.

Тогда масштаб = 2 десятичные дроби, такие как 5.10, хранятся точно так же, как 510. Входы нужно вводить точно; например, читать в строке mmm.nnnn, перенести местоположения масштаба десятичного знака в строку (например, для scale = 2 == > mmmnn.nn, а затем преобразовать строку в float). Дополнение/вычитание таких дробных чисел является точным и не требует каких-либо изменений кода. Умножение и деление теряют некоторую "десятичную" точность и должны быть масштабированы; код, который означает, что x * y необходимо изменить на x * y/scale; x/y необходимо изменить на x * scale/y. Вы можете объединить строку в масштабе, а затем вывести ее.

Этот ответ является дрянной версией использования реального десятичного арифметического пакета, упомянутого другим плакатом.

Ответ 4

Самый маленький приращение, о котором вы упоминаете, обычно называется epsilon. Это наименьшее значение, которое можно добавить в 1.0, чтобы внести заметные изменения. Если вы хотите добавить его к другим номерам, вы должны сначала масштабировать его: x = x + (x * epsilon).

Здесь другое определение epsilon, которое является наибольшей ошибкой округления числа с плавающей запятой. Это определение должно быть половиной первого.

Теоретически добавление значения epsilon перед округлением приведет к появлению столько ошибок, сколько исправляет. На практике это будет не так, поскольку числа, близкие к четному десятичному числу, с большей вероятностью будут встречаться, чем случайная вероятность.

Ответ 5

Я заметил, что в комментарии к одному из ответов было указано, что изменение типа данных было трудным. Тем не менее я собираюсь ответить на вопрос, задав вопрос:

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

Для достижения точных результатов вам нужно будет использовать десятичные представления с плавающей запятой чисел и соответствующих математических подпрограмм. Обратите внимание, что математические библиотеки с фиксированной точкой могут по-прежнему приводить к двоичным ошибкам с плавающей запятой, если они используют двоичные представления чисел.

Ответ 6

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

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

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

Ответ 7

Нет, вы не можете предотвратить накопление ошибок с плавающей запятой, поскольку арифметика машины всегда округляет результаты операции, чтобы вписаться в заданное количество бит. В дополнение к этому учтите, что результаты многих операций требуют бесконечного количества бит, которые должны быть представлены точно (например, 2/10 = 0,2, но для этого требуется бесконечное количество бит, чтобы точно представлять собой базу 2, что и есть компьютеры работают).

Ответ 8

это, к сожалению, не ваш ответ, но он может помочь вам начать.

Объект:

class Object
  # Return only the methods not present on basic objects
  def local_methods
    (self.methods - Object.new.methods).sort
  end
end

Обратный вызов Модуль:

module Hooker
  module ClassMethods
  private
    def following(*syms, &block)
      syms.each do |sym| # For each symbol
        str_id = "__#{sym}__hooked__"
        unless private_instance_methods.include?(str_id)
          alias_method str_id, sym    # Backup original method
          private str_id         # Make backup private
          define_method sym do |*args|  # Replace method
            ret = __send__ str_id, *args # Invoke backup
            rval=block.call(self,       # Invoke hook
             :method => sym, 
             :args => args,
             :return => ret
            )
            if not rval.nil?
              ret=rval[:ret]
            end
            ret # Forward return value of method
          end
        end
      end
    end
  end

  def Hooker.included(base)
    base.extend(ClassMethods)
  end
end

И изменится на Float, чтобы на самом деле выполнить работу:

if 0.1**2 != 0.01 # patch Float so it works by default
  class Float
    include Hooker
    0.1.local_methods.each do |op|
      if op != :round
        following op do |receiver, args|
          if args[:return].is_a? Float
            ret=args[:return].round Float::DIG
            ret=Hash[:ret => ret]
          end
          ret
        end
      end
    end
  end
end

Изменить: несколько лучше использовать Rational. Перемещаемые методы не всегда включены (см. Проблемы после кода):

  class Float
    include Hooker
    0.1.local_methods.each do |op|
      if op != :round
        following op do |receiver, args|
          if args[:return].is_a? Float
            argsin=[]
            args[:args].each do |c|
              argsin=c.rationalize
            end
            rval=receiver.rationalize.send(
                args[:method], 
                argsin
               )
            ret=Hash[:ret => rval.to_f]
          end
          ret
        end
      end
    end
  end

Проблемы. Не все переопределения методов работают, по крайней мере, в 1.9.3p0:

pry(main)> 6543.21 % 137.24
=> 92.93
[... but ...]
pry(main)> 19.5.send(:-.to_sym, 16.8)
=> 2.7
pry(main)> 19.5 - 16.8
=> 2.6999999999999993