Предлагаемая вниманию уважаемых читателей статья посвящена одной сугубо практической задаче, которая сплошь и рядом встречается в обширных классах офисных и бизнес-программ. Прежде всего, позвольте задать вам такой вопрос: случалось ли вам видеть или более того, держать в руках какой-либо денежный документ (например, платежное поручение, кассовый чек или счет-фактуру)? И еще: обращали ли вы внимание на то, что на любой денежной купюре ее номинал указан дважды: в виде последовательности цифр (числовой номинал) и в виде той же суммы прописью? В практике разработки программного обеспечения достаточно часто возникает необходимость преобразования сумм или чисел из цифровой формы в строковое представление. Поясним это на нескольких примерах:
Читатель может с легкостью продолжить этот список. Приведенные выше примеры свидетельствуют, что задача преобразования сумм цифрами в соответствующие строковые представления, достаточно широко распространена и вполне естественно желание эту задачу автоматизировать.
В Internet можно найти немало примеров такого рода программ (их часто называют "конверторами"). Большая часть из них написана на VB или VBA и предназначена для выполнения соответствующих преобразований в приложениях MS Office. Немало существует конверторов для FoxPro. Наша задача заключается в написании Java-приложения, осуществляющего конвертацию сумм из цифрового представления, в строковое. Для определенности, мы напишем приложение, которое будет конвертировать денежные суммы, как наиболее часто встречающийся случай. Внимательный читатель с легкостью сумеет распространить "сферу влияния" этого приложения и на другие категории чисел которые нужно конвертировать в строки.
Прежде чем мы начнем говорить о самом приложении, нам придется на некоторое время "углубиться" в тонкости грамматики. Необходимость этого станет понятна, если мы рассмотрим небольшой пример:
810 | 121.21 | Сто двадцать один рубль 21 копейка |
810 | 121.23 | Сто двадцать один рубль 23 копейки |
810 | 121.25 | Сто двадцать один рубль 25 копеек |
Небольшое пояснение к структуре этой (и следующей) таблицы: первая колонка представляет собой международный числовой код валюты (в данном случае, рублей), вторая - сумма в цифровом выражении и, наконец, третья - строковое представление суммы из второй колонки с учетом написания заданной валюты.
Обратите внимание на выделенные фрагменты. Очевидно, что не только падеж, но и число (единственное или множественное) в третьей колонке таблицы зависят от цифрового значения копеек в ее второй колонке. Теперь немного изменим исходные данные:
810 | 121.21 | Сто двадцать один рубль 21 копейка |
276 | 121.21 | Сто двадцать одна марка 21 пфенниг |
840 | 121.21 | Сто двадцать один доллар 21 цент |
А здесь уже что-то новенькое: оказывается, нам необходимо учитывать не только падежи и числа (единственное или множественное), но и род той или иной денежной единицы. В данном случае марка (код 276) - валюта женского рода, а рубль (810) и доллар (840) - мужского.
Таким образом, как в рекламе зубной щетки: "в общем, вы поняли ...". Потратьте несколько минут на преобразования различных сумм в разных валютах в строки и вы согласитесь, что при всей простоте постановки задачи, ее решение, отнюдь не очевидно.
Однако, поставленная задача актуальна и ее необходимо как то решить, т.е. написать программу на выбранном нами языке программирования Java. Поэтому мы, как тому учат классики программирования (Ч.Хоар, Н.Вирт, Э.Дейкстра и Д.Кнут), займемся выработкой некой абстракции, которая позволит нам учесть все возможные варианты написания сумм в строки.
Ограниченные рамки on-line публикации не позволят нам, к сожалению, изложить детальное описание процесса выработки необходимой нам абстракции и, поэтому, мы приводим только итоговую таблицу для одной валюты (код 810, т.е. рубли):
Последняя цифра | Целая часть (слева) | Дробная часть (справа) | Род |
1 | рубль | копейка | Мужской |
2, 3 ,4 | рубля | копейки | Мужской |
0, 5 | рублей | копеек | Мужской |
Внимательно рассмотрите эту таблицу. Попробуйте с ее помощью конвертировать какую либу сумму (в рублях, разумеется) в соответствующее строковое представление. Забавно, не правда ли ? Задержитесь на несколько минут и постройте подобные таблицы для марок (код 276), долларов (код 840) или любой другой валюты.
Не кажется ли вам, что теперь перед нами забрезжил выход из этой весьма запутанной ситуации с падежами, родами и прочими грамматическими "премудростями" ? Теперь становится понятно, как можно "справиться" с имеющейся сложностью: достаточно где-то (лучше всего в какой-либо базе данных или в конфигурационном файле) хранить приведенную таблицу для каждой нужной валюты и написать некий "обработчик" этой информации.
Конечно, этот обработчик будет, по видимости включать в себя большое количество инструкций выбора (т.е. конструкций if и case) и трудно рассчитывать на то, что он будет компактным, однако принципиально, он (обработчик) не сложен: берем число, разбиваем его на группы и подбираем для каждой группы соответствующее строковое представление из базы данных или файла конфигурации. Поэтому, приступим к программированию, но перед этим сначала решим...
Сразу договоримся, что вышеприведенную таблицу мы будем в дальнейшем называть "настроечной информацией". Мы уже говорили, что настроечную информацию по валютам можно хранить либо в базе данных, либо в конфигурационном файле. Мы остановились на первом способ, поскольку, как правило, бухгалтерские приложения уже включают в себя справочник валют в виде одной или нескольких таблиц базы данных. В качестве базы данных мы выбираем широко распространненный сервер MySQL. Кроме, собственно, сервера нам потребуется еще jdbc-драйвер. Если вы никогда не работали с MySQL или с этим драйвером, ничего страшного: оба они довольно просто инсталлируются и снабжены удобной и понятной документацией.
Более того, принятый нами подход отличается замечательной особенностью: его можно использовать практически с любой базой данных; главное условие - наличие соответствующего jdbc-драйвера. Тем не менее, приведенные примеры кода ориентированы именно на MySQL и, возможно, вам придется их немного изменить. Впрочем, как показывает практика, это не составляет большого труда.
Наше приложение будет состоять из двух основных классов: fplAmount и jAmount. Первый класс носит вспомогательный характер и предназначен для создания тестовой базы данных, таблицы валют и заполнения этой таблицы необходимой настроечной информацией. Кроме того, этот класс предоставлет пользователю простую графическую оболочку для выбора валюты и ввода суммы цифрами. Второй класс осуществляет преобразование введенной суммы из цифрового представления в строковое. Рассмотрим сначала класс fplAmount.
Начнем, как водится, с метода main (точка входа в приложение):
// Импорт необходимых пакетов JDK 1.3 import java.awt.*; import java.awt.event.*; import java.sql.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; // В этой строке будет находиться результат преобразования // суммы цифрами в сумму прописью ........................... // Точка входа ... connect (); // Подсоединиться к серверу MySQL checkAndFill (); // Проверить данные selectCurrency (); // Сформировать список валют frame = new fplAmount (); } }
Здесь же мы привели объявления некоторых переменных, которые потребуются нам в этом приложении. Из приведенного кода видно, что сначала мы пытаемся подсоединиться к серверу MySQL, затем проверяем и заполняем (при необходимости) данными таблицу валют, формируем список валют и, наконец, создаем и выводим на экран основное окно приложения. Для тех, кто работает в среде, отличной от Windows, можно порекомендовать закомментировать строку, определяющую стиль пользовательского интерфейса (UIManager.setLookAndFeel ...). Разберем по порядку перечисленные методы.
static void connect () { try { "jdbc:mysql://localhost:3306/mysql"); } } }
Сначала мы загружаем класс-драйвер, а затем пытаемся получить объект- соединение с сервером MySQL. Поскольку мы не знаем, существует ли на момент создания соединения с сервером MySQL необходимая нам база данных, мы "обманываем" сервер, подсоединяясь к наверняка существующей базе данных mysql (заметим, что для этого у нас должны быть соответствующие права доступа, поэтому, не исключено, что вам придется изменить параметр в методе DriverManager.getConnection и добавить в него имя и пароль). Если соединение установлено успешно, то объект-соединение Connection conn получает значение, отличное от null. Если же соединение установить не удалось (например потому, что сервер MySQL не запущен или в параметре метода DriverManager.getConnection что-то не так), то приложение аварийно завершит свою работу и выйдет в операционную среду.
Теперь рассмотрим метод checkAndFill():
// Проверить существование таблицы валют и заполнить ее данными static void checkAndFill () { Statement stmt = null; // SQL-запрос для создания тестовой базы данных String sqlCreateDB = "create database jAmount"; String sqlUseDB = "use jAmount"; // SQL-запрос для создания тестовой таблицы валют String sqlCreateTable = "create table currency (" + "ID_Currency smallint not null," + "ISO_Currency char (3)," + "Scale int," + "Description char (32)," + "i1 varchar (32)," + "i24 varchar (32)," + "i5 varchar (32)," + "r1 varchar (32)," + "r24 varchar (32)," + "r5 varchar (32)," + "Sex char (1)," + "primary key (ID_Currency))"; // SQL-запросы для заполнения таблицы валют тестовыми данными "insert into currency values (810,'RUR',1,'Российские рубли','рубль','рубля','рублей','копейка','копейки','копеек','M')", "insert into currency values (276,'DEM',1,'Немецкие марки','марка','марки','марок','пфенниг','пфеннига','пфеннигов','F')", "insert into currency values (840,'USD',1,'Доллары США','доллар','доллара','долларов','цент','цента','центов','M')" }; // Создать базу данных и выбрать ее по умолчанию try { stmt = conn.createStatement (); boolean value = stmt.execute (sqlCreateDB); } try { stmt = conn.createStatement (); boolean value = stmt.execute (sqlUseDB); } // Cоздать таблицу со списком валют try { stmt = conn.createStatement (); boolean value = stmt.execute (sqlCreateTable); } // Заполнить тестовую таблицу валют данными for (int i = 0; i <= sqlText.length; i++) { try { int value = stmt.executeUpdate (sqlText [i]); } } }
Этот метод довольно прост и прямолинеен. Сначала создаются строковые переменные для создания базы данных (sqlCreateDB), выбора базы данных по умолчанию (sqlUseDB), создания таблицы валют (sqlCreateTable) и, наконец, массив строк для заполнения таблицы валют данными (sqlText). Далее последовательно выполняются необходимые SQL-инструкции.
Остановимся несколько подробней на переменной sqlCreateTable. Как видно из приведенного кода структура таблицы валют проста и (за исключением нескольких полей) полностью дублирует структуру таблицы из раздела "Строим абстракцию". Поскольку структура этой таблицы будет играть ключевую роль в дальнейшем изложении, мы еще раз рассмотрим ее:
Поле | Тип | Назначение |
ID_Currency | smallint | числовой идентификатор валюты (810, 276, 840 и т.д.) |
ISO_Currency | char (3) | так называемый ISO-код (буквенное обозначение валюты: (рубли - RUR, доллары - USD, марки - 276 и т.д.) |
Scale | int | масштаб (т.е. за какое количество данной валюты устанавливаются котрировки Банка России |
Description | char (32) | наименование валюты |
i1 | varchar (32) | строковое представление последней цифры целой части |
i24 | varchar (32) | строковое представление последней цифры целой части |
i5 | varchar (32) | строковое представление последней цифры целой части |
r1 | varchar (32) | строковое представление последней цифры дробной части |
r24 | varchar (32) | строковое представление последней цифры дробной части |
r5 | varchar (32) | строковое представление последней цифры дробной части |
Sex | char (1) | род валюты (M - мужской, F - женский) |
Наконец, в sqlText указаны данные для трех валют: рублей, марок и долларов. Т.о. мы создаем базу данных, выбираем ее по умолчанию, создаем таблицу валют и заполняем ее в цикле данными из массива sqlText.
Последний метод, который мы должны рассмотреть, это метод selectCurrency(). Его назначение понятно: выбрать числовые коды (ID_Currency) в вектор.
// Выбрать коды валют из таблицы CURRENCY в вектор static void selectCurrency () { try { "select id_currency from currency"); while (rset.next ()) { cv.addElement (rset.getObject (1)); } } } }
Мы оставляем этот метод без комментариев, поскольку он очень прост.
Наконец, чтобы окончательно закончить подготовку к конвертации сумм в строковое представление, представим конструктор, который создает основное окно приложения и позволяет организовать диалог с пользователем:
// Создаем основное окно приложения public fplAmount () { setTitle ("Сумма прописью"); // Разместить компоненты в основном окне lPanel.add (currLabel); lPanel.add (summaLabel); rPanel.add (currBox); rPanel.add (summaField); botPanel.add (convertButton); pack (); setSize (getPreferredSize ().width, getPreferredSize ().height); setResizable (true); // Вывести основное окно в центре экрана монитора if (fSize.height > sSize.height) fSize.height = sSize.height; if (fSize.width > sSize.width) fSize.width = sSize.width; setLocation ((sSize.width - fSize.width)/2, (sSize.height - fSize.height)/2); setVisible (true); // Обработчик события нажатия на кнопку "Преобразовать" // Определить код выбранной валюты из выпадающего списка // Массив суффиксов - окончаний. Элементы массива: // 0...5 - строки целой и дробной частей; // 6 - род валюты (M - мужской, F - женский) // Выбрать из таблицы настроечную информацию, // касающуюся данной валюты try { ResultSet rset = stmt.executeQuery ("select i1,i24,i5,r1,r24,r5,Sex from currency where id_currency=" + code); int cols = meta.getColumnCount (); // Занести настроечную информацию в массив while (rset.next ()) { for (int i = 0; i < cols; i++) suff [i] = rset.getString (i + 1); } } } // Перевести число в строку !!! new jAmount (suff, summaField.getText ()); } }); } }); }
Единственный заслуживающий пристального внимания момент, заключается в том, что после нажатия пользоватлем кнопки "Преобразовать" мы должны обратиться к таблице валют и выбрать из нее в массив suff настроечную информацию для выбранной валюты. Этот массив (вместе со строкой, представляющей собой сумму) передаются классу jAmount для преобразования (конвертирования) суммы цифрами в строку.
Вот наконец-то, мы добрались до цели нашего рассказа: преобразования сумм цифрами в строку. Класс jAmount достаточно объемен, однако, он принципиально не сложен, поскольку всю подготовительную работу мы выполнили ранее. Напомним, что нашей целью было получение т.н. настроечной информации по выбранной валюте. Эту настроечную информацию (в виде массива) мы и передаем конструктору класса jAmount. И еще: кроме собственно настроечной информации мы передаем строку, представляющую собой набор цифр, который класс jAmount, должен преобразовать (конвертировать) в строковое представление суммы. Мы не будем комментировать этот класс и его методы так же, как мы комментировали класс fplAmount. Все заслуживающие внимания моменты содержатся в комментариях к приведенному коду. Не пугайтесь, если сразу вам не удастся понять суть применяемого алгоритма. Скопируйте код на свой диск, вставьте в "подозрительные" или непонятные места отладочную печать на консоль или лучше в файл, откомпилируйте и изучайте !
import java.math.*; public class jAmount { private BigInteger summ; // Конструктор класса. Конструктор в качестве параметров получает: // String suff [] массив наименований (настроечная информация) и // String sum сумма для преобразования this.suff = suff; try { // Преобразуем в копейки (центы, пфенниги и т.д.), // одним словом - убираем дробную часть summ = decimal.toBigInteger (); // Приступить к преобразованию toString (); // Метод для вывода результата преобразования. Можно просто // выводить полученное строковое представление суммы на консоль: // System.out.println (fplAmount.res); jAmountResult jar = new jAmountResult (fplAmount.frame); jar.setVisible (true); } // Ой !!!!! Что-то не так: скорее всего, в строке // представляющей собой сумму цифрами, встретились символы // отличные от цифр и точки. Можно просто выводить сообщение // об ошибках на консоль: // System.out.println (e); jAmountError jae = new jAmountError (fplAmount.frame); jae.setVisible (true); } } // Получить правую (дробную) часть суммы return alignSumm (summ.remainder (hundred).abs ().toString ()); } // Если сумма меньше 10, то выровнять ее дописыванием "0" switch (s.length ()) { case 0: s = "00"; break; case 1: s = "0" + s; break; } return s; }
Далее следует собственно сам преобразователь суммы цифрами в строку. Прежде чем вы посмотрите реализацию этого метода, автор считает своим долгом выразить глубокую благодарность и восхищение программистам компании "Бифит", на чьем сайте была выложена реализация алгоритма преобразования (правда, только для рублей). Автор несколько переработал алгоритм, однако, основная идея осталась без изменений. Переработки были связаны исключительно с тем, что требовалось "подсунуть" алгоритму настроечную информацию по выбранной валюте, однако, повторюсь, идея и реализация принадлежат компании "Бифит" и работающим в ней программистам.
if (divrem [0].signum () == 0) result.append ("Ноль "); divrem = divrem [0].divideAndRemainder (thousand); int group = 0; do { int value = remainder.intValue (); result.insert (0, groupToString (value, group)); // Для нулевой группы добавим в конец соответствующую валюту if (group == 0) { int rank10 = (value % 100) / 10; int rank = value % 10; if (rank10 == 1) { result = result.append (suff [2]); } else { switch (rank) { case 1: result = result.append (suff [0]); break; case 2: case 3: case 4: result = result.append (suff [1]); break; default: result = result.append (suff [2]); break; } } } divrem = quotient.divideAndRemainder (thousand); quotient = divrem [0]; remainder = divrem [1]; group++; } while (!quotient.equals (zero) || !remainder.equals (zero)); // Дробная часть суммы result = result.append (" ").append (s); result = result.append (" "); if (s.charAt (0) == '1') { result = result.append (suff [5]); } else { switch (s.charAt(1)) { case '1': result = result.append (suff [3]); break; case '2': case '3': case '4': result = result.append (suff [4]); break; default: result = result.append (suff [5]); break; } } // По правилам бухгалтерского учета первая буква строкового // представления должна быть в верхнем регистре // Вот ради этой строки все и затевалось: результат получен !!! fplAmount.res = result.toString(); return result.toString(); } // Преобразование группы цифр в строку if (value < 0 || value > 999) throw new IllegalArgumentException ("value must be between 0 an 999 inclusively"); if (value == 0) { return result.toString(); } // Разбор числа по разрядам, начиная с сотен int rank = value / 100; switch (rank) { default: break; case 1: result = result.append ("сто "); break; case 2: result = result.append ("двести "); break; case 3: result = result.append ("триста "); break; case 4: result = result.append ("четыреста "); break; case 5: result = result.append ("пятьсот "); break; case 6: result = result.append ("шестьсот "); break; case 7: result = result.append ("семьсот "); break; case 8: result = result.append ("восемьсот "); break; case 9: result = result.append ("девятьсот "); break; } // Далее, десятки rank = (value % 100) / 10; switch (rank) { default: break; case 2: result = result.append ("двадцать "); break; case 3: result = result.append ("тридцать "); break; case 4: result = result.append ("сорок "); break; case 5: result = result.append ("пятьдесят "); break; case 6: result = result.append ("шестьдесят "); break; case 7: result = result.append ("семьдесят "); break; case 8: result = result.append ("восемьдесят "); break; case 9: result = result.append ("девяносто "); break; } // Если десятки = 1, добавить соответствующее значение. Иначе - единицы int rank10 = rank; rank = value % 10; if (rank10 == 1) { switch (rank) { case 0: result = result.append ("десять "); break; case 1: result = result.append ("одиннадцать "); break; case 2: result = result.append ("двенадцать "); break; case 3: result = result.append ("тринадцать "); break; case 4: result = result.append ("четырнадцать "); break; case 5: result = result.append ("пятнадцать "); break; case 6: result = result.append ("шестнадцать "); break; case 7: result = result.append ("семнадцать "); break; case 8: result = result.append ("восемнадцать "); break; case 9: result = result.append ("девятнадцать "); break; } } else { switch (rank) { default: break; case 1: if (group == 1) // Тысячи result = result.append ("одна "); else // Учесть род валюты (поле "Sex" настроечной информации) if (suff [6].equals ("M")) result = result.append ("один "); if (suff [6].equals ("F")) result = result.append ("одна "); break; case 2: if (group == 1) // Тысячи result = result.append ("две "); else // Учесть род валюты (поле "Sex" настроечной информации) if (suff [6].equals ("M")) result = result.append ("два "); if (suff [6].equals ("F")) result = result.append ("две "); break; case 3: result = result.append ("три "); break; case 4: result = result.append ("четыре "); break; case 5: result = result.append ("пять "); break; case 6: result = result.append ("шесть "); break; case 7: result = result.append ("семь "); break; case 8: result = result.append ("восемь "); break; case 9: result = result.append ("девять "); break; } } // Значение группы: тысячи, миллионы и т.д. switch (group) { default: break; case 1: if (rank10 == 1) result = result.append ("тысяч "); else { switch (rank) { default: result = result.append ("тысяч "); break; case 1: result = result.append ("тысяча "); break; case 2: case 3: case 4: result = result.append ("тысячи "); break; } } break; case 2: if (rank10 == 1) result = result.append ("миллионов "); else { switch (rank) { default: result = result.append ("миллионов "); break; case 1: result = result.append ("миллион "); break; case 2: case 3: case 4: result = result.append ("миллиона "); break; } } break; case 3: if (rank10 == 1) result = result.append ("миллиардов "); else { switch (rank) { default: result = result.append ("миллиардов "); break; case 1: result = result.append ("миллиард "); break; case 2: case 3: case 4: result = result.append ("миллиарда "); break; } } break; case 4: if (rank10 == 1) result = result.append ("триллионов "); else { switch (rank) { default: result = result.append ("триллионов "); break; case 1: result = result.append ("триллион "); break; case 2: case 3: case 4: result = result.append ("триллиона "); break; } } break; } return result.toString(); } }
Вот, собственно, и все. Нам осталось только провести небольшое:
Испытание
Испытать наше приложение достаточно просто:
Обязательно поэкпериментируйте с самыми разными суммами для разных валют. Помните, что мы говорили об отладочной печати на консоль: если что-то осталось вам неясным или непонятным, выводите информацию на консоль и анализируйте, анализируйте, анализируйте...
В нашем изложении "за бортом" осталась реализация двух вспомогательных классов jAmountResult (для вывода результатов преобразования) и jAmountError (для вывода сообщений об ошибках). Это мы оставляем читателям в качестве несложного упражнения.
А вот теперь, действительно, все. Если у читателей возникнут вопросы, пишите на мой адрес и я постараеюсь ответить на них в самые сжатые сроки.
Кроме того, автор может предоставить совершенно бесплатно (т.е. безвозмездно) всем желающим исходные тексты приложения, которое было описано в этой статье. Для этого в поле Subject просто напишите: "jAmount (source files)".
Помните, в начале статьи мы говорили о том, что не только денежные суммы должны преобразовываться в строки ? В строки необходимо переводить метры, килограммы, штуки, литры и другие единицы измерения. Рассмотренное нами приложение представляет собой отправную точку для разработки подобных задач.
Дерзайте и вам улыбнется удача !