diff --git a/parser.go b/parser.go index fc260f3..d77c993 100644 --- a/parser.go +++ b/parser.go @@ -275,6 +275,47 @@ func OneOf(chars string) Parser[string] { } } +// NoneOf returns a [Parser] that recognises any char other than any of the provided characters +// from the start of input. +// +// It can be considered as the opposite to [OneOf]. +// +// If the input or chars is empty, an error will be returned. +// Likewise if one of the chars was recognised. +func NoneOf(chars string) Parser[string] { + return func(input string) (string, string, error) { + if input == "" { + return "", "", errors.New("NoneOf: input text is empty") + } + + if chars == "" { + return "", "", errors.New("NoneOf: chars must not be empty") + } + + r, width := utf8.DecodeRuneInString(input) + if r == utf8.RuneError { + return "", "", errors.New("NoneOf: input not valid utf-8") + } + + found := false + for _, char := range chars { + if char == r { + // Found one that's not a match + found = true + break + } + } + + // If we get here and found is true, the first char in the input matched one + // of the requested chars, which for NoneOf is bad + if found { + return "", "", fmt.Errorf("NoneOf: found match (%s) in input", string(r)) + } + + return input[:width], input[width:], 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 diff --git a/parser_test.go b/parser_test.go index 643cc53..856e3d9 100644 --- a/parser_test.go +++ b/parser_test.go @@ -934,6 +934,137 @@ func TestOneOf(t *testing.T) { } } +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 + 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: "NoneOf: input text is empty", + }, + { + name: "empty chars", + input: "some input", + chars: "", + value: "", + remainder: "", + wantErr: true, + err: "NoneOf: chars must not be empty", + }, + { + name: "empty input and chars", + input: "", + chars: "", + value: "", + remainder: "", + wantErr: true, + err: "NoneOf: input text is empty", + }, + { + name: "bad utf8", + input: "\xf8\xa1\xa1\xa1\xa1", + chars: "doesn't matter", + value: "", + remainder: "", + wantErr: true, + err: "NoneOf: input not valid utf-8", + }, + { + name: "match a", + input: "abcdef", + chars: "abc", + value: "", + remainder: "", + wantErr: true, + err: "NoneOf: found match (a) in input", + }, + { + name: "match b", + input: "bacdef", + chars: "abc", + value: "", + remainder: "", + wantErr: true, + err: "NoneOf: found match (b) in input", + }, + { + name: "match c", + input: "cabdef", + chars: "abc", + value: "", + remainder: "", + wantErr: true, + err: "NoneOf: found match (c) in input", + }, + { + name: "no match", + input: "abcdef", + chars: "xyz", + value: "a", + remainder: "bcdef", + wantErr: false, + err: "", + }, + { + name: "no match unicode", + input: "語ç日ð本Ê語", + chars: "ç日ð", + value: "語", + remainder: "ç日ð本Ê語", + wantErr: false, + err: "", + }, + { + name: "no match unicode single", + input: "本Ê語", + chars: "Ê", + value: "本", + remainder: "Ê語", + wantErr: false, + err: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value, remainder, err := parser.NoneOf(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 @@ -1105,6 +1236,19 @@ func BenchmarkOneOf(b *testing.B) { } } +func BenchmarkNoneOf(b *testing.B) { + input := "abcdef" + chars := "xyz" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := parser.NoneOf(chars)(input) + if err != nil { + b.Fatal(err) + } + } +} + func BenchmarkMap(b *testing.B) { input := "Hello, World!" @@ -1228,6 +1372,23 @@ func ExampleOneOf() { // Remainder: "bcdefg" } +func ExampleNoneOf() { + input := "abcdefg" + + chars := "xyz" // Match anything other than 'x', 'y', or 'z' from input + + value, remainder, err := parser.NoneOf(chars)(input) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + + fmt.Printf("Value: %q\n", value) + fmt.Printf("Remainder: %q\n", remainder) + + // Output: Value: "a" + // Remainder: "bcdefg" +} + func ExampleMap() { input := "27 <- this is a number" // Let's convert it to an int!