div.main {margin-left: 20pt; margin-right: 20pt}
Раз ромашка, два
ромашка…
С задачей преобразования цифровой записи чисел в словесную форму я
столкнулся летом ушедшего 2001 года, когда в программах, печатающих платежные
требования, потребовалось ввести расшифровку сумм "прописью". Нельзя сказать,
чтобы ранее я не задумывался над этим вопросом, но, как в анекдоте о
математиках, для которых задача перестает быть интересной, когда доказано
существование решения, мои размышления до этого не выливались в работающий
код. В общем, возникла необходимость — нашлось и решение. Потратив немного
времени, я написал функцию на FoxPro, выполняющую это преобразование, и забыл
об этом. И хотя программа печатала что-то вроде "Сто двадцать три белорусских
рублей", бухгалтерию подобный "акцент" вполне устраивал, поэтому дальнейшее
совершенствование в плане соответствия нормам русского языка
"заморозилось". Спустя несколько месяцев мне снова понадобилось вернуться к
этой задаче, но теперь уже в среде электронных таблиц Microsoft Excel. Я был
практически уверен, что найду решение среди стандартных функций листа, но,
потратив около часа, убедился в обратном. Конечно, я расширил свой кругозор в
области текстовых функций (особенно "порадовала" функция РУБЛЬ(), на которую
из-за ее названия я возлагал такие надежды), но отсутствие искомого озадачило
и разочаровало. Чувство незавершенной в прошлый раз работы вызвало
решимость реализовать витающие в воздухе идеи в универсальном коде, который с
чистой совестью можно было бы использовать сегодня и в дальнейшем. Поэтому
было решено подойти к решению задачи системно: сформулировать проблему,
оговорить входные и выходные данные, указать ограничения и лишь после этого
приступать к кодированию. Постановка задачи звучит предельно просто:
разработать функцию, которая получает число и возвращает строку — текстовую
запись полученного числа (на русском языке). Теперь можно указать на
ограничения, которым должны удовлетворять входные данные. Прежде всего,
зададимся целью обрабатывать только натуральные числа. Действительно,
бухгалтерия, как правило, имеет дело с натуральным счетом, будь то денежные
средства или материалы на складе. Даже в случае ввода в обращение разменной
монеты, никто не будет говорить: "Сто пятьдесят целых тринадцать сотых рубля",
— дробная часть будет выражена в минорных единицах валюты, например, в
копейках. Таким образом, дробная часть будет являться самостоятельно
обрабатываемым натуральным числом. Итак, ограничение множества обрабатываемых
чисел незначительно сузит круг задач, в решении которых сможет помочь
проектируемая функция. Результат работы функции, если вспомнить школьный
курс, должен являться именем числительным. Чтобы предвидеть проблемы, которые
могут возникнуть при выполнении нашего преобразования, не будет лишним
обратиться к какому-нибудь справочнику по русскому языку и узнать об этой
части речи поподробнее. Вот что говорится, например, в книге Баранова М. Т.
"Русский язык: Справ. материалы" (примеры к правилам и определениям приводятся
без изменений):
"Имя числительное — часть речи, которая обозначает
количество предметов, число, а также порядок предметов при счете.(…)
По
значению и грамматическим признакам имена числительные делятся на
количественные и порядковые. Количественные числительные обозначают количество
или число и отвечают на вопрос сколько?: один, два, три, четыре, пять, шесть,
двадцать, тридцать.(…)
Имена числительные изменяются по
падежам. Начальная форма числительного — именительный падеж. По
количеству слов числительные бывают простые и составные. Простые числительные
состоят из одного слова, а составные из двух и более слов."
Здесь мы
можем уточнить, что результатом работы функции будет являться количественное
имя числительное, в общем случае составное. Условимся, что склонение
количественных числительных по падежам нас не интересует, так как для
экономических и бухгалтерских приложений вполне достаточно научиться
формировать начальную форму числительных. Теперь посмотрим, что говорится в
правилах относительно определяемых слов.
1. При составных числительных,
имеющих в конце один, одна, одно, существительное ставится в именительном
падеже единственного числа: сто один ученик, сто одна ученица. 2. При
составных числительных, оканчивающихся на два (две), три, четыре,
существительные употребляются в родительном падеже единственного числа: сто
четыре ученицы. 3. Если же в конце стоят числительные, начиная с пяти, то
существительные ставятся в родительном падеже множественного числа: тридцать
семь тракторов.
Главное, что мы можем отметить для себя после
знакомства с пунктами правила — это то, что определяемое слово должно быть
согласовано с числительным в роде и числе. Это означает, что форма
определяемого слова (а их, по количеству пунктов в правиле, три) зависит от
числительного. Поэтому добавим к списку входных параметров разрабатываемой
функции, кроме преобразуемого числа, еще четыре: род определяемого слова и его
формы для каждого из трех возможных вариантов (см. правило). На этом этапе,
когда определены все входные данные, указаны их ограничения и определен
результат работы функции, можно приступить к ее разработке. В данной статье ее
реализация будет базироваться на Visual Basic, что позволит использовать
результат во всех продуктах Microsoft Office. Назовем разрабатываемую
функцию NumbToStr, тогда ее описание будет выглядеть следующим образом:
Public Function NumbToStr(ByVal Numb As
Currency, Cl As Byte, Item1 As String, Item2 As String, Item3 As
String) As String
где Numb — преобразуемое натуральное число, Cl — род определяемого слова (0
— средний, 1 — мужской, 2 — женский), ItemN — формы определяемого слова в
соответствии с пунктами приведенного выше правила. Чтобы решить, каким
образом лучше выполнять преобразование, рассмотрим пример. Возьмем число
123,345,123,345 и запишем его "прописью": "сто двадцать три миллиарда триста
сорок пять миллионов сто двадцать три тысячи триста сорок пять". Заметили?
Независимо от того, в какой позиции стоит тройка цифр, в группе миллиардов или
миллионов, тысяч или единиц, ее текстовое представление выглядит одинаково. В
дальнейшем такие наборы из трех цифр будем называть "триадами". Внутри
триады каждая цифра имеет вес сотен, десятков или единиц, чем и определяется
ее словесная запись. Надо лишь следить, чтобы имя числительное, которое
получается в результате обработки триады, согласовывалось с соответствующим
определяемым словом. Заметим, что определяемыми словами являются и "тысяча",
"миллион", "миллиард"… (недаром в упомянутом справочнике Баранова М. Т.
"Русский язык: Справ. материалы" отмечается, что "некоторые ученые относят
слова тысяча, миллион, миллиард к существительным", а не к
числительным). Для выполнения преобразования нам нужно знать, как
записывается каждая цифра в зависимости от ее позиции в триаде. Кроме того,
нельзя забывать и о группе числительных, соответствующих числам от 11 до 19.
Поэтому заведем двумерный массив из четырех столбцов на девять строк и опишем
его в области описаний как Private SN(9, 4) As String. Его элементы должны
быть инициализированы следующим образом:
SN(1, 1) =
"один" … SN(9, 1) = "девять" SN(1, 2) = "десять" … SN(9, 2) =
"девяносто" SN(1, 3) = "сто" … SN(9, 3) = "девятьсот" SN(1, 4) =
"одиннадцать" … SN(9, 4) = "девятнадцать"
После таких
предварительных рассуждений можно записать часть кода, которая выделяет триады
числа, обрабатывает каждую из них и выполняет слияние всех промежуточных
результатов в одну строку (то есть, приведем тело функции NumbToStr). Public Function NumbToStr(ByVal Numb As Currency,
Cl As Byte, Item1 As String, Item2 As String,
Item3 As String) As String
Static fInitData As Boolean
Dim NTS As String, Tmp As String, St As Byte, Triad As Integer
'--------------------------------------------
' Подготовка к работе глобальных переменных.
'--------------------------------------------
If Not fInitData Then
InitData
fInitData = True
End If
'-------------------------------------------
' Подготовка к работе локальных переменных.
'-------------------------------------------
NTS = "" ' текстовая запись числа
St = 1 ' номер обрабатываемой триады
'-----------------------------------------
' Перебор триад числа, начиная с младшей.
'-----------------------------------------
While Numb > 0
Triad = Numb - Int(Numb * 0.001) * 1000 ' выделение триады
Numb = Int(Numb * 0.001) ' отброс выделенной триады
Select Case St
Case 1 ' единицы
NTS = TriadToStr(Triad, Cl)
If Triad > 0 Then NTS = NTS + " "
NTS = NTS + GetDeterm(Triad, Item1, Item2, Item3)
Case 2 ' тысячи
Tmp = TriadToStr(Triad, 2)
If Triad > 0 Then Tmp = Tmp + " " + GetDeterm(Triad, "тысяча", "тысячи", "тысяч")
If Tmp + NTS <> "" Then NTS = " " + NTS
NTS = Tmp + NTS
Case 3 ' миллионы
Tmp = TriadToStr(Triad, 1)
If Triad > 0 Then Tmp = Tmp + " " + GetDeterm(Triad, "миллион", "миллиона", "миллионов")
If Tmp + NTS <> "" Then NTS = " " + NTS
NTS = Tmp + NTS
Case 4 ' миллиарды
Tmp = TriadToStr(Triad, 1)
If Triad > 0 Then Tmp = Tmp + " " + GetDeterm(Triad, "миллиард", "миллиарда", "миллиардов")
If Tmp + NTS <> "" Then NTS = " " + NTS
NTS = Tmp + NTS
Case Else ' неизвестные
NTS = "? " + NTS
End Select
St = St + 1 ' следующая триада
Wend
NumbToStr = NTS
End Function
Обратите внимание на способ инициализации глобальных переменных (то есть, описанного выше массива SN). Чтобы избежать многочисленных присвоений начальных значений переменным при каждом обращении к функции NumbToStr
(а это может снизить производительность при использовании множества этих функций на листе Microsoft Excel),
инициализация выполняется в отдельной процедуре InitData, а факт ее выполнения регистрируется в статической переменной
fInitData, которая сохраняет свое значение между вызовами функции NumbToStr. В приведенной реализации функции NumbToStr на этапе формирования текстовой записи триады используются вспомогательные
функции. Так, TriadToStr получает числовое значение триады и род определяемого слова, а возвращает текстовую запись триады,
согласованную с определяемым словом в роде; GetDeterm получает числовое определение триады и три формы определяемого слова,
а возвращает определяемое слово, согласованное с триадой в числе.
Приведем и реализации этих функций:Private Function TriadToStr(ByVal Triad As Integer, Cl As Byte) As String
Dim N1 As Byte, N2 As Byte, N3 As Byte, TTS As String
TriadToStr = ""
If Triad = 0 Then Exit Function ' выход при нулевой триаде
'----------------------------
' Выделение разрядов триады.
'----------------------------
N1 = Triad Mod 10 ' единицы
Triad = Int(Triad * 0.1)
N2 = Triad Mod 10 ' сотни
N3 = Int(Triad * 0.1) ' тысячи
'---------------------------------------
' Формирование текстовой записи триады.
'---------------------------------------
TTS = "" ' текстовая запись триады
If N2 = 1 Then
'--------------------------------------
' Обработка разрядов десятков и единиц
' для 9 < N2*10+N1 < 20.
'--------------------------------------
If N1 = 0 Then TTS = SN(1, 2) Else TTS = SN(N1, 4)
Else
'---------------------------
' Обработка разряда единиц.
'---------------------------
If N1 > 0 Then
If N1 = 1 Then
Select Case Cl
Case 0
TTS = "одно"
Case 1
TTS = SN(N1, 1)
Case 2
TTS = "одна"
End Select
ElseIf N1 = 2 Then
Select Case Cl
Case 0, 1
TTS = SN(N1, 1)
Case 2
TTS = "две"
End Select
Else
TTS = SN(N1, 1)
End If
End If
'-----------------------------
' Обработка разряда десятков.
'-----------------------------
If N2 > 0 Then
If N1 > 0 Then TTS = " " + TTS
TTS = SN(N2, 2) + TTS
End If
End If
'--------------------------
' Обработка разряда сотен.
'--------------------------
If N3 > 0 Then
If N1 > 0 Or N2 > 0 Then TTS = " " + TTS
TTS = SN(N3, 3) + TTS
End If
TriadToStr = TTS
End Function
Private Function GetDeterm(ByVal Triad As Integer, Item1 As String, Item2 As String, Item3 As String) As String
Dim N1 As Byte, N2 As Byte
N1 = Triad Mod 10
Triad = Int(Triad * 0.1)
N2 = Triad Mod 10
If N2 <> 1 Then
Select Case N1
Case 1
GetDeterm = Item1
Case 2 To 4
GetDeterm = Item2
Case Else
GetDeterm = Item3
End Select
Else
GetDeterm = Item3
End If
End Function
Чтобы получить работающий вариант, достаточно собрать описанные выше
функции в один файл с расширением BAS, расписав при этом, естественно,
сокращенную запись инициализации массива. Полученный файл можно подключать как
внешний модуль к любому продукту Microsoft Office. При этом не стоит называть
этот модуль NumbToStr — иначе не сможете обратиться к функции, которую с таким
трудом разработали! Итак, сформулированная в начале статьи задача нашла
свое логическое решение. Предложенный подход реализован автором в ряде решений
в рамках продуктов Microsoft Excel и Microsoft Access. За время эксплуатации
этих приложений работа функции преобразования числа в текстовую запись не
вызвала никаких нареканий. При этом необходимо понимать, что результат ее
работы может являться как "конечным продуктом", так и "полуфабрикатом",
который нуждается в дополнительной обработке. Например, требованиями бухучета
предписывается, чтобы первая буква в текстовой записи числа была прописной.
Излишне говорить, что сделать первую букву строки "большой" — пара
пустяков. Возвращаясь к пройденному пути, не будет лишним еще раз
вспомнить, как от подсознательного образа проблемы был совершен переход к
постановке конкретной задачи, как ее формулировка уточнялась параллельно с
накоплением знаний о сути проблемы. Пожалуй, главное, о чем хотелось
рассказать в этой статье, — не пример конкретной функции (хотя, возможно,
кому-то и он окажется полезен), а история поиска решения. На примере этого
простого упражнения можно в очередной раз убедиться, что достигнуть можно
только четко сформулированной и понятной цели. Если же конечная цель не ясна,
то решение проблемы превращается в погоню за миражами.
Игорь
Орещенков, 2002 г.
|