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

от автора

Предисловие

Одной из частых рутин на работе является преобразование и извлечение чисел из строк текста. Самый наивный и простой подход в языке 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).      esign - знак экспоненты. */ public static double parse(String num, String e, char et,                              int base, int sign, int esign)   {         if(num == null || num.length() == 0 || base < 1) // null значения => NaN.             return Double.NaN;          double exp = 1; // Экспонента    // Двоичная экспонента (по основанию 2)         if((et == 'P' || et == 'p') && e != null && e.length() > 0)             exp = Math.pow(2.0, parseNumber(e, 10));      // Десятичная экспонента  (по основанию 10)         else if( (et == 'E' || et == 'e' || et == 'H' || et == 'h')                   && e != null && e.length() > 0)             exp = Math.pow(10.0, parseNumber(e, 10));      //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;          if(esign < 0)       exp = 1 / exp;          double result = parseNumber(num, base); // Преобразовать численную часть.        result = (result == Double.NaN) ? result : result * exp;              if(sign < 0) result = 0 - result; //make number negative (include minus sign)      return result; } }

Методу 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 esign = 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);        //Когда нет экспоненты (idx + 1) == 0.         if(idx + 1 == str.length())// no more digits after exponent letter ('12E' or 'FFP')             return Double.NaN;          String exp = null;          //Отрицательная экспонента, но нет цифр 'E-' or 'P-' or 'h-'     if(str.charAt(idx + 1) == '-' && idx != -1 && idx + 2 == str.length())         return Double.NaN;            //Отрицательная экспонента. 'E-2' or 'p-10'     if(str.charAt(idx + 1) == '-' && idx != -1){          exp = str.substring(idx + 2);          esign = -1;      }     else         exp = str.substring(idx + 1); //Положительная экспонента (после idx следует цифра)          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, esign); } }

Данный метод начинает со стандартной проверки на 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/articles/575456/