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

Kate

Administrator
Команда форума

Предисловие​

Одной из частых рутин на работе является преобразование и извлечение чисел из строк текста. Самый наивный и простой подход в языке 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, 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;

if(esign < 0)
exp = 1 / exp;

double result = parseNumber(num, base); // Преобразовать численную часть.
result = (result == Double.NaN) ? result : result * exp;

if(sign < 0)
result = -result; //unary minus (negation)

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

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. Однако процедура допускает наличие лидирующих нулей вначале числа.

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

 
Сверху