30 ноября 2020 года    
Понедельник | 21:00    
Главная
 Новости
Базы данных
Безопасность PC
Всё о компьютерах
Графика и дизайн
Интернет-технологии
Мобильные устройства
Операционные системы
Программирование
Программы
Связь
Сети
 Документация
Статьи
Самоучители
 Общение
Форум







Разделы / Программирование / Другие

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


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

С задачей преобразования цифровой записи чисел в словесную форму я столкнулся летом ушедшего 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 г.

 Раз ромашка, два ромашка…
Лента новостей


2006 (c) Copyright Hardline.ru