Найти в Дзене
Oracle APEX

Форматирование чисел для калькулятора с текстом

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

Эта задача - часть более сложной задачи формирования микроотчетов, задаваемых пользователем в виде текста с вкраплениями некоторых условных конструкций. Здесь мы рассмотрим задачу в предположении, что заданная пользователем строка уже переведена на привычный Ораклу язык. Рассмотрим пример.

  • Заданная строка:

'''Число пи примерно равно 3.1415926. Одна треть - это один разделить на три, т.е. '' || 1 / 3 || '', а минус четыре трети - это уже целых '' || -4 / 3 || ''.''''

  • Число знаков после десятичного разделителя (точка или запятая): 3;
    в заданных пользователям константах следует сохранить использованный пользователем разделитель - точку или запятую соответственно, а в вычисленных значениях использовать разделитель, предусмотренный текущей сессией.
  • Желаемый результат:

Число пи примерно равно 3.142. Одна треть - это один разделить на три, т.е. 0,333, а минус четыре трети - это уже целых -1,333.

Сами вычисления для заданной таким образом строки мы можем выполнить очень просто - достаточно подставить ее вместо звездочки в запрос

select * from dual
;

Запишем на PL/SQL и выполним:

declare
p_str varchar2(4000) := '''Число пи примерно равно 3.1415926. Одна треть - это один разделить на три, т.е. '' || 1 / 3 || '', а минус четыре трети - это уже целых '' || -4 / 3 || ''.''';
v_sql varchar2(32000);
v_str varchar2(32000);
begin
v_sql := 'select ' || p_str || ' from dual';
dbms_output.put_line(v_sql);
execute immediate v_sql into v_str;
dbms_output.put_line(v_str);
end;

Получим текст SQL запроса и результат его исполнения, т.е. вычисления:

select 'Число пи примерно равно 3.1415926. Одна треть - это один разделить на три, т.е. ' || 1 / 3 || ', а минус четыре трети - это уже целых ' || -4 / 3 || '.' from dual

Число пи примерно равно 3.1415926. Одна треть - это один разделить на три, т.е. ,333333333333333333333333333333333333333, а минус четыре трети - это уже целых -1,3333333333333333333333333333333333333.

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

Поступим следующим образом:

  • Выделим из текста подстроки-кандидаты в дробные числа (округлять целые числа смысла нет);
  • Запомним для каждого из них использованный разделитель;
  • Проверим их на возможность преобразования к числу и преобразуем;
  • Округлим до заданного числа десятичных символов;
  • Преобразуем округленные числа в строки с использованием запомненного для них разделителя;
  • Выполним замену подтвержденных на возможность преобразования к числу подстрок-кандидатов на полученные на предыдущем шаге строки.

Кроме перечисленных шагов нам понадобятся некоторые вспомогательные.

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

c_num_mask varchar2(400) := '[\-]?[0-9]*[\.,][0-9]+';

Подсчитаем количество совпадений с маской в строке, полученной на этапе вычислений, т.е. при исполнении ранее сформированного динамического SQL запроса:

v_num_cou := regexp_count(v_str, c_num_mask);
dbms_output.put_line(v_num_cou);

3

Все правильно: в тексте - "смотрим глазами" - действительно 3 дробных числа, пока еще в форме подстрок, конечно. Выделим эти подстроки циклом:

for i in 1..v_num_cou loop
v_num_str := trim(regexp_substr(v_str, c_num_mask, 1, i));
dbms_output.put_line(v_num_str);
end loop;

3.1415926
,333333333333333333333333333333333333333
-1,3333333333333333333333333333333333333

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

select substr(value, 1, 1) into c_delim from nls_session_parameters
where parameter = 'NLS_NUMERIC_CHARACTERS';
dbms_output.put_line('Сессионный десятичный разделитель: ' || c_delim);

Сессионный десятичный разделитель: ,

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

for i in 1..v_num_cou loop
v_num_str := trim(regexp_substr(v_str, c_num_mask, 1, i));
dbms_output.put(v_num_str);
vv_delim := regexp_substr(v_num_str, '[\.,]');
v_num_str2 := regexp_replace(v_num_str, '[\.,]', c_delim);
dbms_output.put_line(' --> ' || v_num_str2);
end loop;

3.1415926 --> 3,1415926

,333333333333333333333333333333333333333 --> ,333333333333333333333333333333333333333

-1,3333333333333333333333333333333333333 --> -1,3333333333333333333333333333333333333

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

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

begin
v_num := round(to_number(v_num_str2), p_round);
dbms_output.put_line('-- ' || v_num);
exception when value_error then null;
end;

-- 3,142
-- ,333
-- -1,333

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

v_replace := trim(replace(to_char(v_num, c_format||rpad('.', p_round + 1, '9')), '.', vv_delim));
dbms_output.put_line('-- ' || v_replace);

-- 3.142
-- 0,333
-- -1,333

Мы получили три замены для трех подстрок-кандидатов. Выполним замены. Напомним, мы находимся в пределах цикла:

v_str := replace(v_str, v_num_str, v_replace);

На этом задача решена, выведем результат:

dbms_output.put_line(v_str);

Число пи примерно равно 3.142. Одна треть - это один разделить на три, т.е. 0,333, а минус четыре трети - это уже целых -1,333.

В этом примере мы объединили как вычисления арифметических выражений, так и задачу округления чисел в тексте. На практике, скорее всего, задача состоит из трех частей:

  1. Трансляция с пользовательского языка на язык конкатенаций, пригодный для вычислений;
  2. Собственно вычисления;
  3. Округления и форматирования.

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