Excel как распарсить строку


Часто текстовая строка может содержать несколько значений. Например, адрес компании: «г.Москва, ул.Тверская, д.13», т.е. название города, улицы и номер дома. Если необходимо определить все компании в определенном городе, то нужно «разобрать» адрес на несколько составляющих. Аналогичный подход потребуется, если необходимо разнести по столбцам Имя и фамилию, артикул товара или извлечь число или дату из текстовой строки.

Данная статья является сводной, т.е. в ней содержатся ссылки на другие статьи, в которых решены определенные задачи. Начнем с адресов.

Самый простейший случай, если адрес, состоящий из названия города, улицы и т.д., импортирован в ячейку MS EXCEL из другой информационной системы. В этом случае у адреса имеется определенная структура (если элементы адреса хранились в отдельных полях) и скорее всего нет (мало) опечаток. Разгадав структуру можно быстро разнести адрес по столбцам. Например, адрес

«г.Москва, ул.Тверская, д.13»

очевидно состоит из 3-х блоков: город, улица, дом, разделенных пробелами и запятыми. Кроме того, перед названием стоят сокращения г., ул., д. С такой задачей достаточно легко справится инструмент MS EXCEL

Текст по столбцам

. Как это сделать написано в статье

Текст-по-столбцам (мастер текстов) в MS EXCEL

.

Очевидно, что не всегда адрес имеет четкую структуру, например, могут быть пропущены пробелы (запятые все же стоят). В этом случае помогут функции, работающие с текстовыми строками. Вот эти функции:

Функция ЛЕВСИМВ() в MS EXCEL

— выводит нужное количество левых символов строки;

Функция ПРАВСИМВ() в MS EXCEL

— выводит нужное количество правых символов строки;

Функция ПСТР() в MS EXCEL

— выводит часть текста из середины строки.

Используя комбинации этих функций можно в принципе разобрать любую строку, имеющую определенную структуру. Об этом смотри статью

Разнесение в MS EXCEL текстовых строк по столбцам

.

Еще раз отмечу, что перед использованием функций необходимо понять структуру текстовой строки, которую требуется разобрать. Например, извлечем номер дома из вышеуказанного адреса. Понятно, что потребуется использовать функцию ПРАВСИМВ(), но сколько символов извлечь? Два? А если в других адресах номер дома состоит из 1 или 3 цифр? В этом случае можно попытаться найти подстроку «д.», после которой идет номер дома. Это можно сделать с помощью

функции ПОИСК()

(см. статью

Нахождение в MS EXCEL позиции n-го вхождения символа в слове

). Далее нужно вычислить количество цифр номера дома. Это сделано в файле примера , ссылка на который внизу статьи.

Усложним ситуацию. Пусть подстрока «д.» может встречаться в адресе несколько раз, например, при указании названия

деревни

используется сокращение «д.», т.е. совпадает с префиксом номера дома. В этом случае нужно определить все строки, в которых имеется название деревень (первые 2 символа, т.к. это адрес населенного пункта) и исключить их. Также можно извлечь все цифры из строки в отдельный диапазон (см. статью

Извлекаем в MS EXCEL число из конца текстовой строки

). Но, что делать, если в названии улицы есть числа? Например, «26 Бакинских комиссаров». Короче, тут начинается творчество.

Не забудьте про пробелы! Каждый пробел — это отдельный символ. Часто при печати их ставят 2 или 3 подряд, а это совсем не то же самое, что один пробел. Используйте функцию

Функция СЖПРОБЕЛЫ() в MS EXCEL

, чтобы избавиться от лишних пробелов.


Об извлечении чисел из текстовой строки

см. здесь:

Извлекаем в MS EXCEL число из начала текстовой строки

или здесь

Извлекаем в MS EXCEL число из середины текстовой строки

.


Об извлечении названия файла из полного пути

см.

Извлечение имени файла в MS EXCEL

.


Про разбор фамилии

см.

Разделяем пробелами Фамилию, Имя и Отчество

.

Часто в русских текстовых строках попадаются

английские буквы

. Их также можно обнаружить и извлечь, см.

Есть ли в слове в MS EXCEL латинские буквы, цифры, ПРОПИСНЫЕ символы

.

Все статьи сайта, связанные с преобразованием текстовых строк собраны в этом разделе:

Изменение Текстовых Строк (значений)

.

Артикул товара

Пусть имеется перечень артикулов товара: 2-3657; 3-4897; …

Как видно, артикул состоит из 2-х числовых частей, разделенных дефисом. Причем, числовые части имеют строго заданный размер: первое число состоит из 1 цифры, второе — из 4-х.

Задача состоит в том, чтобы определить артикулы, у которых левый индекс <=2 и вывести для них правый индекс.

Первая часть задачи решается формулой =—ЛЕВСИМВ(A16;1)<=2 или =ЗНАЧЕН(ЛЕВСИМВ(A16;НАЙТИ(«-«;A16;1)-1))<=2 . Вторая формула понадобится, если длина первого индекса не обязательна равна 1 (см. файл примера ).

Вторая часть задачи решается формулой =ЗНАЧЕН(ПРАВСИМВ(A16;4)) .

Зачем нам потребовалась функция ЗНАЧЕН() ? Дело в том, что текстовые функции, такие ка ПРАВСИМВ() , возвращают текст, а не число (т.е. в нашем случае число в текстовом формате). Для того, чтобы применить к таким числам в текстовом формате операцию сравнения с другим числом, т.е. <=2, потребуется сначала

преобразовать текстовый формат в числовой формат

. Самый простой для этого способ — использовать функцию ЗНАЧЕН() или попытаться применить к нему арифметическую операцию, например, двойное вычитание — или *1 или +0.

ВНИМАНИЕ!

Если у Вас есть примеры или вопросы, связанные с разбором текстовых строк — смело пишите в комментариях к этой статье или в группу

]]>
https://vk.com/excel2ru

]]> ! Я дополню эту статью самыми интересными из них.

Содержание

  1. Разбор текстовых строк в EXCEL
  2. Артикул товара
  3. Excel как распарсить строку

Разбор текстовых строк в EXCEL

history 18 ноября 2017 г.

Часто текстовая строка может содержать несколько значений. Например, адрес компании: «г.Москва, ул.Тверская, д.13», т.е. название города, улицы и номер дома. Если необходимо определить все компании в определенном городе, то нужно «разобрать» адрес на несколько составляющих. Аналогичный подход потребуется, если необходимо разнести по столбцам Имя и фамилию, артикул товара или извлечь число или дату из текстовой строки.

Данная статья является сводной, т.е. в ней содержатся ссылки на другие статьи, в которых решены определенные задачи. Начнем с адресов.

Самый простейший случай, если адрес, состоящий из названия города, улицы и т.д., импортирован в ячейку MS EXCEL из другой информационной системы. В этом случае у адреса имеется определенная структура (если элементы адреса хранились в отдельных полях) и скорее всего нет (мало) опечаток. Разгадав структуру можно быстро разнести адрес по столбцам. Например, адрес «г.Москва, ул.Тверская, д.13» очевидно состоит из 3-х блоков: город, улица, дом, разделенных пробелами и запятыми. Кроме того, перед названием стоят сокращения г., ул., д. С такой задачей достаточно легко справится инструмент MS EXCEL Текст по столбцам . Как это сделать написано в статье Текст-по-столбцам (мастер текстов) в MS EXCEL .

Очевидно, что не всегда адрес имеет четкую структуру, например, могут быть пропущены пробелы (запятые все же стоят). В этом случае помогут функции, работающие с текстовыми строками. Вот эти функции:

— Функция ЛЕВСИМВ() в MS EXCEL — выводит нужное количество левых символов строки;

— Функция ПРАВСИМВ() в MS EXCEL — выводит нужное количество правых символов строки;

— Функция ПСТР() в MS EXCEL — выводит часть текста из середины строки.

Используя комбинации этих функций можно в принципе разобрать любую строку, имеющую определенную структуру. Об этом смотри статью Разнесение в MS EXCEL текстовых строк по столбцам .

Еще раз отмечу, что перед использованием функций необходимо понять структуру текстовой строки, которую требуется разобрать. Например, извлечем номер дома из вышеуказанного адреса. Понятно, что потребуется использовать функцию ПРАВСИМВ(), но сколько символов извлечь? Два? А если в других адресах номер дома состоит из 1 или 3 цифр? В этом случае можно попытаться найти подстроку «д.», после которой идет номер дома. Это можно сделать с помощью функции ПОИСК() (см. статью Нахождение в MS EXCEL позиции n-го вхождения символа в слове ). Далее нужно вычислить количество цифр номера дома. Это сделано в файле примера , ссылка на который внизу статьи.

Усложним ситуацию. Пусть подстрока «д.» может встречаться в адресе несколько раз, например, при указании названия деревни используется сокращение «д.», т.е. совпадает с префиксом номера дома. В этом случае нужно определить все строки, в которых имеется название деревень (первые 2 символа, т.к. это адрес населенного пункта) и исключить их. Также можно извлечь все цифры из строки в отдельный диапазон (см. статью Извлекаем в MS EXCEL число из конца текстовой строки ). Но, что делать, если в названии улицы есть числа? Например, «26 Бакинских комиссаров». Короче, тут начинается творчество.

Не забудьте про пробелы! Каждый пробел — это отдельный символ. Часто при печати их ставят 2 или 3 подряд, а это совсем не то же самое, что один пробел. Используйте функцию Функция СЖПРОБЕЛЫ() в MS EXCEL , чтобы избавиться от лишних пробелов.

Об извлечении названия файла из полного пути см. Извлечение имени файла в MS EXCEL .

Часто в русских текстовых строках попадаются английские буквы . Их также можно обнаружить и извлечь, см. Есть ли в слове в MS EXCEL латинские буквы, цифры, ПРОПИСНЫЕ символы .

Все статьи сайта, связанные с преобразованием текстовых строк собраны в этом разделе: Изменение Текстовых Строк (значений) .

Артикул товара

Пусть имеется перечень артикулов товара: 2-3657; 3-4897; .

Как видно, артикул состоит из 2-х числовых частей, разделенных дефисом. Причем, числовые части имеют строго заданный размер: первое число состоит из 1 цифры, второе — из 4-х.

Источник

Excel как распарсить строку

Модератор форума: китин, _Boroda_

Мир MS Excel » Вопросы и решения » Вопросы по Excel » Распарсить данные ячейки таблицы (Формулы/Formulas)

Распарсить данные ячейки таблицы

AngelOfLegend Дата: Пятница, 07.10.2016, 19:43 | Сообщение № 1

Необходимо по особенному распарсить таблицу. Что только не пробовал (разделитель по запятым в том числе). Это необходимо для дальнейшей работы в PowerBI. Массив данных очень большой, просто руками не сделать

Буду очень благодарен за идеи!

Необходимо по особенному распарсить таблицу. Что только не пробовал (разделитель по запятым в том числе). Это необходимо для дальнейшей работы в PowerBI. Массив данных очень большой, просто руками не сделать

Буду очень благодарен за идеи! AngelOfLegend

Сообщение Необходимо по особенному распарсить таблицу. Что только не пробовал (разделитель по запятым в том числе). Это необходимо для дальнейшей работы в PowerBI. Массив данных очень большой, просто руками не сделать

Буду очень благодарен за идеи! Автор — AngelOfLegend
Дата добавления — 07.10.2016 в 19:43

Manyasha Дата: Пятница, 07.10.2016, 19:47 | Сообщение № 2

AngelOfLegend, приложите пример в файле.

Характеристики всегда разделены запятыми или могут быть другие варианты?

AngelOfLegend, приложите пример в файле.

Характеристики всегда разделены запятыми или могут быть другие варианты? Manyasha

ЯД: 410013299366744 WM: R193491431804

Сообщение AngelOfLegend, приложите пример в файле.

Характеристики всегда разделены запятыми или могут быть другие варианты? Автор — Manyasha
Дата добавления — 07.10.2016 в 19:47

AngelOfLegend Дата: Пятница, 07.10.2016, 19:54 | Сообщение № 3
AngelOfLegend Дата: Пятница, 07.10.2016, 19:54 | Сообщение № 4
Udik Дата: Пятница, 07.10.2016, 19:55 | Сообщение № 5
вот вам барабан
яд 41001231307558 wm R419131876897
udik1968@gmail.com

Ответить

AngelOfLegend Дата: Пятница, 07.10.2016, 20:02 | Сообщение № 6
Manyasha Дата: Пятница, 07.10.2016, 20:10 | Сообщение № 7
ЯД: 410013299366744 WM: R193491431804

Ответить

Karataev Дата: Пятница, 07.10.2016, 20:11 | Сообщение № 8

200?’200px’:»+(this.scrollHeight+5)+’px’);»> Sub Парсинг()

Dim shSrc As Worksheet, shRes As Worksheet, arr1(), arr2
Dim lr As Long, i As Long

Set shSrc = ActiveSheet
Set shRes = Worksheets.Add(After:=shSrc)

lr = shSrc.Columns(«A»).Find(What:=»*», LookIn:=xlFormulas, LookAt:= _
xlPart, SearchOrder:=xlByRows, SearchDirection:=xlPrevious, MatchCase:=False _
, SearchFormat:=False).Row
arr1() = shSrc.Range(«A1:B» & lr).Value

For i = 2 To UBound(arr1)
lr = shRes.UsedRange.Row + shRes.UsedRange.Rows.Count
arr2 = Split(arr1(i, 2), «,»)
With shRes.Rows(lr).Resize(UBound(arr2) + 1)
.Columns(«A»).Value = arr1(i, 1)
.Columns(«B»).Value = WorksheetFunction.Transpose(arr2)
End With
Next

200?’200px’:»+(this.scrollHeight+5)+’px’);»> Sub Парсинг()

Dim shSrc As Worksheet, shRes As Worksheet, arr1(), arr2
Dim lr As Long, i As Long

Set shSrc = ActiveSheet
Set shRes = Worksheets.Add(After:=shSrc)

lr = shSrc.Columns(«A»).Find(What:=»*», LookIn:=xlFormulas, LookAt:= _
xlPart, SearchOrder:=xlByRows, SearchDirection:=xlPrevious, MatchCase:=False _
, SearchFormat:=False).Row
arr1() = shSrc.Range(«A1:B» & lr).Value

For i = 2 To UBound(arr1)
lr = shRes.UsedRange.Row + shRes.UsedRange.Rows.Count
arr2 = Split(arr1(i, 2), «,»)
With shRes.Rows(lr).Resize(UBound(arr2) + 1)
.Columns(«A»).Value = arr1(i, 1)
.Columns(«B»).Value = WorksheetFunction.Transpose(arr2)
End With
Next

200?’200px’:»+(this.scrollHeight+5)+’px’);»> Sub Парсинг()

Dim shSrc As Worksheet, shRes As Worksheet, arr1(), arr2
Dim lr As Long, i As Long

Set shSrc = ActiveSheet
Set shRes = Worksheets.Add(After:=shSrc)

lr = shSrc.Columns(«A»).Find(What:=»*», LookIn:=xlFormulas, LookAt:= _
xlPart, SearchOrder:=xlByRows, SearchDirection:=xlPrevious, MatchCase:=False _
, SearchFormat:=False).Row
arr1() = shSrc.Range(«A1:B» & lr).Value

For i = 2 To UBound(arr1)
lr = shRes.UsedRange.Row + shRes.UsedRange.Rows.Count
arr2 = Split(arr1(i, 2), «,»)
With shRes.Rows(lr).Resize(UBound(arr2) + 1)
.Columns(«A»).Value = arr1(i, 1)
.Columns(«B»).Value = WorksheetFunction.Transpose(arr2)
End With
Next

AngelOfLegend Дата: Пятница, 07.10.2016, 20:15 | Сообщение № 9
Manyasha Дата: Пятница, 07.10.2016, 20:46 | Сообщение № 10

AngelOfLegend, вопросы по своей теме задавайте здесь, не нужно в личку писать.

Для того, чтобы перенести макрос в другой файл, нажмите alt+f11 (или вкладка Разработчик-Visual Basic) — Вы в редакторе VBA. В модуле module1 есть макрос test. Его нужно скопировать в модуль рабочего файла. Для этого откройте редактор VBA рабочего файла — щелкните правой кнопкой мыши (пкм) по VB-проекту книги (см. скрин)

нажмите insert-module — 2-м щелчком откройте созданный модуль — вставьте код макроса.

Или вариант попроще: модуль с макросом (все там же, в редакторе VBA) можно перетащить мышкой из одного VB-проекта в другой.

Чтобы привязать макрос к кнопке, щелкаете пкм по кнопке-назначить макрос-выбираете test (ну или как Вы его там назовете).

[p.s.]И вариант Karataevа посмотрите, он тоже старался![/p.s.]

AngelOfLegend, вопросы по своей теме задавайте здесь, не нужно в личку писать.

Для того, чтобы перенести макрос в другой файл, нажмите alt+f11 (или вкладка Разработчик-Visual Basic) — Вы в редакторе VBA. В модуле module1 есть макрос test. Его нужно скопировать в модуль рабочего файла. Для этого откройте редактор VBA рабочего файла — щелкните правой кнопкой мыши (пкм) по VB-проекту книги (см. скрин)

нажмите insert-module — 2-м щелчком откройте созданный модуль — вставьте код макроса.

Или вариант попроще: модуль с макросом (все там же, в редакторе VBA) можно перетащить мышкой из одного VB-проекта в другой.

Чтобы привязать макрос к кнопке, щелкаете пкм по кнопке-назначить макрос-выбираете test (ну или как Вы его там назовете).

[p.s.]И вариант Karataevа посмотрите, он тоже старался![/p.s.] Manyasha

ЯД: 410013299366744 WM: R193491431804

Ответить

Сообщение AngelOfLegend, вопросы по своей теме задавайте здесь, не нужно в личку писать.

Для того, чтобы перенести макрос в другой файл, нажмите alt+f11 (или вкладка Разработчик-Visual Basic) — Вы в редакторе VBA. В модуле module1 есть макрос test. Его нужно скопировать в модуль рабочего файла. Для этого откройте редактор VBA рабочего файла — щелкните правой кнопкой мыши (пкм) по VB-проекту книги (см. скрин)

нажмите insert-module — 2-м щелчком откройте созданный модуль — вставьте код макроса.

Или вариант попроще: модуль с макросом (все там же, в редакторе VBA) можно перетащить мышкой из одного VB-проекта в другой.

Чтобы привязать макрос к кнопке, щелкаете пкм по кнопке-назначить макрос-выбираете test (ну или как Вы его там назовете).

Источник

Adblock
detector

Студворк — интернет-сервис помощи студентам

Здравствуйте, подскажите пожалуйста, как можно решить такую задачу:

В таблице Excel есть столбец со строками вида:

«ЭРТ №2 Такой-то край, Такой-то район, в/г ВоенныйГородок № 42 Кот 174 Здание контрольно-пропускного пункта (караулка)(встроенная) №174» без кавычек.

Необходимо в три соседних ячейки той же строки, выдернуть числа после первого, второго и третьего символа «№» соответственно.
Учитывая, что между № и цифрой может быть или не быть пробел.

Я так понимаю, необходимо 3 формулы, но никак не получается сообразить, как получить число неизвестной длинны после символа.

Excel 7-10 года, Windows.

Разбор текста регулярными выражениямиОдной из самых трудоемких и неприятных задач при работе с текстом в Excel является парсинг — разбор буквенно-цифровой «каши» на составляющие и извлечение из нее нужных нам фрагментов. Например:

  • извлечение почтового индекса из адреса (хорошо, если индекс всегда в начале, а если нет?)
  • нахождение номера и даты счета из описания платежа в банковской выписке
  • извлечение ИНН из разношерстных описаний компаний в списке контрагентов
  • поиск номера автомобиля или артикула товара в описании и т.д.

Обычно во подобных случаях, после получасового муторного ковыряния в тексте вручную, в голову начинают приходить мысли как-то автоматизировать этот процесс (особенно если данных много). Решений тут несколько и с разной степенью сложности-эффективности:

  • Использовать встроенные текстовые функции Excel для поиска-нарезки-склейки текста: ЛЕВСИМВ (LEFT), ПРАВСИМВ (RIGHT), ПСТР (MID), СЦЕПИТЬ (CONCATENATE) и ее аналоги, ОБЪЕДИНИТЬ (JOINTEXT), СОВПАД (EXACT) и т.д. Этот способ хорош, если в тексте есть четкая логика (например, индекс всегда в начале адреса). В противном случае формулы существенно усложняются и, порой, дело доходит даже до формул массива, что сильно тормозит на больших таблицах.
  • Использование оператора проверки текстового подобия Like из Visual Basic, обернутого в пользовательскую макро-функцию. Это позволяет реализовать более гибкий поиск с использованием символов подстановки (*,#,? и т.д.) К сожалению, этот инструмент не умеет извлекать нужную подстроку из текста — только проверять, содержится ли она в нем.

Кроме вышеперечисленного, есть еще один подход, очень известный в узких кругах профессиональных программистов, веб-разработчиков и прочих технарей — это регулярные выражения (Regular Expressions = RegExp = «регэкспы» = «регулярки»). Упрощенно говоря, RegExp — это язык, где с помощью специальных символов и правил производится поиск нужных подстрок в тексте, их извлечение или замена на другой текст. Регулярные выражения — это очень мощный и красивый инструмент, на порядок превосходящий по возможностям все остальные способы работы с текстом. Многие языки программирования (C#, PHP, Perl, JavaScript…) и текстовые редакторы (Word, Notepad++…) поддерживают регулярные выражения.

Microsoft Excel, к сожалению, не имеет поддержки RegExp по-умолчанию «из коробки», но это легко исправить с помощью VBA. Откройте редактор Visual Basic с вкладки Разработчик (Developer) или сочетанием клавиш Alt+F11. Затем вставьте новый модуль через меню Insert — Module и скопируйте туда текст вот такой макрофункции:

Public Function RegExpExtract(Text As String, Pattern As String, Optional Item As Integer = 1) As String
    On Error GoTo ErrHandl
    Set regex = CreateObject("VBScript.RegExp")
    regex.Pattern = Pattern
    regex.Global = True
    If regex.Test(Text) Then
        Set matches = regex.Execute(Text)
        RegExpExtract = matches.Item(Item - 1)
        Exit Function
    End If
ErrHandl:
    RegExpExtract = CVErr(xlErrValue)
End Function

Теперь можно закрыть редактор Visual Basic и, вернувшись в Excel, опробовать нашу новую функцию. Синтаксис у нее следующий:

=RegExpExtract( Txt ; Pattern ; Item )

где

  • Txt — ячейка с текстом, который мы проверяем и из которого хотим извлечь нужную нам подстроку
  • Pattern — маска (шаблон) для поиска подстроки
  • Item — порядковый номер подстроки, которую надо извлечь, если их несколько (если не указан, то выводится первое вхождение)

Самое интересное тут, конечно, это Pattern — строка-шаблон из спецсимволов «на языке» RegExp, которая и задает, что именно и где мы хотим найти. Вот самые основные из них — для начала:

 Паттерн  Описание
 . Самое простое — это точка. Она обозначает любой символ в шаблоне на указанной позиции.
 s Любой символ, выглядящий как пробел (пробел, табуляция или перенос строки).
 S Анти-вариант предыдущего шаблона, т.е. любой НЕпробельный символ.
 d Любая цифра
 D Анти-вариант предыдущего, т.е. любая НЕ цифра
 w Любой символ латиницы (A-Z), цифра или знак подчеркивания
 W Анти-вариант предыдущего, т.е. не латиница, не цифра и не подчеркивание.
[символы] В квадратных скобках можно указать один или несколько символов, разрешенных на указанной позиции в тексте. Например ст[уо]л будет соответствовать любому из слов: стол или стул.
Также можно не перечислять символы, а задать их диапазоном через дефис, т.е. вместо [ABDCDEF] написать [A-F]. или вместо [4567] ввести [4-7]. Например, для обозначения всех символов кириллицы можно использовать шаблон [а-яА-ЯёЁ].
[^символы] Если после открывающей квадратной скобки добавить символ «крышки» ^, то набор приобретет обратный смысл — на указанной позиции в тексте будут разрешены все символы, кроме перечисленных. Так, шаблон [^ЖМ]уть найдет Путь или Суть или Забудь, но не Жуть или Муть, например.
 | Логический оператор ИЛИ (OR) для проверки по любому из указанных критериев. Например чет|счёт|invoice) будет искать в тексте любое из указанных слов. Обычно набор вариантов заключается в скобки.
 ^ Начало строки
 $ Конец строки
 b Край слова

Если мы ищем определенное количество символов, например, шестизначный почтовый индекс или все трехбуквенные коды товаров, то на помощь нам приходят квантификаторы или кванторы — специальные выражения, задающие количество искомых знаков. Квантификаторы применяются к тому символу, что стоит перед ним:

  Квантор  Описание
 ? Ноль или одно вхождение. Например .? будет означать один любой символ или его отсутствие.
 + Одно или более вхождений. Например d+ означает любое количество цифр (т.е. любое число от 0 до бесконечности).
 * Ноль или более вхождений, т.е. любое количество. Так s* означает любое количество пробелов или их отсутствие.
{число} или
{число1,число2}
Если нужно задать строго определенное количество вхождений, то оно задается в фигурных скобках. Например d{6} означает строго шесть цифр, а шаблон s{2,5} — от двух до пяти пробелов

Теперь давайте перейдем к самому интересному — разбору применения созданной функции и того, что узнали о паттернах на практических примерах из жизни.

Извлекаем числа из текста

Для начала разберем простой случай — нужно извлечь из буквенно-цифровой каши первое число, например мощность источников бесперебойного питания из прайс-листа:

Извлекаем первое число из текста

Логика работы регулярного выражения тут простая: d — означает любую цифру, а квантор + говорит о том, что их количество должно быть одна или больше. Двойной минус перед функцией нужен, чтобы «на лету» преобразовать извлеченные символы в полноценное число из числа-как-текст.

Почтовый индекс

На первый взгляд, тут все просто — ищем ровно шесть цифр подряд. Используем спецсимвол d для цифры и квантор {6} для количества знаков:

Извлекаем почтовый индекс

Однако, возможна ситуация, когда левее индекса в строке стоит еще один большой набор цифр подряд (номер телефона, ИНН, банковский счет и т.д.) Тогда наша регулярка выдернет из нее первых 6 цифр, т.е. сработает некорректно:

Ошибочное извлечение индекса

Чтобы этого не происходило, необходимо добавить в наше регулярное выражение по краям модификатор b означающий конец слова. Это даст понять Excel, что нужный нам фрагмент (индекс) должен быть отдельным словом, а не частью другого фрагмента (номера телефона):

Извлекаем ровно 6-разрядное число

Телефон

Проблема с нахождением телефонного номера среди текста состоит в том, что существует очень много вариантов записи номеров — с дефисами и без, через пробелы, с кодом региона в скобках или без и т.д. Поэтому, на мой взгляд, проще сначала вычистить из исходного текста все эти символы с помощью нескольких вложенных друг в друга функций ПОДСТАВИТЬ (SUBSTITUTE), чтобы он склеился в единое целое, а потом уже примитивной регуляркой d{11} вытаскивать 11 цифр подряд:

Вытаскиваем номер телефона из текста

ИНН

Тут чуть сложнее, т.к. ИНН (в России) бывает 10-значный (у юрлиц) или 12-значный (у физлиц). Если не придираться особо, то вполне можно удовлетвориться регуляркой d{10,12}, но она, строго говоря, будет вытаскивать все числа от 10 до 12 знаков, т.е. и ошибочно введенные 11-значные. Правильнее будет использовать два шаблона, связанных логическим ИЛИ оператором | (вертикальная черта):

Извлечение ИНН из текстовой строки

Обратите внимание, что в запросе мы сначала ищем 12-разрядные, и только потом 10-разрядные числа. Если же записать нашу регулярку наоборот, то она будет вытаскивать для всех, даже длинных 12-разрядных ИНН, только первые 10 символов. То есть после срабатывания первого условия дальнейшая проверка уже не производится:

Некорректное извлечение ИНН

Это принципиальное отличие оператора | от стандартной экселевской логической функции ИЛИ (OR), где от перестановки аргументов результат не меняется.

Артикулы товаров

Во многих компаниях товарам и услугам присваиваются уникальные идентификаторы — артикулы, SAP-коды, SKU и т.д. Если в их обозначениях есть логика, то их можно легко вытаскивать из любого текста с помощью регулярных выражений. Например, если мы знаем, что наши артикулы всегда состоят из трех заглавных английских букв, дефиса и последующего трехразрядного числа, то:

Артикулы

Логика работы шаблона тут проста. [A-Z] — означает любые заглавные буквы латиницы. Следующий за ним квантор {3} говорит о том, что нам важно, чтобы таких букв было именно три. После дефиса мы ждем три цифровых разряда, поэтому добавляем на конце d{3}

Денежные суммы

Похожим на предыдущий пункт образом, можно вытаскивать и цены (стоимости, НДС…) из описания товаров. Если денежные суммы, например, указываются через дефис, то:

Извлечь стоимость (цену) из текста

Паттерн d с квантором + ищет любое число до дефиса, а d{2} будет искать копейки (два разряда) после.

Если нужно вытащить не цены, а НДС, то можно воспользоваться третьим необязательным аргументом нашей функции RegExpExtract, задающим порядковый номер извлекаемого элемента. И, само-собой, можно заменить функцией ПОДСТАВИТЬ (SUBSTITUTE) в результатах дефис на стандартный десятичный разделитель и добавить двойной минус в начале, чтобы Excel интерпретировал найденный НДС как нормальное число:

Извлекаем суммы и НДС из текста

Автомобильные номера

Если не брать спецтранспорт, прицепы и прочие мотоциклы, то стандартный российский автомобильный номер разбирается по принципу «буква — три цифры — две буквы — код региона». Причем код региона может быть 2- или 3-значным, а в качестве букв применяются только те, что похожи внешне на латиницу. Таким образом, для извлечения номеров из текста нам поможет следующая регулярка:

Извлекаем автомобильный номер из текста

Время

Для извлечения времени в формате ЧЧ:ММ подойдет такое регулярное выражение:

Извлекаем время из текста

После двоеточия фрагмент [0-5]d, как легко сообразить, задает любое число в интервале 00-59. Перед двоеточием в скобках работают два шаблона, разделенных логическим ИЛИ (вертикальной чертой):

  • [0-1]d — любое число в интервале 00-19
  • 2[0-3] — любое число в интервале 20-23

К полученному результату можно применить дополнительно еще и стандартную Excel’евскую функцию ВРЕМЯ (TIME), чтобы преобразовать его в понятный программе и пригодный для дальнейших расчетов формат времени.

Проверка пароля

Предположим, что нам надо проверить список придуманных пользователями паролей на корректность. По нашим правилам, в паролях могут быть только английские буквы (строчные или прописные) и цифры. Пробелы, подчеркивания и другие знаки препинания не допускаются.

Проверку можно организовать с помощью вот такой несложной регулярки:

Проверка пароля регулярным выражением

По сути, таким шаблоном мы требуем, чтобы между началом (^) и концом ($) в нашем тексте находились только символы из заданного в квадратных скобках набора. Если нужно проверить еще и длину пароля (например, не меньше 6 символов), то квантор + можно заменить на интервал «шесть и более» в виде {6,}:

Проверка длины пароля

Город из адреса

Допустим, нам нужно вытащить город из строки адреса. Поможет регулярка, извлекающая текст от «г.» до следующей запятой:

Извлечь город из адреса

Давайте разберем этот шаблон поподробнее.

Если вы прочитали текст выше, то уже поняли, что некоторые символы в регулярных выражениях (точки, звездочки, знаки доллара и т.д.) несут особый смысл. Если же нужно искать сами эти символы, то перед ними ставится обратная косая черта (иногда это называют экранированием). Поэтому при поиске фрагмента «г.» мы должны написать в регулярке г. если ищем плюсик, то + и т.д.

Следующих два символа в нашем шаблоне — точка и звездочка-квантор — обозначают любое количество любых символов, т.е. любое название города.

На конце шаблона стоит запятая, т.к. мы ищем текст от «г.» до запятой. Но ведь в тексте может быть несколько запятых, правда? Не только после города, но и после улицы, дома и т.д. На какой из них будет останавливаться наш запрос? Вот за это отвечает вопросительный знак. Без него наша регулярка вытаскивала бы максимально длинную строку из всех возможных:

Извлекаем город из адреса - жадный квантор

В терминах регулярных выражений, такой шаблон является «жадным». Чтобы исправить ситуацию и нужен вопросительный знак — он делает квантор, после которого стоит, «скупым» — и наш запрос берет текст только до первой встречной запятой после «г.»:

Скупой квантор

Имя файла из полного пути

Еще одна весьма распространенная ситуация — вытащить имя файла из полного пути. Тут поможет простая регулярка вида:

Извлечь имя файла из полного пути

Тут фишка в том, что поиск, по сути, происходит в обратном направлении — от конца к началу, т.к. в конце нашего шаблона стоит $, и мы ищем все, что перед ним до первого справа обратного слэша. Бэкслэш заэкранирован, как и точка в предыдущем примере.

P.S.

«Под занавес» хочу уточнить, что все вышеописанное — это малая часть из всех возможностей, которые предоставляют регулярные выражения. Спецсимволов и правил их использования очень много и на эту тему написаны целые книги (рекомендую для начала хотя бы эту). В некотором смысле, написание регулярных выражений — это почти искусство. Почти всегда придуманную регулярку можно улучшить или дополнить, сделав ее более изящной или способным работать с более широким диапазоном вариантов входных данных.

Для анализа и разбора чужих регулярок или отладки своих собственных есть несколько удобных онлайн-сервисов: RegEx101, RegExr и др.

К сожалению, не все возможности классических регулярных выражений поддерживаются в VBA (например, обратный поиск или POSIX-классы) и умеют работать с кириллицей, но и того, что есть, думаю, хватит на первое время, чтобы вас порадовать.

Если же вы не новичок в теме, и вам есть чем поделиться — оставляйте полезные при работе в Excel регулярки в комментариях ниже. Один ум хорошо, а два сапога — пара!

Ссылки по теме

  • Замена и зачистка текста функцией ПОДСТАВИТЬ (SUBSTITUTE)
  • Поиск и подсветка символов латиницы в русском тексте
  • Поиск ближайшего похожего текста (Иванов = Ивонов = Иваноф и т.д.)

General

1 — The first line in UserSelectedRange is setting the return value to its default. At this point in the function it is already Nothing:

Private Function UserSelectRangeO(ByRef lastRow As Long) As Range
    Set UserSelectRange = Nothing    '<- Does nothing

Similarly, in GetUserInputRange() you do this if Application.InputBox throws:

    '...
    Exit Function
InputError:
    Set GetUserInputRange = Nothing
End Function

But if it throws, GetUserInputRange is never set. This function can be simplified to…

Private Function GetUserInputRange() As Range
    'This is segregated because of how excel handles cancelling a range input
    On Error Resume Next
    Set GetUserInputRange = Application.InputBox("Please click a cell in the column to parse", _
                                                 "Column Parser", Type:=8)
End Function

…and at that point I’m not sure I see why you wouldn’t just inline it because you are using the return value of Nothing to throw a different error anyway:

If columnToParse Is Nothing Then Err.Raise ParseError.InputRangeIsNothing

2 — There isn’t any need for UserSelectedRange to return lastRow by reference. You can simply get the last row from the selected Range itself. Since you aren’t even using lastRow in ParseIntoColumns, it allows you to get rid of this dead code in that procedure:

Dim lastRow As Long
lastRow = 1

3 — MsgBox returns a VbMsgBoxResult, which is an Integer. When you make tests of the return value, you are implicitly cast it to a String, then comparing it to an Integer (vbCancel), which implicitly casts it back to an Integer:

Dim result As String
result = MsgBox("The column you've selected to parse is column " & columnLetter, vbOKCancel)
If result = vbCancel Then Err.Raise ParseError.ProcessCancelled

If you need to store the return value, declare it as the appropriate type:

Dim result As VbMsgBoxResult

If you don’t (for example if you’re only testing it once), you can simply omit the variable declaration and test the return value directly:

If MsgBox("The column you've selected to parse is column " & columnLetter, _
          vbOKCancel) = vbCancel Then
    Err.Raise ParseError.ProcessCancelled
End If

4 — I’d put your ParseError enumeration in its own module and make it public instead of private. That way if you have other procedures that use custom error numbers you both easily can reuse them and avoid the possibility of collisions in error numbers.

5 — Named parameters after line continuations should be indented consistently. This is incredibly difficult to read:

workingRange.TextToColumns _
Destination:=workingRange, _
DataType:=xlDelimited, _
    TextQualifier:=xlTextQualifierNone, _
    ConsecutiveDelimiter:=True, _
    Tab:=False, _
    Semicolon:=False, _
    Comma:=False, _
    Space:=False, _
    Other:=True, OtherChar:=vbLf

6 — Consider using a regular expression to remove duplicate line feeds in ParseIntoRows. This can also avoid the possible bug if the data contains a vbCr. Since you immediately split the result, I’d use a function like this…

'Needs a reference to Microsoft VBScript Regular Expressions x.x
Private Function SplitLinesNoEmpties(target As String) As String()
    With New RegExp
        .Pattern = "[n]+"
        .MultiLine = True
        .Global = True
        SplitLinesNoEmpties = Split(.Replace(Replace$(target, vbCr, vbLf), vbLf), vbLf)
    End With
End Function

…instead of: Do Until cellContent = replacementCellContent

Then you can simply use stringParts = SplitLinesNoEmpties(cellContent) to get your array.

7 — Guard clauses should be in the procedure that they guard — not in the calling procedure. I’d move this code…

stringParts = Split(cellContent, vbLf)
numberOfParts = UBound(stringParts) - LBound(stringParts) + 1
If numberOfParts > 1 Then CreateNewRows stringParts(), numberOfParts, cellToParse

…to Sub CreateNewRows:

Private Sub CreateNewRows(ByRef partsOfString() As String, ByVal cellToParse As Range)
    Dim bottom As Long
    Dim top As Long
    bottom = LBound(partsOfString)
    top = UBound(partsOfString)
    If top <= bottom Then Exit Sub

    With cellToParse
        .EntireRow.Copy
        .Offset(1, 0).Resize(top - bottom, 1).EntireRow.Insert
        .Resize(numberOfParts, 1).Value = Application.WorksheetFunction.Transpose(partsOfString)
    End With
End Sub

Note that this does a couple things — it avoids the need to add one to the UBoundLBound calculation and then just subtract it again. If you’re testing to see if an array has at least 2 elements, UBound > LBound is sufficient (and protects from cases where LBound andor UBound is negative). It also explicitly protects against the case of UBound(Split(vbNullString)), which returns -1. This leads me to…

8 — Your guard clauses have a very subtle bug. Before you process the cell, you use this test:

Set cellToParse = Cells(currentRow, workingColumn)
If Not IsEmpty(cellToParse) Then
    cellContent = cellToParse.Value
    '...

IsEmpty isn’t doing what you think it is here. It doesn’t test whether a cell is empty — it tests whether the Variant passed to it is equal to vbEmpty.

Private Sub TleBug()
    Cells(1, 1).Formula = "=" & Chr$(34) & Chr$(34)  ' =""
    Debug.Print IsEmpty(Cells(1, 1))                 'False
    Debug.Print Cells(1, 1).Value = vbNullString     'True
End Sub

If you need to test whether a cell evaluates to vbNullString, do it explicitly:

Set cellToParse = Cells(currentRow, workingColumn)
cellContent = cellToParse.Value
If cellToParse <> vbNullString Then
    '...

9 — You have another (less) subtle bug. If you use Application.InputBox to have the user select the range to work with, you can’t use the global Range or Cells collections — they have to be qualified. The reason is that you yield control to the user, who is free to select a cell in a different Workbook than the one that was active when the macro started.

Private Sub TleBugTwo()
    Dim target As Range
    'User selects a cell in a different Workbook
    Set target = Application.InputBox("Select cell", "Input", Type:=8)
    Dim globalRange As Range
    Set globalRange = Range("A1")
    Debug.Print globalRange.Worksheet Is ActiveSheet      'True
    Debug.Print target.Worksheet Is globalRange.Worksheet 'False
End Sub

User Interface

1. GetUserInputRange() doesn’t display appropriate errors

If the user simply hits «OK» when the Application.InputBox is displayed, Excel shows this error dialog:

Error if empty

2. The range selection interface duplicates Excel functionality

Note that this is more a matter of personal preference than anything, but if Excel already provides an interface to select a cell or range of cells, why duplicate that? I’d simply use the existing Selection object when the macro starts. You’re already prompting the user to confirm that the Range that they selected when prompted is the one they want to work on, so why not just skip that entire process and use the Selection object instead?

Errors

@Zak already addressed the big issue with the error handling, so I’ll nitpick a little instead.

1. Duplicated code

Your error handlers in ParseIntoColumns and ParseIntoRows are identical, and only display the error condition to the user. I’d recommend extracting that section to it’s own Sub:

Private Sub DisplayErrorMessage(Err As Object)
    Select Case Err.Number
        Case ParseError.InputRangeIsNothing
            MsgBox "Process cancelled: You have not selected a range.", vbExclamation
        Case ParseError.MultipleColumnsSelected
            MsgBox "Process cancelled: You may not select more than 1 column at a time", vbExclamation
        Case ParseError.ProcessCancelled
            MsgBox "Process cancelled", vbExclamation
        Case ParseError.NoOverwrite
            MsgBox "Process cancelled: Please alter your data structure to allow overwriting cells to the right of your selection.", vbExclamation
        Case ParseError.NoData
            MsgBox "Process cancelled: your selection does not have data to parse", vbExclamation
        Case Else
            MsgBox "An error has occured: " & Err.Number & "- " & Err.Description, vbCritical
    End Select
End Sub

Then you can simply do this for your error handlers:

CleanUp:
    'Do stuff
    Exit Sub
ErrHandler:
    DisplayErrorMessage Err
    Resume CleanUp

2. User cancellation is not an error condition

I’d consider this section to be an abuse of the error handler:

Dim confirmOverwrite As String
confirmOverwrite = MsgBox("Do you want to overwrite all data to the right of your selection?", vbYesNo)
If confirmOverwrite = vbNo Then Err.Raise ParseError.NoOverwrite

I’m not even sure that you need to display any sort of confirmation that the process has been cancelled. My personal expectation would be that it would simply exit after I told it not to continue:

If confirmOverwrite = vbNo Then Exit Sub

Одной из самых интересных задач, с которыми нам пришлось столкнуться в процессе работы над компонентом Spreadsheet, стал механизм вычисления формул. Работая над ним, мы основательно углубились в механику функционирования аналогичного механизма в MS Excel.

И сегодня я хочу рассказать вам о принципах его работы, хитростях и подводных камнях. А чтобы не сводиться к сухим пересказам документации, разбавленным дополнениями «из жизни» — я заодно вкратце расскажу, как мы реализовывали подобный механизм.

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

Внутреннее представление выражения

Выражение в Excel хранится в обратной польской записи, RPN. Выражение в RPN форме представляет из себя простой массив, элементы которого называются ParsedThing.

Полный набор ParsedThing состоит из следующих элементов:

Операнды – константы, массивы, ссылки;

Константы:

  • ParsedThingNumeric
  • ParsedThingInt
  • ParsedThingString
  • ParsedThingBool
  • ParsedThingMissingArg
  • ParsedThingError

Массивы:

  • ParsedThingArray

Ссылки:

  • ParsedThingName, ParsedThingNameX
  • ParsedThingArea, ParsedThingAreaErr, ParsedThingArea3d, ParsedThingAreaErr3d, ParsedThingAreaN, ParsedThingArea3dRel
  • ParsedThingRef, ParsedThingRefErr, ParsedThingRef3d, ParsedThingErr3d, ParsedThingRefRel, ParsedThingRef3dRel
  • ParsedThingTable, ParsedThingTableExt

Операторы – математические, логические, ссылочные, а так же вызовы функций;

Вызовы функций:

  • ParsedThingFunc
  • ParsedThingFuncVar

Бинарные операторы:

  • ParsedThingAdd
  • ParsedThingSubtract
  • ParsedThingMultiply
  • ParsedThingDivide
  • ParsedThingPower
  • ParsedThingConcat
  • ParsedThingLess
  • ParsedThingLessEqual
  • ParsedThingEqual
  • ParsedThingGreaterEqual
  • ParsedThingGreater
  • ParsedThingNotEqual
  • ParsedThingIntersect
  • ParsedThingUnion
  • ParsedThingRange

Унарные операторы:

  • ParsedThingUnaryPlus
  • ParsedThingUnaryMinus
  • ParsedThingPercent

Вспомогательные элементы и атрибуты (для оптимизации скорости вычислений, сохранения пробелов и переводов строк в выражении, т.е. не влияющие на результат вычисления выражения).

Вспомогательные:

  • ParsedThingMemArea
  • ParsedThingMemNoMem
  • ParsedThingMemErr
  • ParsedThingMemFunc
  • ParsedThingParentheses

Атрибуты:

  • ParsedThingAttrSemi
  • ParsedThingAttrIf
  • ParsedThingAttrChoose
  • ParsedThingAttrGoto
  • ParsedThingAttrSum
  • ParsedThingAttrSpace

Приведу пару примеров.

  1. «=A1*(1+true)». Во внутреннем представлении будет выглядеть так: {ParsedThingRef(A1), ParsedThingInt(1), ParsedThingBool(true), ParsedThingAdd, ParsedThingMultiply}
  2. «=SUM(A1,1,”2”,)». Во внутреннем представлении будет выглядеть так: {ParsedThingRef(A1), ParsedThingInt(1), ParsedThingString(“2”), ParsedThingMissing, ParsedThingFuncVar(“SUM”, 4 аргумента)}

Вычисления

Вычисление выражения, записанного в обратной польской записи, выполняется с использованием стековой машины. Хороший пример приведен в википедии.

Но в вычислении выражений из Excel не обошлось и без хитростей. Разработчики наделили все операнды свойством «тип значения». Это свойство указывает, как должен быть преобразован операнд перед вычислением оператора или функции. Например, обычные математические операторы не могут выполняться над ссылками, а могут только над простыми значениями (числовыми, логическими и т.д.). Чтобы выражение “A1 + B1:C1” работало корректно, Excel указывает для ссылок A1 и B1:C1, что те должны быть преобразованы к простому значению перед помещением результата вычисления в стек.

Существует три типа операндов:

  • Reference;
  • Value;
  • Array.

Каждый операнд имеет тип «по умолчанию»:

Все виды ссылок Reference
Константы кроме массивов Value
Массивы Array
Вызовы функций Value, Reference, Array

Результат вычисления функции может быть любого типа. Большинство функций возвращают Value, некоторые (например, INDIRECT) -Reference, остальные — Array(MMULT).

Конечным пользователям не нужно забивать голову типами данных: Excel сам подбирает нужный тип операнда уже на этапе разбора выражения. А на этапе вычисления не обойтись без «неявного приведения типов». Оно происходит в соответствии со следующей схемой:

Значение типа Value можно преобразовать к Array, в этом случае создастся массив из одного значения. В обратном направлении (Array->Value) преобразование тоже достаточно простое — из массива берется первый элемент.

Как видно из схемы, значение типа Reference невозможно получить из Value или Array. Это вполне логично, из числа, строки и т.п. получить ссылку не получится.

При преобразовании Reference к Array все значения из ячеек, входящих в диапазон, переписываются в массив. В случае когда диапазон комплексный (состоящий из двух или более других диапазонов) — результат преобразования равен ошибке #VALUE!

Интересным образом происходит преобразование Reference к Value. Между собой это правило мы прозвали «Кроссинг». Проще всего объяснить его суть на примере:

Пусть мы хотим привести к Value значения ячеек с A1 по A4, в которых находится одинаковая формула “=B2:B3”, имеющая тип Reference. Диапазон B2:B3 состоит из одной колонки. Если бы это было не так и колонок было бы больше, преобразование Reference к Value для всех ячеек с A1 по A4 вернуло бы #VALUE! и на этом бы завершилось. Ячейки A2 и A3 находятся в строках, пересекающихся с диапазоном B2:B3. Преобразование Reference->Value для этих ячеек вернет соответствующее значение из диапазона B2:B3, т.е. преобразование для A2 вернет 1, а для A3 вернет 2. Для остальных ячеек, таких как A1 и A4, преобразование вернет #VALUE!

Точно таким же поведение будет и для диапазона B7:C7, состоящего из одной строки. Для ячеек B6 и C6 преобразование вернет значения 3 и 4 соответственно, а для A6 и — D6 #VALUE! Аналогично, если бы строк в диапазоне было больше, преобразование вернуло бы #VALUE! для всех ячеек с A6 по D6

Существует несколько правил преобразования типов.

Значения всех формул, находящихся внутри ячеек, всегда приводятся к типу Value.

Например:

  • «=123» В этой формуле задана константа, она уже типа Value. Ничего преобразовывать не надо.
  • «={1,2,3}» Тут задан массив. Преобразование к Value по правилу дает нам первый элемент массива — 1. Он и будет результатом вычисления выражения.
  • Формула «=A1:B1» находящаяся в ячейке B2. Операнд-ссылка на диапазон по умолчанию имеет тип Reference. При вычислении он будет приведен к Value по правилу «кроссинг». Результатом в данном случае будет значение из ячейки B1.

Математические, логические и текстовые операторы не могут работать со ссылками. Поэтому аргументы для них подготавливаются и приводятся либо к Value либо к Array. Второй вариант при этом возможен только внутри Array формул. Например, при вычислении выражения «=B1:B2+A3:B3», записанного в ячейку A1, оба аргумента математического оператора сложения сначала будут приведены к типу Value по правилу «Кроссинг», а затем результаты будут сложены. Т.е. значение будет равно сумме значений ячеек B1 и A3.

Операторы ссылки не могут работать ни с каким другим типом, кроме Reference. К примеру, формула «=A1:«test»» будет неправильной, ввод такой формулы приведет к ошибке — Excel просто не даст такую формулу записать в ячейку.

Выражения внутри “имен” и некоторых других конструкций приводятся к типу «по умолчанию». В отличие от формул внутри ячеек, выражения в которых приводятся к типу Value. Выражение внутри некоторого “имени” name «=A1:B1» в результате вычисления будет равно диапазону A1:B1. Это же выражение в ячейке будет вычисляться и в результате будет либо одно значение, либо ошибка #VALUE! Но выражение в ячейке «=name» уже будет иметь тип Value и будет вычисляться в зависимости от текущей ячейки.

Парсер

Написав на коленке первый вариант парсера мы поняли, что монстр слишком велик и слабо поддается модернизации. А она в нашем случае была неизбежна, поскольку большое количество тонкостей мы познавали уже когда парсер худо-бедно работал. Для интереса решил попробовать другие методы и вооружился для этого генератором трансляторов Coco/R. Выбор на него в тот момент пал в основном из-за того, что я был с ним уже неплохо знаком. Coco/R оправдал мои надежды. Сгенеренный им парсер показал весьма неплохие результаты по скорости работы, поэтому решили остановиться на этом варианте.

Конечно, в рамках этой статьи я не стану останавливаться на описании возможностей и пересказе документации Coco/R. Благо, что документация написана на мой взгляд весьма понятно. Кроме этого рекомендую почитать статью на хабре.

Собираем Coco/R из исходников

В некоторых местах Coco/R генерирует не CLS-compliant код.

Проблема в публичных константах, имя которых начинается со знака подчеркивания. Правильный выход из ситуации только один — поправить Coco/R, благо полный исходный код его доступен на сайте разработчиков.

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

void GenTokens() {
	foreach (Symbol sym in tab.terminals) {
		if (Char.IsLetter(sym.name[0]))
			gen.WriteLine("tpublic const int _{0} = {1};", sym.name, sym.n);
	}
}

Далее, получившийся код продолжает быть невалидным, теперь уже по мнению FxCop. В нашей компании сборки постоянно тестируются на соответствие большому числу правил. Конечно, поскольку код сгенерирован, можно было бы сделать для него исключение и подавить проверку сгенерированных классов. Но это не лучший выход. К счастью, проблема только одна – публичные поля не соответствуют правилу Microsoft.Design: CA1051. Чтобы все исправить достаточно внести необходимые правки в файлы Parser.frame и Scanner.frame, которые располагаются рядом с файлом грамматики. То есть, сам Coco/R пересобирать не надо. Вот примеры:

public Scanner scanner;
public Errors  errors;
public Token t;    // last recognized token
public Token la;   // lookahead token

Некоторые из этих полей вообще не используются за пределами класса – их просто делаем приватными, остальным – создаем публичные свойства.

При разработке грамматики для Coco/R я пользовался плагином для студии.

Его плюшки

  • Подсветка синтаксиса для файла с грамматикой;
  • Автоматический запуск генератора при сохранении файла с грамматикой;
  • Intellisense для ключевых слов;
  • Показывает ошибки компиляции, возникающие в файле парсера в соответствующем месте в файле с грамматикой

Последняя фича была бы весьма удобна, но, к сожалению, у меня она работала некорректно — показывала ошибки не там где надо.

Плагин тоже пришлось научить генерировать CLS compliant код. Скачиваем исходный код плагина, и повторяем те же операции, что и с самим Coco/R.

Модернизируем сканер и парсер

Напомню, что для разбора выражения Coco/R создает пару классов – Parser и Scanner. Оба они создаются заново для каждого нового выражения. Поскольку в нашем случае выражений много, то пересоздание сканера может занять дополнительное время на большом количестве вызовов. В целом, нам достаточно одного комплекта парсер-сканер. Первая модернизация коснулась именно этого.

Вторая модернизация коснулась вспомогательного класса Buffer, который создается сканером для чтения входящего потока символов. “Из коробки” Coco/R содержит пару реализаций Buffer и UTF8Buffer. Оба они работают с потоком. Нам же поток не нужен: достаточно работы со строкой. Для этого создадим третью реализацию StringBuffer, попутно выделив интерфейс IBuffer:

public interface IBuffer {
	string GetString(int beg, int end);
	int Peek();
	int Pos { get; set; }
	int Read();
}

Сама реализация StringBuffer простая:

public class StringBuffer : IBuffer {
    int stringLen;
    int bufPos;
    string str;
    public StringBuffer(string str) {
        stringLen = str.Length;
        this.str = str;
        if (stringLen > 0)
            Pos = 0;
        else
            bufPos = 0;
    }
    public int Read() {
        if (bufPos < stringLen)
            return str[bufPos++];
        else
            return StreamBuffer.EOF;
    }
    public int Peek() {
        int curPos = Pos;
        int ch = Read();
        Pos = curPos;
        return ch;
    }
    public string GetString(int beg, int end) {
        return str.Substring(beg, end - beg);
    }
    public int Pos {
        get { return bufPos; }
        set {
            if (value < 0 || value > stringLen)
                throw new FatalError("buffer out of bounds access, position: " + value);
            bufPos = value;
        }
    }
}

Тестируем

На всякий случай проверяем, что создали дополнительный класс не зря. Запускаем тестирование трех сценариев для строки длиной N:

  1. инициализация из строки;
  2. чтение символа (вызов метода IBuffer.Read() N раз) ;
  3. получение 10 символов из строки(вызов IBuffer.GetString(i-10, i) (N-10) раз).

При N = 100:
Init x 100000:
Buffer: 171 мс
StringBuffer: 2 мс
Read xNx10000:
Buffer: 14 мс
StringBuffer: 8мс
GetString x (N-10) x 10000:
Buffer: 250 мс
StringBuffer: 20 мс

Разработка грамматики

Грамматика для Coco/R описывается в РБНФ(EBNF). Разработка грамматики для Coco/R сводится к построению РБНФ и оформлению ее в соответсвии с грамматикой Coco/R в файле с расширением atg.

Парсер строится на основе рекурсивного спуска, грамматика должна удовлетворять LL(k). Сканер основывается на детерминированном конечном автомате.

Итак, приступим. Первым в файле грамматики идет название будущего компилятора:

COMPILER FormulaParserGrammar

Далее должна следовать спецификация сканера. Сканер будет case-insensitive, указываем это при помощи ключевого слова IGNORECASE. Теперь надо определиться с символами. Нам надо отделить цифры, буквы, управляющие символы. Получилось следующее:

CHARACTERS
  digit		= "0123456789".
  chars		= "~!@#$%^&*()_-+={[]}|\:;"',./?<> ".
  eol			= 'r'.
  blank		= ' '.
  letter 		= ANY - digit - chars - eol - blank + '_'.

Coco/R позволяет не только складывать множества символов, но и вычитать. Так, в описании letter применено ключевое слово ANY, которое подставляет все множество символов, из которого вычитаются определенные выше другие множества.

Важной задачей сканера является идентификация токенов во входной последовательности. Во время работы над парсером набор токенов постоянно менялся и в итоге выглядит так:

TOKENS
  ident		= letter {letter | digit | '.'}.  
  wideident		= letter {letter | digit} ('?'|'\') {letter | digit | '?'|'\'}.
  positiveinumber	= digit {digit}.
  fnumber 		=
	"." digit {digit} [("e" | "E") ["+" | "-"] digit {digit}] 
	| 
	digit {digit} 
		( 
				"." digit {digit}	
				[("e" | "E" ) ["+" | "-"] digit {digit} ] 
			|
				("e" | "E") ["+" | "-"] digit {digit}
		). 
  space		= blank. 
  quotedOpenBracket	= "'[".
  quotedSymbol	= "''" | "']" | "'@" | "'#".
  pathPart		= ":\".
  trueConstant 	= "TRUE".
  falseConstant 	= "FALSE".

Обратите внимание, что идентификатор может содержать одну или несколько символов “точка”. Таким, например, может быть имя листа в ссылке на диапазон. Так же необходим дополнительный, расширенный, идентификатор. Он отличается от обычного наличием знака вопроса или бекслеша. Отмечу, что в Excel понятие идентификатора достаточно сложное и его трудно описать в грамматике. Вместо этого все строчки, идентифированные сканером как ident и wideident, проверяю уже в коде на соответствие следующим правилам:

  • Может содержать только буквы, цифры, и символы: _,.,,?;
  • Не может быть равен TRUE или FALSE;
  • Первый символ может быть только буквой, знаком подчеркивания, или бекслешем;
  • Если первый символ строки – бекслеш, то второго символа может не быть, либо это должен быть один из: _,.,,?;
  • Не должен быть схож с названием диапазона (например, A10);
  • Не должен начинаться на строку, которая может быть воспринята как ссылка в формате R1C1. Природа этого условия сложнообъяснима, приведу только несколько примеров идентификаторов, которые ему не удовлетворяют: “R1_test”, “R1test”,“RC1test”,“R”,“C”. При этом «RCtest» – вполне подходит.

Выделение quotedOpenBracket, quotedSymbol и pathPart в отельный токен – не более чем хитрость. Она позволила пропустить символы в именах колонок в табличной ссылке, перед которыми должен следовать апостроф. Например, в выражении “=Table1[Column'[1′]]” имя колонки начинается после символа ‘[’ и продолжается до символа ‘]’. При этом первый такой символ вместе с предшествующим ему апострофом будет прочитан сканером как терминал quotedSymbol(‘]) и, тем самым, чтение имени колонки на нем не остановится.

Наконец, укажем сканеру, чтобы он пропускал переводы строк и табуляции.
IGNORE eol + ‘n’ + ‘t’. Сами выражения могут быть написаны в несколько строк, но на грамматику это не влияет.

Мы подошли к основному разделу PRODUCTIONS. В нем необходимо описать все нетерминалы. В примерах дальше я буду приводить упрощенный код, большинство семантических вставок вырезаю, т.к. они внесут сложности в понимание грамматики. Оставлю лишь места построения самого выражения, без оптимизаций.

По всем нетерминалам (из которых Coco/R сделает методы) будет передаваться ссылка на выражение в RPN форме, а так же тип данных, к которому надо его привести. При вызове парсера для формулы внутри ячейки начальный тип данных – Value. Далее во время разбора он будет меняться, и в ветви дерева разбора будет передаваться подготовленный тип. К примеру, при разборе выражения “=OFFSET(A1:B1, A1, A2)” элемент польской записи — функция OFFSET — получит тип Value, при разборе же аргументов первый будет приводиться к Reference, другие два к Value. Для всех функциий мы храним информацию, какие аргументы и каких типов должны в нее передаваться.

Задачей парсера также является проверка формулы на правильность. Формулу будем считать некорректной, если Excel не дает записать ее в ячейку. Кроме синтаксических ошибок формулу некорректной могут сделать и неправильное количество аргументов, переданное в функцию или же несовпадение типа данных запрошенному. Например, функция ROW либо вообще не нуждается в параметрах либо только в одном, и он должен быть исключительно Reference. Мы уже говорили, что к Reference невозможно привести ни один другой тип, а это значит, что выражения «=ROW(1)», «=ROW(“A1”)» будут невалидными.

Т.к. наша грамматика является усложненной грамматикой для обычных математических выражений, строится она так же в зависимости от старшинства операций. Первым следуют логические выражения, последним – операторы работы с диапазонами. Т.е. в порядке увеличения приоритета.

Для визуализации РБНФ использую небольшую программку EBNF Visualizer. Вот так будет выглядеть первый нетерминал в нашей грамматике – логическое выражение:

Далее грамматика для Coco/R. В семантических вставках, оформленных между “(.” и “.)” я добавлю нужный ParsedThing к выражению.

LogicalClause<OperandDataType dataType, ParsedExpression expression>
	(.	IParsedThing thing = null;	.)
	= 
	ConcatenateClause<dataType, expression> 
	{
		(
			'<'	(.thing = ParsedThingLess.Instance;		.)
			| '>'	(.thing = ParsedThingGreater.Instance;	.)
			| "<="	(.thing = ParsedThingLessEqual.Instance;	.)
			| ">="	(.thing = ParsedThingGreaterEqual.Instance;	.)
			| "<>"	(.thing = ParsedThingNotEqual.Instance;	.)
			| '='	(.thing = ParsedThingEqual.Instance;		.)
		)
		ConcatenateClause<dataType, expression> 
		(.	expression.Add(thing);	.)
	}
.

По этому принципу будут строится: ConcatenateClause, AddClause, MultipyClause, PowerClause, UnaryClause, PercentClause, RangeUnionClause, RangeIntersectionClause, CellRangeClause. На CellRangeClause заканчиваются нетерминалы, описывающие операторы. За ним следует первый операнд – OperandCommonClause. Он будет выглядеть примерно так:

Однако, в приведенной грамматике есть неоднозначность. Она заключается в том, что SheetName и OperandClause могут начинаться с одного и того же терминала — с идентификатора. Например, может следовать выражение “=Sheet!A1”, а может “=name”. Тут “Sheet” и “name” – идентификаторы. К счастью, Coco/R позволяет разрешать конфликты, просматривая входящий поток сканером на несколько терминалов вперед. Т.е. мы можем просмотреть в поисках символа ‘!’, если таковой будет найден – то мы разбираем SheetName, иначе – OperandClause. Вот так будет выглядеть грамматика:

OperandCommonClause<OperandDataType dataType, ParsedExpression expression>
	=
	(
		IF(IsSheetDefinition())
		(
			(
				SheetNameQuoted<sheetDefinitionContext>
			|  
				[ '[' FileDefinitionSimple ]
				[ 
					SheetName<out sheetName> 
					[ 
						':' SheetName<out sheetName> 
						|
						'|'
							SingleQuotedIdent<out ddeTopic>
					]
				]
			)
			'!'
		    CellReferenceClause<dataType, expression> 
		)
	|
		OperandClause<dataType, expression>
	) 
.

Для разрешения конфликта используется метод IsSheetDefinition(), определенный в классе Parser. Подобные методы удобно писать в отдельном файле, пометив класс как partial.

Нетерминал SheetName может начинаться с цифры или состоять только из цифр. В этом случае, имя листа должно быть заключено в апострофы. В противном случае, Excel добавляет недостающие апрострофы.

SheetName<out string sheetName> 
	(.	int sheetNameStart = la.pos;.)
	=
	(
		[positiveinumber | fnumber]
		[ident]
	)
	(.	sheetName = scanner.Buffer.GetString(sheetNameStart, la.pos);	.)
.

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

SheetName<out string sheetName> 
	(.	int sheetNameStart = la.pos;.)
	= 
	(
		positiveinumber | fnumber
		[	
			IF(la.pos - t.pos == t.val.Length)
			ident
		]
		|
		ident
	)
	(.	sheetName = scanner.Buffer.GetString(sheetNameStart, la.pos);	.)
.

В OperandClause мы будем попадать из OperandCommonClause, если нет ссылки на лист, внешнюю книгу или источник DDE. Из этого нетерминала мы можем попасть в ArrayClause, StringConstant(оба не могут иметь перед собой ссылку на лист), CellReferenceClause, либо встретим скобку и перейдем к началу всего дерева разбора – к LogicalClause.

OperandClause<OperandDataType dataType, ParsedExpression expression>
	=
	(
			CellReferenceClause<dataType, expression> 
		| 
			ArrayClause<dataType, expression>
		| 
			StringConstant<expression, dataType>
	)
	|
		'(' 
			CommonCellReference<dataType, expression>
		')' 
		(.	expression.Add(ParsedThingParentheses.Instance);.)
.

CellReferenceClause наверно самый большой нетерминал, в нем собраны почти все виды операндов:

CellReferenceClause<OperandDataType dataType, ParsedExpression expression>
	=
	(
			IF (IsTableDefinition())
			TableReferenceExpressionClause<dataType, expression>
		|
			IF (IsFunctionDefinition())
			FunctionClause<dataType, expression>
		|
			IF (IsDefinedNameDefinition())
			DefinedNameClause<dataType, expression> 
		| 
			IF(IsRCCellPosition()) 
			CellPositionRCClause<dataType, expression> 
		|
			IF(IsA1CellPosition()) 
			CellPositionA1Clause<dataType, expression> 
		|
			CellError<dataType, expression> 
		| 
			TermNumber<expression>
		| 
			BoolConstant<expression, dataType>
		| 
			wideident
			(.	expression.Add(new ParsedThingName(t.val);.)
	)
.

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

Парсинг неполных выражений и «предсказания»

Рассмотрим задачу подсветки диапазонов, участвующих в формуле.

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

void RegisterCellRange(CellRange range, int sheetDefinitionIndex, int position, int length)

После чтения, вне зависимости удачно оно завершилось или нет, у нас есть набор ссылок.

На этом же стал основываться еще один механизм – предсказания. В выражении “=1*(1+2” нарушен баланс скобок, но, с большой вероятностью, пользователь забыл поставить скобку именно в конце выражения. То есть можно попробовать исправить эту формулу, дописав к ней недостающую скобку. Конечно, парсер сам этим заниматься не будет, он только скажет где и чего по его мнению не хватает. Так, например, в уже знаком нам OperandClause появилась следующие строки:

'(' 
	CommonCellReference<dataType, expression>
	(.
		if(la.val != ")")
			parserContext.RegisterSuggestion(new FunctionCloseBracketSuggestion(la.pos));
	.)
')' 

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

Оптимизация производительности вычислений

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

Атрибут AttrSemi. Этот атрибут добавляется первым элементом в те выражения, которые содержат volatile-функции.

Класс атрибутов Mem. Сюда входят сразу несколько атрибутов. Их объединяет то, что они созданы для оптимизации вычисления ссылок. По сути, они являются оберткой над некоторым выражением. Во время вычисления внутреннее выражение может и не вычисляться, за него результат выдаст Mem. Отличительной особенностью этих элементов является то, что они вставляются в обратной польской записи до выражения, которое оптимизируют.

  • ParsedThingMemFunc — указывает на то, что выражение внутри должно вычисляться каждый раз и не результат не может быть закеширован. Например, все выражение =INDIRECT(«A1»):B1 будет обернуто в MemFunc, т.к. функция INDIRECT является volatile функцией и при следующем расчете может вернуть уже другое значение.
  • ParsedThingMemArea. Заключает в себе выражение, значение которого уже посчитано и не будет меняться. Это значение сохранится внутри атрибута и при следующем расчете в стек будет добавлено именно оно, а внутреннее выражение вычисляться вообще не будет.
  • ParsedThingMemErr. Заключает в себе выражение, значение которого посчитано, не будет меняться и равно ошибке.
  • ParsedThingMemNoMem. При вычислении выражения внутри Excel столкнулся с нехваткой памяти. На практике я такое ни разу не встречал.

Атрибут AttrSum применяется в качестве упрощенной формы записи функции SUM в том случае, когда в функцию передан только один аргумент.

Атрибут AttrIf применяется совместно с одним или двумя операторами Goto для оптимизации вычисления функции IF. Напомню синтаксис функции IF: IF(условие, значение_истина, [значение_ложь]). Из двух значений можно вычислить только одно и сэкономить время на вычислении другого, если сразу после вычисления условия перейти к нужному значению. Тем самым, простое выражение =IF(condition,”v_true”,”v_false”) Excel густо разбавляет атрибутами. Получается примерно следующее:

Вычисление идет так. Значение condition помещается в стек. Следующим на очереди идет атрибут IF. Он смотрит на значение на вершине стека. Если оно истинно — ничего не делает. Если ложно, прибавляет текущий счетчик элементов в выражении на записанное внутри смещение, тем самым счетчик начинает указывать на “v_false”. Следующим рассчитывается либо “v_true”, либо “v_false” и результат помещается в стек. Далее идет Goto, первый или второй. Но оба они ссылаются на конец выражения (либо на следующие операторы в выражении, если таковые имеются).

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

Атрибут Space используется для сохранения формата введенного пользователем выражения, а именно переводов строк и пробелов. Каждый такой атрибут кроме количества имеет тип. Типов всего шесть:

  • SpaceBeforeBaseExpression,
  • CarriageReturnBeforeBaseExpression,
  • SpaceBeforeOpenParentheses,
  • CarriageReturnBeforeOpenParentheses,
  • SpaceBeforeCloseParentheses,
  • CarriageReturnBeforeCloseParentheses,
  • SpaceBeforeExpression;

Взглянув на эти типы можно догадаться, что Excel не умеет сохранять пробелы в конце строки и перед знаком ‘=’. Кроме этого пробелы внутри структурированных ссылок и массивов так же сохранены не будут.

Тестирование

Когда наш контрол только-только научился читать и писать файлы в формате OpenXML, мы поспешили проверить его в деле. И самый лучший для этого способ – найти много- много файлов и попробовать погонять чтение и запись этих файлов. Так мы накачали около 10к случайных OpenXML файлов и написали несложную программку. Создали для нее задачу на тестовой ферме. Каждую ночь задача автоматически запускается и читает-пишет файлы. При возникновении каких либо ошибок вся необходимая информация записывается в лог. Так мы смогли отладить огромное количество ошибок.

По мере развития контрола добавлялись как поддерживаемые форматы, так и фичи. Так сейчас постоянно тестируются 20к xls файлов и 15к csv файлов. И тестируются не только на чтение-запись, но и проверяются сторонними утилитами, которые также нам очень помогают.

Огромное количество знаний о работе формул в Excel мы получили, когда запустили задачу на тестирование вычислений формул из тех же 10к OpenXML и 20к xls файлов. Файл открывается, записывается в модель данных. Затем поочередно мы начинаем помечать ячейки на листе как не посчитанные, вычисляем и сравниваем новое значение с тем значением, которое было прочитано из файла. Тем самым мы убили двух зайцев – отладили парсер формул и привели результаты вычислений максимально близко к тем, что получаются при использовании Excel.

Конечно, мы не избавились от всех проблем, связанных с формулами – уж слишком обширная тема. Но уже очень много всего изучили и реализовали, и не останавливаемся на достигнутом. Лично мне было интересно работать над ним, надеюсь, что и Вам было интересно читать эту статью.

Спасибо за внимание!

This article will demonstrate how to parse a string in VBA.

Often we will use Excel to import data from other files. Occasionally this data might be in a format that is not very user friendly, or that we need to amend to bring the information into Excel in a logical way. There are a number of string functions we can use in Excel VBA to extract the data correctly from the string that is brought in.

VBA Split Function

If the string comes into Excel as a delimited string (ie separated by commas, semi-colons, etc.), we can easily split the string into the individual values by using the Split function.

For example, say we have this string of names:

“John, Mary, Jack, Fred, Melanie, Steven, Paul, Robert”

Using the split function, we can return these names to Excel individually:

Sub SplitText()
 Dim strT As String
 Dim strArray() As String
 Dim name As Variant

'populate the string with names
 strT = "John,Mary,Jack,Fred,Melanie,Steven,Paul,Robert"

'populate the array and indicate the delmiter
 strArray = Split(strT, ",")

'loop through each name and display in immediate window
 For Each name In strArray
   Debug.Print name
 Next
End Sub

VBASplit Example

VBA Left, Right and Mid Functions

We can also extract data from strings by using the Left, Right and Mid functions.  They are not as efficient as using the Split function to get multiple values from a string, but if you need to separate a line into specific areas, they can be useful.

For example, say our file name is “C:DataTestFile.xls” .  Now this includes the drive, the folder on the drive, the name of the file and the file extension.

To get the drive that the file is stored on we can use:

LEFT(“C:DataTestFile.xls”, 1) – which will return C.

To get the Path including the drive we can use:

LEFT(“C:DataTestFile.xls”, 7) – which will return C:Data.

To get the name of the file only, we can use MID:

MID(“C:DataTestFile.xls”, 9,8) – which will return TestFile

To get the extension of the file we can use:

RIGHT(“C:DataTestFile.xls”, 3)

Sub ExtractData()
 Dim strData As String
 Dim strLeft As String
 Dim strRight As String
 Dim strMid As String

'populate the string
 strData = "C:DataTestFile.xls"

'break down the name
 strLeft = Left(strData, 7)
 strMid = Mid(strData, 9, 8)
 strRight = Right(strData, 3)

'return the results
 MsgBox "The path is " & strLeft & ", the File name is " & strMid & " and the extension is " & strRight
End Sub

The result of which would be:

VBASplit Left

VBA Replace Function

Another useful string function to manipulate strings in Excel, is the Replace function. This can be used to remove anything from a string and replace it with something else. This is particularly useful if the string that you have brought into Excel has characters that your coding will not recognize, or will mess up your data.

For example:

Consider the following string:

“John””Mary””Jack””Fred””Melanie””Steven””Paul””Robert”””

We can replace the double-quotes with commas using the Replace function.

Sub ExtractData()
  Dim StrData As String
  StrData = "John""Mary""Jack""Fred""Melanie""Steven""Paul""Robert"""
  StrData = Replace(StrData, """", ",")
  MsgBox StrData
End Sub

VBASplit Replace

VBA Coding Made Easy

Stop searching for VBA code online. Learn more about AutoMacro — A VBA Code Builder that allows beginners to code procedures from scratch with minimal coding knowledge and with many time-saving features for all users!
vba save as

Learn More!

Понравилась статья? Поделить с друзьями:
  • Excel как раскрасить ячейки в зависимости от значения
  • Excel как раскрасить строки через одну
  • Excel как разъеденить ячейки
  • Excel как разрешить редактирование ячейки
  • Excel как разрешить группировку на защищенном листе