-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: property backtracking implementation
- Loading branch information
1 parent
d5c0cee
commit ec9a53c
Showing
39 changed files
with
603 additions
and
1,806 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package backtracking | ||
|
||
type Backtracker[T any, P any] interface { | ||
// The function executes the backtrack algorithm as configured and returns the result | ||
RunBacktrack() []*Match[T, P] | ||
} | ||
|
||
type Input[I any, T any, P any] interface { | ||
// GetInput shall return the input of a configured source, such that the Parse function can parse the input into the Node structure | ||
GetInput() I | ||
// Parse shall map the input source into the node structure | ||
Parse(I) *Node[T, P] | ||
// GetSearch shall return the instance of the search | ||
GetSearch() Search[T, P] | ||
} | ||
|
||
type Search[T any, P any] interface { | ||
// The predicate is the fragment, the backtracking algorithm searches for | ||
Predicate() *T | ||
// The Comparable shall process a node and generate an object comparable to the predicate | ||
Comparable(*Node[T, P]) *T | ||
// IsMatch compares the predicate with a given compare value and returns true if they match and false if they do not | ||
IsMatch(*T, *T) bool | ||
// StopEarly shall return true if the algorithm must backtrack and reject the current branch. | ||
// False if the algorithm shall process further even if no full match has been reached. | ||
StopEarly(*T, *T) bool | ||
} | ||
|
||
// Node is the structure used inside the backtrack algorithm and describes a tree structure. | ||
type Node[T any, P any] struct { | ||
value *T | ||
pointer *P // pointer to the actual element which belongs to the node | ||
parent *Node[T, P] | ||
children []*Node[T, P] | ||
} | ||
|
||
type Match[T any, P any] struct { | ||
Input *T | ||
Match *T | ||
MatchNode *Node[T, P] | ||
} | ||
|
||
// Implementation of a backtracking algorithm to find matches on path as described on Wikipedia: https://en.wikipedia.org/wiki/Backtracking | ||
func Backtrack[I any, T any, P any](inp Input[I, T, P]) []*Match[T, P] { | ||
|
||
result := make([]*Match[T, P], 0) | ||
return backtrack(inp.GetSearch(), root(inp.Parse(inp.GetInput())), result) | ||
} | ||
|
||
func backtrack[T any, P any](problem Search[T, P], n *Node[T, P], result []*Match[T, P]) []*Match[T, P] { | ||
|
||
if n == nil { | ||
return result | ||
} | ||
|
||
// Currently no rejection makes sense | ||
// if reject(problem, n) { | ||
// return backtrack(problem, next(problem, n), result) | ||
// } | ||
|
||
if accept(problem, n) { | ||
result = appendOutput(problem, n, result) | ||
} | ||
|
||
return backtrack(problem, next(problem, n), result) | ||
} | ||
|
||
func root[T any, P any](n *Node[T, P]) *Node[T, P] { | ||
|
||
current := n | ||
for current.parent != nil { | ||
current = current.parent | ||
} | ||
|
||
return current | ||
} | ||
|
||
func reject[T any, P any](search Search[T, P], n *Node[T, P]) bool { | ||
|
||
// can be dangerous, because results are ommited possibly ommitted | ||
return search.StopEarly(search.Predicate(), search.Comparable(n)) | ||
} | ||
|
||
func accept[T any, P any](search Search[T, P], n *Node[T, P]) bool { | ||
|
||
return search.IsMatch(search.Predicate(), search.Comparable(n)) | ||
} | ||
|
||
func first[T any, P any](search Search[T, P], n *Node[T, P]) *Node[T, P] { | ||
|
||
return n.children[0] | ||
} | ||
|
||
func next[T any, P any](search Search[T, P], n *Node[T, P]) *Node[T, P] { | ||
|
||
if n == nil { | ||
return nil | ||
} | ||
|
||
if len(n.children) == 0 { | ||
// backtrack | ||
return walkUp(n.parent, n) | ||
} | ||
|
||
return first(search, n) | ||
} | ||
|
||
func walkUp[T any, P any](parent *Node[T, P], current *Node[T, P]) *Node[T, P] { | ||
|
||
found := false | ||
if parent != nil && current != nil && current.parent == parent { | ||
for _, chld := range parent.children { | ||
if found { | ||
return chld | ||
} else if chld == current { | ||
found = true | ||
} | ||
} | ||
} | ||
|
||
if found && parent.parent != nil { | ||
// this indicates that the current node was the last of level, thus we need to walk up further | ||
// if the parent is nil, this indicates we are at root and traversed the whole tree | ||
if parent.parent == nil { | ||
return nil | ||
} | ||
|
||
return walkUp(parent.parent, parent) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func appendOutput[T any, P any](search Search[T, P], n *Node[T, P], matches []*Match[T, P]) []*Match[T, P] { | ||
|
||
match := new(Match[T, P]) | ||
match.Input = search.Predicate() | ||
match.Match = n.value | ||
match.MatchNode = n | ||
|
||
matches = append(matches, match) | ||
|
||
return matches | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package backtracking_test | ||
|
||
import ( | ||
"fmt" | ||
"momentum-core/backtracking" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
) | ||
|
||
func TestBacktracker(t *testing.T) { | ||
|
||
wd, err := os.Getwd() | ||
if err != nil { | ||
fmt.Println(err) | ||
t.FailNow() | ||
} | ||
|
||
testFilePath := filepath.Join(wd, "test.yaml") | ||
|
||
backtracker := backtracking.NewPropertyBacktracker("test.for", testFilePath, backtracking.NewYamlPropertyParser()) | ||
result := backtracker.RunBacktrack() | ||
|
||
if len(result) != 1 { | ||
fmt.Println("expected one result") | ||
t.FailNow() | ||
} | ||
|
||
for _, res := range result { | ||
fmt.Println(res) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package backtracking | ||
|
||
import ( | ||
"momentum-core/yaml" | ||
"strings" | ||
) | ||
|
||
const PROPERTY_SEPARATOR = "." | ||
|
||
type PropertyParser interface { | ||
ParseProperties(string) *Node[string, yaml.ViewNode] | ||
} | ||
|
||
// The property backtracker finds matches within a property structure. | ||
// A property search is concepted as hierarchical string separated by dots, | ||
// where each level of the | ||
type PropertyBacktracker struct { | ||
Backtracker[string, yaml.ViewNode] | ||
Input[string, string, yaml.ViewNode] | ||
Search[string, yaml.ViewNode] | ||
|
||
inputParser PropertyParser | ||
predicate string | ||
path string // path to the yaml file which shall be processed | ||
} | ||
|
||
func NewPropertyBacktracker(predicate string, path string, parser PropertyParser) *PropertyBacktracker { | ||
|
||
backtracker := new(PropertyBacktracker) | ||
backtracker.predicate = predicate | ||
backtracker.inputParser = parser | ||
backtracker.path = path | ||
|
||
return backtracker | ||
} | ||
|
||
func (backtracker *PropertyBacktracker) RunBacktrack() []*Match[string, yaml.ViewNode] { | ||
|
||
return Backtrack[string, string, yaml.ViewNode](backtracker) | ||
} | ||
|
||
func (backtracker *PropertyBacktracker) GetInput() string { | ||
|
||
return backtracker.path | ||
} | ||
|
||
func (backtracker *PropertyBacktracker) Parse(input string) *Node[string, yaml.ViewNode] { | ||
|
||
return backtracker.inputParser.ParseProperties(backtracker.path) | ||
} | ||
|
||
func (backtracker *PropertyBacktracker) GetSearch() Search[string, yaml.ViewNode] { | ||
|
||
return backtracker | ||
} | ||
|
||
func (backtracker *PropertyBacktracker) Predicate() *string { | ||
|
||
return &backtracker.predicate | ||
} | ||
|
||
func (backtracker *PropertyBacktracker) Comparable(n *Node[string, yaml.ViewNode]) *string { | ||
|
||
propertyString := "" | ||
current := n | ||
for current != nil { | ||
propertyString = strings.Join([]string{*current.value, propertyString}, PROPERTY_SEPARATOR) | ||
current = current.parent | ||
} | ||
|
||
cutted, _ := strings.CutPrefix(propertyString, PROPERTY_SEPARATOR) | ||
cutted, _ = strings.CutSuffix(cutted, PROPERTY_SEPARATOR) | ||
//fmt.Println("cutted propertyString:", propertyString) | ||
return &cutted | ||
} | ||
|
||
func (backtracker *PropertyBacktracker) IsMatch(predicate *string, argument *string) bool { | ||
|
||
return strings.EqualFold(*predicate, *argument) | ||
} | ||
|
||
func (backtracker *PropertyBacktracker) StopEarly(predicate *string, argument *string) bool { | ||
|
||
return !strings.HasPrefix(*predicate, *argument) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
test: | ||
for: | ||
backtracker: test | ||
not-for: | ||
backtracker: test | ||
go-never: to | ||
trees: | ||
are: | ||
your: friends |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package backtracking | ||
|
||
import ( | ||
"fmt" | ||
"momentum-core/utils" | ||
"momentum-core/yaml" | ||
) | ||
|
||
type YamlPropertyParser struct { | ||
PropertyParser | ||
} | ||
|
||
func NewYamlPropertyParser() *YamlPropertyParser { | ||
|
||
parser := new(YamlPropertyParser) | ||
|
||
return parser | ||
} | ||
|
||
func (parser *YamlPropertyParser) ParseProperties(path string) *Node[string, yaml.ViewNode] { | ||
|
||
if !utils.FileExists(path) { | ||
return nil | ||
} | ||
|
||
root, err := yaml.ParseFile(path) | ||
if err != nil { | ||
fmt.Println(err) | ||
return nil | ||
} | ||
|
||
if root.Children[0].Kind == yaml.Mapping { | ||
|
||
return viewNodeToNode(root.Children[0], nil) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func viewNodeToNode(n *yaml.ViewNode, parent *Node[string, yaml.ViewNode]) *Node[string, yaml.ViewNode] { | ||
|
||
r := new(Node[string, yaml.ViewNode]) | ||
r.parent = parent | ||
r.value = &n.Path | ||
r.pointer = n | ||
|
||
children := make([]*Node[string, yaml.ViewNode], 0) | ||
for _, child := range n.Children { | ||
children = append(children, viewNodeToNode(child, r)) | ||
} | ||
r.children = children | ||
|
||
return r | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.