Советы тем, кто программирует на VB & VBA
Совет 227. Представление числового значения прописью
Совет 228. Как практически воспользоваться функцией SummaString?
Совет 229. Используйте реентерабельные и рекурсивные процедуры. Но осторожно!
Совет 230. Используйте рекурсию для передвижения по файлам
Совет 231. Преобразование имен файлов в определенный регистр
Совет 232. Используйте Preserve для сохранения содержимого массива
Совет 233. Используйте FreeFile для получения номера файла. Но будьте при этом бдительны!
Все программные примеры, которые использовались в приведенных здесь советах, можно найти по адресу: www.visual.2000.ru/develop/vb/source/.
Внимание! В опубликованной в КомпьютерПресс статье «Особенности работы со строковыми переменными в VB. Часть 1» в разделах, связанных с национальными особенностями преобразования кодов символов, использовались символы из различных кодовых таблиц (Cyrillic, Western и Central European). К сожалению, в результате преобразований форматов файлов статьи в окончательном варианте (VBSTRING.HTM) имеется несколько искажений изображения символов. Это касается разделов «Еще несколько любопытных моментов» и «Как перекодировать символы в Word 97» (включая табл. 3). Правильное изображение всех примеров можно найти в документах, записанных по приведенному выше адресу.
Совет 227. Представление числового значения прописью
Задача автоматического представления числового значения прописью наиболее часто встречается при составлении юридических документов. При этом речь может идти не только о денежных суммах, но и о количестве каких-либо товаров.
Кстати, многие из первых макросов, присланных на конкурс MS Office Extensions (www.microsoft.ru/offext/) еще в 1997 году, решали именно эту задачу и некоторые из них получили призы. Но предлагаемые там решения имеют уже «встроенный» характер, к тому же, на наш взгляд, не очень универсальны и неоптимальны. Исходный код у многих таких расширений закрыт, а у тех, где он открыт, требуется, честно говоря, слишком много усилий, чтобы в нем разобраться.
Поэтому мы решили предложить читателям собственный вариант.
В листинге 1 находится процедура SummaString, которая, в свою очередь, использует внутреннюю служебную подпрограмму SummaStringThree. Там же имеется описание входных и выходных параметров. Эта процедура учитывает правильное склонение числительных в зависимости от грамматического рода единицы измерения (мужской, женский, средний), и ее практическое применение может выглядеть следующим образом:
Source& = 200001 CALL SummaString(Summa$, Source&, 1, “рубль”, “рубля”, _ “рублей”) Print Summa$ ‘ два миллиона одиннадцать рублей ‘ Source& = 22 CALL SummaString(Summa$, Source&, 2, “копейка”, _ “копейки”, “копеек”) Print Summa$ ‘ двадцать две копейки ‘ Source& = 1231 Call SummaString(Summa$, Source&, 3, “колесо”, _ “колеса”, “колес”) Print Summa$ ‘ одна тысяча двести тридцать одно колесо ‘ Call SummaString(Summa$, Source&, 1, “”, “”, “”) Print Summa$ ‘ одна тысяча двести тридцать один Print Summa$ + “ руб.” ‘ одна тысяча двести тридцать ‘ один руб.
Еще несколько дополнительных замечаний:
- Для хранения исходного числа используется переменная типа Long, что позволяет работать с числами до 2 147 483 647 (2^31-1). Если нужно увеличить диапазон, следует просто заменить тип переменных Source и TempValue на Double или Currency, а также добавить в SummaString код для учета триллионов и т.д. (см. листинг 1).
- Обратите внимание, что процедуры SummaString и SummaStringThree не используют каких-либо экзотических программных конструкций: они могут работать с любыми версиями VB, VBA и Basic/DOS.
- По правилам оформления бухгалтерских документов сумма прописью должна обязательно начинаться с прописной буквы. Для такого преобразования можно использовать функцию StrConv, но при этом нужно помнить, что ее корректная работа с национальными алфавитами в VB до версии 5.0 включительно обеспечивается только при установке в системе соответствующей кодовой таблицы (подробнее об этом см. КомпьютерПресс № 10’99, «Особенности работы со строками в VB»).
Чтобы выполнить нужное преобразование независимо от версии VB и системных установок Windows, гораздо проще применить следующую конструкцию:
Mid$(Summa$, 1) = Chr$(Asc(Summa$) - 32)
Для DOS’овских версий, в которых преобразование регистра выполняется только для английских букв, такая операция будет выглядеть немного сложнее (альтернативная русская таблица DOS — cp866):
s% = ASC(Summa$) IF s% <= 175 THEN s% = s% - 32 ELSE s% = s% = 80 MID$(Summa$, 1) = CHR$(s%)
Здесь мы воспользовались тем обстоятельством, что полученная символьная строка гарантированно (так работает наш алгоритм) начинается со строчной русской буквы. В противном случае пришлось бы сделать небольшую проверку на попадание ASCII-кода в нужный диапазон.
Совет 228. Как практически воспользоваться функцией SummaString?
Вопрос можно сформулировать по-другому: как получить исходное числовое значение Source, которое может быть задано в виде символьной цифровой строки? Для этого можно также использовать функцию ResultSumma$ (листинг 2), которая сначала выполняет необходимые проверки на допустимость преобразования символьной строки в число, а уже потом обращается к SummaSrting.
Например, при работе с документами Word 97/2000 вы хотите реализовать такую дополнительную функцию: выделив некое число, записанное цифрами, легким движением приписать в скобках после данного числа его представление прописью.
Для этого в среде VBA (в нашем случае данный пример реализован в виде документа Summa.doc) необходимо загрузить BAS-модуль с уже созданными нами процедурами ResultString, SummaString и SummaStringThree, а потом написать такую макрокоманду:
Sub ЧислоПрописью() ‘ Dim Summa$ Summa$ = ResultSumma$(Selection.Text, 1, “”, “”, “”, 0) If Summa$ <> “” Then ‘ допустимое значение Selection.Text = Selection.Text + “ (“ + Summa$ + “) “ End If End Sub
Тогда, выделив в документе, например, строку “1732” и обратившись к макрокоманде, вы увидите в тексте: “1732 (одна тысяча семьсот тридцать два)”.
При желании можно также написать автономную утилиту (Summa.vbp), которая выполняет нужное преобразование на основе данных, введенных в диалоговом режиме (рис. 1). Обратите внимание: эта утилита сохраняет все параметры, введенные при предыдущем запуске. Данный VB-проект несложно преобразовать в отдельный ActiveX-сервер или интегрировать в среду приложения, поддерживающего технологию VBA.
Короче говоря, перед разработчиками открываются широкие возможности создания собственных вариантов встроенного использования приведенных здесь процедур. Нам кажется, что дальше читатели смогут уже самостоятельно реализовать и такой вариант процедуры или макроса, который, например, преобразует выражение типа «100 руб. 21 коп.» в формат «Сто руб. 21 коп.» Будут проблемы — пишите.
Совет 229. Используйте реентерабельные и рекурсивные процедуры. Но осторожно!
Реентерабельные процедуры (reenterable, с возможностью повторного входа) — это процедуры, которые позволяют использовать одну копию программы для обработки нескольких одновременных запросов. Если речь идет об однопроцессорной системе, то такая ситуация возможна в случае параллельной (точнее, псевдопараллельной) работы отдельных исполняемых модулей, которые обращаются к одной библиотеке: первая программа обращается к процедуре некой библиотеки, ее прерывает другая, более приоритетная программа и обращается к той же процедуре.
Такая же ситуация может возникнуть и в однозадачной системе, допускающей распараллеливание вычислительных процессов (для многопроцессорных компьютеров).
Подобный случай возможен и внутри одной программы, работающей на обычном ПК. Например, вы написали процедуру Integral для вычисления определенного интеграла для функции, которая задается в виде указателя (для VB какой режим стал в принципе возможен с появлением в VB 5.0 функции AddressOf). Если же вам нужно взять двойной интеграл, легко может получиться, что подынтегральная функция в свою очередь еще раз обратится к Intergal.
Идея реентерабельности процедур основывается на реализации двух постулатов:
- Программный код процедур не должен модифицироваться во время выполнения. К счастью, в языках высокого уровня (в том числе и в VB) такое в принципе невозможно, а вот на ассемблере такие штуки проделываются легко. Более того, в старые добрые времена подобная коррекция кода порой предлагалась даже в качестве стандартного приема «изысканного» программирования.
- Для каждого вызова в процедуре формируется отдельный уникальный набор переменных.
Это требование реализуется двумя способами. Первый: переменные хранятся в буфере, который передается из вызывающей программы. При втором способе процедура сама предоставляет такой отдельный буфер, используя принцип стековой памяти.
Очевидно, что второй вариант гораздо проще в реализации; для VB-процедур это, естественно, означает, что все внутренние переменные должны быть динамическими.
Частным, но наиболее распространенным вариантом реентерабельных процедур для однозадачных программ являются рекурсивные процедуры — те, которые в явном виде обращаются сами к себе. Использование рекурсивных алгоритмов может быть очень полезным, но следует помнить, что в общем случае оно может вызывать снижение скорости вычислений (идет очень активное динамическое перераспределение памяти). Кроме того, глубокий уровень вложенности рекурсии может быть причиной выхода программы за пределы стекового пространства, то есть появления ошибки или даже аварийного останова системы.
Приведем несколько примеров использования рекурсивных процедур.
Пример 1. Процедура реверсивного преобразования строковой переменной (обратная перестановка символов). Такая встроенная функция появилась в VB 6.0 (SrtReverse), и разные способы ее реализации традиционными средствами Basic мы уже приводили (см. «КомпьютерПресс» № 11’99, «Особенности работы со строковыми переменными в VB», часть 2). Еще один вариант с использованием рекурсивной функции выглядит следующим образом:
Public Function ReverseRecursion$(s$) ‘ ‘ инверсия строковой переменной с помощью ‘ рекурсивной функции Dim c$ c$ = Left$(s$, 1) If c$ = “” Then ‘ исходная строка пустая, завершаем рекурсию ReverseRecursion$ = “” Else ‘ не пустая строка - продолжаем рекурсию ReverseRecursion$ = ReverseRecursion$(Mid$(s$, 2)) & c$ End If End Function
Данный вариант выглядит не сложнее, чем традиционный алгоритм:
Function ReverseString$ (Source$) Dim sReverse$, i% For i = Len(Source$) To 1 Step -1 sReverse = sReverse & Mid$(Source, i, 1) Next I ReverseString$ = sReverse End Function
Однако при этом рекурсивный алгоритм работает в три-четыре раза медленнее. Обратите внимание на то, что все внутренние переменные ReverseRecursion (в данном случае это одна переменная c$) являются динамическими. Если же поставить ключевое слово Static в описании функции, то она будет просто неверно работать. (Попробуете сами — будет выдаваться пустая строка! Это объясняется просто: рекурсия заканчивается, когда переменная с$=””, и именно она участвует в «обратном» формировании строки.)
Пример 2. Вычисление факториала N! = N Ѕ (N-1) Ѕ... 1 Это классический пример рекурсивного соотношения, которое записывается следующим образом:
N! = N * (N-1)! 0!=1
Такое вычисление также может быть реализовано в виде двух алгоритмов:
- рекурсивный:
Public Function FactorialRecursion&(N%) ‘ вычисление факториала методом рекурсии If N% = 0 Then ‘ Завершаем рекурсию - 0! FactorialRecursion& = 1 ‘ 0! = 1 Else ‘ продолжаем рекурсию FactorialRecursion& = N% * FactorialRecursion&(N% - 1) End If End Function
- обычный цикл:
Public Function Factorial&(N%) ‘ вычисление факториала методом рекурсии Dim Fact&, i% Fact& = 1 If N% > 1 Then ‘ нетривиальный вариант For i% = 2 To N% Fact& = Fact& * i% Next End If Factorial& = Fact& End Function
Код в рекурсивной функции заметно короче, но работает в четыре раза медленнее.
Если вы сравните два варианта решения данной задачи, то увидите, что в них используется, казалось бы, один и тот же алгоритм — следующее значение функции вычисляется с помощью результата, полученного на предыдущем шаге. Однако при ближайшем рассмотрении мы обнаружим принципиальную разницу: при использовании цикла «значение предыдущего шага» уже известно, а в случае рекурсии — его еще только предстоит вычислить.
Представленные выше примеры слишком тривиальны, так как речь в них идет о простых линейных структурах заданной длины. В этом случае использование обычных циклов гораздо эффективнее (мы привели здесь рекурсию из чисто познавательных соображений). Но есть случаи, когда рекурсия просто необходима, — например, при обработке древовидных структур, когда, двигаясь от корня дерева, вам нужно пройти все его ветви, число и глубина которых заранее неизвестны. Классическим примером здесь является задача передвижения по каталогам файловой системы, которую мы рассмотрим в следующем совете.
Совет 230. Используйте рекурсию для передвижения по файлам
Предположим, мы хотим определить число всех файлов, соответствующих заданному шаблону имени, в некотором каталоге, а заодно — и количество вложенных подкаталогов. Такая задача решается с помощью процедуры HowManyFilesInThisDirectory, приведенной в листинге 3, а обращение к ней может выглядеть следующим образом:
PathName$ = “d:\qb_lib\” ‘ стартовый каталог FileName$ = “*.bas” ‘ шаблон имени файла FileCounter% = 0 ‘ начальное значение счетчика найденных ‘ файлов DirCounter% = 0 ‘ начальное значение счетчика ‘ просмотренных подкаталогов Call HowManyFilesInThisDirectory(PathName$, FileName$, _ FileCounter%)
Алгоритм этой процедуры достаточно прост — сначала выполняется поиск имен файлов в заданном каталоге, там же определяется список подкаталогов, а потом в цикле процедура обращается сама к себе с новым именем каталога. Тут все понятно, но следует обратить внимание на следующие моменты.
- Результат работы процедуры формируется в двух переменных: FileCounter и DirCounter. Однако первая передается в качестве параметра функции (и при этом постоянно модифицируется), а вторая является глобальной, статической переменной рекурсивной функции.
- При просмотре содержимого каталогов, казалось бы, проще использовать такую конструкцию:
- В процедуре формирования списка элементов каталога в массиве мы используем конструкцию:
- Здесь, к сожалению, приходится отметить, что в VB не допускается передача адресов функций в качестве параметров VB-процедуры (AddressOf можно использовать только при обращении к DLL). Если бы это было возможно, то мы могли бы использовать одну и ту же процедуру HowManyFilesInThisDirectory для выполнения разнообразных операций с найденными файлами (например, контекстного поиска в них). А пока нам придется брать исходный код этой процедуры и «вшивать» нужные операции, создавая новые варианты программ.
MyDir$ = Dir(PathName$, vbDirectory) Do While MyDir$ <> “” If MyDir$ <> “.” And MyDir$ <> “..” Then If GetAttr(PathName$ + MyDir$) = vbDirectory Then ‘ найден каталог DirCounter% = MyDirCounter% + 1 NewPathName$ = PathName$ + MyDir$(i) + “\” Call HowManyFilesInThisDirectory(NewPathName$, _ FileName$, FileCounter%) End If End If MyDir$ = Dir ‘ следующий поиск Loop
Однако такой код будет неправильным. Дело в том, что функция Dir не является реентерабельной. Более того, ее применение основано на цикле последовательных обращений, когда первое обращение (вызов функции с параметрами) задает шаблон поиска (который запоминается внутри функции в ее статической переменной), а затем (вызов функции без параметров) происходит как бы продолжение первоначально заданной операции.
Приведенный выше код нарушает такой порядок обращения: еще до завершения цикла по текущему шаблону управление передается в процедуру HowManyFilesInThisDirectory, где инициализируется новая операция первого поиска. Поэтому необходимо сначала составить полный список подкаталогов (процедура CurrentDirCounter), а уже потом, перебирая его в цикле, рекурсивно обращаться к обработке очередного каталога.
If MyDirCount% > UBound(arrPath$) Then ReDim Preserve arrPath$(UBound(arrPath$) + 100) End If
Это обеспечивает автоматическое увеличение начального размера динамического массива с сохранением ранее записанной информации.
Совет 231. Преобразование имен файлов в определенный регистр
Традиционно системы DOS/Windows являются нечувствительными к регистру символов в именах файлов и каталогов: имена FileName, FiLENAME и т.д. являются идентичными. Однако в Unix-системах это не так, и мы, например, столкнулись с этой проблемой, когда начали заниматься созданием Web-узла, переписывая файлы с нашего локального компьютера на сервер.
Мы приняли однозначное правило: имена элементов оглавления пишутся только строчными буквами. Однако если соблюдение этого правила в тексте HTML-файла не вызывает никаких сложностей — там все видно, то управление физическими названиями в оглавлении оказалось не таким простым. Так, вид имени файла в окне Windows Explorer довольно часто совершенно не соответствует тому, каким он является на самом деле (не вполне понятно, почему).
Решением этой проблемы стало создание процедуры, которая автоматически просматривает заданные каталоги и производит замену всех символов в именах файлов и подкаталогов на строчные буквы.
Работа с отдельным файлов выглядит достаточно просто:
‘ PathName$ - имя каталога ‘ MyFile$ - исходный файл NewFile$ = LCase$(MyFile$) If NewFile$ <> MyName$ Then ‘ нужна замена имени Name PathName$ + MyName$ As PathName$ + NewFile$ End If
А как выполнить такую операцию для всех файлов по заданному шаблону? Казалось бы, это можно сделать так:
MyFile$ = Dir(PathName$ + FileName$) ‘ первый поиск ‘ анализ имени и переименование (см. код выше) Do While MyFile$ <> “” Loop
Но на самом деле такая конструкция работать не будет, опять же из-за отсутствия реентерабельности Dir. Действительно, еще до завершения операции с заданным шаблоном мы производим коррекцию списка элементов каталога. Поэтому корректная реализация изменения имен файлов в списке выглядит следующим образом (аналогично обработке подкаталогов в примере, приведенном выше):
ReDim arrPath$(100) ‘ для списка файлов Call CurrentDirCounter(PathName$, MyDirCount%, _ arrPath$(), vbNormal) ‘ If MyDirCount% > 0 Then ‘ есть подкаталоги For i% = 1 To MyDirCount% ‘ анализ имени и переименование (см. код выше) Next End If
Совет 232. Используйте Preserve для сохранения содержимого массива
При работе с массивами довольно часто случается так, что размеров массива не хватает для реального объема данных и нужно увеличить эти размеры с сохранением уже записанной информации. Типичным примером является запись в массив текстового файла, число строк которого неизвестно (другой случай, связанный с формированием списка элементов каталога, приведен в Совете 230, процедура HowManyFilesInThisDirectory). В этом случае следует использовать ключевое слово Preserve в операторе Redim:
StartSize = 100 ‘ начальное значение массива ReDim arrStrings$(1 To StartSize) AddSize = 20 ‘ дискретность увеличения длины Open FileName$ For Input As #1 iLen = 0 ‘ текущая позиция для записи While Not EOF(1) iLen = iLen + 1 If iLen > UBound(arrStrings$) Then ReDim Preserve arrStrings$(UBound(arrStrings$) + _ AddSize) End If Line Input #1, arrStrings$(iLen) Wend Close #1 ‘ ‘ Если хотите, то после окончания ввода можно уменьшить ‘ размеры массива до реального числа введенных элементов ReDim Preserve arrStrings$(iLen)
Коррекция длины массива с использованием Preserve возможна и для многомерных массивов, но при этом допускается изменение только последнего индекса (что связано с порядком хранения многомерных данных — по строкам). Например:
ReDim SomeArray(10, 10, 10) ReDim Preserve SomeArray(10, 10, 15) ‘ допустимо ReDim Preserve SomeArray(10, 15, 10) ‘ недопустимо
Совет 233. Используйте FreeFile для получения номера файла. Но будьте при этом бдительны!
При работе с файлами можно использовать два метода их нумерации: статический, когда номер файла задается константой, например #2, или динамический, когда номер выбирается из списка свободных и хранится в переменной:
FileNumber = FreeFile Open FileName$ For Input As #FileNumber Line Input #FileNumber, a$
Динамический вариант обеспечивает защиту от ситуации, когда номер файла назначается повторно. Но здесь же кроется и определенная опасность: при работе с большим числом динамически открываемых файлов нужно четко следить за их своевременным закрытием. (Номер файла закрепляется за файлом в момент открытия и освобождается в момент закрытия. Поэтому обращение к FreeFile нужно выполнять непосредственно перед оператором Open.) Во избежание таких проблем желательно при отладке следить за числом открытых файлов в программе.
Но самое главное — в одной программе нельзя использовать одновременно два метода. В крайнем случае нужно в начале программы открыть все файлы со статическими номерами, а уже потом работать с динамически выделяемыми адресами.
КомпьютерПресс 1'2000