Skip to content

Commit

Permalink
Initial transfer of former stringex matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
themue committed Mar 25, 2023
1 parent 4a4a771 commit 189558d
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 3 deletions.
3 changes: 2 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
BSD 3-Clause License

Copyright (c) 2023, Tideland
Copyright (c) 2023 Frank Mueller / Tideland / Oldenburg / Germany
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand Down
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,32 @@
# go-matcher
Tideland Go Matcher
# Tideland Go Matcher

[![GitHub release](https://img.shields.io/github/release/tideland/go-matcher.svg)](https://github.com/tideland/go-matcher)
[![GitHub license](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://raw.githubusercontent.com/tideland/go-matcher/master/LICENSE)
[![Go Module](https://img.shields.io/github/go-mod/go-version/tideland/go-matcher)](https://github.com/tideland/go-matcher/blob/master/go.mod)
[![GoDoc](https://godoc.org/tideland.dev/go/matcher?status.svg)](https://pkg.go.dev/mod/tideland.dev/go/matcher?tab=packages)
[![Workflow](https://img.shields.io/github/workflow/status/tideland/go-matcher/Go)](https://github.com/tideland/go-matcher/actions/)
[![Go Report Card](https://goreportcard.com/badge/github.com/tideland/go-matcher)](https://goreportcard.com/report/tideland.dev/go/matcher)

## Description

The **Tideland Go Matcher** provides a simple pattern matching. It matches
the following pattterns:

- ? matches one char
- * matches a group of chars
- [abc] matches any of the chars inside the brackets
- [a-z] matches any of the chars of the range
- [^abc] matches any but the chars inside the brackets
- \ escapes any of the pattern chars

## Examples

```go
if matcher.Matches("g*e g?", "Google Go", matcher.IgnoreCase) { ... }

if matcher.Matches("[oO][kK]", "ok", matcher.ValidateCase) { .... }
```

## Contributors

- Frank Mueller (https://github.com/themue / https://github.com/tideland / https://themue.dev)
19 changes: 19 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Tideland Go Matcher
//
// Copyright (C) 2019-2023 Frank Mueller / Tideland / Oldenburg / Germany
//
// All rights reserved. Use of this source code is governed
// by the new BSD license.

// Package matcher provides the matching of simple patterns against strings.
// It matches the following pattterns:
//
// - ? matches one char
// - * matches a group of chars
// - [abc] matches any of the chars inside the brackets
// - [a-z] matches any of the chars of the range
// - [^abc] matches any but the chars inside the brackets
// - \ escapes any of the pattern chars
package matcher // import "tideland.dev/go/matcher"

// EOF
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module tideland.dev/go/matcher

go 1.20

require tideland.dev/go/audit v0.7.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tideland.dev/go/audit v0.7.0 h1:lr4LkNu7i5qLJuqQ6lUfnt0J09anZNfrdXdB1I9JlTs=
tideland.dev/go/audit v0.7.0/go.mod h1:Jua+IB3KgAC7fbuZ1YHT7gKhwpiTOcn3Q7AOCQsrro8=
217 changes: 217 additions & 0 deletions matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Tideland Go Matcher
//
// Copyright (C) 2019-2023 Frank Mueller / Tideland / Oldenburg / Germany
//
// All rights reserved. Use of this source code valuePos governed
// by the new BSD license.

package matcher // import "tideland.dev/go/matcher"

//--------------------
// IMPORTS
//--------------------

import (
"strings"
)

//--------------------
// MATCHER
//--------------------

const (
IgnoreCase bool = true
ValidateCase bool = false

matchSuccess int = iota
matchCont
matchFail
)

// matcher is a helper type for string pattern matching.
type matcher struct {
patternRunes []rune
patternLen int
patternPos int
valueRunes []rune
valueLen int
valuePos int
}

// newMatcher creates the helper type for string pattern matching.
func newMatcher(pattern, value string, ignoreCase bool) *matcher {
if ignoreCase {
return newMatcher(strings.ToLower(pattern), strings.ToLower(value), false)
}
prs := append([]rune(pattern), '\u0000')
vrs := append([]rune(value), '\u0000')
return &matcher{
patternRunes: prs,
patternLen: len(prs) - 1,
patternPos: 0,
valueRunes: vrs,
valueLen: len(vrs) - 1,
valuePos: 0,
}
}

// matches checks if the value matches the pattern.
func (m *matcher) matches() bool {
// Loop over the pattern.
for m.patternLen > 0 {
switch m.processPatternRune() {
case matchSuccess:
return true
case matchFail:
return false

}
m.patternPos++
m.patternLen--
if m.valueLen == 0 {
for m.patternRunes[m.patternPos] == '*' {
m.patternPos++
m.patternLen--
}
break
}
}
if m.patternLen == 0 && m.valueLen == 0 {
return true
}
return false
}

// processPatternRune handles the current leading pattern rune.
func (m *matcher) processPatternRune() int {
switch m.patternRunes[m.patternPos] {
case '*':
return m.processAsterisk()
case '?':
return m.processQuestionMark()
case '[':
return m.processOpenBracket()
case '\\':
m.processBackslash()
fallthrough
default:
return m.processDefault()
}
}

// processAsterisk handles groups of characters.
func (m *matcher) processAsterisk() int {
for m.patternRunes[m.patternPos+1] == '*' {
m.patternPos++
m.patternLen--
}
if m.patternLen == 1 {
return matchSuccess
}
for m.valueLen > 0 {
patternCopy := make([]rune, len(m.patternRunes[m.patternPos+1:]))
valueCopy := make([]rune, len(m.valueRunes[m.valuePos:]))
copy(patternCopy, m.patternRunes[m.patternPos+1:])
copy(valueCopy, m.valueRunes[m.valuePos:])
pam := newMatcher(string(patternCopy), string(valueCopy), false)
if pam.matches() {
return matchSuccess
}
m.valuePos++
m.valueLen--
}
return matchFail
}

// processQuestionMark handles a single character.
func (m *matcher) processQuestionMark() int {
if m.valueLen == 0 {
return matchFail
}
m.valuePos++
m.valueLen--
return matchCont
}

// processOpenBracket handles an open bracket for a group of characters.
func (m *matcher) processOpenBracket() int {
m.patternPos++
m.patternLen--
not := (m.patternRunes[m.patternPos] == '^')
match := false
if not {
m.patternPos++
m.patternLen--
}
group:
for {
switch {
case m.patternRunes[m.patternPos] == '\\':
m.patternPos++
m.patternLen--
if m.patternRunes[m.patternPos] == m.valueRunes[m.valuePos] {
match = true
}
case m.patternRunes[m.patternPos] == ']':
break group
case m.patternLen == 0:
m.patternPos--
m.patternLen++
break group
case m.patternRunes[m.patternPos+1] == '-' && m.patternLen >= 3:
start := m.patternRunes[m.patternPos]
end := m.patternRunes[m.patternPos+2]
vr := m.valueRunes[m.valuePos]
if start > end {
start, end = end, start
}
m.patternPos += 2
m.patternLen -= 2
if vr >= start && vr <= end {
match = true
}
default:
if m.patternRunes[m.patternPos] == m.valueRunes[m.valuePos] {
match = true
}
}
m.patternPos++
m.patternLen--
}
if not {
match = !match
}
if !match {
return matchFail
}
m.valuePos++
m.valueLen--
return matchCont
}

// processBackslash handles escaping via baskslash.
func (m *matcher) processBackslash() int {
if m.patternLen >= 2 {
m.patternPos++
m.patternLen--
}
return matchCont
}

// processDefault handles any other rune.
func (m *matcher) processDefault() int {
if m.patternRunes[m.patternPos] != m.valueRunes[m.valuePos] {
return matchFail
}
m.valuePos++
m.valueLen--
return matchCont
}

// Matches checks if the pattern matches a given value.
func Matches(pattern, value string, ignoreCase bool) bool {
m := newMatcher(pattern, value, ignoreCase)
return m.matches()
}

// EOF
Loading

0 comments on commit 189558d

Please sign in to comment.