Visual2000 · Архив статей А.Колесова & О.Павловой

Особенности работы со строковыми переменными в VB. Часть 2

Андрей Колесов

© 1999, Андрей Колесов
Авторский вариант. Статья была опубликована c незначительной литературной правкой в журнале "КомпьютерПресс" № 01/2000, на компакт-диске.

VBP-проекты примеров, приведенных в данной статье, вы можете найти на Web-узле по адресу: www.visual.2000.ru/develop/vb/source/.


Visual Basic включает достаточно большой набор встроенных функций обработки строковых переменных: преобразование, сравнение, поиск и пр. Многие из них обсуждались в первой части статьи и целом ряде наших Советов, опубликованных ранее. В VB 6 появилась большая группа дополнительных функций, о которых и пойдет речь ниже. Но сначала вспомним об одном операторе, который появился в VB уже довольно давно (еще в версии 3), но о нем почему-то знают далеко не все пользователи VB.

Оператор Like — сравнение на "похожесть"

Cинтаксис этого оператора выглядит следующим образом:

result = expression Like pattern

где expression — символьное выражение, а pattern — символьный шаблон. Если выражение "соответствует" шаблону, то результат равен истине (True), в противном случае — лжи (False). Если выражение или шаблон равны Null (нулевая строка), то и результат будет равнен Null. Для создания шаблонов можно использовать набор символов обобщения:

Символ Операция соответствия
? любой одиночный символ
* множество (в том числе и пустое) символов
# любой одиночный цифровой символ (0-9)
[НаборСимволов] любой набор одиночных символов
[!НаборСимволов] любой символ, который не входит в указанный набор

Для задания набора символов можно использовать знак дефиса для определения диапазона (но обязательно в порядке возрастания ANSI-кодов). При этом допускается применение нескольких диапазонов. Например, [a-zA-Z0-9] позволит выбрать все алфавитно-цифровые символы для английского языка. Варианты использования разных типов сравнения приведены в таблице 1.

Оператор Like можно использовать также в SQL-запросах, например, так:

SELECT * FROM Employees WHERE LastName Like "[A-D]"

В этом случае будут выбраны записи о сотрудниках, фамилии которых начинаются с букв A, B, C, D.

Сортировка символов и учет чувствительности регистра (строчные или прописные буквы) выполняется на основе установки оператора Option Compare (по умолчанию он равен 0 — Binary) в данном программном модуле. Это нужно иметь в виду, потому что другие функции сравнения (InStr, StrComp) позволяют указывать режим сравнения (режим поиска: двоичный, текстовый, специальный) непосредственно в качестве параметра функции. Поэтому, если вы хотите управлять такими режимами самостоятельно (например, в одном модуле желательно иметь и тот, и другой вариант), то можно предложить такое решение.

Сделайте два отдельных BAS-модуля следующего содержания:

  1. LikeBinary.bas:

    Option Compare 0 ' Binary, двоичный
    Public Function LikeBinary(expression$, pattern$) As Boolean 
      LikeBinary = expression$ Like pattern$)
    End Function
    

  2. LikeText.bas:

    Option Compare 1 ' Text, текстовый 
    Public Function LikeText(expression$, pattern$) As Boolean 
      LikeText = expression$ Like pattern$)
    End Function
    

Далее обращайтесь соответственно к функциями LikeBinary или LikeText. Следует иметь в виду, что правила сравнения для национальных языков определяются текущей кодовой таблицей (для VB до версии 5 включительно) или региональными установками Windows (для VB 6). Подробнее об этом — см. 1-ю часть статьи. Для реализации "нечувствительного к регистру" режиму поиска можно использовать и такой простой вариант:

result = UCase$(expression) Like pattern

Но при этом нужно отметить, что для сортировки символьных переменных с помощью функции StrConv такой вариант не очень подходит, если вы используете русскую букву "ё". При двоичном сравнении "ё" попадает в конец списка, после "я". При текстовом же сравнении обеспечивается не только нечувствительность к регистру, но и перемещение "ё" на свое законное место — между "е" и "ж". Для оператора Like это также актуально, если вы используете поиск по шаблону с использованием диапазонов. В частности, выполнение строки

Print "ёж" Like "[а-я]*

приведет к получению результата True для режима Option Compare Text и результата False для Option Compare Binary.

Это тем более существенно, если вы работаете с расширенными символами европейских языков (разные "а" с диакрическими знаками — крышкой, точкой и прочие знаки над основным символом.), Более того, для некоторых европейских языков в режиме текстового поиска определенные двухсимвольные комбинации обрабатываются как одна уникальная буква (например, в испанском языке ch является одной буквой и ставится при сортировке между c и d). И наоборот, один символ преобразуется в два знака, в частности в немецком языке символ eszett эквивалентен ss:

В начало статьи

Новые функции обработки символьных переменных

В VB 6 появились три группы дополнительных функций по обработке символьных переменных. Их краткое описание приведено в таблицах 2-4, а с более подробными сведениями о них можно познакомиться в документации о системе.

Конечно, эти новые функции будет полезны при разработке программ, хотя лично я довольно скептически отношусь к идее подобного расширения состава функций. На самом деле все эти новшества могут быть довольно легко реализованы средствами VB, имеющимися даже в версии 3 (а может быть, даже в более ранней — с версиями 1 и 2 я не знаком). Их появление в составе VB может произвести впечатление на новичка, но не на опытного программиста. Проблема-то заключается в том, что для более сложных операций обработки строк все равно потребуется "ручное кодирование" собственных процедур.

В этом плане было бы гораздо полезнее, если бы Microsoft вместо довольно бессмысленной гонки по расширению встроенных функций реализовала простой механизм подключения повторно используемых процедур на уровне объектных OBJ- библиотек (что было сделано в свое время еще 15 лет назад в MS QuickBasic для DOS). Но эти рассуждения, конечно, из области ностальгических воспоминаний: Microsoft — "большой, ему видней". Корпорация категорически не желает давать VB- программистам возможность использования OBJ-библиотек.

Далее мы попробуем реализовать некоторые альтернативные варианты новых символьных функций VB 6, имея в виду следующие цели:

  1. Показать, что создание таких процедур вполне доступно программистам, работающим на ранних версиях VB. Более того, это может пригодиться и тем, кто уже использует VB 6: все новые функции работают только с символьными данными и не имеют вариантов для двоичных байтов (на эту тему мы говорили первой части статьи).
  2. Проанализировать разные варианты алгоритмов обработки строк, в том числе с использованием байтовых переменных.
  3. Еще раз продемонстрировать, что не всегда более простые программные конструкции оказываются более оптимальными с точки зрения скорости обработки данных.

В начало статьи

Ищем животных одного цвета

Попробуем написать программу для решения следующей задачи. Имеется символьная строка с полями данных и нужно сформировать новую строку, собрав в нее из исходной только те поля, которые содержат заданный ключ. Например, эта задача может выглядеть так:

' Исходная строка: список животных 
Animals$ = "Белая Собака,Черная Лошадь,Красная Кошка,_ 
           черная птица,Черный ризеншнауцер,голубая кошка"
Delim1$ = "," ' разделитель в исходной строке 
Search$ = "черн" ' ключ поиска 
vbComp% = vbTextCompare  ' режим поиска — текстовый 
Delim2$ = "//" ' разделитель в результирующей строке 
  'обращение к процедуре, решающей поставленную задачу:
Result$ = Test$(Animals$, Delim1$, Search$, vbComp%, Delim2$) 
Print Result$ ' должно быть напечатано: 
  ' Черная Лошадь//черная птица//Черный ризеншнауцер

С использованием новых функций VB 6 написать нужную процедуру достаточно просто:

Public Function Test1$ _
  (Source$, Delim1$, Search$, vbComp%, Delim2$)
  Dim arr1$(), arr2$()
    ' преобразуем строку в массив.
    ' разделитель — Delim1$
  arr1$ = Split(Source$, Delim1$)
    ' Из массива выделяем поля со словом Search$
    ' в режиме сравнения vbComp%
  arr2$ = Filter(arr1$, Search$, , vbComp%)
    ' объединение массива в одну строку
  Test1$ = Join(arr2, Delim2$)
End Function

При использовании традиционные возможности Basic, которые имелись еще в версиях 15-летней давности, реализация такого алгоритма будет выглядеть посложнее (см. листинг 1). Однако самое удивительное, что второй вариант работает в 1,5 раза быстрее, чем первый (см. рис.1)!

Рис. 1

Почему это происходит, понятно — сами функции Split, Filter и Join работают достаточно быстро, но постоянное создание динамических массивов требует довольно много времени. Так что, если скорость обработки действительно важна (например, при работе с большими объемами данных), то может быть имеет смысл потратить лишних 10 минут, чтобы написать не 4, а 16 строк программного кода.

В начало статьи

Сравнение встроенных и "самострочных" функций

Для иллюстрации идеи реализации различных алгоритмов преобразования символьных строк, предлагаю посмотреть на несколько вариантов процедур, которые являются аналогами новых встроенных функций StrReverse, InstrRev и Replace (соответственно листинги 2, 3 и 4). Время их выполнения приведено в таблице 5.

В качестве комментария к этим программам нужно отметить следующие моменты:

1. Замена кода (функция ReverseString):

For i = Len(Source$) To 1 Step -1 
  Reverse$ = Reverse$ + Mid$(Source$, i, 1)
Next

на код (функция ReverseStringMy):

Reverse$ = Space$(Len(Source$)) 
For i = Len(Source$) To 1 Step -1 
  j = j + 1: Mid$(Reverse$, j, 1) = Mid$(Source$, i, 1)
Next

приводит к увеличению быстродействия на 25% (использование коррекции переменной оператором Mid, вместо слияния двух переменных). Однако быстрее всего работает вариант с использование байтового массива ReverseStringByteMy. Но тут надо обратить внимание, что инверсия строки обеспечивается переносом пары байтов (один символ занимает два байта).

2. Замена кода (RevInstrByteMy)

bytArray = Source$ 
iBytes = Len(Source$) * 2 - 2 
For i = iBytes To 0 Step -2 
If Chr(bytArray(i)) = SubString Then 
  RevInstrByteMy = i / 2 + 1: Exit Function
End If Next

на код (RevInstrByteMy2):

bytArray = Source$: bytSub = Asc(SubString) 
iBytes = Len(Source$) * 2 - 2 
For i = iBytes To 0 Step -2 
  If bytArray(i) = bytSub Then
    RevInstrByteMy2 = i / 2 + 1: Exit Function
  End If
Next

увеличивает производительность в 3,5 раза (рис. 2).

Рис. 1

В этом случае скорость повышается за счет перехода к использованию байтового массива. Интересно, что оба эти варианта не являются точными аналогами InstrRev — они работают только в том случае, если разделитель является односимвольной переменной, причем с ASCII-кодом из нижней части таблицы (< 128). А универсальный вариант InstrReverseMy работает с любыми данными и что интересно — быстрее всех.

3. Быстродействие моего варианта ReplaceMy тоже не сильно уступает фирменному Replace.

В начало статьи

ПРИЛОЖЕНИЯ

В начало статьи

Таблица 1. Примеры операций соответствия символьной переменной шаблону с использованием оператора Like

Тип соответствия Шаблон Соответствуют шаблону (результат = True) Не соответствуют шаблону (результат = False)
Несколько символов a*a aa, aBa, aBBBa aBC
*ab* abc, AABB, Xab aZb, bac
Указанный символ a[*]a a*a aaa
Единичный символ a?a aaa, a3a, aBa aBBBa
Единичная цифра a#a a0a, a1a, a2a aaa, a10a
Диапазон символов [a-z] f, p, j 2, &
Вне диапазона [!a-z] 9, &, % b, a
Не цифра [!0-9] A, a, &, ~ 0, 1, 9
Комбинированный вариант a[!b-m]# An9, az0, a99 abc, aj0

В начало статьи

Таблица 2. Новые строковые функции VB6 (1-я группа)

Преобразование данных в отформатированные символьные строки.

Имя функции Описание Синтаксис
MonthName Возвращает строку, содержащую указанный месяц. MonthName(month[, abbreviate])
WeekdayName Возвращает строку, содержащую указанный день недели. С помощью этой функции можно задать день недели, который будет являться ее началом. WeekdayName(weekday, abbreviate, firstdayofweek)
FormatCurrency Возвращает выражение в виде денежной единицы вместе с символом валюты, который определен в системной панели управлени FormatCurrency(Expression[, NumDigitsAfterDecimal[, IncludeLeadingDigit[, UseParensForNegativeNumbers[, GroupDigits]]]])
FormatDateTime Возвращает выражение в виде даты или времени. FormatDateTime(Date[, NamedFormat])
FormatNumber Возвращает выражение в виде отформатированного числа. Предоставляет возможность добавлять нули слева от числа, использовать круглые скобки для отрицательных чисел и выбирать точность возвращаемого числа. FormatNumber(Expression[, NumDigitsAfterDecimal[, IncludeLeadingDigit[, UseParensForNegativeNumbers[, GroupDigits]]]])
FormatPercent Возвращает строку в виде процентного отношения умноженного на 100), после которого идет символ %. FormatPercent(Expression[, NumDigitsAfterDecimal[, IncludeLeadingDigit[, UseParensForNegativeNumbers[, GroupDigits]]]])

В начало статьи

Таблица 3. Новые строковые функции VB6 (2-я группа)

Эта группа функций разбивает строки на отдельные фрагменты с помощью разделителя или воссоздает строки из более мелких частей. Функции Filter и Split используют новую, появившуюся в VB6 возможность возвращать массивы строк.

Имя функции Описание Синтаксис
Filter Возвращает массив (нумерация индексов с нуля), который содержит подмножество строкового массива, полученное с помощью заданного критерия фильтрации Filter(InputStrings, Value[, Include[, Compare]]) as String()
Split Возвращает строковый массив (нумерация индексов с нуля), содержащий заданное число подстрок. Эта функция может вернуть как все подмножество строк, так и любое требуемое количество строк. Split (expression[, delimiter[, count[, compare]]]) as String()
Join Возвращает строку, созданную путем объединения нескольких подстрок, содержащихся в передаваемом пользователем массиве строк. В качестве разделителя может задаваться любой символ. Join(list[, delimiter]) as String

В начало статьи

Таблица 4. Новые строковые функции VB6 (3-я группа)

Данная группа функций позволяет выполнять ряд полезных операций над символьными строками.

Имя функции Описание Синтаксис
InstrRev Возвращает величину типа Long, указывающую положение первого экземпляра одной строки внутри другой, начиная с конца строки. InStr([start, ]string1, string2[, compare])
Replace Возвращает строку, в которой указанная подстрока заменяется на другую заданное число раз. С помощью этой функции можно задавать количество производимых замен или же начинать замены в любом месте исходной строки. Replace(expression, find, replacewith[, start[, count[, compare]]]) as String
Round Возвращает число, округленное до указанного числа десятичных знаков. Round(expression[, numdecimalplaces])
StrReverse Возвращает строку, в которой порядок символов заданной строки изменен на обратный. StrReverse(string1) as String

В начало статьи

Таблица 5. Результаты тестирования на время выполнения встроенных функций VB 6.0 и их аналогов, реализованных традиционными средствами Basic

Функция Среднее время выполнения (в "тиках")
1. Source$ = "We are going to use this string for a string reversal example"
StrReverse (VB6) 0,005
ReverseString 0,650
ReverseStringMy 0,510
ReverseStringByteMy 0,290
2. Source$ = "This line has several : colons : in it for an example"
InstrRev (VB6) 0,005
RevInstrMy 0,160
InstrReverseMy 0,010
RevInstrByteMy 0,225
RevInstrByteMy2 0,090
3. Source$ = "Мы пошли погулять в сад, а забрели в какой-то полисадник"
Заменить "по" на "при".
Replace (VB6) 0,56
ReplaceMy 0,87

В начало статьи

Листинг 1. Реализация комбинации функций Split, Filter и Join традиционными средствами Basic

Public Function Test2$(Source$, Delim1$, Search$, vbComp%, Delim2$)
  ' Вариант 2
  ' Используя традиционные (известные еще 15 лет назад) функции
    Dim Word$, Result$, i%, i1%, Source1$
    i1% = 1
    Source1$ = Source$ + Delim1$
    Do
      i% = InStr(i1%, Source$, Delim1$)
      If i% = 0 Then Exit Do
      ' выделяем очередное поле
      Word$ = Mid$(Source1$, i1, i% - i1%)
      If InStr(1, Word$, Search$, vbComp%) > 0 Then
        ' поле отвечает критерию поиска
        Result$ = Result$ + Delim2$ + Word$
      End If
      i1% = i% + Len(Delim1$)
    Loop
    If Result$ > "" Then
      Test2$ = Mid$(Result$, Len(Delim2$) + 1)
    Else: Test2$ = Result$
    End If
End Function

В начало статьи

Листинг 2. Варианты реализация функции StrReverse традиционными средствами Basic

Function ReverseString(sSource As String) As String
  Dim sReverse As String
  Dim i%
  For i = Len(sSource) To 1 Step -1
    sReverse = sReverse & Mid$(sSource, i, 1)
  Next i
  ReverseString = sReverse
End Function

Public Function ReverseStringMy(Source$) As String
  Dim Reverse$, i%, j%
  Reverse$ = Space$(Len(Source$))
  For i = Len(Source$) To 1 Step -1
    j = j + 1
    Mid$(Reverse$, j, 1) = Mid$(Source$, i, 1)
  Next
  ReverseStringMy = Reverse$
End Function

Public Function ReverseStringByteMy(Source$) As String
  ' revised procedure (VBPJ 12'98, p.129)
  Dim bString() As Byte, bString2() As Byte
  Dim i%, j%, iLen%
  '
  bString = Source$
  iLen = Len(Source$) * 2 - 1
  ReDim bString2(iLen)
  For i = iLen To 0 Step -2
    bString2(j) = bString(i - 1):
    bString2(j + 1) = bString(i):
  j = j + 2
  Next i
  ReverseStringByteMy = bString2
End Function

В начало статьи

Листинг 3. Варианты реализация функции InstrRev традиционными средствами Basic

Public Function RevInstrMy(Source$, SubString$) As Integer
  Dim i%, iStart%, iSub%

  iSub = Len(SubString$)
  iStart = Len(Source$) - iSub + 1
  For i = iStart To 1 Step -1
    If Mid$(Source$, i, iSub) = SubString$ Then
      RevInstrMy = i: Exit Function
    End If
  Next
End Function

Public Function InstrReverseMy(Source$, SubString$) As Integer
  Dim i%
  Do
    i = InStr(i + 1, Source$, SubString$)
    If i <= 0 Then Exit Do
    InstrReverseMy = i
  Loop
End Function

Public Function RevInstrByteMy(Source$, SubString$) As Integer
  Dim i%, bytArray() As Byte, iBytes%

  bytArray = Source$
  iBytes = Len(Source$) * 2 - 2
  For i = iBytes To 0 Step -2
  If Chr(bytArray(i)) = SubString Then
    RevInstrByteMy = i / 2 + 1
    Exit Function
  End If
  Next
End Function

Public Function RevInstrByteMy2(Source$, SubString$) As Integer
  Dim i%, bytArray() As Byte, bytSub As Byte, iBytes%

  bytArray = Source$: bytSub = Asc(SubString)
  iBytes = Len(Source$) * 2 - 2
  For i = iBytes To 0 Step -2
    If bytArray(i) = bytSub Then
      RevInstrByteMy2 = i / 2 + 1
      Exit Function
    End If
  Next
End Function

В начало статьи

Листинг 4. Варианты реализация функции Replase традиционными средствами Basic

Function ReplaceMy$(Source$, LookFor$, ReplaceWith$)
'
'  Замена кода строкой переменной
'===================================
  Dim strTemp$, iStart%, iStrLen%
  strTemp$ = Source$
  If Len(strTemp$) > 0 Then
    iStart% = 1
    iStrLen% = Len(ReplaceWith$)
    Do
      iStart% = InStr(iStart%, strTemp$, LookFor$)
      If iStart% = 0 Then Exit Do
      strTemp$ = Left$(strTemp$, iStart% - 1)  +  _
        ReplaceWith$ + Mid$(strTemp$, iStart% + Len(LookFor$))
      iStart% = iStart% + iStrLen%
    Loop
  End If
  ReplaceMy$ = strTemp$
End Function

В начало статьи