Skip to content

Instantly share code, notes, and snippets.

@tsukanov-as
Last active August 17, 2019 07:05
Show Gist options
  • Save tsukanov-as/728501a9dc9558120a8d68c9f6877360 to your computer and use it in GitHub Desktop.
Save tsukanov-as/728501a9dc9558120a8d68c9f6877360 to your computer and use it in GitHub Desktop.
// MIT License
// Copyright (c) 2019 Tsukanov Alexander
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#Region Public
// Function - Проверяет соответствует ли строка регулярному выражению.
// Сопоставление выполняется обходом в глубину с мемоизацией.
//
// Parameters:
// Regex - Array - скомпилированная функцией Build() регулярка
// Str - String - проверяемая строка
//
// Returns:
// Boolean - признак что строка соответствует регулярному выражению
//
Function Match(Regex, Str) Export
Memo = New Array(Regex.Count(), 1);
Ok = MatchRecursive(Regex, Memo, Str, 1, 1);
Return Ok;
EndFunction
// Function - Ищет в строке первое совпадение с регулярным выражением.
// Сопоставление выполняется обходом в ширину.
// В текущей реализации возвращает только один захват на все выражение (скобки в выражении игнорируются).
//
// Не тестировалось. Использовать в бою нельзя.
//
// Parameters:
// Regex - Array - скомпилированная функцией Build() регулярка
// Str - String - проверяемая строка
//
// Returns:
// Undefined, Structure("Pos, Len") - найденный диапазон
//
Function Search(Regex, Str) Export
List = New Array;
NewList = New Array;
For Beg = 1 To StrLen(Str) Do
List.Add(1);
For Pos = Beg To StrLen(Str) + 1 Do
Char = Mid(Str, Pos, 1);
For Each Target In List Do
While Target <> Undefined Do
Node = Regex[Target];
If Node["end"] = True Then // это разрешенное конечное состояние?
Return New Structure("Pos, Len", Beg, Pos - Beg);
EndIf;
Target = Node[Char];
If Target = Undefined Then
Target = Node["any"]; // разрешен любой символ?
EndIf;
If Target <> Undefined Then
If NewList.Find(Target) = Undefined Then
NewList.Add(Target);
EndIf;
EndIf;
Target = Node["next"]; // можно пропустить без поглощения символа?
EndDo;
EndDo;
Temp = List;
List = NewList;
NewList = Temp;
NewList.Clear();
If List.Count() = 0 Then
Break;
EndIf;
EndDo;
For Each Target In List Do // если образец найден в конце строки, то эти узлы еще не проверены
If Regex[Target]["end"] = True Then // это разрешенное конечное состояние?
Return New Structure("Pos, Len", Beg, Pos - Beg);
EndIf;
EndDo;
List.Clear();
EndDo;
Return Undefined;
EndFunction
// Function - Возвращает скомпилированную регулярку
//
// Parameters:
// Pattern - String - регулярное выражение
// \w - любая буква
// \W - любой символ кроме буквы
// \d - любая цифра
// \D - любой символ кроме цифры
// \s - любой невидимый символ
// \S - любой символ кроме невидимых
// \n - перевод строки
// \r - возврат каретки
// \t - табуляция
// . - любой символ
// * - замыкание Клини
// + - один или несколько
// ? - один или ничего
// () - захваты
// [...] - один символ из набора
// [^...] - любой символ, кроме символов из набора
// _ - включить/выключить режим case insensitive
// $ - конец строки
//
// Returns:
// Array - скомпилированная регулярка
//
Function Build(Pattern) Export
Lexer = Lexer(Pattern);
Regex = New Array;
CharSet = NextChar(Lexer);
Balance = 0;
Node = NewNode(Regex); // нулевой узел
Node = NewNode(Regex); // первый узел
While CharSet <> "" Do
If CharSet = "\" Then
CharSet = CharSet(Lexer, NextChar(Lexer));
ElsIf CharSet = "." Then
CharSet = Lexer.AnyChar;
ElsIf CharSet = "[" Then
Set = New Map;
Complement = False;
CharSet = NextChar(Lexer);
If CharSet = "^" Then
Complement = True;
CharSet = NextChar(Lexer);
ElsIf CharSet = "]" Then
Set[CharSet] = Complement;
CharSet = NextChar(Lexer);
EndIf;
While CharSet <> "]" Do
If CharSet = "\" Then
CharSet = CharSet(Lexer, NextChar(Lexer));
EndIf;
If CharSet = "" Then
Raise "expected ']'";
EndIf;
If Complement Then
Lexer.Complement = Not Lexer.Complement;
EndIf;
Set[CharSet] = Lexer.Complement;
Lexer.Complement = False;
CharSet = NextChar(Lexer);
EndDo;
CharSet = Set;
ElsIf CharSet = "(" Then
Count = 0;
While CharSet = "(" Do
Count = Count + 1;
CharSet = NextChar(Lexer);
EndDo;
Balance = Balance + Count;
Node["++"] = Count;
Node["next"] = Regex.Count();
Node = NewNode(Regex);
Continue;
ElsIf CharSet = ")" Then
Count = 0;
While CharSet = ")" Do
Count = Count + 1;
CharSet = NextChar(Lexer);
EndDo;
Balance = Balance - Count;
Node["next"] = Regex.Count();
Node["--"] = Count;
Node = NewNode(Regex);
Continue;
ElsIf CharSet = "_" Then
Lexer.IgnoreCase = Not Lexer.IgnoreCase;
CharSet = NextChar(Lexer);
Continue;
ElsIf CharSet = "$" Then
Node[""]= Regex.Count();
Node = NewNode(Regex);
Break;
ElsIf CharSet = "*" Or CharSet = "+" Or CharSet = "?" Then
Raise StrTemplate("unexpected character '%1' in position '%2'", CharSet, Lexer.Pos);
EndIf;
NextChar = NextChar(Lexer);
If NextChar = "*" Then
AddArrows(Lexer, Node, CharSet, Regex.UBound());
Node["next"] = Regex.Count();
CharSet = NextChar(Lexer);
ElsIf NextChar = "+" Then
AddArrows(Lexer, Node, CharSet, Regex.Count());
Node = NewNode(Regex);
AddArrows(Lexer, Node, CharSet, Regex.UBound());
Node["next"] = Regex.Count();
CharSet = NextChar(Lexer);
ElsIf NextChar = "?" Then
AddArrows(Lexer, Node, CharSet, Regex.Count());
Node["next"] = Regex.Count();
CharSet = NextChar(Lexer);
Else
AddArrows(Lexer, Node, CharSet, Regex.Count());
CharSet = NextChar;
EndIf;
Node = NewNode(Regex);
Lexer.Complement = False;
EndDo;
If Balance <> 0 Then
Raise "unbalanced brackets";
EndIf;
Node["end"] = True; // разрешенное конечное состояние
Return Regex;
EndFunction
// Function - Возвращает захваченные диапазоны из регулярки.
// Порядок диапазонов в списке определен так:
// если строка "xy" и регулярное выражение "((x)(y))",
// то будут захвачены диапазоны [{"Pos":1,"Len":1},{"Pos":2,"Len":1},{"Pos":1,"Len":2}]
// т.е. "x", "y", "xy"
//
// Parameters:
// Regex - Array - регулярка после выполнения на ней Match()
//
// Returns:
// Array - список захваченных диапазонов
//
Function Captures(Regex) Export
Number = New TypeDescription("Number");
Captures = New Array;
Stack = New Map;
Level = 0;
For Each Node In Regex Do
Pos = Number.AdjustValue(Node["pos"]);
Inc = Node["++"];
If Inc <> Undefined Then
For n = 1 To Inc Do
Level = Level + 1;
Stack[Level] = Pos;
EndDo;
EndIf;
Dec = Node["--"];
If Dec <> Undefined Then
For n = 1 To Dec Do
Beg = Number.AdjustValue(Stack[Level]);
Captures.Add(New Structure("Pos, Len", Beg, Pos - Beg));
Level = Level - 1;
EndDo;
EndIf;
EndDo;
Return Captures;
EndFunction
#EndRegion // Public
#Region Private
Function MatchRecursive(Regex, Memo, Str, Val Target = 1, Val Pos = 1)
Node = Regex[Target];
NodeMemo = Memo[Target];
If NodeMemo.Find(Pos) <> Undefined Then
Return False;
EndIf;
Node["pos"] = Pos;
Char = Mid(Str, Pos, 1);
Target = Node["next"]; // можно пропустить без поглощения символа?
If Target <> Undefined Then
If MatchRecursive(Regex, Memo, Str, Target, Pos) Then // попытка сопоставить символ со следующим узлом
Return True;
EndIf;
EndIf;
Target = Node[Char];
If Target = Undefined Then
Target = Node["any"]; // разрешен любой символ?
EndIf;
If Target <> Undefined Then
If MatchRecursive(Regex, Memo, Str, Target, Pos + 1) Then
Return True;
EndIf;
EndIf;
NodeMemo.Add(Pos);
Return Node["end"] = True; // это разрешенное конечное состояние?
EndFunction
Function NewNode(Regex)
Node = New Map;
Regex.Add(Node);
Return Node;
EndFunction
Function Lexer(Pattern)
AlphaSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZабвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ";
DigitSet = "0123456789";
SpaceSet = " " + Chars.NBSp + Chars.Tab + Chars.LF;
Lexer = New Structure;
// константы
Lexer.Insert("AlphaSet", AlphaSet);
Lexer.Insert("DigitSet", DigitSet);
Lexer.Insert("SpaceSet", SpaceSet);
Lexer.Insert("Pattern", Pattern);
Lexer.Insert("AnyChar", New Structure);
// состояние
Lexer.Insert("Pos", 0);
Lexer.Insert("IgnoreCase", False);
Lexer.Insert("Complement", False);
Return Lexer;
EndFunction
Function NextChar(Lexer)
Lexer.Pos = Lexer.Pos + 1;
Char = Mid(Lexer.Pattern, Lexer.Pos, 1);
Return Char;
EndFunction
Function CharSet(Lexer, Char)
If Char = "n" Then
CharSet = Chars.LF;
ElsIf Char = "r" Then
CharSet = Chars.CR;
ElsIf Char = "t" Then
CharSet = Chars.Tab;
ElsIf Char = "w" Then
CharSet = Lexer.AlphaSet;
ElsIf Char = "d" Then
CharSet = Lexer.DigitSet;
ElsIf Char = "s" Then
CharSet = Lexer.SpaceSet;
ElsIf Char = "W" Then
CharSet = Lexer.AlphaSet;
Lexer.Complement = True;
ElsIf Char = "D" Then
CharSet = Lexer.DigitSet;
Lexer.Complement = True;
ElsIf Char = "S" Then
CharSet = Lexer.SpaceSet;
Lexer.Complement = True;
Else
CharSet = Char;
EndIf;
Return CharSet;
EndFunction
Procedure AddArrows(Lexer, Node, CharSet, Val Target)
If CharSet = Lexer.AnyChar Then
Node["any"] = Target; // стрелка для любого символа
Node[""] = 0; // кроме конца текста
ElsIf TypeOf(CharSet) = Type("Map") Then
For Each Item In CharSet Do
Lexer.Complement = Item.Value;
AddArrows(Lexer, Node, Item.Key, Target);
EndDo;
Else
If Lexer.Complement Then
Node["any"] = Target;
Target = 0; // стрелка на нулевой узел (запрещенное состояние)
EndIf;
For Num = 1 To Max(1, StrLen(CharSet)) Do
Char = Mid(CharSet, Num, 1);
If Lexer.IgnoreCase Then
Node[Lower(Char)] = Target;
Node[Upper(Char)] = Target;
Else
Node[Char] = Target;
EndIf;
EndDo;
EndIf;
EndProcedure
#EndRegion // Private
#Region Tests
Procedure RunAllTests() Export
For Num = 1 To 24 Do
Try
Execute StrTemplate("Test%1()", Num);
Except
Message(ErrorDescription());
EndTry;
EndDo;
EndProcedure
Function Elapsed(Start)
Return (CurrentUniversalDateInMilliseconds() - Start) / 1000;
EndFunction
Procedure Test1() Export
Regex = Build(".*_world_.*my \w*$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "World in my eyes");
Message(StrTemplate("Test1 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test2() Export
Regex = Build(".*_world_.*my \w*$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "Word in my eyes");
Message(StrTemplate("Test2 - %1 (%2 sec)", ?(Ok, "Failed!", "Passed"), Elapsed(Start)));
EndProcedure
Procedure Test3() Export
Regex = Build("a*a*a*a*a*a*a*a*a*a*a*a*a*a*aaaaaaaaaaaaaaa$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "aaaaaaaaaaaaaa");
Message(StrTemplate("Test3 - %1 (%2 sec)", ?(Ok, "Failed!", "Passed"), Elapsed(Start)));
EndProcedure
Procedure Test4() Export
Regex = Build("\W*digits$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "123456789digits");
Message(StrTemplate("Test4 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test5() Export
Regex = Build("\w*digits$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "123456789digits");
Message(StrTemplate("Test5 - %1 (%2 sec)", ?(Ok, "Failed!", "Passed"), Elapsed(Start)));
EndProcedure
Procedure Test6() Export
Regex = Build("_Case_.*_When_.*_Then_.*_Else_.*END$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "CASE Value WHEN 1 THEN '1' ELSE '0' END");
Message(StrTemplate("Test6 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test7() Export
Regex = Build("_\SoRd*_$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "wordDDD");
Message(StrTemplate("Test7 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test8() Export
Regex = Build("word[147]*word1$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "word1417word1");
Message(StrTemplate("Test8 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test9() Export
Regex = Build("word[147]*word1$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "word14217word1");
Message(StrTemplate("Test9 - %1 (%2 sec)", ?(Ok, "Failed!", "Passed"), Elapsed(Start)));
EndProcedure
Procedure Test10() Export
Regex = Build("word[\d]*word1$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "word14217word1");
Message(StrTemplate("Test10 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test11() Export
Regex = Build("word[\D]*word1$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "word14217word1");
Message(StrTemplate("Test11 - %1 (%2 sec)", ?(Ok, "Failed!", "Passed"), Elapsed(Start)));
EndProcedure
Procedure Test12() Export
Regex = Build("word[\D]*word1$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "wordword1");
Message(StrTemplate("Test12 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test13() Export
Regex = Build("If.*(_then_).*_ENDIF_;$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "If x > 0 Then x = 0 EndIf;");
Message(StrTemplate("Test13 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test14() Export
Regex = Build("_If.*__(_then_).*_ENDIF_;$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "If x > 0 Then x = 0 EndIf;");
Message(StrTemplate("Test14 - %1 (%2 sec)", ?(Ok, "Failed!", "Passed"), Elapsed(Start)));
EndProcedure
Procedure Test15() Export
Regex = Build(".*xyz$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "sfds1 $565 fdv\_fvdf dfvdf\0\9\)xyz");
Message(StrTemplate("Test15 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test16() Export
Regex = Build("\\\(\)\_\*$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "\()_*");
Message(StrTemplate("Test16 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test17() Export
Regex = Build("a+b+c+$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "aaabcccc");
Message(StrTemplate("Test17 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test18() Export
Regex = Build("\.$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, ".");
Message(StrTemplate("Test18 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test19() Export
Regex = Build("\.$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "a");
Message(StrTemplate("Test19 - %1 (%2 sec)", ?(Ok, "Failed!", "Passed"), Elapsed(Start)));
EndProcedure
Procedure Test20() Export
Regex = Build("[]]*$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "]]]]");
Message(StrTemplate("Test20 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test21() Export
Regex = Build("[\w]+$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "sadadw");
Message(StrTemplate("Test21 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test22() Export
Regex = Build("[\W]+$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "sadadw");
Message(StrTemplate("Test22 - %1 (%2 sec)", ?(Ok, "Failed!", "Passed"), Elapsed(Start)));
EndProcedure
Procedure Test23() Export
Regex = Build("[\W]+$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "12314324");
Message(StrTemplate("Test23 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
Procedure Test24() Export
Regex = Build("[^\W]+$");
Start = CurrentUniversalDateInMilliseconds();
Ok = Match(Regex, "sadadw");
Message(StrTemplate("Test24 - %1 (%2 sec)", ?(Ok, "Passed", "Failed!"), Elapsed(Start)));
EndProcedure
#EndRegion // Tests
#Region Examples
// Вывод списка экспортных процедур из этого модуля
// Например, для данной процедуры будет выведено:
// Example1()
Procedure Example1() Export
Regex = Build("Procedure (\w*\d*\(.*\)) Export");
Reader = New TextReader;
Reader.Open("ObjectModule.bsl");
Start = CurrentUniversalDateInMilliseconds();
Str = Reader.ReadLine();
While Str <> Undefined Do
If Match(Regex, Str) Then
Captures = Captures(Regex);
For Each Item In Captures Do
Message(Mid(Str, Item.Pos, Item.Len));
EndDo;
EndIf;
Str = Reader.ReadLine();
EndDo;
Message(StrTemplate("Example1 - Completed (%1 sec)", Elapsed(Start)));
EndProcedure
// Вывод списка имен и номеров экспортных процедур из этого модуля
// Например, для данной процедуры будет выведено:
// Example
// 2
Procedure Example2() Export
Regex = Build("Procedure (\w*)(\d*)\(\) Export");
Reader = New TextReader;
Reader.Open("ObjectModule.bsl");
Start = CurrentUniversalDateInMilliseconds();
Str = Reader.ReadLine();
While Str <> Undefined Do
If Match(Regex, Str) Then
Captures = Captures(Regex);
For Each Item In Captures Do
Message(Mid(Str, Item.Pos, Item.Len));
EndDo;
EndIf;
Str = Reader.ReadLine();
EndDo;
Message(StrTemplate("Example2 - Completed (%1 sec)", Elapsed(Start)));
EndProcedure
// Вывод списка простых условий на равенство. Это пример вложенных захватов.
// Например, для кода 'ElsIf Char = "d" Then' будет выведено:
// Char
// "d"
// Char = "d"
Procedure Example3() Export
Regex = Build(".*If\s+((\S+)\s*=\s*(\S+))\s*Then.*");
Reader = New TextReader;
Reader.Open("ObjectModule.bsl");
Start = CurrentUniversalDateInMilliseconds();
Str = Reader.ReadLine();
While Str <> Undefined Do
If Match(Regex, Str) Then
Captures = Captures(Regex);
For Each Item In Captures Do
Message(Mid(Str, Item.Pos, Item.Len));
EndDo;
Message(Chars.LF);
EndIf;
Str = Reader.ReadLine();
EndDo;
Message(StrTemplate("Example3 - Completed (%1 sec)", Elapsed(Start)));
EndProcedure
// Поиск первого совпадения с регулярным выражением. Это пример использования Search().
// Будет выведено:
// `Example4() Export`
Procedure Example4() Export
Regex = Build("Example4.*Export");
Reader = New TextReader;
Reader.Open("ObjectModule.bsl");
Str = Reader.Read();
Start = CurrentUniversalDateInMilliseconds();
Capture = Search(Regex, Str);
If Capture = Undefined Then
Message("fail");
Else
Message("`" + Mid(Str, Capture.Pos, Capture.Len) + "`");
EndIf;
Message(StrTemplate("Example4 - Completed (%1 sec)", Elapsed(Start)));
EndProcedure
#EndRegion // Examples
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment