Здесь я предлагал вариант реализации функции, которая форматирует заданное число в строку прописью на английском языке. Однако для нас, жителей России, эта функция не особо-то и интересна...
Предлагаю вариант функции, которая форматирует число на русском языке.
Зачем это нужно именно на PL/SQL?
Прямой необходимости в реализации такой функции на стороне сервера нет. Функция, очевидно, нужна для формирования отчётов, её можно реализовать на стороне клиента (Delphi-приложение, или web-страница). Однако наличие этой функции на стороне сервера:
- позволяет иметь единственную реализацию функции, что удобно при сопровождении кода;
- функция на PL/SQL работает в самой Oracle, реализация на PL/SQL никак не привязывается к платформе;
- позволяет создавать view, ссылающиеся на эту функцию, а view можно использовать из любого клиента
- …
Пару слов об алгоритме.
Алгоритм форматирования достаточно прост. Сначала разбиваем число на триады (по три разряда, справа на лево, делением на тысячи). Затем перебираем триады в обратном порядке, и форматируем строку для каждой триады, добавляя перед строкой наименование триады (миллион, триллион и т.п.). При этом необходимо помнить об отдельном написании числительных от 11 до 19, десятков (от 20, 30 и до 90), сотен (100, 200 .. 900). Так же не забываем о склонении числительных (одна тысяча, две тысячи, пять тысяч). Ну и напоследок вспомним, что число прописью может использоваться совместно с существительным мужского, женского, либо среднего рода. Чтобы все эти нюансы учесть красиво, само-собой напрашивается решение использовать массивы со всеми возможными написаниями числительных. О работе с массивами расскажу ниже.
Реализация кода (пакет на PL/SQL)
create or replace package fmt_ru is -- function num_declination возвращает склонение числа: 0, 1 или 2 function num_declination(number_ in integer) return integer; -- function number_in_words форматирут число прописью -- gender_ указывает род числа: 'M' - мужской, 'F' - женский, 'S' - средний -- short_ указывает, использовать ли сокращённые наименования триад (если > 0) или нет (0) function number_in_words(number_ in number, gender_ in varchar2 default 'M', short_ in integer default 0) return varchar2; end; / create or replace package body fmt_ru is s_zero constant varchar2(32) := 'ноль'; s_minus constant varchar2(32) := 'минус'; -- единицы: nw_med constant ta_vc_32 := ta_vc_32('один', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять');-- мужской род nw_fed constant ta_vc_32 := ta_vc_32('одна', 'две', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять');-- женский род nw_sed constant ta_vc_32 := ta_vc_32('одно', 'два', 'три', 'четыре', 'пять', 'шесть', 'семь', 'восемь', 'девять');-- средний род -- от 11 до 19: nw_eds constant ta_vc_32 := ta_vc_32('одиннадцать', 'двенадцать', 'тринадцать', 'четырнадцать', 'пятнадцать', 'шестнадцать', 'семнадцать', 'восемнадцать', 'девятнадцать'); -- десятки: nw_dds constant ta_vc_32 := ta_vc_32('десять', 'двадцать', 'тридцать', 'сорок', 'пятьдесят', 'шестьдесят', 'семьдесят', 'восемьдесят', 'девяносто'); -- сотни: nw_hds constant ta_vc_32 := ta_vc_32('сто', 'двести', 'триста', 'четыреста', 'пятьсот', 'шестьсот', 'семьсот', 'восемьсот', 'девятьсот'); -- триады nw_tst constant ta_vc_32 := ta_vc_32('тыс.', 'млн.', 'млрд.', 'трлн.', 'квадр.', 'квинт.', 'секст.', 'септ.', 'окт.', 'нон.', 'дец.', 'ундец.', ''); -- сокр. nw_tf0 constant ta_vc_32 := ta_vc_32('тысяч', 'миллионов', 'миллиардов', 'триллионов', 'квадриллионов', 'квинтиллионов', 'секстиллионов', 'септиллионов', 'октиллионов', 'нониллионов', 'дециллионов', 'ундециллионов', ''); nw_tf1 constant ta_vc_32 := ta_vc_32('тысяча', 'миллион', 'миллиард', 'триллион', 'квадриллион', 'квинтиллион', 'секстиллион', 'септиллион', 'октиллион', 'нониллион', 'дециллион', 'ундециллион' ,''); nw_tf2 constant ta_vc_32 := ta_vc_32('тысячи', 'миллиона', 'миллиарда', 'триллиона', 'квадриллиона', 'квинтиллиона', 'секстиллиона', 'септиллиона', 'октиллиона', 'нониллиона', 'дециллиона', 'ундециллиона', ''); nw_triads ta_integer := ta_integer(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); function num_declination(number_ in integer) return integer is num_ integer; begin num_ := abs(number_) mod 100; if num_ > 20 then num_ := num_ mod 10; end if; case num_ when 1 then return 1; when 2 then return 2; when 3 then return 2; when 4 then return 2; else return 0; end case; end; function number_in_words(number_ in number, gender_ in varchar2 default 'M', short_ in integer default 0) return varchar2 is result varchar2(4000); num number(38, 0); tmp number(38, 0); cnt integer; triada integer; e integer; d integer; h integer; begin if number_ is null then return null; elsif number_ = 0 then return s_zero; elsif number_ > 0 then num := number_; result := null; else num := -number_; result := s_minus; end if; -- разбиваем число на триады: cnt := 1; loop -- HINT: mod - дорогостоящая операция, поэтому здесь и далее вместо mod используется -- деление и вычитание с умножением --nw_triads(cnt) := num mod 1000; tmp := trunc(num / 1000); nw_triads(cnt) := num - tmp * 1000; exit when tmp = 0; num := tmp; cnt := cnt + 1; end loop; -- формируем строку слева направо: for i in reverse 1 .. cnt loop triada := nw_triads(i); if triada > 0 then --h := trunc(triada / 100); --d := trunc(triada / 10) mod 10; --e := triada mod 10; h := trunc(triada / 100); tmp := triada - h * 100; d := trunc(tmp / 10); e := tmp - d * 10; -- добавляем сотни if h > 0 then str.rappend(result, nw_hds(h), ' '); end if; -- добавляем десятки: if d > 0 then if d = 1 and e > 0 then str.rappend(result, nw_eds(e), ' '); e := 0; else str.rappend(result, nw_dds(d), ' '); end if; end if; -- добавляем единицы: if e > 0 then case i when 1 then -- до 1 000 - смотрим указанный род case gender_ when 'M' then str.rappend(result, nw_med(e), ' '); when 'F' then str.rappend(result, nw_fed(e), ' '); when 'S' then str.rappend(result, nw_sed(e), ' '); else raise_application_error(-20100, 'unknown gender: "'||gender_||'"'); end case; when 2 then -- от 1 000 до 1 000 000 - женский род str.rappend(result, nw_fed(e), ' '); else -- свыше 1 000 000 - мужской род str.rappend(result, nw_med(e), ' '); end case; end if; -- добавляем наименование триады: if i > 1 then tmp := i - 1; if short_ > 0 then -- краткое наименование: str.rappend(result, nw_tst(tmp), ' '); else -- полное наименование с учётом склонения: case num_declination(triada) when 0 then str.rappend(result, nw_tf0(tmp), ' '); when 1 then str.rappend(result, nw_tf1(tmp), ' '); when 2 then str.rappend(result, nw_tf2(tmp), ' '); end case; end if; end if; end if; end loop; return result; end; end; /
Типы данных, на которые ссылается приведённый код.
Для работы приведённого кода, потребуются следующие типы.
create or replace type ta_number is table of number; create or replace type ta_integer is table of integer; create or replace type ta_vc_32 is table of varchar2(32);
Это типы для объявления массивов: целочисленного и строкового (до 32х символов – нам этого хватит).
Фрагмент пакета STR
create or replace package str is -- function append возвращает результат в виде left_||delim_||right_ -- если left_ или right_ is null, то delim_ не используется function append(left_ in varchar2, delim_ in varchar2, right_ in varchar2) return varchar2; -- процедуры lappend и rappend добавляют строку src_ к строке dest_ через разделитель delim_ -- если src_ is null - dest_ не меняется -- если dest_ is null - результат равен src_ -- lappend добавляет src_ слева от dest_: dest_ := src_||delim_||dest_ -- rappend добавляет src_ справа к dest_: dest_ := dest_||delim_||src_ procedure lappend(dest_ in out varchar2, src_ in varchar2, delim_ in varchar2 default null); procedure rappend(dest_ in out varchar2, src_ in varchar2, delim_ in varchar2 default null); end; / create or replace package body str is function append(left_ in varchar2, delim_ in varchar2, right_ in varchar2) return varchar2 is begin if left_ is null then return right_; elsif right_ is null then return left_; else return left_||delim_||right_; end if; end; procedure lappend(dest_ in out varchar2, src_ in varchar2, delim_ in varchar2) is begin if dest_ is null then dest_ := src_; elsif src_ is not null then dest_ := src_||delim_||dest_; end if; end; procedure rappend(dest_ in out varchar2, src_ in varchar2, delim_ in varchar2) is begin if dest_ is null then dest_ := src_; elsif src_ is not null then dest_ := dest_||delim_||src_; end if; end; end; /
Фрагмент пакета ARR
create or replace package arr is ---------------------------------------------------------------- -- procedure init инициализирует массив: -- - если ещё не создан - создаёт -- - если указан clear_flag_, то происходит очистка массива ---------------------------------------------------------------- procedure init(array_ in out nocopy ta_number, clear_flag_ in boolean default true); procedure init(array_ in out nocopy ta_integer, clear_flag_ in boolean default true); procedure init(array_ in out nocopy ta_vc_32, clear_flag_ in boolean default true); ---------------------------------------------------------------- -- procedure add добавляет элемент в массив соответствующего типа -- для строковых аргументов происходит усечение value_, если его -- длина превышает допустимый размер ---------------------------------------------------------------- procedure add(array_ in out nocopy ta_number, value_ in number); procedure add(array_ in out nocopy ta_integer, value_ in integer); procedure add(array_ in out nocopy ta_vc_32, value_ in varchar2); ---------------------------------------------------------------- -- procedure list_to_array рассматривает входную строку list_ -- как набор значений, разделённых символом-разделителем delim_ -- и преобразует список в массив; -- если параметр clear_flag_ = false, то исходный массив не очищается (т.е. массив можно -- построить на основе нескольких исходных строк) ---------------------------------------------------------------- procedure list_to_array(list_ in varchar2, delim_ in varchar2, array_ in out nocopy ta_number, clear_flag_ in boolean default true); procedure list_to_array(list_ in varchar2, delim_ in varchar2, array_ in out nocopy ta_integer, clear_flag_ in boolean default true); procedure list_to_array(list_ in varchar2, delim_ in varchar2, array_ in out nocopy ta_vc_32, clear_flag_ in boolean default true); ---------------------------------------------------------------- -- Функции для использования в SQL -- эти функции не рекомендуется использовать в PL/SQL из-за копирования массивов ---------------------------------------------------------------- -- to_XXX_array преобразует строку в массив function to_number_array(list_ in varchar2, delim_ in varchar2) return ta_number; end; / create or replace package body arr is ---------------------------------------------------------------- -- инициализация массива: ---------------------------------------------------------------- procedure init(array_ in out nocopy ta_number, clear_flag_ in boolean default true) is begin if array_ is null then array_ := ta_number(); elsif clear_flag_ then array_.delete; end if; end; procedure init(array_ in out nocopy ta_integer, clear_flag_ in boolean default true) is begin if array_ is null then array_ := ta_integer(); elsif clear_flag_ then array_.delete; end if; end; procedure init(array_ in out nocopy ta_vc_32, clear_flag_ in boolean default true) is begin if array_ is null then array_ := ta_vc_32(); elsif clear_flag_ then array_.delete; end if; end; ---------------------------------------------------------------- -- добавление элемента в массив: ---------------------------------------------------------------- procedure add(array_ in out nocopy ta_number, value_ in number) is begin array_.extend; array_(array_.count) := value_; end; procedure add(array_ in out nocopy ta_integer, value_ in integer) is begin array_.extend; array_(array_.count) := value_; end; procedure add(array_ in out nocopy ta_vc_32, value_ in varchar2) is begin array_.extend; array_(array_.count) := substrb(value_, 1, 32); end; ---------------------------------------------------------------- -- процедура list_to_array рассматривает входную строку list_ как набор значений, разделённых -- символом-разделителем delim_ и преобразует список в массив -- procedure list_to_array(list_ in varchar2, delim_ in varchar2, -- array_ in out nocopy ta_XXX, clear_flag_ in boolean default true) ---------------------------------------------------------------- procedure list_to_array(list_ in varchar2, delim_ in varchar2, array_ in out nocopy ta_number, clear_flag_ in boolean default true) is dl pls_integer; p1 pls_integer; p2 pls_integer; begin init(array_, clear_flag_); -- если строка пуста - выход if list_ is null then return; end if; -- dl = длина разделителя, если разделитель не указан - не страшно dl := length(delim_); -- основной цикл p1 := 1; p2 := instr(list_, delim_, p1); while p2 > 0 loop add(array_, substr(list_, p1, p2 - p1)); p1 := p2 + dl; p2 := instr(list_, delim_, p1); end loop; add(array_, substr(list_, p1)); end; procedure list_to_array(list_ in varchar2, delim_ in varchar2, array_ in out nocopy ta_integer, clear_flag_ in boolean default true) is dl pls_integer; p1 pls_integer; p2 pls_integer; begin init(array_, clear_flag_); if list_ is null then return; end if; dl := length(delim_); p1 := 1; p2 := instr(list_, delim_, p1); while p2 > 0 loop add(array_, substr(list_, p1, p2 - p1)); p1 := p2 + dl; p2 := instr(list_, delim_, p1); end loop; add(array_, substr(list_, p1)); end; procedure list_to_array(list_ in varchar2, delim_ in varchar2, array_ in out nocopy ta_vc_32, clear_flag_ in boolean default true) is dl pls_integer; p1 pls_integer; p2 pls_integer; begin init(array_, clear_flag_); if list_ is null then return; end if; dl := length(delim_); p1 := 1; p2 := instr(list_, delim_, p1); while p2 > 0 loop add(array_, substr(list_, p1, p2 - p1)); p1 := p2 + dl; p2 := instr(list_, delim_, p1); end loop; add(array_, substr(list_, p1)); end; ---------------------------------------------------------------- -- функции для использования в SQL: ---------------------------------------------------------------- function to_number_array(list_ in varchar2, delim_ in varchar2) return ta_number is array_ ta_number; begin list_to_array(list_, delim_, array_); return array_; end; end; /
Итак
Чтобы можно было скомпилировать пакет FMT_RU, надо: создать типы для определения массивов и скомпилировать приведённые пакеты STR и ARR
Бонус
В качестве бонуса предлагаю функцию, которая форматирует число прописью на английском языке (эта функция получилась значительно проще).
create or replace package fmt_en is -- function number_in_words форматирут число прописью function number_in_words(number_ in number) return varchar2; end; / create or replace package body fmt_en is s_zero_en constant varchar2(32) := 'zero'; s_minus_en constant varchar2(32) := 'negative'; s_hundred_en constant varchar2(32) := 'hundred'; nw_eed constant ta_vc_32 := ta_vc_32('one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'); -- единицы (от 1 до 9) nw_eds constant ta_vc_32 := ta_vc_32('eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'); -- от 11 до 19: nw_dds constant ta_vc_32 := ta_vc_32('ten', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'); -- десятки (10, 20 .. 90) nw_trs constant ta_vc_32 := ta_vc_32('thousand', 'million', 'billion', 'trillion', 'quadrillion', 'quintillion', 'sextillion', 'septillion', 'octillion', 'nonillion', 'decillion', 'undecillion', ''); -- триады (по короткой шкале) nw_triads ta_integer := ta_integer(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); function number_in_words(number_ in number) return varchar2 is result varchar2(4000); num number(38, 0); tmp number(38, 0); cnt integer; triada integer; e integer; d integer; h integer; begin if number_ is null then return null; elsif number_ = 0 then return s_zero_en; elsif number_ > 0 then num := number_; result := null; else num := -number_; result := s_minus_en; end if; -- разбиваем число на триады: cnt := 1; loop -- HINT: mod - дорогостоящая операция, поэтому здесь и далее вместо mod используется -- деление и вычитание с умножением --nw_triads(cnt) := num mod 1000; tmp := trunc(num / 1000); nw_triads(cnt) := num - tmp * 1000; exit when tmp = 0; num := tmp; cnt := cnt + 1; end loop; -- формируем строку слева направо: for i in reverse 1 .. cnt loop triada := nw_triads(i); if triada > 0 then --h := trunc(triada / 100); --d := trunc(triada / 10) mod 10; --e := triada mod 10; h := trunc(triada / 100); tmp := triada - h * 100; d := trunc(tmp / 10); e := tmp - d * 10; -- добавляем сотни if h > 0 then str.rappend(result, nw_eed(h) || ' ' || s_hundred_en, ' '); end if; -- добавляем десятки: if d > 0 then if d = 1 and e > 0 then str.rappend(result, nw_eds(e), ' '); e := 0; else str.rappend(result, nw_dds(d), ' '); end if; end if; -- добавляем единицы: if e > 0 then str.rappend(result, nw_eed(e), ' '); end if; -- добавляем полное наименование триады: if i > 1 then tmp := i - 1; str.rappend(result, nw_trs(tmp), ' '); end if; end if; end loop; return result; end; end; /
Альтернативный вариант, который я предлагал ранее, хоть и проще для понимания, но работает в 2-3 раза медленнее, и у него есть ограничение: число по модулю должно быть меньше 1010. Приведённые же реализации такого ограничения не имеют: можно передавать целое число длинной до 38 знаков включительно (максимум для Oracle).
Пример использования функций.
Тестовый запрос:
select column_value num, fmt_en.number_in_words(column_value) in_words_en, fmt_ru.number_in_words(column_value) in_words_ru from table(arr.to_number_array( '-123456789,-1,0,1,12,123,1234,12345,123456,'|| '1234567,1721057,1721058,1721300,1721423,'|| '1721424,5373484,5373485,7777777,9999999,'|| '12345678,20000000,77777777,123456789,200000000,'|| '777777007,777777011,777777777,'|| '999999999999999999999999999999999999', ','))
Результат запроса:
NUM | IN_WORDS_EN | IN_WORDS_RU |
---|---|---|
-123456789 | negative one hundred twenty three million four hundred fifty six thousand seven hundred eighty nine | минус сто двадцать три миллиона четыреста пятьдесят шесть тысяч семьсот восемьдесят девять |
-1 | negative one | минус один |
0 | zero | ноль |
1 | one | один |
12 | twelve | двенадцать |
123 | one hundred twenty three | сто двадцать три |
1234 | one thousand two hundred thirty four | одна тысяча двести тридцать четыре |
12345 | twelve thousand three hundred forty five | двенадцать тысяч триста сорок пять |
123456 | one hundred twenty three thousand four hundred fifty six | сто двадцать три тысячи четыреста пятьдесят шесть |
1234567 | one million two hundred thirty four thousand five hundred sixty seven | один миллион двести тридцать четыре тысячи пятьсот шестьдесят семь |
1721057 | one million seven hundred twenty one thousand fifty seven | один миллион семьсот двадцать одна тысяча пятьдесят семь |
1721058 | one million seven hundred twenty one thousand fifty eight | один миллион семьсот двадцать одна тысяча пятьдесят восемь |
1721300 | one million seven hundred twenty one thousand three hundred | один миллион семьсот двадцать одна тысяча триста |
1721423 | one million seven hundred twenty one thousand four hundred twenty three | один миллион семьсот двадцать одна тысяча четыреста двадцать три |
1721424 | one million seven hundred twenty one thousand four hundred twenty four | один миллион семьсот двадцать одна тысяча четыреста двадцать четыре |
5373484 | five million three hundred seventy three thousand four hundred eighty four | пять миллионов триста семьдесят три тысячи четыреста восемьдесят четыре |
5373485 | five million three hundred seventy three thousand four hundred eighty five | пять миллионов триста семьдесят три тысячи четыреста восемьдесят пять |
7777777 | seven million seven hundred seventy seven thousand seven hundred seventy seven | семь миллионов семьсот семьдесят семь тысяч семьсот семьдесят семь |
9999999 | nine million nine hundred ninety nine thousand nine hundred ninety nine | девять миллионов девятьсот девяносто девять тысяч девятьсот девяносто девять |
12345678 | twelve million three hundred forty five thousand six hundred seventy eight | двенадцать миллионов триста сорок пять тысяч шестьсот семьдесят восемь |
20000000 | twenty million | двадцать миллионов |
77777777 | seventy seven million seven hundred seventy seven thousand seven hundred seventy seven | семьдесят семь миллионов семьсот семьдесят семь тысяч семьсот семьдесят семь |
123456789 | one hundred twenty three million four hundred fifty six thousand seven hundred eighty nine | сто двадцать три миллиона четыреста пятьдесят шесть тысяч семьсот восемьдесят девять |
200000000 | two hundred million | двести миллионов |
777777007 | seven hundred seventy seven million seven hundred seventy seven thousand seven | семьсот семьдесят семь миллионов семьсот семьдесят семь тысяч семь |
777777011 | seven hundred seventy seven million seven hundred seventy seven thousand eleven | семьсот семьдесят семь миллионов семьсот семьдесят семь тысяч одиннадцать |
777777777 | seven hundred seventy seven million seven hundred seventy seven thousand seven hundred seventy seven | семьсот семьдесят семь миллионов семьсот семьдесят семь тысяч семьсот семьдесят семь |
1E36 | nine hundred ninety nine decillion nine hundred ninety nine nonillion nine hundred ninety nine octillion nine hundred ninety nine septillion nine hundred ninety nine sextillion nine hundred ninety nine quintillion nine hundred ninety nine quadrillion nine hundred ninety nine trillion nine hundred ninety nine billion nine hundred ninety nine million nine hundred ninety nine thousand nine hundred ninety nine | девятьсот девяносто девять дециллионов девятьсот девяносто девять нониллионов девятьсот девяносто девять октиллионов девятьсот девяносто девять септиллионов девятьсот девяносто девять секстиллионов девятьсот девяносто девять квинтиллионов девятьсот девяносто девять квадриллионов девятьсот девяносто девять триллионов девятьсот девяносто девять миллиардов девятьсот девяносто девять миллионов девятьсот девяносто девять тысяч девятьсот девяносто девять |
Конец.
Такой вот получился пост. Много кода но, надеюсь, здесь всё понятно.
P.S.: весь приведённый в этом посте код написан мною лично. Пакеты ARR и STR выложены не полностью, но представленного кода достаточно, чтобы заработали функции number_in_words. Возможно, я найду время, и расскажу, зачем эти пакеты создавались и как их можно использовать в повседневной жизни.
5 коммент.:
Во тут можно найти пример реализации прописи. Без кучи кода и пакетов -- всего одна небольшая функция.
http://oraclenotes.ru/?p=63
JayDi, Вы хотя бы смотрели код, который предлагаете?.. А Вы знаете, сколько стоит работа функции replace?
Я подобных ужасных реализаций видел много, поэтому и предлагаю ОПТИМАЛЬНЫЙ (на мой взгляд - достаточно оптимальный) алгоритм, который формирует число максимально быстро.
P.S. По указанной ссылке - варианту уже несколько лет, а блог oraclenotes, к сожалению, страдает плагиатом.
Николай Зверев, код по ссылке работает отлично. Никаких тормозов с его стороны не видно -- как при формировании отдельных документов, так и при выводе большого количества данных.
Не вижу смысла заниматься оптимизацией кода там, где его влияние меньше 1%.
create or replace type ta_number is table of number;
похоже нигде не используется
возможно, я вырезал код из пакетов и вырезал что-то лишнее
Отправить комментарий