Skip to content

Commit

Permalink
Implement AnyOf (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
FollowTheProcess authored Jan 25, 2024
1 parent 110eed0 commit 920e374
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 1 deletion.
37 changes: 37 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,43 @@ func NoneOf(chars string) Parser[string] {
}
}

// AnyOf returns a [Parser] that continues taking characters so long as they are contained in the
// passed in set of chars.
//
// If the input or chars is empty, an error will be returned.
// Likewise if none of the chars is present.
func AnyOf(chars string) Parser[string] {
return func(input string) (string, string, error) {
if input == "" {
return "", "", errors.New("AnyOf: input text is empty")
}

if chars == "" {
return "", "", errors.New("AnyOf: chars must not be empty")
}

if !utf8.ValidString(input) {
return "", "", errors.New("AnyOf: input not valid utf-8")
}

end := 0 // The end of the matching sequence
for pos, char := range input {
if !strings.ContainsRune(chars, char) {
end = pos
break
}
}

// If we've broken the loop but end is still 0, there was no matches
// in the entire input
if end == 0 {
return "", "", fmt.Errorf("AnyOf: no match for any char in (%s) found in input", chars)
}

return input[:end], input[end:], nil
}
}

// Map returns a [Parser] that applies a function to the result of another parser.
//
// It is particularly useful for parsing a section of string input, then converting
Expand Down
154 changes: 153 additions & 1 deletion parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ func TestNoneOf(t *testing.T) {
tests := []struct {
name string // Identifying test case name
input string // Entire input to be parsed
chars string // The chars to match one of
chars string // The chars to match none of
value string // The parsed value
remainder string // The remaining unparsed input
err string // The expected error message (if there is one)
Expand Down Expand Up @@ -1065,6 +1065,128 @@ func TestNoneOf(t *testing.T) {
}
}

func TestAnyOf(t *testing.T) {
tests := []struct {
name string // Identifying test case name
input string // Entire input to be parsed
chars string // The chars to match any of
value string // The parsed value
remainder string // The remaining unparsed input
err string // The expected error message (if there is one)
wantErr bool // Whether it should have returned an error
}{
{
name: "empty input",
input: "",
chars: "abc", // Doesn't matter
value: "",
remainder: "",
wantErr: true,
err: "AnyOf: input text is empty",
},
{
name: "empty chars",
input: "some input",
chars: "",
value: "",
remainder: "",
wantErr: true,
err: "AnyOf: chars must not be empty",
},
{
name: "empty input and chars",
input: "",
chars: "",
value: "",
remainder: "",
wantErr: true,
err: "AnyOf: input text is empty",
},
{
name: "bad utf8",
input: "\xf8\xa1\xa1\xa1\xa1",
chars: "doesn't matter",
value: "",
remainder: "",
wantErr: true,
err: "AnyOf: input not valid utf-8",
},
{
name: "no match",
input: "123 is a number",
chars: "abcdefg",
value: "",
remainder: "",
wantErr: true,
err: "AnyOf: no match for any char in (abcdefg) found in input",
},
{
name: "match a number",
input: "123 is a number",
chars: "1234567890",
value: "123",
remainder: " is a number",
wantErr: false,
err: "",
},
{
name: "match a hex digit",
input: "BADBABEsomething",
chars: "1234567890ABCDEF",
value: "BADBABE",
remainder: "something",
wantErr: false,
err: "",
},
{
name: "match with space",
input: "DEADBEEF and the rest",
chars: "1234567890ABCDEF",
value: "DEADBEEF",
remainder: " and the rest",
wantErr: false,
err: "",
},
{
name: "match unicode",
input: "語ç日ð本Ê語",
chars: "ðç日語",
value: "語ç日ð",
remainder: "本Ê語",
wantErr: false,
err: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value, remainder, err := parser.AnyOf(tt.chars)(tt.input)

// 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("\nGot:\t%q\nWanted:\t%q\n", msg, tt.err)
}
}

// The value should be as expected
if value != tt.value {
t.Errorf("\nGot:\t%q\nWanted:\t%q\n", value, tt.value)
}

// Likewise the remainder
if remainder != tt.remainder {
t.Errorf("\nGot:\t%q\nWanted:\t%q\n", remainder, tt.remainder)
}
})
}
}

func TestMap(t *testing.T) {
type test[T1, T2 any] struct {
name string // Identifying test case name
Expand Down Expand Up @@ -1249,6 +1371,19 @@ func BenchmarkNoneOf(b *testing.B) {
}
}

func BenchmarkAnyOf(b *testing.B) {
input := "DEADBEEF and the rest"
chars := "1234567890ABCDEF"

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, err := parser.AnyOf(chars)(input)
if err != nil {
b.Fatal(err)
}
}
}

func BenchmarkMap(b *testing.B) {
input := "Hello, World!"

Expand Down Expand Up @@ -1389,6 +1524,23 @@ func ExampleNoneOf() {
// Remainder: "bcdefg"
}

func ExampleAnyOf() {
input := "DEADBEEF and the rest"

chars := "1234567890ABCDEF" // Any hexadecimal digit

value, remainder, err := parser.AnyOf(chars)(input)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}

fmt.Printf("Value: %q\n", value)
fmt.Printf("Remainder: %q\n", remainder)

// Output: Value: "DEADBEEF"
// Remainder: " and the rest"
}

func ExampleMap() {
input := "27 <- this is a number" // Let's convert it to an int!

Expand Down

0 comments on commit 920e374

Please sign in to comment.