Раз ромашка, два ромашка…

Раз ромашка, два ромашка… С задачей преобразования цифровой записи чисел в словесную форму я столкнулся летом ушедшего 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 Byt
e, 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 C
ase 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 <> "&quo
t; Then NTS = " " + NTS NTS = Tmp + NTS Case 4 ' миллиарды Tmp = TriadToStr(Triad, 1) If T
riad > 0 Then Tmp = Tmp + " " + GetDeterm(Triad, "миллиард", "миллиарда&
quot;, "миллиардов") 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 A
s 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(Tria
d * 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 &g
t; 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 = &qu
ot; " + TTS TTS = SN(N3, 3) + TTS End If TriadToStr = TTS End Function Private Function GetDete
rm(ByVal Triad As Integer, Item1 As String, Item2 As String, Item3 As String) As String Dim N1 As By
te, N2 As Byte N1 = Triad Mod 10 Triad = Int(Triad * 0.1) N2 = Triad Mod 10 If N2 <> 1 Then Se
lect 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 г.



Компьютерная газета. Статья была опубликована в номере 04 за 2002 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета