From 5f44c40cfdd37b806ba7d3076e618178223a362f Mon Sep 17 00:00:00 2001 From: Tom Fleet Date: Tue, 30 Jan 2024 20:53:43 +0000 Subject: [PATCH 1/2] Implement Count --- bench_test.go | 12 +++++++ parser.go | 28 +++++++++++++++- parser_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/bench_test.go b/bench_test.go index 7c3fe64..7280499 100644 --- a/bench_test.go +++ b/bench_test.go @@ -199,3 +199,15 @@ func BenchmarkMany(b *testing.B) { } } } + +func BenchmarkCount(b *testing.B) { + input := "abcabcabc" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := parser.Count(parser.Exact("abc"), 3)(input) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/parser.go b/parser.go index 8955e3e..d884345 100644 --- a/parser.go +++ b/parser.go @@ -593,7 +593,7 @@ func Try[T any](parsers ...Parser[T]) Parser[T] { } } -// Many returns a [Parser] that chains a series of sub-parsers, passing the remainder from one +// Many returns a [Parser] that calls a series of sub-parsers, passing the remainder from one // as input to the next and returning a slice of values; one from each parser, and any remaining input // after applying all the parsers. // @@ -621,3 +621,29 @@ func Many[T any](parsers ...Parser[T]) Parser[[]T] { return values, finalRemainder, nil } } + +// Count returns a [Parser] that applies another parser a certain number of times, +// returning the values in a slice along with any remaining input. +// +// If the parser fails or the input is exhausted before the parser has been applied +// the requested number of times, an error will be returned. +func Count[T any](parser Parser[T], count int) Parser[[]T] { + return func(input string) ([]T, string, error) { + values := make([]T, 0, count) + + nextInput := input // The input to the next parser in the loop, starts as our overall input + var finalRemainder string // The final remainder to eventually return, updated with each parser + + for i := 0; i < count; i++ { + value, remainder, err := parser(nextInput) + if err != nil { + return nil, "", fmt.Errorf("Count: parser failed: %w", err) + } + values = append(values, value) + nextInput = remainder + finalRemainder = remainder + } + + return values, finalRemainder, nil + } +} diff --git a/parser_test.go b/parser_test.go index 3cb79c2..46973b9 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1771,6 +1771,82 @@ func TestMany(t *testing.T) { } } +func TestCount(t *testing.T) { + type test[T any] struct { + p parser.Parser[T] // The parser to apply + name string // Identifying test case name + input string // Input to the parser + remainder string // Expected remainder after parsing + err string // The expected error message, if there was one + value []T // The expected value after parsing + count int // Number of times to apply p to input + wantErr bool // Whether or not we wanted an error + } + + tests := []test[string]{ + { + name: "empty input", + input: "", + p: parser.Take(2), + count: 2, + value: nil, + remainder: "", + wantErr: true, + err: "Count: parser failed: Take: cannot take from empty input", + }, + { + name: "take pairs", + input: "123456", + p: parser.Take(2), + count: 3, + value: []string{"12", "34", "56"}, + remainder: "", + wantErr: false, + err: "", + }, + { + name: "input too short", + input: "abcabcabc", + p: parser.Exact("abc"), + count: 4, + value: nil, + remainder: "", + wantErr: true, + err: "Count: parser failed: Exact: cannot match on empty input", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value, remainder, err := parser.Count(tt.p, tt.count)(tt.input) + + // Can't use the helper as []string is not comparable + + // Should only error if we wanted one + if (err != nil) != tt.wantErr { + t.Fatalf("\nGot error:\t%v\nWanted error:\t%v\n", err, tt.wantErr) + } + + // If we did get an error, the message should match what we expect + if err != nil { + if msg := err.Error(); msg != tt.err { + t.Fatalf("\nError message:\t%q\nWanted:\t%q\n", msg, tt.err) + } + } + + // The value should be as expected + if !reflect.DeepEqual(value, tt.value) { + t.Errorf("\nValue:\t%#v\nWanted:\t%#v\n", value, tt.value) + } + + // Likewise the remainder + if remainder != tt.remainder { + t.Errorf("\nRemainder:\t%q\nWanted:\t%q\n", remainder, tt.remainder) + } + }) + } +} + func ExampleTake() { input := "Hello I am some input for you to parser" @@ -2023,6 +2099,21 @@ func ExampleMany() { // Remainder: "rest..." } +func ExampleCount() { + input := "12345678rest..." // Pairs of digits with a bit on the end + + value, remainder, err := parser.Count(parser.Take(2), 4)(input) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + + fmt.Printf("Value: %#v\n", value) + fmt.Printf("Remainder: %q\n", remainder) + + // Output: Value: []string{"12", "34", "56", "78"} + // Remainder: "rest..." +} + // parserTest is a simple structure to encapsulate everything we need to test about // the result of applying a parser to some input. type parserTest[T comparable] struct { From 0c43566e5dc7c6759dfedfb90f3dc8376656e50f Mon Sep 17 00:00:00 2001 From: Tom Fleet Date: Tue, 30 Jan 2024 20:56:14 +0000 Subject: [PATCH 2/2] Swap Many for Count in the example --- README.md | 2 +- example_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ed7ad6..d9acd25 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ func main() { } // We want 3 hex pairs - pairs, _, err := parser.Many(hexPair, hexPair, hexPair)(colour) + pairs, _, err := parser.Count(hexPair, 3)(colour) if err != nil { log.Fatalln(err) } diff --git a/example_test.go b/example_test.go index 068a7a0..eb9e4b1 100644 --- a/example_test.go +++ b/example_test.go @@ -41,7 +41,7 @@ func Example() { } // We want 3 hex pairs - pairs, _, err := parser.Many(hexPair, hexPair, hexPair)(colour) + pairs, _, err := parser.Count(hexPair, 3)(colour) if err != nil { fmt.Fprintln(os.Stderr, err) return