Преобразуем строки в числа в разных системах счисления

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Предисловие

Одной из частых рутин на работе является преобразование и извлечение чисел из строк текста. Самый наивный и простой подход в языке Java при преобразовании строки в число, это использовать Double.parseDouble(String num). Проблема этого метода в том, что он имеет баги в различных SDK, например в Android. Кроме того, данному методу не передаётся информация об основании системы счисления. Можно, конечно, использовать классы оболочки, передавая им в конструктор основание системы, но хотелось бы извлекать данную информацию из самой строки автоматически.

Исходная задача

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

Для каждой системы счисления, кроме десятичной определим соответствующий префикс:

  • 0x для 16-ричной.

  • 0c для 8-ричной.

  • 0b для двоичной.

У числа может быть задана экспонента. Определим три вида экспоненты, имеющие следующие префиксы:

  • 'H' | 'h' : десятичная экспонента для 16-ричных чисел, поскольку буква E уже занята (является цифрой 14 в данной системе).

  • 'E' | 'e' : десятичная экспонента для остальных чисел, чьё основание системы ниже 14.

  • 'P' | 'p' : двоичная экспонента для всех представленных чисел.

Пишем код.

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

public class ProcessNumber {
	  private static final String digits = "0123456789ABCDEF";
   
    /* Преобразует строку num в десятичное число типа double
       из указанного основания base
       Может вызвать переполнение (выход за пределы диапазона целых чисел)! 
    */
    private static double parseNumber(String num, int base){
        num = num.toUpperCase(); // digits are in UPPER_CASE
        double val = 0;
        int i = 0;
        while(i < num.length()) // пока не кончилась строка
        {
            char c = num.charAt(i);
            if(c == '.') { // нашли точку '.'
                i++; // Переместить на следующий символ и выйти из цикла. 
                break;
            }
            int d = digits.indexOf(c); // Индексы совпадают с числами из [0..15]
            if(d == -1 || d >= base)
                return Double.NaN;
            val = base * val + d;
            i++;
        }
      
        int power = 1; // вычислить лишний порядок.
        while(i < num.length())
        {
            char c = num.charAt(i);
            int d = digits.indexOf(c);
            if(d == -1 || d >= base)
                return Double.NaN;
            power *= base; // увеличиваем степень порядка на единицу 
            val = base * val + d;
            i++;
        }
        return val / power;
    }
   
}

Сейчас метод parseNumber() выполняет ровно одну задачу. Он пытается преобразовать строку num в число типа double, начиная с указанного основания base. Если обнаружен недопустимый символ в строке num, то метод вернёт специальную константу класса Double не-число (NaN - Not a Number).

Самому методу нужно передавать строку без экспоненты, знака, и префикса основания системы счисления. Их предстоит вычислить заранее. Если есть знак минус ('-') то число просто умножается на минус единицу (-1). Если есть экспонента, то число дополнительно умножается на неё. Прежде чем приступить к их вычислению, допустим, что нам уже известны данные компоненты. Напишем метод, который делает выбор на основе полученной информации, и выполняет соответствующее умножение преобразованного числа из заданного основания на полученную экспоненту и минус единицу, если необходимо.

public class ProcessNumber {
  // ... parseNumber(String str, int base) { ... }
	
  /* num - Число
  	 e - экспонента
   	 et - тип экспоненты
     base - основание системы счисления
     sign - знак числа (num > 0 => positive, num < 0 => negative).
	*/
	public static double parse(String num, String e, char et, int base, int sign){
        if(num == null || num.length() == 0 || base < 1) // null значения => NaN.
            return Double.NaN;

        double exp = 1; // Экспонента.
  
        if(sign < 0) // Отрицательное число
            sign = -1;
        else // Положительное число
            sign = 1;

  			// Двоичная экспонента (по основанию 2)
        if((et == 'P' || et == 'p') && e != null && e.length() > 0)
            exp = Math.pow(2.0, parseNumber(e, base));
  
  			// Десятичная экспонента  (по основанию 10)
        else if( (et == 'E' || et == 'e' || et == 'H' || et == 'h') 
                 && e != null && e.length() > 0)
            exp = Math.pow(10.0, parseNumber(e, base));
  
  			//e == null or e.length() == 0.
        // Указан тип экспоненты, но сама она отсутствует 
        else if(et == 'E' || et == 'e' || et == 'H' || et == 'h'
                || et == 'P' || et == 'p')
        {
            return Double.NaN;
        }
        else // et is not [PpEeHh] => ignore exponent (exp == 1) (Нет экспоненты)
            exp = 1;
    	 double result = parseNumber(num, base); // Преобразовать численную часть.
    	 
       return (result == Double.NaN) ? result : result * exp * sign;
	}
}

Методу parse() уже передаются вычисленные компоненты числа, а именно: само число num, его экспонента e, основание экспоненты et, основание системы самого числа base и знак числа sign. В данном методе уже предусмотрена защита от противоречивых данных (например, когда экспонента равна null, но указан её тип, или когда основание системы счисления не является натуральным числом (меньше единицы)). В простом случае, если строка равна null, то данный метод вернёт не-число (NaN). Метод выполняет простую задачу, он просто вычисляет множители итогового выражения result, и выполняет умножение преобразованных строк (экспоненты и самого числа без неё) на переменную знака sign. А вызываемый метод processNumber() переводит строку компонента в число.

Теперь остаётся написать последний метод, который вычисляет знак, экспоненту и основание системы. Ниже дан его код.

public class ProcessNumber {
  ...
	// parseNumber(String num, int base) { ... }

	// parse(String num, String exp, char etype, int base, int sign) { ... }

  /* В отличие от parseNumber(String num, int base)
     автоматически вычисляет основание base, экспоненту e, и её тип, а также
     знак числа sign. В случае успешного вычисления, передаёт вычисленные элементы
     методу parse(), который делает выбор (условный переход) множителей
     и преобразование строковых компонент уже через parseNumber(num, base).
  */
  public static double parseNumber(String str){
        if(str == null || str.length() == 0) //null is NaN.
            return Double.NaN;
  
        int sign = 1; // знак числа.
        int base = 10; // по умолчанию основание равно 10.
        int i = 0;
        if(str.charAt(0) == '-') { // Минус -> sign < 0.
            sign = -1;
            i = 1; // перейти к следующему знаку.
        }
        if(i > 0 && i == str.length()) //str is '-' (строка состоит только из '-')
            return Double.NaN;

        // suffix '0x' => 16 (hex)
        if(str.charAt(i) == '0' && (i + 1 != str.length()) && str.charAt(i + 1) == 'x') {
            base = 16;
            i += 2;
        }
        //suffix '0b' => 2 (binary)
        else if(str.charAt(i) == '0' && (i + 1 != str.length()) && str.charAt(i + 1) == 'b') {
            base = 2;
            i += 2;
        }
        //suffix '0c' => 8 (octal)
        else if(str.charAt(i) == '0' && (i + 1 != str.length()) && str.charAt(i + 1) == 'c'){
            base = 8;
            i += 2;
        }
        if(i == str.length())// строки вида (-0x -0b -0c 0x 0b 0c)
            return Double.NaN;

        //Вычислить экспоненту.
        int idx = str.indexOf('H');
        idx = (idx == -1) ? str.indexOf('h') : idx;
        idx = (idx == -1) ? str.indexOf('P') : idx;
        idx = (idx == -1) ? str.indexOf('p') : idx;
        idx = (idx == -1 && base != 16) ? str.indexOf('E') : idx;
        idx = (idx == -1 && base != 16) ? str.indexOf('e') : idx;

        char etype = (idx == -1) ? 'N' : str.charAt(idx);

        if(idx + 1 == str.length())// no more digits after exponent letter ('12E' or 'FFP')
            return Double.NaN;

        String exp = (idx == -1) ? null : str.substring(idx + 1);
        idx = (idx == -1) ? str.length() : idx; //if no exponent then idx = length(str)

        String number = str.substring(i, idx);
        return parse(number, exp, etype, base, sign);
	}
}

Данный метод начинает со стандартной проверки на null-значения. Далее, если строка не null и имеет символы, то проверяется её самый первый символ. Если он имеет знак минуса ('-') то множитель sign становится равен (-1). Иначе он остаётся равен 1. После вычисления знака идёт вычисление основания системы счисления по префиксу строки. После обработки префикса, снова проверяется наличие оставшейся части символов в строке. Если больше символов нет, то опять возвращается не-число (NaN). Если префикс основания отсутствует, то основание base считается равным 10. Затем вычисляется экспонента exp числа и индекс idx её начала для её последующего отделения от исходной строки. После вычисления всех компонентов, управление передаётся методу parse().

Заключение

Метод достаточно хорош, но ещё не идеален. При выходе из диапазона значений стандартных типов, можно получить неверный результат (а именно, отрицательные числа, когда как строка представляет положительное число, и наоборот). Он минует исключение NumberFormatException, возвращая не-число NaN когда обнаруживает недопустимый символ (не принадлежащий диапазону цифр в основании) а также NullPointerException, так как есть проверки на null (сводящиеся к замене null на NaN).

Следует также отметить, что самая последняя процедура processNumber(String num) имеет место уже с готовой лексемой num, лишённой лишних пробельных символов. При дублировании знака числа (минуса), результат будет снова NaN. Также, если сама экспонента NaN то и итоговое значение будет NaN. Однако процедура допускает наличие лидирующих нулей вначале числа.

Данную утилиту можно использовать только уже с коллекцией строк (цепочек), заранее выделенных из входного потока.

Источник: https://habr.com/ru/post/575456/


Интересные статьи

Интересные статьи

Некоторые ошибки трудно воспроизвести на вашем персональном компьютере, но их легко воспроизвести на производственных или тестовых машинах. Это обычная ситуация, с которой часто сталкиваю...
В работе с некоторыми командами бывают ситуации, когда что-то работает само, и об этом не надо думать. Сами доделываются задачи, сама развёртывается Continuous Integration — есть люди, ко...
Многие компании в определенный момент приходят к тому, что ряд процессов в бизнесе нужно автоматизировать, чтобы не потерять свое место под солнцем и своих заказчиков. Поэтому все...
Это третья, заключительная часть из цикла. В предыдущей статье мы подробно рассказали об УСН, патенте и налоге для самозанятых. В этой части рассчитаем налоговую нагрузку для ИП с доходом 100, ...
На сегодняшний день у сервиса «Битрикс24» нет сотен гигабит трафика, нет огромного парка серверов (хотя и существующих, конечно, немало). Но для многих клиентов он является основным инструментом ...