Подтвердить что ты не робот

Как вручную проанализировать число с плавающей запятой из строки

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

Предположим, что float задан как в C или Java-программе (кроме суффикса 'f' или 'd'), например "4.2e1", ".42e2" или просто "42", В общем случае мы имеем "целую часть" перед десятичной точкой, "дробную часть" после десятичной точки и "показатель". Все три являются целыми числами.

Легко найти и обработать отдельные цифры, но как их скомпилировать в значение типа float или double без потери точности?

Я думаю о том, чтобы умножить целочисленную часть на 10 ^ n, где n - число цифр в дробной части, а затем добавление дробной части в целую часть и вычитание n из экспоненты. Например, это эффективно превращает 4.2e1 в 42e0. Затем я мог бы использовать функцию pow для вычисления показателя 10 ^ и умножить результат на новую целочисленную часть. Вопрос в том, гарантирует ли этот метод максимальную точность во всем?

Любые мысли об этом?

4b9b3361

Ответ 1

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

Прочитайте символ номер один за другим и сначала найдите все цифры. Сделайте это в целочисленной арифметике. Также отслеживайте десятичную точку и показатель экспоненты. Это будет важно позже.

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

Биты, следующие сразу после первого однобитового, являются вашей мантиссой.

Получение экспоненты тоже не сложно. Вы знаете первую однобитовую позицию, положение десятичной точки и необязательный показатель из научной нотации. Объедините их и добавьте смещение экспоненты с плавающей запятой (я думаю, это 127, но, пожалуйста, проверьте некоторые ссылки).

Этот показатель должен находиться где-то в диапазоне от 0 до 255. Если оно больше или меньше, у вас есть положительное или отрицательное бесконечное число (частный случай).

Храните экспонента в виде битов с 24 по 30 вашего поплавка.

Самый значащий бит - это просто знак. Один означает отрицательный, нуль означает положительный.

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

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

Ответ 2

(EDIT: немного добавил статью Дэвида Голдберга)

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

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

Мой лучший совет - начать с выполнения работы atoi и перейти оттуда. Вы быстро обнаружите, что вам не хватает вещей, но некоторые из них смотрят на источник strtod, и вы будете на правильном пути (который длинный, длинный путь). В конце концов вы похвалите здесь вставку, в которой есть стандартные библиотеки.

/* use this to start your atof implementation */

/* atoi - [email protected] */
/* PUBLIC DOMAIN */
long atoi(const char *value) {
  unsigned long ival = 0, c, n = 1, i = 0, oval;
  for( ; c = value[i]; ++i) /* chomp leading spaces */
    if(!isspace(c)) break;
  if(c == '-' || c == '+') { /* chomp sign */
    n = (c != '-' ? n : -1);
    i++;
  }
  while(c = value[i++]) { /* parse number */
    if(!isdigit(c)) return 0;
    ival = (ival * 10) + (c - '0'); /* mult/accum */
    if((n > 0 && ival > LONG_MAX)
    || (n < 0 && ival > (LONG_MAX + 1UL))) {
      /* report overflow/underflow */
      errno = ERANGE;
      return (n > 0 ? LONG_MAX : LONG_MIN);
    }
  }
  return (n>0 ? (long)ival : -(long)ival);
}

Ответ 3

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

Алгоритмы для перехода в другую сторону, печать лучшего десятичного числа из числа с плавающей запятой, найдены в Burger and Dybvig Быстро и точно печатать числа с плавающей запятой, загружаемый здесь. Это также требует целочисленной арифметики с цельной точностью

См. также David M Gay Правильно округленные двоично-десятичные и десятичные двоичные преобразования для алгоритмов, идущих в обоих направлениях.

Ответ 4

Вы можете игнорировать десятичное значение при разборе (кроме его местоположения). Скажем, вход был: 156.7834e10... Это может быть легко проанализировано в целое число 1567834, за которым следует e10, который затем следует изменить на e6, поскольку десятичное число было 4 цифры от конца "числовой" части поплавка.

Точность - проблема. Вам нужно будет проверить спецификацию IEEE на используемом вами языке. Если количество бит в Mantissa (или Fraction) больше, чем количество бит в вашем типе Integer, то вы, возможно, потеряете точность, когда кто-то вводит число, например:

5123.123123e0 - преобразуется в 5123123123 в наш метод, который НЕ вписывается в целое число, но биты для 5.123123123 могут вписываться в мантиссу спецификации float.

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

В любом случае, удачи!

Ответ 5

Моя первая мысль состоит в том, чтобы разделить строку на int64 mantissa и десятичный показатель int, используя только первые 18 цифр мантиссы. Например, 1.2345e-5 будет анализироваться на 12345 и -9. Тогда я бы продолжал умножать мантиссу на 10 и декретировать экспонента до тех пор, пока мантисса не будет 18 цифр ( > 56 бит точности). Затем я бы посмотрел десятичный показатель в таблице, чтобы найти коэффициент и двоичный показатель, который можно использовать для преобразования числа из десятичного числа n * 10 ^ m в двоичную форму p * 2 ^ q. Коэффициент будет другим int64, поэтому я бы умножил мантиссу таким образом, чтобы получить верхние 64-битные данные полученного 128-битного числа. Эта мантия int64 может быть отлита для поплавка, теряющего только необходимую точность, и показатель 2 ^ q может быть применен с использованием умножения без потери точности.

Я ожидаю, что это будет очень точно и очень быстро, но вы также можете обрабатывать специальные числа NaN, -infinity, -0.0 и бесконечность. Я не думал о денормализованных числах или режимах округления.

Ответ 6

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

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

НО ВИДЕТЬ, КАК ПОКАЖИТЕ МЫ МОЖЕМ:

Если вы тщательно восстановите float следующим образом:

if(biasedExponent >= 0)
    return integerMantissa * (10^biasedExponent);
else
    return integerMantissa / (10^(-biasedExponent));

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

К счастью, если первые две операции точны, то вы можете позволить себе последнюю неточную операцию * или /, благодаря свойствам IEEE результат будет округлен правильно.

Пусть это применимо к одиночным прецизионным поплавкам с точностью до 24 бит.

10^8 > 2^24 > 10^7

Отмечая, что кратное 2 будет только увеличивать экспоненту и оставить мантису без изменений, нам нужно иметь дело только с степенями 5 для экспонирования 10:

5^11 > 2^24 > 5^10

Хотя вы можете позволить себе 7 цифр точности в integerMantissa и смещенном экспоненте между -10 и 10.

В двойной точности 53 бит,

10^16 > 2^53 > 10^15
5^23 > 2^53 > 5^22

Таким образом, вы можете позволить себе 15 десятичных цифр и смещенный показатель между -22 и 22.

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

В противном случае вам придется использовать некоторую расширенную точность.
Если ваш язык предоставляет произвольные прецизионные целые числа, то это немного сложно сделать правильно, но не так сложно, я сделал это в Smalltalk и написал об этом в блоге http://smallissimo.blogspot.fr/2011/09/clarifying-and-optimizing.html и http://smallissimo.blogspot.fr/2011/09/reviewing-fraction-asfloat.html

Обратите внимание, что это простые и наивные реализации. К счастью, libc более оптимизирован.

Ответ 7

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

Ответ 8

Для этого вы должны понимать стандарт IEEE 754 для правильного двоичного представления. После этого вы можете использовать Float.intBitsToFloat или Double.longBitsToDouble.

http://en.wikipedia.org/wiki/IEEE_754

Ответ 9

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

Ответ 10

Невозможно преобразовать любую произвольную строку, представляющую число, в double или float без потери точности. Существует много дробных чисел, которые могут быть представлены точно в десятичной форме (например, "0,1" ), которые могут быть только аппроксимированы двоичным поплавком или двойным. Это похоже на то, как фракция 1/3 не может быть представлена ​​точно в десятичной форме, вы можете написать только 0.333333...

Если вы не хотите напрямую использовать библиотечную функцию, почему бы не посмотреть исходный код для этих функций библиотеки? Вы упомянули Java; большинство JDK отправляются с исходным кодом для библиотек классов, поэтому вы можете посмотреть, как работает метод java.lang.Double.parseDouble(String). Конечно, что-то вроде BigDecimal лучше для управления режимами точности и округления, но вы сказали, что это должно быть float или double.

Ответ 11

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

Проблема не тривиальна.

Я инженер-конструктор, интересующийся проектированием аппаратных средств с плавающей запятой. Я нахожусь на моей второй реализации.

Я нашел это сегодня http://speleotrove.com/decimal/decarith.pdf

который на стр. 18 дает некоторые интересные тестовые примеры.

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

Все вышеупомянутые ссылки на различные статьи превосходны.

Мне еще нужно зарегистрироваться здесь, но когда я это сделаю, если логин не будет принят, это будет брош. (Broh-точка).

Клайд