102 Commits

Author SHA1 Message Date
073f231b89 Added function and examples for ReplaceAllFunc() 2025-02-10 21:35:51 -05:00
3b7257c921 Wrote function and example for ReplaceAllLiteral() 2025-02-10 21:25:49 -05:00
668df8b70a Wrote MarshalText() and UnmarshalText() to implement TextMarshaler and TextUnmarshaler 2025-02-10 12:30:48 -05:00
214acf7e0f Wrote example for ReplaceAll(); fixed out-of-bounds bug in Expand() 2025-02-10 12:30:17 -05:00
50221ff4d9 Wrote ReplaceAll(), to replace all matches of the regex with a given string 2025-02-10 12:29:54 -05:00
5ab95f512a Updated docs 2025-02-10 09:36:00 -05:00
e7da678408 Removed obsolete documentation 2025-02-10 09:35:16 -05:00
ab363e2766 Rewrote test for 'FindString()' to use lookarounds 2025-02-10 09:24:47 -05:00
c803e45415 Added example for 'FindStringSubmatch()' 2025-02-10 09:19:24 -05:00
525296f239 Added examples for 'FindAllString()' , 'FindAllSubmatch()' and 'FindAllStringSubmatch()' 2025-02-10 09:10:39 -05:00
eb0ab9f7ec Wrote test for FindAllStringSubmatch() 2025-02-10 08:39:20 -05:00
17a7dbae4c Wrote FindAllStringSubmatch() 2025-02-10 08:39:10 -05:00
f2279acd98 Fixed mistake in docs 2025-02-10 08:12:09 -05:00
662527c478 Merge pull request 'Implement PCRE Matching (prefer left-branch)' (#2) from implementPCREMatchingRules into master
Reviewed-on: #2
2025-02-09 15:24:26 -06:00
d1958f289c Commented out tests that would only pass with Longest() 2025-02-09 16:08:16 -05:00
15ee49f42e Rename method receivers from 'regex' to 're' (it's shorter) 2025-02-09 15:51:46 -05:00
b60ded4136 Don't break when a match is found, if we are looking for the longest match 2025-02-09 15:48:33 -05:00
9fbb99f86c Wrote example for Longest() 2025-02-09 15:47:57 -05:00
af15904f3b Updated documentation 2025-02-09 15:41:13 -05:00
d522f50b50 Wrote new example functions 2025-02-09 15:40:59 -05:00
fb47e082eb Wrote new methods Expand() and preferLongest(); Use new function signatures (with preferLongest); only characters should be added to next state list 2025-02-09 15:40:39 -05:00
1f5a363539 Use new function signatures (with preferLongest) 2025-02-09 15:39:09 -05:00
9e12f9dcb3 Added field to Reg, denoting if we prefer longest match (POSIX style) or not (perl style) 2025-02-09 15:38:26 -05:00
47f88c817f Fixed typo 2025-02-09 15:14:17 -05:00
835d495990 Removed capitalization for error message (staticcheck) 2025-02-09 09:14:45 -05:00
76e0170cb9 Removed unused function 2025-02-09 09:13:52 -05:00
d172a58258 Throw error if match isn't found but test.result has >0 elements 2025-02-09 09:13:29 -05:00
7231169270 Removed unused functions 2025-02-09 09:13:03 -05:00
e546f01c20 Removed redundant return (staticcheck) 2025-02-09 09:12:55 -05:00
b7467a00f1 Removed priorityQueue (unused) 2025-02-09 09:07:43 -05:00
c6ad4caa0d Removed a bunch of unused code (let's go!!!) 2025-02-09 09:06:40 -05:00
6334435b83 Updated tests since the engine uses Perl matching instead of POSIX matching; added tests for FindStringSubmatch 2025-02-09 09:01:42 -05:00
78fb5606dd Use new definition of Reg 2025-02-09 08:59:16 -05:00
eddd2ae700 Updated documentation 2025-02-09 08:58:58 -05:00
c577064977 Added string field to Reg, that contains the expression string; wrote method to return the string 2025-02-09 08:58:46 -05:00
d4e3942d27 Added Match() and FindStringSubmatch(); removed old code; updated comments 2025-02-09 08:58:09 -05:00
f15a5cae34 Store all states visited in a single run of 'addStateToList()' in a slice 2025-02-08 16:07:01 -05:00
62ca1a872a Made zeroLengthMatchState() return a pointer; reduced the number of comparisons performd by nfaState.equals 2025-02-08 16:06:14 -05:00
99230b49de Use new function signature for zeroLengthMatchState() 2025-02-08 16:05:35 -05:00
22ead83625 Fixed assertion matching 2025-02-07 16:19:36 -05:00
3604486a9b Used Pike's algorithm (an extension to Thompson's algorithm) (see Russ Cox's 2nd article); I think I almost have a working PCRE-style engine 2025-02-07 16:06:45 -05:00
052de55826 question() now returns 2 values 2025-02-07 16:04:46 -05:00
d2ad0d95a8 Modified question operator so that it doesn't create an unnecessary zero-state 2025-02-07 16:04:26 -05:00
ccf3b3b299 More progress on implementing PCRE matching 2025-02-06 22:08:56 -05:00
1d4f695f8f Wrote function to check if a state is in an nfaState, based on the Equals function 2025-02-06 22:06:51 -05:00
8534174ea1 Use pointers instead of values 2025-02-06 22:06:22 -05:00
ed4ffde64e REFACTOR NEEDED: Added another special case; insert instead of appending into currentStates 2025-02-05 22:51:55 -05:00
fbc9bea9fb Commented out unused functions; use new nfaState parameters 2025-02-05 22:23:31 -05:00
cca8c7cda2 Got rid of transitions parameter, changed how kleene state is processed
I replaced the transition parameter for nfaState, replacing it with a
single nfaState pointer. This is because any non-alternation state will
only have one next state, so the map was just added complexity.

I changed alternation processing - instead of having their own dedicated
fields, they just use the new 'next' parameter, and another one called
'splitState'.

I also changed the kleene state processing to remove the unecessary
empty state in the right-side alternation (it actually messed up my
matching).
2025-02-05 22:20:28 -05:00
858e535fba Continued implementing Thompson's algorithm 2025-02-05 18:01:36 -05:00
7c62ba6bfd Started implementing Thompson's algorithm for matching, because the old one was completely backtracking (so it would enter infinite loops on something like '(a*)*' )
The git diff claims that a ton of code was changed, but most of it was just indentation changes.
2025-02-05 12:21:12 -05:00
d4e8cb74fd Replaced pointer to nfaState with nfaState 2025-02-05 11:32:20 -05:00
3ce611d121 More work towards implementing PCRE matching 2025-02-04 14:09:24 -05:00
e0253dfaf3 Change kleene() to an alternation-style construct 2025-02-04 14:09:04 -05:00
753e973d82 Started rewrite of matching algorithm, got concatenation and alternation done, kleene and zero-state stuff is next 2025-02-03 22:01:52 -05:00
5563a70568 Reverse the order in which I pop states for alternation, because this messes with the left branch-right branch thing 2025-02-03 21:59:41 -05:00
de0d7345a8 Store left and right branches of alternation separately 2025-02-03 21:59:05 -05:00
ad273b0c68 Trying to emulate backtracking by using string pointers within threads (something similar to rsc's 2nd regexp article) 2025-02-03 16:50:11 -05:00
e167cdb2cb Fixed mistake in test output 2025-02-03 16:49:30 -05:00
1fd48ae614 Store the current string pointer as a 'thread variable' (allows us to simulate backtracking) 2025-02-03 16:49:10 -05:00
09812956ac Disable all optimizations 2025-02-03 16:48:09 -05:00
fbc9dfcc95 Trying something out; we'll see if it works 2025-02-03 16:47:53 -05:00
bc32e0cb76 Started working on converting to PCRE matching rules (prefer left branch of alternation) 2025-02-03 14:06:14 -05:00
ad0f7d0178 Added new state fields to tell if a state is a question or alternation 2025-02-03 14:05:53 -05:00
4e597f8eb1 Implemented a priority-queue to use while matching 2025-02-03 14:05:30 -05:00
ef476e8875 Reverse the order of the numeric range before adding it, to maintain compatibility with PCRE matching rules (even though I don't implement them, if I do in the future) 2025-02-02 13:46:48 -05:00
7e6b02632f Added more tests; commented out tests that I am failing 2025-02-02 13:46:08 -05:00
f94e3f2e71 Added comments 2025-02-02 12:44:06 -05:00
b129d83c3f Added function to reset threads 2025-02-02 12:43:40 -05:00
43aa7b5876 Updated documentation 2025-02-02 12:42:38 -05:00
9a3bfca313 Renamed unique_append to uniqueAppend 2025-02-02 12:42:29 -05:00
b6ab54f6dd Reset threads when findAllSubmatchHelper is called 2025-02-02 12:42:00 -05:00
6a96c98d04 Fixed bug where the regex '(()|.)(b)' wouldn't compile 2025-02-01 19:20:33 -05:00
3cfc2a6854 Updated Makefile 2025-02-01 18:52:26 -05:00
5d7a02e796 Added gcflags to go build 2025-02-01 18:51:58 -05:00
a46d2f4546 Updated comments 2025-02-01 18:07:31 -05:00
c88ebd1aa5 Added comments explaining what a Match is 2025-02-01 18:05:55 -05:00
fd102292c6 Added example for FindSubmatch 2025-02-01 18:05:43 -05:00
6d692d0dfc Rename Group.toString() to Group.String() 2025-02-01 12:51:32 -05:00
7c4538a259 Added 'example' file that contains testable examples 2025-02-01 12:50:49 -05:00
2a9ae0b68a Wrote test for 'FindSubmatch' 2025-02-01 11:09:05 -05:00
783ae2ad10 Updated call to 'isValid' with call to 'IsValid' 2025-02-01 11:06:26 -05:00
b5e6bc112c Wrote 'reg.FindSubmatch()' which returns the leftmost match with submatches, renamed 'isValid' to 'IsValid' to export it, renamed 'ToString' to 'String' 2025-02-01 11:06:03 -05:00
206fea34cd Added function to return the number of subexpressions in the group 2025-02-01 11:04:49 -05:00
fcdb23524a Added more documentation 2025-02-01 11:04:24 -05:00
ac936659b6 Updated documentation 2025-01-31 16:52:26 -05:00
e6dba9fdcf Updated documentation 2025-01-31 16:51:46 -05:00
30779a446b Updated documentation 2025-01-31 16:46:19 -05:00
f629a0f08f Added 'mustCompile' which panicks if there is an error compiling 2025-01-31 16:46:05 -05:00
6869cd00a2 Return error instead of nil when 'Find' fails 2025-01-31 10:52:38 -05:00
02bc8f30a2 Added test for 'Find' 2025-01-31 10:52:27 -05:00
ac05bceda3 Use method instead of function 2025-01-31 10:13:02 -05:00
037ac75ea6 Wrote new method to return 0-group of leftmost match; reorganized some functions for better clarity; made 'FindNthMatch' a method 2025-01-31 10:12:53 -05:00
e9d4e857cf Run 'TestFindAllStrings' since that function has been implemented 2025-01-31 10:11:52 -05:00
b685d2fd5f Renamed 'findAllMatchesHelper' to 'findAllSubmatchHelper' 2025-01-31 09:56:30 -05:00
8eda5055ff Replaced call to 'FindAllMatches' with call to 'FindAll' or 'FindAllSubmatch' depending on whether I need submatches 2025-01-31 09:55:36 -05:00
45b6566b2c Replaced function call with method call 2025-01-31 09:54:35 -05:00
e22822e619 Renamed 'FindAllMatches' to 'FindAll' and made it a method; made it return a slice of 0-groups; the functionality of 'FindAllMatches' is now in 'FindAllSubmatch' 2025-01-31 09:54:09 -05:00
692de2a32b Added lookarounds and numeric ranges to documentation 2025-01-31 09:26:21 -05:00
0d19664044 Cleared up some comments, wrote a skeleton for FindAllString 2025-01-30 22:57:35 -05:00
1bfb09b6c7 Made 'FindString' a method of 'Reg' 2025-01-30 22:51:31 -05:00
b0b8bf23af Updated documentation 2025-01-30 22:51:16 -05:00
11 changed files with 1148 additions and 522 deletions

View File

@@ -6,8 +6,8 @@ fmt:
vet: fmt
go vet ./...
buildLib: vet
go build -gcflags="-N -l" ./...
go build -gcflags="all=-N -l" ./...
buildCmd: buildLib
go build -C cmd/ -o re ./...
go build -C cmd/ -gcflags="all=-N -l" -o re ./...
test: buildCmd
go test -v ./...

View File

@@ -121,12 +121,12 @@ func main() {
}
matchIndices := make([]reg.Match, 0)
if matchNumFlagEnabled {
tmp, err := reg.FindNthMatch(regComp, test_str, *matchNum)
tmp, err := regComp.FindNthMatch(test_str, *matchNum)
if err == nil {
matchIndices = append(matchIndices, tmp)
}
} else {
matchIndices = reg.FindAllMatches(regComp, test_str)
matchIndices = regComp.FindAllSubmatch(test_str)
}
if *printMatchesFlag {
@@ -137,7 +137,7 @@ func main() {
fmt.Fprintf(out, "Line %d:\n", lineNum)
}
for _, m := range matchIndices {
fmt.Fprintf(out, "%s\n", m.ToString())
fmt.Fprintf(out, "%s\n", m.String())
}
err := out.Flush()
if err != nil {

View File

@@ -16,7 +16,6 @@ func (s *uniq_arr[T]) add(vals ...T) {
s.backingMap[item] = struct{}{}
}
}
return
}
func (s uniq_arr[T]) contains(val T) bool {

View File

@@ -12,10 +12,43 @@ var notDotChars []rune
// A Reg represents the result of compiling a regular expression. It contains
// the startState of the NFA representation of the regex, and the number of capturing
// groups in the regex.
// groups in the regex. It also contains the expression string.
type Reg struct {
start *nfaState
numGroups int
start *nfaState
numGroups int
str string
preferLongest bool
}
// NumSubexp returns the number of sub-expressions in the given [Reg]. This is equivalent
// to the number of capturing groups.
func (re Reg) NumSubexp() int {
return re.numGroups
}
// String returns the string used to compile the expression.
func (re Reg) String() string {
return re.str
}
// MarshalText implements [encoding.TextMarshaler]. The output is equivalent to that of [Reg.String].
// Any flags passed as arguments (including calling [Reg.Longest]) are lost.
func (re *Reg) MarshalText() ([]byte, error) {
return []byte(re.String()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler]. It calls [Reg.Compile] on the given byte-slice. If it returns successfully,
// then the result of the compilation is stored in re. The result of [Reg.Compile] is returned.
func (re *Reg) UnmarshalText(text []byte) error {
newReg, err := Compile(string(text))
if err == nil {
*re = newReg
}
return err
}
func (re *Reg) Longest() {
re.preferLongest = true
}
const concatRune rune = 0xF0001
@@ -810,13 +843,12 @@ func thompson(re []postfixNode) (Reg, error) {
// In these cases, we will return an NFA with 1 state, with an assertion that is always true.
if len(re) == 0 {
start := zeroLengthMatchState()
nfa = append(nfa, &start)
nfa = append(nfa, start)
}
for _, c := range re {
if c.nodetype == characterNode || c.nodetype == assertionNode {
stateToAdd := nfaState{}
stateToAdd.transitions = make(map[int][]*nfaState)
if c.allChars {
stateToAdd.allChars = true
if len(c.except) != 0 {
@@ -928,7 +960,6 @@ func thompson(re []postfixNode) (Reg, error) {
s.isEmpty = true
s.output = make([]*nfaState, 0)
s.output = append(s.output, s)
s.transitions = make(map[int][]*nfaState)
// LPAREN nodes are just added normally
if c.nodetype == lparenNode {
numGroups++
@@ -943,7 +974,9 @@ func thompson(re []postfixNode) (Reg, error) {
// and added back in.
// If the middle node doesn't exist (ie. something like '()' ), that's fine, I just connect the LPAREN
// and RPAREN nodes.
// If neither node exists, that's a problem so I return an error.
// If the middle node exists but is itself the start of a group, then that _must_ be the opening paren for
// the closing paren that I'm on. I put the third node back (because it isn't involved in the capturing group), then
// I concatenate those two and add them. If neither node exists, that's a problem so I return an error.
if c.nodetype == rparenNode {
s.groupEnd = true
middleNode, err1 := pop(&nfa)
@@ -958,6 +991,11 @@ func thompson(re []postfixNode) (Reg, error) {
s.groupNum = lparenNode.groupNum
to_add := concatenate(lparenNode, s)
nfa = append(nfa, to_add)
} else if middleNode.groupBegin && middleNode.numTransitions() == 0 { // The middle node is a lone lparen - something like '(())', and I'm looking at the first rparen
nfa = append(nfa, lparenNode) // I shouldn't have popped this out, because it is not involved in the current capturing group
s.groupNum = middleNode.groupNum // In this case, the 'middle' node is actually an lparen
to_add := concatenate(middleNode, s)
nfa = append(nfa, to_add)
} else {
// At this point, we assume all three nodes are valid ('lparenNode', 'middleNode' and 's')
if lparenNode.groupBegin {
@@ -976,7 +1014,8 @@ func thompson(re []postfixNode) (Reg, error) {
if c.nodetype == charclassNode { // A Character class consists of all the nodes in it, alternated
// Map the list of nodes to a list of states, each state containing the contents of a specific node
states := funcMap(c.nodeContents, func(node postfixNode) *nfaState {
s := newState()
s := &nfaState{}
s.output = append(s.output, s)
nodeContents := node.contents
if caseInsensitive {
nodeContents = slices.Concat(funcMap(nodeContents, func(r rune) []rune {
@@ -990,7 +1029,7 @@ func thompson(re []postfixNode) (Reg, error) {
return n.contents
})...)
}
return &s
return s
})
// Reduce the list of states down to a single state by alternating them
toAdd := funcReduce(states, func(s1 *nfaState, s2 *nfaState) *nfaState {
@@ -1017,14 +1056,14 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, fmt.Errorf("error applying kleene star")
}
stateToAdd, err := kleene(*s1)
stateToAdd, err := kleene(s1)
if err != nil {
return Reg{}, err
}
nfa = append(nfa, stateToAdd)
case plusNode: // a+ is equivalent to aa*
s1 := mustPop(&nfa)
s2, err := kleene(*s1)
s2, err := kleene(s1)
if err != nil {
return Reg{}, err
}
@@ -1035,7 +1074,10 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, fmt.Errorf("error applying question operator")
}
s2 := question(s1)
s2, err := question(s1)
if err != nil {
return Reg{}, err
}
nfa = append(nfa, s2)
case pipeNode:
// A pipe operator doesn't actually need either operand to be present. If an operand isn't present,
@@ -1046,21 +1088,21 @@ func thompson(re []postfixNode) (Reg, error) {
// '|a'
// '^a|'
// '^|a'
s1, err1 := pop(&nfa)
s2, err2 := pop(&nfa)
if err2 != nil || (s2.groupBegin && len(s2.transitions) == 0) { // Doesn't exist, or its just an LPAREN
s2, err1 := pop(&nfa)
s1, err2 := pop(&nfa)
if err2 != nil || (s2.groupBegin && s2.numTransitions() == 0) { // Doesn't exist, or its just an LPAREN
if err2 == nil { // Roundabout way of saying that this node existed, but it was an LPAREN, so we append it back
nfa = append(nfa, s2)
}
tmp := zeroLengthMatchState()
s2 = &tmp
s2 = tmp
}
if err1 != nil || (s1.groupBegin && len(s1.transitions) == 0) { // Doesn't exist, or its just an LPAREN
if err1 != nil || (s1.groupBegin && s1.numTransitions() == 0) { // Doesn't exist, or its just an LPAREN
if err1 == nil { // See above for explanation
nfa = append(nfa, s1)
}
tmp := zeroLengthMatchState()
s1 = &tmp
s1 = tmp
}
s3 := alternate(s1, s2)
nfa = append(nfa, s3)
@@ -1087,14 +1129,18 @@ func thompson(re []postfixNode) (Reg, error) {
stateToAdd = concatenate(stateToAdd, cloneState(poppedState))
}
if c.endReps == infinite_reps { // Case 3
s2, err := kleene(*poppedState)
s2, err := kleene(poppedState)
if err != nil {
return Reg{}, err
}
stateToAdd = concatenate(stateToAdd, s2)
} else { // Case 2
for i := c.startReps; i < c.endReps; i++ {
stateToAdd = concatenate(stateToAdd, question(cloneState(poppedState)))
tmp, err := question(cloneState(poppedState))
if err != nil {
return Reg{}, fmt.Errorf("error processing bounded repetition")
}
stateToAdd = concatenate(stateToAdd, tmp)
}
}
nfa = append(nfa, stateToAdd)
@@ -1104,16 +1150,21 @@ func thompson(re []postfixNode) (Reg, error) {
return Reg{}, fmt.Errorf("invalid regex")
}
verifyLastStates(nfa)
lastState := newState()
lastState.isLast = true
return Reg{nfa[0], numGroups}, nil
concatenate(nfa[0], &lastState)
// The string is empty here, because we add it in Compile()
return Reg{nfa[0], numGroups, "", false}, nil
}
// Compiles the given regular expression into a Reg type, suitable for use with the
// matching functions. The second return value is non-nil if a compilation error has
// occured. As such, the error value must be checked before using the Reg returned by this function.
// The second parameter is an optional list of flags, passed to the parsing function shuntingYard.
// Compile compiles the given regular expression into a [Reg].
//
// An error value != nil indicates that the regex was invalid; the error message should provide
// detailed information on the nature of the error.
// The second parameter is a sequence of zero or more [ReFlag] values, that modify the behavior of the regex.
func Compile(re string, flags ...ReFlag) (Reg, error) {
nodes, err := shuntingYard(re, flags...)
if err != nil {
@@ -1123,5 +1174,15 @@ func Compile(re string, flags ...ReFlag) (Reg, error) {
if err != nil {
return Reg{}, fmt.Errorf("error compiling regex: %w", err)
}
reg.str = re
return reg, nil
}
// MustCompile panics if Compile returns an error. They are identical in all other respects.
func MustCompile(re string, flags ...ReFlag) Reg {
reg, err := Compile(re, flags...)
if err != nil {
panic(err)
}
return reg
}

View File

@@ -4,6 +4,8 @@ Package regex implements regular expression search, using a custom non-bracktrac
The engine relies completely on UTF-8 codepoints. As such, it is capable of matching characters
from other languages, emojis and symbols.
The API and regex syntax are largely compatible with that of the stdlib's [regexp], with a few key differences (see 'Key Differences with regexp').
The full syntax is specified below.
# Syntax
@@ -31,7 +33,7 @@ Perl classes:
\d Match any digit character ([0-9])
\D Match any non-digit character ([^0-9])
\w Match any word character ([a-zA-Z0-9_])
\W Match any word character ([^a-zA-Z0-9_])
\W Match any non-word character ([^a-zA-Z0-9_])
\s Match any whitespace character ([ \t\n])
\S Match any non-whitespace character ([^ \t\n])
@@ -55,8 +57,8 @@ POSIX classes (inside normal character classes):
Composition:
def Match d, followed by e, followed by f
x|y Match x or y (prefer longer one)
xy|z Match xy or z
x|y Match x or y (prefer x)
xy|z Match xy or z (prefer xy)
Repitition (always greedy, preferring more):
@@ -84,9 +86,77 @@ Assertions:
\b Match at a word boundary (a word character followed by a non-word character, or vice-versa)
\B Match at a non-word boundary (a word character followed by a word character, or vice-versa)
# Flags
Lookarounds:
Flags are used to change the behavior of the engine. None of them are enabled by default. They are passed as an [ReFlag] slice to [Compile].
The list of flags, and their purpose, is provided in the type definition.
x(?=y) Positive lookahead - Match x if followed by y
x(?!y) Negative lookahead - Match x if NOT followed by y
(?<=x)y Positive lookbehind - Match y if preceded by x
(?<!x)y Negative lookbehind - Match y if NOT preceded by x
Numeric ranges:
<x-y> Match any number from x to y (inclusive) (x and y must be positive numbers)
\<x Match a literal '<' followed by x
# Key Differences with regexp
The engine and the API differ from [regexp] in a few ways, some of them very subtle.
The key differences are mentioned below.
1. Greediness:
This engine currently does not support non-greedy operators.
2. Byte-slices and runes:
My engine does not support byte-slices. When a matching function receives a string, it converts it into a
rune-slice to iterate through it. While this has some space overhead, the convenience of built-in unicode
support made the tradeoff worth it.
3. Return values
Rather than using primitives for return values, my engine defines two types that are used as return
values: a [Group] represents a capturing group, and a [Match] represents a list of groups.
[regexp] specifies a regular expression that gives a list of all the matching functions that it supports. The
equivalent expression for this engine is shown below. Note that 'Index' is the default.
Find(All)?(String)?(Submatch)?
[Reg.Find] returns the index of the leftmost match in the string.
If a function contains 'All' it returns all matches instead of just the leftmost one.
If a function contains 'String' it returns the matched text, rather than the index in the string.
If a function contains 'Submatch' it returns the match, including all submatches found by
capturing groups.
The term '0-group' is used to refer to the 0th capturing group of a match (which is the entire match).
Given the following regex:
x(y)
and the input string:
xyz
The 0th group would contain 'xy' and the 1st group would contain 'y'. Any matching function without 'Submatch' in its name
returns the 0-group.
# Feature Differences
The following features from [regexp] are (currently) NOT supported:
1. Named capturing groups
2. Non-greedy operators
3. Unicode character classes
4. Embedded flags (flags are instead passed as arguments to [Compile])
5. Literal text with \Q ... \E
The following features are not available in [regexp], but are supported in my engine:
1. Lookarounds
2. Numeric ranges
I hope to shorten the first list, and expand the second.
*/
package regex

181
regex/example_test.go Normal file
View File

@@ -0,0 +1,181 @@
package regex_test
import (
"fmt"
"strings"
"gitea.twomorecents.org/Rockingcool/kleingrep/regex"
)
func ExampleReg_Find() {
regexStr := "b|a"
regexComp := regex.MustCompile(regexStr)
match, _ := regexComp.Find("banana")
fmt.Println(match.String())
// Output: 0 1
}
func ExampleReg_FindAll() {
regexStr := "b|a"
regexComp := regex.MustCompile(regexStr)
matches := regexComp.FindAll("banana")
for _, group := range matches {
fmt.Println(group.String())
}
// Output: 0 1
// 1 2
// 3 4
// 5 6
}
func ExampleReg_FindString() {
regexStr := `\w+\s+(?=sheep)`
regexComp := regex.MustCompile(regexStr)
matchStr := regexComp.FindString("pink cows and yellow sheep")
fmt.Println(matchStr)
// Output: yellow
}
func ExampleReg_FindSubmatch() {
regexStr := `(\d)\.(\d)(\d)`
regexComp := regex.MustCompile(regexStr)
match, _ := regexComp.FindSubmatch("3.14")
fmt.Println(match[0])
fmt.Println(match[1])
fmt.Println(match[2])
// Output: 0 4
// 0 1
// 2 3
}
func ExampleReg_FindStringSubmatch() {
regexStr := `(\d{4})-(\d{2})-(\d{2})`
regexComp := regex.MustCompile(regexStr)
inputStr := `The date is 2025-02-10`
match := regexComp.FindStringSubmatch(inputStr)
fmt.Println(match[1])
fmt.Println(match[3])
// Output: 2025
// 10
}
func ExampleReg_FindAllSubmatch() {
regexStr := `(\d)\.(\d)(\d)`
regexComp := regex.MustCompile(regexStr)
matches := regexComp.FindAllSubmatch("3.14+8.97")
fmt.Println(matches[0][0]) // 0-group (entire match) of 1st match (0-indexed)
fmt.Println(matches[0][1]) // 1st group of 1st match
fmt.Println(matches[1][0]) // 0-group of 2nd match
fmt.Println(matches[1][1]) // 1st group of 2nd math
// Output: 0 4
// 0 1
// 5 9
// 5 6
}
func ExampleReg_FindAllString() {
regexStr := `<0-255>\.<0-255>\.<0-255>\.<0-255>`
inputStr := `192.168.220.7 pings 9.9.9.9`
regexComp := regex.MustCompile(regexStr)
matchStrs := regexComp.FindAllString(inputStr)
fmt.Println(matchStrs[0])
fmt.Println(matchStrs[1])
// Output: 192.168.220.7
// 9.9.9.9
}
func ExampleReg_FindAllStringSubmatch() {
// 'https' ...
// followed by 1 or more alphanumeric characters (including period) ...
// then a forward slash ...
// followed by one more of :
// word character,
// question mark,
// period,
// equals sign
regexStr := `https://([a-z0-9\.]+)/([\w.?=]+)`
regexComp := regex.MustCompile(regexStr, regex.RE_CASE_INSENSITIVE)
inputStr := `You can find me at https://twomorecents.org/index.html and https://news.ycombinator.com/user?id=aadhavans`
matchIndices := regexComp.FindAllStringSubmatch(inputStr)
fmt.Println(matchIndices[0][1]) // 1st group of 1st match (0-indexed)
fmt.Println(matchIndices[0][2]) // 2nd group of 1st match
fmt.Println(matchIndices[1][1]) // 1st group of 2nd match
fmt.Println(matchIndices[1][2]) // 2nd group of 2nd match
// Output: twomorecents.org
// index.html
// news.ycombinator.com
// user?id=aadhavans
}
func ExampleReg_Expand() {
inputStr := `option1: value1
option2: value2`
regexStr := `(\w+): (\w+)`
templateStr := "$1 = $2\n"
regexComp := regex.MustCompile(regexStr, regex.RE_MULTILINE)
result := ""
for _, submatches := range regexComp.FindAllSubmatch(inputStr) {
result = regexComp.Expand(result, templateStr, inputStr, submatches)
}
fmt.Println(result)
// Output: option1 = value1
// option2 = value2
}
func ExampleReg_LiteralPrefix() {
regexStr := `a(b|c)d*`
regexComp := regex.MustCompile(regexStr)
prefix, complete := regexComp.LiteralPrefix()
fmt.Println(prefix)
fmt.Println(complete)
// Output: a
// false
}
func ExampleReg_Longest() {
regexStr := `x|xx`
inputStr := "xx"
regexComp := regex.MustCompile(regexStr)
fmt.Println(regexComp.FindString(inputStr))
regexComp.Longest()
fmt.Println(regexComp.FindString(inputStr))
// Output: x
// xx
}
func ExampleReg_ReplaceAll() {
regexStr := `(\d)(\w)`
inputStr := "5d9t"
regexComp := regex.MustCompile(regexStr)
fmt.Println(regexComp.ReplaceAll(inputStr, `$2$1`))
// Output: d5t9
}
func ExampleReg_ReplaceAllLiteral() {
regexStr := `fox|dog`
inputStr := "the quick brown fox jumped over the lazy dog"
regexComp := regex.MustCompile(regexStr)
fmt.Println(regexComp.ReplaceAllLiteral(inputStr, `duck`))
// Output: the quick brown duck jumped over the lazy duck
}
func ExampleReg_ReplaceAllFunc() {
regexStr := `\w{5,}`
inputStr := `all five or more letter words in this string are capitalized`
regexComp := regex.MustCompile(regexStr)
fmt.Println(regexComp.ReplaceAllFunc(inputStr, strings.ToUpper))
// Output: all five or more LETTER WORDS in this STRING are CAPITALIZED
}

View File

@@ -2,13 +2,20 @@ package regex
import (
"fmt"
"sort"
"strconv"
"unicode"
)
// a Match stores a slice of all the capturing groups in a match.
// A Match represents a match found by the regex in a given string.
// It is represented as a list of groups, where the nth element contains
// the contents of the nth capturing group. Note that the group may not be valid
// (see [Group.IsValid]). The element at index 0 is known
// as the 0-group, and represents the contents of the entire match.
//
// See [Reg.FindSubmatch] for an example.
type Match []Group
// a Group represents a group. It contains the start index and end index of the match
// a Group represents a capturing group. It contains the start and index of the group.
type Group struct {
StartIdx int
EndIdx int
@@ -23,151 +30,153 @@ func newMatch(size int) Match {
return toRet
}
// Returns the number of valid groups in the match
func (m Match) numValidGroups() int {
numValid := 0
for _, g := range m {
if g.StartIdx >= 0 && g.EndIdx >= 0 {
numValid++
}
}
return numValid
}
// Returns a string containing the indices of all (valid) groups in the match
func (m Match) ToString() string {
func (m Match) String() string {
var toRet string
for i, g := range m {
if g.isValid() {
if g.IsValid() {
toRet += fmt.Sprintf("Group %d\n", i)
toRet += g.toString()
toRet += g.String()
toRet += "\n"
}
}
return toRet
}
// Converts the Group into a string representation:
func (idx Group) toString() string {
// String converts the Group into a string representation.
func (idx Group) String() string {
return fmt.Sprintf("%d\t%d", idx.StartIdx, idx.EndIdx)
}
// Returns whether a group contains valid indices
func (g Group) isValid() bool {
// IsValid returns whether a group is valid (ie. whether it matched any text). It
// simply ensures that both indices of the group are >= 0.
func (g Group) IsValid() bool {
return g.StartIdx >= 0 && g.EndIdx >= 0
}
// takeZeroState takes the 0-state (if such a transition exists) for all states in the
// given slice. It returns the resulting states. If any of the resulting states is a 0-state,
// the second ret val is true.
// If a state begins or ends a capturing group, its 'thread' is updated to contain the correct index.
func takeZeroState(states []*nfaState, numGroups int, idx int) (rtv []*nfaState, isZero bool) {
for _, state := range states {
if len(state.transitions[epsilon]) > 0 {
for _, s := range state.transitions[epsilon] {
if s.threadGroups == nil {
s.threadGroups = newMatch(numGroups + 1)
}
copy(s.threadGroups, state.threadGroups)
if s.groupBegin {
s.threadGroups[s.groupNum].StartIdx = idx
// openParenGroups = append(openParenGroups, s.groupNum)
}
if s.groupEnd {
s.threadGroups[s.groupNum].EndIdx = idx
// closeParenGroups = append(closeParenGroups, s.groupNum)
}
}
rtv = append(rtv, state.transitions[epsilon]...)
}
}
for _, state := range rtv {
if len(state.transitions[epsilon]) > 0 {
return rtv, true
}
}
return rtv, false
// Simple function, makes it easier to map this over a list of matches
func getZeroGroup(m Match) Group {
return m[0]
}
// zeroMatchPossible returns true if a zero-length match is possible
// from any of the given states, given the string and our position in it.
// It uses the same algorithm to find zero-states as the one inside the loop,
// so I should probably put it in a function.
func zeroMatchPossible(str []rune, idx int, numGroups int, states ...*nfaState) bool {
zeroStates, isZero := takeZeroState(states, numGroups, idx)
tempstates := make([]*nfaState, 0, len(zeroStates)+len(states))
tempstates = append(tempstates, states...)
tempstates = append(tempstates, zeroStates...)
num_appended := 0 // number of unique states addded to tempstates
for isZero == true {
zeroStates, isZero = takeZeroState(tempstates, numGroups, idx)
tempstates, num_appended = unique_append(tempstates, zeroStates...)
if num_appended == 0 { // break if we haven't appended any more unique values
break
}
}
for _, state := range tempstates {
if state.isEmpty && (state.assert == noneAssert || state.checkAssertion(str, idx)) && state.isLast {
return true
}
}
return false
func copyThread(to *nfaState, from nfaState) {
to.threadGroups = append([]Group{}, from.threadGroups...)
}
// Prunes the slice by removing overlapping indices.
func pruneIndices(indices []Match) []Match {
// First, sort the slice by the start indices
sort.Slice(indices, func(i, j int) bool {
return indices[i][0].StartIdx < indices[j][0].StartIdx
})
toRet := make([]Match, 0, len(indices))
current := indices[0]
for _, idx := range indices[1:] {
// idx doesn't overlap with current (starts after current ends), so add current to result
// and update the current.
if idx[0].StartIdx >= current[0].EndIdx {
toRet = append(toRet, current)
current = idx
} else if idx[0].EndIdx > current[0].EndIdx {
// idx overlaps, but it is longer, so update current
current = idx
}
// Find returns the 0-group of the leftmost match of the regex in the given string.
// An error value != nil indicates that no match was found.
func (re Reg) Find(str string) (Group, error) {
match, err := re.FindNthMatch(str, 1)
if err != nil {
return Group{}, fmt.Errorf("no matches found")
}
// Add last state
toRet = append(toRet, current)
return toRet
return getZeroGroup(match), nil
}
// FindString returns a _string_ containing the _text_ of the _leftmost_ match of
// the regex, in the given string. The return value will be an empty string in two situations:
// Match returns a boolean value, indicating whether the regex found a match in the given string.
func (re Reg) Match(str string) bool {
_, err := re.Find(str)
return err == nil
}
// CompileMatch compiles expr and returns true if str contains a match of the expression.
// It is equivalent to [regexp.Match].
// An optional list of flags may be provided (see [ReFlag]).
// It returns an error (!= nil) if there was an error compiling the expression.
func CompileMatch(expr string, str string, flags ...ReFlag) (bool, error) {
re, err := Compile(expr, flags...)
if err != nil {
return false, err
}
return re.Match(str), nil
}
// FindAll returns a slice containing all the 0-groups of the regex in the given string.
// A 0-group represents the match without any submatches.
func (re Reg) FindAll(str string) []Group {
indices := re.FindAllSubmatch(str)
zeroGroups := funcMap(indices, getZeroGroup)
return zeroGroups
}
// FindString returns the text of the leftmost match of the regex in the given string.
// The return value will be an empty string in two situations:
// 1. No match was found
// 2. The match was an empty string
func FindString(regex Reg, str string) string {
match, err := FindNthMatch(regex, str, 1)
func (re Reg) FindString(str string) string {
match, err := re.FindNthMatch(str, 1)
if err != nil {
return ""
}
return str[match[0].StartIdx:match[0].EndIdx]
zeroGroup := getZeroGroup(match)
return str[zeroGroup.StartIdx:zeroGroup.EndIdx]
}
// FindAllString is the 'all' version of FindString.
// It returns a _slice of strings_ containing the _text_ of _all_ matches of
// the regex, in the given string.
//func FindAllString(regex Reg, str []string) []string {
//
//}
// FindSubmatch returns the leftmost match of the regex in the given string, including
// the submatches matched by capturing groups. The returned [Match] will always contain the same
// number of groups. The validity of a group (whether or not it matched anything) can be determined with
// [Group.IsValid], or by checking that both indices of the group are >= 0.
// The second-return value is nil if no match was found.
func (re Reg) FindSubmatch(str string) (Match, error) {
match, err := re.FindNthMatch(str, 1)
if err != nil {
return Match{}, fmt.Errorf("no match found")
} else {
return match, nil
}
}
// FindNthMatch finds the 'n'th match of the regex represented by the given start-state, with
// the given string.
// FindStringSubmatch is the 'string' version of [FindSubmatch]. It returns a slice of strings,
// where the string at index i contains the text matched by the i-th capturing group.
// The 0-th index represents the entire match.
// An empty string at index n could mean:
// ,
// 1. Group n did not find a match
// 2. Group n found a zero-length match
//
// A return value of nil indicates no match.
func (re Reg) FindStringSubmatch(str string) []string {
matchStr := make([]string, re.numGroups+1)
match, err := re.FindSubmatch(str)
if err != nil {
return nil
}
nonEmptyMatchFound := false
for i := range match {
if match[i].IsValid() {
matchStr[i] = str[match[i].StartIdx:match[i].EndIdx]
nonEmptyMatchFound = true
} else {
matchStr[i] = ""
}
}
if nonEmptyMatchFound == false {
return nil
}
return matchStr
}
// FindAllString is the 'all' version of [FindString].
// It returns a slice of strings containing the text of all matches of
// the regex in the given string.
func (re Reg) FindAllString(str string) []string {
zerogroups := re.FindAll(str)
matchStrs := funcMap(zerogroups, func(g Group) string {
return str[g.StartIdx:g.EndIdx]
})
return matchStrs
}
// FindNthMatch return the 'n'th match of the regex in the given string.
// It returns an error (!= nil) if there are fewer than 'n' matches in the string.
func FindNthMatch(regex Reg, str string, n int) (Match, error) {
func (re Reg) FindNthMatch(str string, n int) (Match, error) {
idx := 0
matchNum := 0
str_runes := []rune(str)
var matchFound bool
var matchIdx Match
for idx <= len(str_runes) {
matchFound, matchIdx, idx = findAllMatchesHelper(regex.start, str_runes, idx, regex.numGroups)
matchFound, matchIdx, idx = findAllSubmatchHelper(re.start, str_runes, idx, re.numGroups, re.preferLongest)
if matchFound {
matchNum++
}
@@ -179,237 +188,273 @@ func FindNthMatch(regex Reg, str string, n int) (Match, error) {
return nil, fmt.Errorf("invalid match index - too few matches found")
}
// FindAllMatches tries to find all matches of the regex represented by given start-state, with
// the given string
func FindAllMatches(regex Reg, str string) []Match {
// FindAllSubmatch returns a slice of matches in the given string.
func (re Reg) FindAllSubmatch(str string) []Match {
idx := 0
str_runes := []rune(str)
var matchFound bool
var matchIdx Match
indices := make([]Match, 0)
for idx <= len(str_runes) {
matchFound, matchIdx, idx = findAllMatchesHelper(regex.start, str_runes, idx, regex.numGroups)
matchFound, matchIdx, idx = findAllSubmatchHelper(re.start, str_runes, idx, re.numGroups, re.preferLongest)
if matchFound {
indices = append(indices, matchIdx)
}
}
if len(indices) > 0 {
return pruneIndices(indices)
}
return indices
}
// FindAllSubmatch returns a double-slice of strings. Each slice contains the text of a match, including all submatches.
// A return value of nil indicates no match.
func (re Reg) FindAllStringSubmatch(str string) [][]string {
match := re.FindAllSubmatch(str)
if len(match) == 0 {
return nil
}
rtv := make([][]string, len(match))
for i := range rtv {
rtv[i] = make([]string, re.numGroups+1)
}
rtv = funcMap(match, func(m Match) []string {
return funcMap(m, func(g Group) string {
if g.IsValid() {
return str[g.StartIdx:g.EndIdx]
} else {
return ""
}
})
})
return rtv
}
func addStateToList(str []rune, idx int, list []nfaState, state nfaState, threadGroups []Group, visited []nfaState, preferLongest bool) []nfaState {
if stateExists(list, state) || stateExists(visited, state) {
return list
}
visited = append(visited, state)
if state.isKleene || state.isQuestion {
copyThread(state.splitState, state)
list = addStateToList(str, idx, list, *state.splitState, threadGroups, visited, preferLongest)
copyThread(state.next, state)
list = addStateToList(str, idx, list, *state.next, threadGroups, visited, preferLongest)
return list
}
if state.isAlternation {
copyThread(state.next, state)
list = addStateToList(str, idx, list, *state.next, threadGroups, visited, preferLongest)
copyThread(state.splitState, state)
list = addStateToList(str, idx, list, *state.splitState, threadGroups, visited, preferLongest)
return list
}
state.threadGroups = append([]Group{}, threadGroups...)
if state.assert != noneAssert {
if state.checkAssertion(str, idx, preferLongest) {
copyThread(state.next, state)
return addStateToList(str, idx, list, *state.next, state.threadGroups, visited, preferLongest)
}
}
if state.groupBegin {
state.threadGroups[state.groupNum].StartIdx = idx
return addStateToList(str, idx, list, *state.next, state.threadGroups, visited, preferLongest)
}
if state.groupEnd {
state.threadGroups[state.groupNum].EndIdx = idx
return addStateToList(str, idx, list, *state.next, state.threadGroups, visited, preferLongest)
}
return append(list, state)
}
// Helper for FindAllMatches. Returns whether it found a match, the
// first Match it finds, and how far it got into the string ie. where
// the next search should start from.
//
// Might return duplicates or overlapping indices, so care must be taken to prune the resulting array.
func findAllMatchesHelper(start *nfaState, str []rune, offset int, numGroups int) (bool, Match, int) {
func findAllSubmatchHelper(start *nfaState, str []rune, offset int, numGroups int, preferLongest bool) (bool, Match, int) {
// Base case - exit if offset exceeds string's length
if offset > len(str) {
// The second value here shouldn't be used, because we should exit when the third return value is > than len(str)
return false, []Group{}, offset
}
resetThreads(start)
// Hold a list of match indices for the current run. When we
// can no longer find a match, the match with the largest range is
// chosen as the match for the entire string.
// This allows us to pick the longest possible match (which is how greedy matching works).
// COMMENT ABOVE IS CURRENTLY NOT UP-TO-DATE
tempIndices := newMatch(numGroups + 1)
foundPath := false
startIdx := offset
endIdx := offset
currentStates := make([]*nfaState, 0)
tempStates := make([]*nfaState, 0) // Used to store states that should be used in next loop iteration
i := offset // Index in string
startingFrom := i // Store starting index
currentStates := make([]nfaState, 0)
nextStates := make([]nfaState, 0)
i := offset // Index in string
// If the first state is an assertion, makes sure the assertion
// is true before we do _anything_ else.
if start.assert != noneAssert {
if start.checkAssertion(str, offset) == false {
if start.checkAssertion(str, offset, preferLongest) == false {
i++
return false, []Group{}, i
}
}
// Increment until we hit a character matching the start state (assuming not 0-state)
if start.isEmpty == false {
for i < len(str) && !start.contentContains(str, i) {
i++
}
startIdx = i
startingFrom = i
i++ // Advance to next character (if we aren't at a 0-state, which doesn't match anything), so that we can check for transitions. If we advance at a 0-state, we will never get a chance to match the first character
}
start.threadGroups = newMatch(numGroups + 1)
// Check if the start state begins a group - if so, add the start index to our list
if start.groupBegin {
start.threadGroups[start.groupNum].StartIdx = i
// tempIndices[start.groupNum].startIdx = i
}
currentStates = append(currentStates, start)
// Main loop
for i < len(str) {
foundPath = false
zeroStates := make([]*nfaState, 0)
// Keep taking zero-states, until there are no more left to take
// Objective: If any of our current states have transitions to 0-states, replace them with the 0-state. Do this until there are no more transitions to 0-states, or there are no more unique 0-states to take.
zeroStates, isZero := takeZeroState(currentStates, numGroups, i)
tempStates = append(tempStates, zeroStates...)
num_appended := 0
for isZero == true {
zeroStates, isZero = takeZeroState(tempStates, numGroups, i)
tempStates, num_appended = unique_append(tempStates, zeroStates...)
if num_appended == 0 { // Break if we haven't appended any more unique values
break
}
}
currentStates, _ = unique_append(currentStates, tempStates...)
tempStates = nil
// Take any transitions corresponding to current character
numStatesMatched := 0 // The number of states which had at least 1 match for this round
assertionFailed := false // Whether or not an assertion failed for this round
lastStateInList := false // Whether or not a last state was in our list of states
var lastStatePtr *nfaState = nil // Pointer to the last-state, if it was found
lastLookaroundInList := false // Whether or not a last state (that is a lookaround) was in our list of states
for _, state := range currentStates {
matches, numMatches := state.matchesFor(str, i)
if numMatches > 0 {
numStatesMatched++
tempStates = append(tempStates, matches...)
foundPath = true
for _, m := range matches {
if m.threadGroups == nil {
m.threadGroups = newMatch(numGroups + 1)
}
copy(m.threadGroups, state.threadGroups)
}
}
if numMatches < 0 {
assertionFailed = true
}
if state.isLast {
if state.isLookaround() {
lastLookaroundInList = true
}
lastStateInList = true
lastStatePtr = state
}
}
if assertionFailed && numStatesMatched == 0 { // Nothing has matched and an assertion has failed
// If I'm being completely honest, I'm not sure why I have to check specifically for a _lookaround_
// state. The explanation below is my attempt to explain this behavior.
// If you replace 'lastLookaroundInList' with 'lastStateInList', one of the test cases fails.
//
// One of the states in our list was a last state and a lookaround. In this case, we
// don't abort upon failure of the assertion, because we have found
// another path to a final state.
// Even if the last state _was_ an assertion, we can use the previously
// saved indices to find a match.
if lastLookaroundInList {
break
} else {
if i == startingFrom {
i++
}
return false, []Group{}, i
}
}
// Check if we can find a state in our list that is:
// a. A last-state
// b. Empty
// c. Doesn't assert anything
for _, s := range currentStates {
if s.isLast && s.isEmpty && s.assert == noneAssert {
lastStatePtr = s
lastStateInList = true
}
}
if lastStateInList { // A last-state was in the list of states. add the matchIndex to our MatchIndex list
for j := 1; j < numGroups+1; j++ {
tempIndices[j] = lastStatePtr.threadGroups[j]
}
endIdx = i
tempIndices[0] = Group{startIdx, endIdx}
}
// Check if we can find a zero-length match
if foundPath == false {
if ok := zeroMatchPossible(str, i, numGroups, currentStates...); ok {
if tempIndices[0].isValid() == false {
tempIndices[0] = Group{startIdx, startIdx}
}
}
// If we haven't moved in the string, increment the counter by 1
// to ensure we don't keep trying the same string over and over.
// if i == startingFrom {
startIdx++
// i++
// }
if tempIndices.numValidGroups() > 0 && tempIndices[0].isValid() {
if tempIndices[0].StartIdx == tempIndices[0].EndIdx { // If we have a zero-length match, we have to shift the index at which we start. Otherwise we keep looking at the same paert of the string over and over.
return true, tempIndices, tempIndices[0].EndIdx + 1
} else {
return true, tempIndices, tempIndices[0].EndIdx
}
}
return false, []Group{}, startIdx
}
currentStates = make([]*nfaState, len(tempStates))
copy(currentStates, tempStates)
tempStates = nil
i++
}
// End-of-string reached. Go to any 0-states, until there are no more 0-states to go to. Then check if any of our states are in the end position.
// This is the exact same algorithm used inside the loop, so I should probably put it in a function.
zeroStates, isZero := takeZeroState(currentStates, numGroups, i)
tempStates = append(tempStates, zeroStates...)
num_appended := 0 // Number of unique states addded to tempStates
for isZero == true {
zeroStates, isZero = takeZeroState(tempStates, numGroups, i)
tempStates, num_appended = unique_append(tempStates, zeroStates...)
if num_appended == 0 { // Break if we haven't appended any more unique values
start.threadGroups[0].StartIdx = i
currentStates = addStateToList(str, i, currentStates, *start, start.threadGroups, nil, preferLongest)
var match Match = nil
for idx := i; idx <= len(str); idx++ {
if len(currentStates) == 0 {
break
}
}
for currentStateIdx := 0; currentStateIdx < len(currentStates); currentStateIdx++ {
currentState := currentStates[currentStateIdx]
currentStates = append(currentStates, tempStates...)
tempStates = nil
if currentState.threadGroups == nil {
currentState.threadGroups = newMatch(numGroups + 1)
currentState.threadGroups[0].StartIdx = idx
}
for _, state := range currentStates {
// Only add the match if the start index is in bounds. If the state has an assertion,
// make sure the assertion checks out.
if state.isLast && i <= len(str) {
if state.assert == noneAssert || state.checkAssertion(str, i) {
for j := 1; j < numGroups+1; j++ {
tempIndices[j] = state.threadGroups[j]
if currentState.isLast {
currentState.threadGroups[0].EndIdx = idx
match = append([]Group{}, currentState.threadGroups...)
if !preferLongest {
break
}
} else if !currentState.isAlternation && !currentState.isKleene && !currentState.isQuestion && !currentState.groupBegin && !currentState.groupEnd && currentState.assert == noneAssert { // Normal character
if currentState.contentContains(str, idx, preferLongest) {
nextStates = addStateToList(str, idx+1, nextStates, *currentState.next, currentState.threadGroups, nil, preferLongest)
}
endIdx = i
tempIndices[0] = Group{startIdx, endIdx}
}
}
currentStates = append([]nfaState{}, nextStates...)
nextStates = nil
}
if match != nil {
if offset == match[0].EndIdx {
return true, match, match[0].EndIdx + 1
}
return true, match, match[0].EndIdx
}
return false, []Group{}, i + 1
}
if tempIndices.numValidGroups() > 0 {
if tempIndices[0].StartIdx == tempIndices[0].EndIdx { // If we have a zero-length match, we have to shift the index at which we start. Otherwise we keep looking at the same paert of the string over and over.
return true, tempIndices, tempIndices[0].EndIdx + 1
// Expand appends template to dst, expanding any variables in template to the relevant capturing group.
//
// A variable is of the form '$n', where 'n' is a number. It will be replaced by the contents of the n-th capturing group.
// To insert a literal $, do not put a number after it. Alternatively, you can use $$.
// src is the input string, and match must be the result of [Reg.FindSubmatch].
func (re Reg) Expand(dst string, template string, src string, match Match) string {
templateRuneSlc := []rune(template)
srcRuneSlc := []rune(src)
i := 0
for i < len(templateRuneSlc) {
c := templateRuneSlc[i]
if c == '$' {
i += 1
// The dollar sign is the last character of the string, or it is proceeded by another dollar sign
if i >= len(templateRuneSlc) || templateRuneSlc[i] == '$' {
dst += "$"
i++
} else {
numStr := ""
for i < len(templateRuneSlc) && unicode.IsDigit(templateRuneSlc[i]) {
numStr += string(templateRuneSlc[i])
i++
}
if numStr == "" {
dst += "$"
} else {
num, _ := strconv.Atoi(numStr)
if num < len(match) {
dst += string(srcRuneSlc[match[num].StartIdx:match[num].EndIdx])
} else {
dst += "$" + numStr
}
}
}
} else {
return true, tempIndices, tempIndices[0].EndIdx
dst += string(c)
i++
}
}
if startIdx == startingFrom { // Increment starting index if we haven't moved in the string. Prevents us from matching the same part of the string over and over.
startIdx++
}
return false, []Group{}, startIdx
return dst
}
// LiteralPrefix returns a string that must begin any match of the given regular expression.
// The second return value is true if the string comprises the entire expression.
func (re Reg) LiteralPrefix() (prefix string, complete bool) {
state := re.start
if state.assert != noneAssert {
state = state.next
}
for !(state.isLast) && (!state.isAlternation) && len(state.content) == 1 && state.assert == noneAssert {
if state.groupBegin || state.groupEnd {
state = state.next
continue
}
prefix += string(rune(state.content[0]))
state = state.next
}
if state.isLast {
complete = true
} else {
complete = false
}
return prefix, complete
}
// ReplaceAll replaces all matches of the expression in src, with the text in repl. In repl, variables are interpreted
// as they are in [Reg.Expand]. The resulting string is returned.
func (re Reg) ReplaceAll(src string, repl string) string {
matches := re.FindAllSubmatch(src)
i := 0
currentMatch := 0
dst := ""
for i < len(src) {
if currentMatch < len(matches) && matches[currentMatch][0].IsValid() && i == matches[currentMatch][0].StartIdx {
dst += re.Expand("", repl, src, matches[currentMatch])
i = matches[currentMatch][0].EndIdx
currentMatch++
} else {
dst += string(src[i])
i++
}
}
return dst
}
// ReplaceAllLiteral replaces all matches of the expression in src, with the text in repl. The text is replaced directly,
// without any expansion.
func (re Reg) ReplaceAllLiteral(src string, repl string) string {
zerogroups := re.FindAll(src)
currentMatch := 0
i := 0
dst := ""
for i < len(src) {
if currentMatch < len(zerogroups) && i == zerogroups[currentMatch].StartIdx {
dst += repl
i = zerogroups[currentMatch].EndIdx
currentMatch += 1
} else {
dst += string(src[i])
i++
}
}
return dst
}
// ReplaceAllFunc replaces every match of the expression in src, with the return value of the function replFunc.
// replFunc takes in the matched string. The return value is substituted in directly without expasion.
func (re Reg) ReplaceAllFunc(src string, replFunc func(string) string) string {
zerogroups := re.FindAll(src)
currentMatch := 0
i := 0
dst := ""
for i < len(src) {
if currentMatch < len(zerogroups) && i == zerogroups[currentMatch].StartIdx {
dst += replFunc(src[zerogroups[currentMatch].StartIdx:zerogroups[currentMatch].EndIdx])
i = zerogroups[currentMatch].EndIdx
currentMatch += 1
} else {
dst += string(src[i])
i++
}
}
return dst
}

View File

@@ -48,30 +48,6 @@ func isNormalChar(c rune) bool {
return !slices.Contains(specialChars, c)
}
// Ensure that the given elements are only appended to the given slice if they
// don't already exist. Returns the new slice, and the number of unique items appended.
func unique_append[T comparable](slc []T, items ...T) ([]T, int) {
num_appended := 0
for _, item := range items {
if !slices.Contains(slc, item) {
slc = append(slc, item)
num_appended++
}
}
return slc, num_appended
}
// Returns true only if all the given elements are equal
func allEqual[T comparable](items ...T) bool {
first := items[0]
for _, item := range items {
if item != first {
return false
}
}
return true
}
// Map function - convert a slice of T to a slice of V, based on a function
// that maps a T to a V
func funcMap[T, V any](slc []T, fn func(T) V) []V {

View File

@@ -25,21 +25,25 @@ const (
)
type nfaState struct {
content stateContents // Contents of current state
isEmpty bool // If it is empty - Union operator and Kleene star states will be empty
isLast bool // If it is the last state (acept state)
output []*nfaState // The outputs of the current state ie. the 'outward arrows'. A union operator state will have more than one of these.
transitions map[int][]*nfaState // Transitions to different states (maps a character (int representation) to a _list of states. This is useful if one character can lead multiple states eg. ab|aa)
isKleene bool // Identifies whether current node is a 0-state representing Kleene star
assert assertType // Type of assertion of current node - NONE means that the node doesn't assert anything
allChars bool // Whether or not the state represents all characters (eg. a 'dot' metacharacter). A 'dot' node doesn't store any contents directly, as it would take up too much space
except []rune // Only valid if allChars is true - match all characters _except_ the ones in this block. Useful for inverting character classes.
lookaroundRegex string // Only for lookaround states - Contents of the regex that the lookaround state holds
lookaroundNFA *nfaState // Holds the NFA of the lookaroundRegex - if it exists
lookaroundNumCaptureGroups int // Number of capturing groups in lookaround regex if current node is a lookaround
groupBegin bool // Whether or not the node starts a capturing group
groupEnd bool // Whether or not the node ends a capturing group
groupNum int // Which capturing group the node starts / ends
content stateContents // Contents of current state
isEmpty bool // If it is empty - Union operator and Kleene star states will be empty
isLast bool // If it is the last state (acept state)
output []*nfaState // The outputs of the current state ie. the 'outward arrows'. A union operator state will have more than one of these.
// transitions map[int][]*nfaState // Transitions to different states (maps a character (int representation) to a _list of states. This is useful if one character can lead multiple states eg. ab|aa)
next *nfaState // The next state (not for alternation or kleene states)
isKleene bool // Identifies whether current node is a 0-state representing Kleene star
isQuestion bool // Identifies whether current node is a 0-state representing the question operator
isAlternation bool // Identifies whether current node is a 0-state representing an alternation
splitState *nfaState // Only for alternation states - the 'other' branch of the alternation ('next' is the first)
assert assertType // Type of assertion of current node - NONE means that the node doesn't assert anything
allChars bool // Whether or not the state represents all characters (eg. a 'dot' metacharacter). A 'dot' node doesn't store any contents directly, as it would take up too much space
except []rune // Only valid if allChars is true - match all characters _except_ the ones in this block. Useful for inverting character classes.
lookaroundRegex string // Only for lookaround states - Contents of the regex that the lookaround state holds
lookaroundNFA *nfaState // Holds the NFA of the lookaroundRegex - if it exists
lookaroundNumCaptureGroups int // Number of capturing groups in lookaround regex if current node is a lookaround
groupBegin bool // Whether or not the node starts a capturing group
groupEnd bool // Whether or not the node ends a capturing group
groupNum int // Which capturing group the node starts / ends
// The following properties depend on the current match - I should think about resetting them for every match.
zeroMatchFound bool // Whether or not the state has been used for a zero-length match - only relevant for zero states
threadGroups []Group // Assuming that a state is part of a 'thread' in the matching process, this array stores the indices of capturing groups in the current thread. As matches are found for this state, its groups will be copied over.
@@ -68,8 +72,9 @@ func cloneStateHelper(stateToClone *nfaState, cloneMap map[*nfaState]*nfaState)
isEmpty: stateToClone.isEmpty,
isLast: stateToClone.isLast,
output: make([]*nfaState, len(stateToClone.output)),
transitions: make(map[int][]*nfaState),
isKleene: stateToClone.isKleene,
isQuestion: stateToClone.isQuestion,
isAlternation: stateToClone.isAlternation,
assert: stateToClone.assert,
zeroMatchFound: stateToClone.zeroMatchFound,
allChars: stateToClone.allChars,
@@ -87,26 +92,48 @@ func cloneStateHelper(stateToClone *nfaState, cloneMap map[*nfaState]*nfaState)
clone.output[i] = cloneStateHelper(s, cloneMap)
}
}
for k, v := range stateToClone.transitions {
clone.transitions[k] = make([]*nfaState, len(v))
for i, s := range v {
if s == stateToClone {
clone.transitions[k][i] = clone
} else {
clone.transitions[k][i] = cloneStateHelper(s, cloneMap)
}
}
}
if stateToClone.lookaroundNFA == stateToClone {
clone.lookaroundNFA = clone
}
clone.lookaroundNFA = cloneStateHelper(stateToClone.lookaroundNFA, cloneMap)
if stateToClone.splitState == stateToClone {
clone.splitState = clone
}
clone.splitState = cloneStateHelper(stateToClone.splitState, cloneMap)
if stateToClone.next == stateToClone {
clone.next = clone
}
clone.next = cloneStateHelper(stateToClone.next, cloneMap)
return clone
}
// Reset any thread-related fields of the NFA starting from the given state.
func resetThreads(start *nfaState) {
visitedMap := make(map[*nfaState]bool) // The value type doesn't matter here
resetThreadsHelper(start, visitedMap)
}
func resetThreadsHelper(state *nfaState, visitedMap map[*nfaState]bool) {
if state == nil {
return
}
if _, ok := visitedMap[state]; ok {
return
}
// Assuming it hasn't been visited
state.threadGroups = nil
visitedMap[state] = true
if state.isAlternation {
resetThreadsHelper(state.next, visitedMap)
resetThreadsHelper(state.splitState, visitedMap)
} else {
resetThreadsHelper(state.next, visitedMap)
}
}
// Checks if the given state's assertion is true. Returns true if the given
// state doesn't have an assertion.
func (s nfaState) checkAssertion(str []rune, idx int) bool {
func (s nfaState) checkAssertion(str []rune, idx int, preferLongest bool) bool {
if s.assert == alwaysTrueAssert {
return true
}
@@ -156,17 +183,18 @@ func (s nfaState) checkAssertion(str []rune, idx int) bool {
strToMatch = string(runesToMatch)
}
matchIndices := FindAllMatches(Reg{startState, s.lookaroundNumCaptureGroups}, strToMatch)
regComp := Reg{startState, s.lookaroundNumCaptureGroups, s.lookaroundRegex, preferLongest}
matchIndices := regComp.FindAll(strToMatch)
numMatchesFound := 0
for _, matchIdx := range matchIndices {
if s.assert == plaAssert || s.assert == nlaAssert { // Lookahead - return true (or false) if at least one match starts at 0. Zero is used because the test-string _starts_ from idx.
if matchIdx[0].StartIdx == 0 {
if matchIdx.StartIdx == 0 {
numMatchesFound++
}
}
if s.assert == plbAssert || s.assert == nlbAssert { // Lookbehind - return true (or false) if at least one match _ends_ at the current index.
if matchIdx[0].EndIdx == idx {
if matchIdx.EndIdx == idx {
numMatchesFound++
}
}
@@ -182,9 +210,12 @@ func (s nfaState) checkAssertion(str []rune, idx int) bool {
}
// Returns true if the contents of 's' contain the value at the given index of the given string
func (s nfaState) contentContains(str []rune, idx int) bool {
func (s nfaState) contentContains(str []rune, idx int, preferLongest bool) bool {
if s.assert != noneAssert {
return s.checkAssertion(str, idx)
return s.checkAssertion(str, idx, preferLongest)
}
if idx >= len(str) {
return false
}
if s.allChars {
return !slices.Contains(slices.Concat(notDotChars, s.except), str[idx]) // Return true only if the index isn't a 'notDotChar', or isn't one of the exception characters for the current node.
@@ -197,74 +228,84 @@ func (s nfaState) isLookaround() bool {
return s.assert == plaAssert || s.assert == plbAssert || s.assert == nlaAssert || s.assert == nlbAssert
}
func (s nfaState) numTransitions() int {
if s.next == nil && s.splitState == nil {
return 0
}
if s.next == nil || s.splitState == nil {
return 1
}
return 2
}
// Returns the matches for the character at the given index of the given string.
// Also returns the number of matches. Returns -1 if an assertion failed.
func (s nfaState) matchesFor(str []rune, idx int) ([]*nfaState, int) {
// Assertions can be viewed as 'checks'. If the check fails, we return
// an empty array and 0.
// If it passes, we treat it like any other state, and return all the transitions.
if s.assert != noneAssert {
if s.checkAssertion(str, idx) == false {
return make([]*nfaState, 0), -1
}
}
listTransitions := s.transitions[int(str[idx])]
for _, dest := range s.transitions[int(anyCharRune)] {
if !slices.Contains(slices.Concat(notDotChars, dest.except), str[idx]) {
// Add an allChar state to the list of matches if:
// a. The current character isn't a 'notDotChars' character. In single line mode, this includes newline. In multiline mode, it doesn't.
// b. The current character isn't the state's exception list.
listTransitions = append(listTransitions, dest)
}
}
numTransitions := len(listTransitions)
return listTransitions, numTransitions
}
//func (s nfaState) matchesFor(str []rune, idx int) ([]*nfaState, int) {
// // Assertions can be viewed as 'checks'. If the check fails, we return
// // an empty array and 0.
// // If it passes, we treat it like any other state, and return all the transitions.
// if s.assert != noneAssert {
// if s.checkAssertion(str, idx) == false {
// return make([]*nfaState, 0), -1
// }
// }
// listTransitions := s.transitions[int(str[idx])]
// for _, dest := range s.transitions[int(anyCharRune)] {
// if !slices.Contains(slices.Concat(notDotChars, dest.except), str[idx]) {
// // Add an allChar state to the list of matches if:
// // a. The current character isn't a 'notDotChars' character. In single line mode, this includes newline. In multiline mode, it doesn't.
// // b. The current character isn't the state's exception list.
// listTransitions = append(listTransitions, dest)
// }
// }
// numTransitions := len(listTransitions)
// return listTransitions, numTransitions
//}
// verifyLastStatesHelper performs the depth-first recursion needed for verifyLastStates
func verifyLastStatesHelper(st *nfaState, visited map[*nfaState]bool) {
if len(st.transitions) == 0 {
st.isLast = true
return
}
// if len(state.transitions) == 1 && len(state.transitions[state.content]) == 1 && state.transitions[state.content][0] == state { // Eg. a*
if len(st.transitions) == 1 { // Eg. a*
var moreThanOneTrans bool // Dummy variable, check if all the transitions for the current's state's contents have a length of one
for _, c := range st.content {
if len(st.transitions[c]) != 1 || st.transitions[c][0] != st {
moreThanOneTrans = true
}
}
st.isLast = !moreThanOneTrans
}
if st.isKleene { // A State representing a Kleene Star has transitions going out, which loop back to it. If all those transitions point to the same (single) state, then it must be a last state
transitionDests := make([]*nfaState, 0)
for _, v := range st.transitions {
transitionDests = append(transitionDests, v...)
}
if allEqual(transitionDests...) {
st.isLast = true
return
}
}
if visited[st] == true {
return
}
visited[st] = true
for _, states := range st.transitions {
for i := range states {
if states[i] != st {
verifyLastStatesHelper(states[i], visited)
}
}
}
}
//func verifyLastStatesHelper(st *nfaState, visited map[*nfaState]bool) {
// if st.numTransitions() == 0 {
// st.isLast = true
// return
// }
// // if len(state.transitions) == 1 && len(state.transitions[state.content]) == 1 && state.transitions[state.content][0] == state { // Eg. a*
// if st.numTransitions() == 1 { // Eg. a*
// var moreThanOneTrans bool // Dummy variable, check if all the transitions for the current's state's contents have a length of one
// for _, c := range st.content {
// if len(st.transitions[c]) != 1 || st.transitions[c][0] != st {
// moreThanOneTrans = true
// }
// }
// st.isLast = !moreThanOneTrans
// }
//
// if st.isKleene { // A State representing a Kleene Star has transitions going out, which loop back to it. If all those transitions point to the same (single) state, then it must be a last state
// transitionDests := make([]*nfaState, 0)
// for _, v := range st.transitions {
// transitionDests = append(transitionDests, v...)
// }
// if allEqual(transitionDests...) {
// st.isLast = true
// return
// }
// }
// if visited[st] == true {
// return
// }
// visited[st] = true
// for _, states := range st.transitions {
// for i := range states {
// if states[i] != st {
// verifyLastStatesHelper(states[i], visited)
// }
// }
// }
//}
// verifyLastStates enables the 'isLast' flag for the leaf nodes (last states)
func verifyLastStates(start []*nfaState) {
verifyLastStatesHelper(start[0], make(map[*nfaState]bool))
}
//func verifyLastStates(start []*nfaState) {
// verifyLastStatesHelper(start[0], make(map[*nfaState]bool))
//}
// Concatenates s1 and s2, returns the start of the concatenation.
func concatenate(s1 *nfaState, s2 *nfaState) *nfaState {
@@ -272,73 +313,84 @@ func concatenate(s1 *nfaState, s2 *nfaState) *nfaState {
return s2
}
for i := range s1.output {
for _, c := range s2.content { // Create transitions for every element in s1's content to s2'
s1.output[i].transitions[c], _ = unique_append(s1.output[i].transitions[c], s2)
}
s1.output[i].next = s2
}
s1.output = s2.output
return s1
}
func kleene(s1 nfaState) (*nfaState, error) {
func kleene(s1 *nfaState) (*nfaState, error) {
if s1.isEmpty && s1.assert != noneAssert {
return nil, fmt.Errorf("previous token is not quantifiable")
}
toReturn := &nfaState{}
toReturn.transitions = make(map[int][]*nfaState)
toReturn.content = newContents(epsilon)
toReturn.isEmpty = true
toReturn.isAlternation = true
toReturn.content = newContents(epsilon)
toReturn.splitState = s1
// toReturn := &nfaState{}
// toReturn.transitions = make(map[int][]*nfaState)
// toReturn.content = newContents(epsilon)
toReturn.isKleene = true
toReturn.output = append(toReturn.output, toReturn)
toReturn.output = append([]*nfaState{}, toReturn)
for i := range s1.output {
for _, c := range toReturn.content {
s1.output[i].transitions[c], _ = unique_append(s1.output[i].transitions[c], toReturn)
}
}
for _, c := range s1.content {
toReturn.transitions[c], _ = unique_append(toReturn.transitions[c], &s1)
s1.output[i].next = toReturn
}
// for _, c := range s1.content {
// toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], &s1)
// }
//toReturn.kleeneState = &s1
return toReturn, nil
}
func alternate(s1 *nfaState, s2 *nfaState) *nfaState {
toReturn := &nfaState{}
toReturn.transitions = make(map[int][]*nfaState)
// toReturn.transitions = make(map[int][]*nfaState)
toReturn.output = append(toReturn.output, s1.output...)
toReturn.output = append(toReturn.output, s2.output...)
// Unique append is used here (and elsewhere) to ensure that,
// for any given transition, a state can only be mentioned once.
// For example, given the transition 'a', the state 's1' can only be mentioned once.
// This would lead to multiple instances of the same set of match indices, since both
// 's1' states would be considered to match.
for _, c := range s1.content {
toReturn.transitions[c], _ = unique_append(toReturn.transitions[c], s1)
}
for _, c := range s2.content {
toReturn.transitions[c], _ = unique_append(toReturn.transitions[c], s2)
}
// // Unique append is used here (and elsewhere) to ensure that,
// // for any given transition, a state can only be mentioned once.
// // For example, given the transition 'a', the state 's1' can only be mentioned once.
// // This would lead to multiple instances of the same set of match indices, since both
// // 's1' states would be considered to match.
// for _, c := range s1.content {
// toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], s1)
// }
// for _, c := range s2.content {
// toReturn.transitions[c], _ = uniqueAppend(toReturn.transitions[c], s2)
// }
toReturn.content = newContents(epsilon)
toReturn.isEmpty = true
toReturn.isAlternation = true
toReturn.next = s1
toReturn.splitState = s2
return toReturn
}
func question(s1 *nfaState) *nfaState { // Use the fact that ab? == a(b|)
s2 := &nfaState{}
s2.transitions = make(map[int][]*nfaState)
s2.content = newContents(epsilon)
s2.output = append(s2.output, s2)
s2.isEmpty = true
s3 := alternate(s1, s2)
return s3
func question(s1 *nfaState) (*nfaState, error) { // Use the fact that ab? == a(b|)
if s1.isEmpty && s1.assert != noneAssert {
return nil, fmt.Errorf("previous token is not quantifiable")
}
toReturn := &nfaState{}
toReturn.isEmpty = true
toReturn.isAlternation = true
toReturn.isQuestion = true
toReturn.content = newContents(epsilon)
toReturn.splitState = s1
toReturn.output = append([]*nfaState{}, toReturn)
toReturn.output = append(toReturn.output, s1.output...)
// s2.transitions = make(map[int][]*nfaState)
return toReturn, nil
}
// Creates and returns a new state with the 'default' values.
func newState() nfaState {
ret := nfaState{
output: make([]*nfaState, 0),
transitions: make(map[int][]*nfaState),
output: make([]*nfaState, 0),
// transitions: make(map[int][]*nfaState),
assert: noneAssert,
except: append([]rune{}, 0),
lookaroundRegex: "",
@@ -350,10 +402,40 @@ func newState() nfaState {
}
// Creates and returns a state that _always_ has a zero-length match.
func zeroLengthMatchState() nfaState {
start := newState()
func zeroLengthMatchState() *nfaState {
start := &nfaState{}
start.content = newContents(epsilon)
start.isEmpty = true
start.assert = alwaysTrueAssert
start.output = append([]*nfaState{}, start)
return start
}
func (s nfaState) equals(other nfaState) bool {
return s.isEmpty == other.isEmpty &&
s.isLast == other.isLast &&
slices.Equal(s.output, other.output) &&
slices.Equal(s.content, other.content) &&
s.next == other.next &&
s.isKleene == other.isKleene &&
s.isQuestion == other.isQuestion &&
s.isAlternation == other.isAlternation &&
s.splitState == other.splitState &&
s.assert == other.assert &&
s.allChars == other.allChars &&
slices.Equal(s.except, other.except) &&
s.lookaroundNFA == other.lookaroundNFA &&
s.groupBegin == other.groupBegin &&
s.groupEnd == other.groupEnd &&
s.groupNum == other.groupNum &&
slices.Equal(s.threadGroups, other.threadGroups)
}
func stateExists(list []nfaState, s nfaState) bool {
for i := range list {
if list[i].equals(s) {
return true
}
}
return false
}

View File

@@ -3,7 +3,9 @@ package regex
import (
"fmt"
"math"
"slices"
"strconv"
"strings"
)
type numRange struct {
@@ -99,28 +101,39 @@ func range2regex(start int, end int) (string, error) {
// Last range - tmp to rangeEnd
ranges = append(ranges, numRange{tmp, rangeEnd})
regex := string(nonCapLparenRune)
regexSlice := make([]string, 0)
// Generate the regex
for i, rg := range ranges {
if i > 0 {
regex += "|"
}
regex += string(nonCapLparenRune)
for _, rg := range ranges {
tmpStr := ""
tmpStr += string(nonCapLparenRune)
startSlc := intToSlc(rg.start)
endSlc := intToSlc(rg.end)
if len(startSlc) != len(endSlc) {
return "", fmt.Errorf("Error parsing numeric range")
return "", fmt.Errorf("error parsing numeric range")
}
for i := range startSlc {
if startSlc[i] == endSlc[i] {
regex += string(rune(startSlc[i] + 48)) // '0' is ascii value 48, 1 is 49 etc. To convert the digit to its character form, we can just add 48.
tmpStr += string(rune(startSlc[i] + 48)) // '0' is ascii value 48, 1 is 49 etc. To convert the digit to its character form, we can just add 48.
} else {
regex += fmt.Sprintf("%c%c-%c%c", lbracketRune, rune(startSlc[i]+48), rune(endSlc[i]+48), rbracketRune)
tmpStr += fmt.Sprintf("%c%c-%c%c", lbracketRune, rune(startSlc[i]+48), rune(endSlc[i]+48), rbracketRune)
}
}
regex += ")"
tmpStr += ")"
regexSlice = append(regexSlice, tmpStr)
}
regex += ")"
// Each element of the slice represents one 'group'. Taking 0-255 as an example, the elements would be:
// 1. 0-9
// 2. 10-99
// 3. 100-199
// 4. 200-249
// 5. 250-255
//
// The reason this is reversed before joining it, is because it is incompatible with the PCRE rule for matching.
// The PCRE rule specifies that the left-branch of an alternation is preferred. Even though this engine uses the POSIX
// rule at the moment (which prefers the longest match regardless of the order of the alternation), reversing the string
// has no downsides. It doesn't affect POSIX matching, and it will reduce my burden if I decide to switch to PCRE matching.
slices.Reverse(regexSlice)
regex := string(nonCapLparenRune) + strings.Join(regexSlice, "|") + ")"
return regex, nil
}

View File

@@ -25,7 +25,9 @@ var reTests = []struct {
{"a*b", nil, "qwqw", []Group{}},
{"(abc)*", nil, "abcabcabc", []Group{{0, 9}, {9, 9}}},
{"((abc)|(def))*", nil, "abcdef", []Group{{0, 6}, {6, 6}}},
{"(abc)*|(def)*", nil, "abcdef", []Group{{0, 3}, {3, 6}, {6, 6}}},
// This match will only happen with Longest()
// {"(abc)*|(def)*", nil, "abcdef", []Group{{0, 3}, {3, 6}, {6, 6}}},
{"(abc)*|(def)*", nil, "abcdef", []Group{{0, 3}, {3, 3}, {4, 4}, {5, 5}, {6, 6}}},
{"b*a*a", nil, "bba", []Group{{0, 3}}},
{"(ab)+", nil, "abcabddd", []Group{{0, 2}, {3, 5}}},
{"a(b(c|d)*)*", nil, "abccbd", []Group{{0, 6}}},
@@ -105,6 +107,9 @@ var reTests = []struct {
{"(a|b){3,4}", nil, "ababaa", []Group{{0, 4}}},
{"(bc){5,}", nil, "bcbcbcbcbcbcbcbc", []Group{{0, 16}}},
{`\d{3,4}`, nil, "1209", []Group{{0, 4}}},
{`\d{3,4}`, nil, "120", []Group{{0, 3}}},
{`\d{3,4}`, nil, "12709", []Group{{0, 4}}},
{`\d{3,4}`, nil, "12", []Group{}},
{`\d{3,4}`, nil, "109", []Group{{0, 3}}},
{`\d{3,4}`, nil, "5", []Group{}},
{`\d{3,4}`, nil, "123135", []Group{{0, 4}}},
@@ -525,7 +530,7 @@ var groupTests = []struct {
}{
{"(a)(b)", nil, "ab", []Match{[]Group{{0, 2}, {0, 1}, {1, 2}}}},
{"((a))(b)", nil, "ab", []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {1, 2}}}},
{"(0)", nil, "ab", []Match{[]Group{}}},
{"(0)", nil, "ab", []Match{}},
{"(a)b", nil, "ab", []Match{[]Group{{0, 2}, {0, 1}}}},
{"a(b)", nil, "ab", []Match{[]Group{{0, 2}, {1, 2}}}},
{"(a|b)", nil, "ab", []Match{[]Group{{0, 1}, {0, 1}}, []Group{{1, 2}, {1, 2}}}},
@@ -534,10 +539,11 @@ var groupTests = []struct {
{"(a+)|(a)", nil, "aaaa", []Match{[]Group{{0, 4}, {0, 4}, {-1, -1}}}},
{"(a+)(aa)", nil, "aaaa", []Match{[]Group{{0, 4}, {0, 2}, {2, 4}}}},
{"(aaaa)|(aaaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {0, 4}, {-1, -1}}}},
{"(aaa)|(aaaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {-1, -1}, {0, 4}}}},
{"(aaa)|(aaaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {-1, -1}, {0, 4}}}},
// This match will only happen with Longest()
// {"(aaa)|(aaaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {-1, -1}, {0, 4}}}},
{"(aaa)|(aaaa)", nil, "aaaa", []Match{[]Group{{0, 3}, {0, 3}, {-1, -1}}}},
{"(aaaa)|(aaa)", nil, "aaaa", []Match{[]Group{{0, 4}, {0, 4}, {-1, -1}}}},
{"(a)|(aa)", nil, "aa", []Match{[]Group{{0, 2}, {-1, -1}, {0, 2}}}},
{"(a)|(aa)", nil, "aa", []Match{[]Group{{0, 1}, {0, 1}}, []Group{{1, 2}, {1, 2}}}},
{"(a?)a?", nil, "b", []Match{[]Group{{0, 0}, {0, 0}}, []Group{{1, 1}, {1, 1}}}},
{"(a?)a?", nil, "ab", []Match{[]Group{{0, 1}, {0, 1}}, []Group{{1, 1}, {1, 1}}, []Group{{2, 2}, {2, 2}}}},
{"(a?)a?", nil, "aa", []Match{[]Group{{0, 2}, {0, 1}}, []Group{{2, 2}, {2, 2}}}},
@@ -575,7 +581,7 @@ var groupTests = []struct {
{`(bc+d$|ef*g.|h?i(j|k))`, nil, `bcdd`, []Match{}},
{`(bc+d$|ef*g.|h?i(j|k))`, nil, `reffgz`, []Match{[]Group{{1, 6}, {1, 6}}}},
{`(((((((((a)))))))))`, nil, `a`, []Match{[]Group{{0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(((((((((a)))))))))\41`, nil, `a`, []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(((((((((a)))))))))\41`, nil, `a!`, []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(.*)c(.*)`, nil, `abcde`, []Match{[]Group{{0, 5}, {0, 2}, {3, 5}}}},
{`\((.*), (.*)\)`, nil, `(a, b)`, []Match{[]Group{{0, 6}, {1, 2}, {4, 5}}}},
@@ -630,7 +636,7 @@ var groupTests = []struct {
{`(bc+d$|ef*g.|h?i(j|k))`, []ReFlag{RE_CASE_INSENSITIVE}, `BCDD`, []Match{}},
{`(bc+d$|ef*g.|h?i(j|k))`, []ReFlag{RE_CASE_INSENSITIVE}, `reffgz`, []Match{[]Group{{1, 6}, {1, 6}}}},
{`(((((((((a)))))))))`, []ReFlag{RE_CASE_INSENSITIVE}, `A`, []Match{[]Group{{0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(((((((((a)))))))))\41`, []ReFlag{RE_CASE_INSENSITIVE}, `A`, []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(((((((((a)))))))))\41`, []ReFlag{RE_CASE_INSENSITIVE}, `A!`, []Match{[]Group{{0, 2}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}, {0, 1}}}},
{`(.*)c(.*)`, []ReFlag{RE_CASE_INSENSITIVE}, `ABCDE`, []Match{[]Group{{0, 5}, {0, 2}, {3, 5}}}},
{`\((.*), (.*)\)`, []ReFlag{RE_CASE_INSENSITIVE}, `(A, B)`, []Match{[]Group{{0, 6}, {1, 2}, {4, 5}}}},
{`(a)(b)c|ab`, []ReFlag{RE_CASE_INSENSITIVE}, `AB`, []Match{[]Group{{0, 2}}}},
@@ -671,9 +677,20 @@ var groupTests = []struct {
{`^([ab]*)(?<!(a))c`, nil, `abc`, []Match{[]Group{{0, 3}, {0, 2}}}},
{`(<389-400>)`, nil, `391`, []Match{[]Group{{0, 3}, {0, 3}}}},
// // Tests from https://wiki.haskell.org/Regex_Posix
// {`(()|.)(b)`, nil, `ab`, []Match{[]Group{{0, 2}, {0, 1}, {-1, -1}, {1, 2}}}},
// {`(()|[ab])(b)`, nil, `ab`, []Match{[]Group{{0, 2}, {0, 1}, {-1, -1}, {1, 2}}}},
// {`(()|[ab])+b`, nil, `aaab`, []Match{[]Group{{0, 4}, {2, 3}, {-1, -1}}}},
// {`([ab]|())+b`, nil, `aaab`, []Match{[]Group{{0, 4}, {2, 3}, {-1, -1}}}},
// // Bug - this should give {0,6},{3,6},{-1,-1} but it gives {0,6},{3,6},{3,3}
// // {`yyyyyy`, nil, `(yyy|(x?)){2,4}`, []Match{[]Group{{0, 6}, {3, 6}, {-1, -1}}, []Group{{6, 6}, {6, 6}, {6, 6}}}},
// {`(a|ab|c|bcd)*(d*)`, nil, `ababcd`, []Match{[]Group{{0, 6}, {3, 6}, {6, 6}}, []Group{{6, 6}, {6, 6}, {6, 6}}}},
// // Bug - this should give {0,3},{0,3},{0,0},{0,3},{3,3} but it gives {0,3},{0,2},{0,1},{1,2},{2,3}
// // {`((a*)(b|abc))(c*)`, nil, `abc`, []Match{[]Group{{0, 3}, {0, 3}, {0, 0}, {0, 3}, {3, 3}}}},
}
func TestFindAllMatches(t *testing.T) {
func TestFind(t *testing.T) {
for _, test := range reTests {
t.Run(test.re+" "+test.str, func(t *testing.T) {
regComp, err := Compile(test.re, test.flags...)
@@ -682,13 +699,35 @@ func TestFindAllMatches(t *testing.T) {
panic(fmt.Errorf("Test Error: %v", err))
}
} else {
matchIndices := FindAllMatches(regComp, test.str)
zeroGroups := make([]Group, len(matchIndices))
for i, m := range matchIndices {
zeroGroups[i] = m[0]
groupIndex, err := regComp.Find(test.str)
if err != nil { // No matches found
if len(test.result) == 0 {
return // Manually pass the test, because this is the expected behavior
} else {
t.Errorf("Wanted %v Got no matches\n", test.result)
}
} else {
if groupIndex != test.result[0] {
t.Errorf("Wanted %v Got %v\n", test.result, groupIndex)
}
}
if !slices.Equal(test.result, zeroGroups) {
t.Errorf("Wanted %v Got %v\n", test.result, zeroGroups)
}
})
}
}
func TestFindAll(t *testing.T) {
for _, test := range reTests {
t.Run(test.re+" "+test.str, func(t *testing.T) {
regComp, err := Compile(test.re, test.flags...)
if err != nil {
if test.result != nil {
panic(fmt.Errorf("Test Error: %v", err))
}
} else {
matchIndices := regComp.FindAll(test.str)
if !slices.Equal(test.result, matchIndices) {
t.Errorf("Wanted %v Got %v\n", test.result, matchIndices)
}
}
})
@@ -704,10 +743,10 @@ func TestFindString(t *testing.T) {
panic(err)
}
} else {
foundString := FindString(regComp, test.str)
foundString := regComp.FindString(test.str)
if len(test.result) == 0 {
if foundString != "" {
t.Errorf("Expected no match got %v\n", foundString)
t.Errorf("Wanted no match got %v\n", foundString)
}
} else {
expectedString := test.str[test.result[0].StartIdx:test.result[0].EndIdx]
@@ -720,7 +759,32 @@ func TestFindString(t *testing.T) {
}
}
func TestFindAllGroups(t *testing.T) {
func TestFindAllString(t *testing.T) {
for _, test := range reTests {
t.Run(test.re+" "+test.str, func(t *testing.T) {
regComp, err := Compile(test.re, test.flags...)
if err != nil {
if test.result != nil {
panic(err)
}
} else {
foundStrings := regComp.FindAllString(test.str)
if len(test.result) != len(foundStrings) {
t.Errorf("Differing number of matches: Wanted %v matches Got %v matches\n", len(test.result), len(foundStrings))
} else {
for idx, group := range test.result {
groupStr := test.str[group.StartIdx:group.EndIdx]
if groupStr != foundStrings[idx] {
t.Errorf("Wanted %v Got %v\n", groupStr, foundStrings[idx])
}
}
}
}
})
}
}
func TestFindSubmatch(t *testing.T) {
for _, test := range groupTests {
t.Run(test.re+" "+test.str, func(t *testing.T) {
regComp, err := Compile(test.re, test.flags...)
@@ -729,13 +793,148 @@ func TestFindAllGroups(t *testing.T) {
panic(err)
}
}
matchIndices := FindAllMatches(regComp, test.str)
match, err := regComp.FindSubmatch(test.str)
if err != nil {
if len(test.result) != 0 {
t.Errorf("Wanted %v got no match\n", test.result[0])
}
} else if len(test.result) == 0 {
t.Errorf("Wanted no match got %v\n", match)
}
for i := range match {
if match[i].IsValid() {
if test.result[0][i] != match[i] {
t.Errorf("Wanted %v Got %v\n", test.result[0], match)
}
} else {
if i < len(test.result) && test.result[0][i].IsValid() {
t.Errorf("Wanted %v Got %v\n", test.result[0], match)
}
}
}
})
}
}
func TestFindStringSubmatch(t *testing.T) {
for _, test := range groupTests {
t.Run(test.re+" "+test.str, func(t *testing.T) {
regComp, err := Compile(test.re, test.flags...)
if err != nil {
if test.result != nil {
panic(err)
}
}
matchStr := regComp.FindStringSubmatch(test.str)
if matchStr == nil {
if len(test.result) != 0 {
expectedStr := funcMap(test.result[0], func(g Group) string {
if g.IsValid() {
return test.str[g.StartIdx:g.EndIdx]
} else {
return ""
}
})
t.Errorf("Wanted %v got no match\n", expectedStr)
}
} else if len(test.result) == 0 {
t.Errorf("Wanted no match got %v\n", matchStr)
} else {
expectedStr := funcMap(test.result[0], func(g Group) string {
if g.IsValid() {
return test.str[g.StartIdx:g.EndIdx]
} else {
return ""
}
})
for i, groupStr := range matchStr {
if groupStr == "" {
if i < len(expectedStr) && expectedStr[i] != "" {
t.Errorf("Wanted %v Got %v\n", expectedStr, matchStr)
}
} else {
if expectedStr[i] != groupStr {
t.Errorf("Wanted %v Got %v\n", expectedStr, matchStr)
}
}
}
}
})
}
}
func TestFindAllStringSubmatch(t *testing.T) {
for _, test := range groupTests {
t.Run(test.re+" "+test.str, func(t *testing.T) {
regComp, err := Compile(test.re, test.flags...)
if err != nil {
if test.result != nil {
panic(err)
}
}
matchStrs := regComp.FindAllStringSubmatch(test.str)
if matchStrs == nil {
if len(test.result) != 0 {
expectedStrs := funcMap(test.result, func(m Match) []string {
return funcMap(m, func(g Group) string {
if g.IsValid() {
return test.str[g.StartIdx:g.EndIdx]
} else {
return ""
}
})
})
t.Errorf("Wanted %v got no match\n", expectedStrs)
}
} else if len(test.result) == 0 {
t.Errorf("Wanted no match got %v\n", matchStrs)
} else {
expectedStrs := funcMap(test.result, func(m Match) []string {
return funcMap(m, func(g Group) string {
if g.IsValid() {
return test.str[g.StartIdx:g.EndIdx]
} else {
return ""
}
})
})
for i, matchStr := range matchStrs {
for j, groupStr := range matchStr {
if groupStr == "" {
if j < len(expectedStrs[i]) && expectedStrs[i][j] != "" {
t.Errorf("Wanted %v Got %v\n", expectedStrs, matchStrs)
}
} else {
if expectedStrs[i][j] != groupStr {
t.Errorf("Wanted %v Got %v\n", expectedStrs, matchStrs)
}
}
}
}
}
})
}
}
func TestFindAllSubmatch(t *testing.T) {
for _, test := range groupTests {
t.Run(test.re+" "+test.str, func(t *testing.T) {
regComp, err := Compile(test.re, test.flags...)
if err != nil {
if test.result != nil {
panic(err)
}
}
matchIndices := regComp.FindAllSubmatch(test.str)
for i := range matchIndices {
for j := range matchIndices[i] {
if matchIndices[i][j].isValid() {
if matchIndices[i][j].IsValid() {
if test.result[i][j] != matchIndices[i][j] {
t.Errorf("Wanted %v Got %v\n", test.result, matchIndices)
}
} else {
if i < len(test.result) && j < len(test.result[i]) && test.result[i][j].IsValid() {
t.Errorf("Wanted %v Got %v\n", test.result, matchIndices)
}
}
}
}