Skip to content

Commit

Permalink
feat: add a new combinator that will keep parsing input text until it…
Browse files Browse the repository at this point in the history
… no longer matches (#35)
  • Loading branch information
purpleclay authored Feb 16, 2024
1 parent e7df717 commit 4ac5038
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 5 deletions.
74 changes: 72 additions & 2 deletions combinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"strings"
)

// Result is the expected output from a [Combinator]
// Result is the expected output from a [Combinator].
type Result interface {
string | []string
}
Expand Down Expand Up @@ -73,7 +73,7 @@ func (e CombinatorParseError) Error() string {
}

// ParserError defines an error that is raised when a parser
// fails to parse the input text due to a failed [Combinator]
// fails to parse the input text due to a failed [Combinator].
type ParserError struct {
// Err contains the [CombinatorParseError] that caused the parser to fail.
Err error
Expand All @@ -91,3 +91,73 @@ func (e ParserError) Error() string {
func (e ParserError) Unwrap() error {
return e.Err
}

// RangedParserError defines an error that is raised when a ranged parser
// fails to parse the input text due to a failed [Combinator] within the
// expected execution range.
type RangedParserError struct {
// Err contains the [CombinatorParseError] that caused the parser to fail.
Err error

// Range contains the execution details of the ranged parser.
Exec RangedParserExec

// Type of [Parser] that failed.
Type string
}

// RangedParserExec details how a ranged [Combinator] was exeucted.
type RangedParserExec struct {
// Min is the minimum number of expected executions.
Min uint

// Max is the maximum number of possible executions.
Max uint

// Count contains the number of executions.
Count uint
}

// String returns a string representation of a [RangedParserExec].
func (e RangedParserExec) String() string {
var buf strings.Builder
buf.WriteString(fmt.Sprintf("[count: %d", e.Count))
if e.Min > 0 {
buf.WriteString(fmt.Sprintf(" min: %d", e.Min))
}

if e.Max > 0 {
buf.WriteString(fmt.Sprintf(" max: %d", e.Max))
}
buf.WriteString("]")
return buf.String()
}

// RangeExecution ...
func RangeExecution(i ...uint) RangedParserExec {
exec := RangedParserExec{}

switch len(i) {
case 1:
exec.Count = i[0]
case 2:
exec.Count = i[0]
exec.Min = i[1]
case 3:
exec.Count = i[0]
exec.Min = i[1]
exec.Max = i[2]
}

return exec
}

// Error returns a friendly string representation of the current error.
func (e RangedParserError) Error() string {
return fmt.Sprintf("(%s) parser failed %s. %v", e.Type, e.Exec, e.Err)
}

// Unwrap returns the inner [CombinatorParseError].
func (e RangedParserError) Unwrap() error {
return e.Err
}
63 changes: 60 additions & 3 deletions sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ func Repeat[T Result](c Combinator[T], n uint) Combinator[[]string] {
for i := uint(0); i < n; i++ {
var out T
if rem, out, err = c(rem); err != nil {
return rem, nil, ParserError{Err: err, Type: "repeat"}
return rem, nil, RangedParserError{
Err: err,
Exec: RangeExecution(i, n),
Type: "repeat",
}
}
ext = combine(ext, out)
}
Expand Down Expand Up @@ -139,9 +143,12 @@ func RepeatRange[T Result](c Combinator[T], n, m uint) Combinator[[]string] {
if i+1 > n {
break
}
return rem, nil, ParserError{Err: err, Type: "repeat_range"}
return rem, nil, RangedParserError{
Err: err,
Exec: RangeExecution(i, n, m),
Type: "repeat_range",
}
}

ext = combine(ext, out)
}

Expand Down Expand Up @@ -283,3 +290,53 @@ func All[T Result](c ...Combinator[T]) Combinator[[]string] {
return rem, ext, nil
}
}

// Many will scan the input text and match the [Combinator] a minimum of one
// time. The combinator will repeatedly be executed until the the first failed
// match. This is the equivalent of calling [ManyN] with an argument of 1.
//
// chomp.Many(one.Of("Ho"))("Hello, World!")
// // ("ello, World!", []string{"H"}, nil)
func Many[T Result](c Combinator[T]) Combinator[[]string] {
return func(s string) (string, []string, error) {
return ManyN(c, 1)(s)
}
}

// ManyN will scan the input text and match the [Combinator] a minimum number
// of times. The combinator will repeatedly be executed until the first failed
// match. The minimum number of times must be executed for this combinator to
// be successful.
//
// chomp.ManyN(chomp.OneOf("W"), 0)("Hello, World!")
// // ("Hello, World!", nil, nil)
func ManyN[T Result](c Combinator[T], n uint) Combinator[[]string] {
return func(s string) (string, []string, error) {
var ext []string
var err error
var count uint

rem := s
for {
var out T
var tmpRem string

if tmpRem, out, err = c(rem); err != nil {
break
}
rem = tmpRem
ext = combine(ext, out)
count++
}

if count < n {
return rem, nil, RangedParserError{
Err: err,
Exec: RangeExecution(count, n),
Type: "many_n",
}
}

return rem, ext, nil
}
}
47 changes: 47 additions & 0 deletions sequence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,50 @@ func TestAll(t *testing.T) {
assert.Equal(t, " ", ext[1])
assert.Equal(t, "こんにちは、おはよう", ext[2])
}

func TestMany(t *testing.T) {
t.Parallel()

rem, ext, err := chomp.Many(chomp.OneOf("はんにこち"))("こんにちは、おはよう")

require.NoError(t, err)
assert.Equal(t, "、おはよう", rem)
require.Len(t, ext, 5)
assert.Equal(t, "こ", ext[0])
assert.Equal(t, "ん", ext[1])
assert.Equal(t, "に", ext[2])
assert.Equal(t, "ち", ext[3])
assert.Equal(t, "は", ext[4])
}

func TestManyNoMatches(t *testing.T) {
t.Parallel()

_, _, err := chomp.Many(chomp.OneOf("eHl"))("Good Morning")

require.EqualError(t, err, "(many_n) parser failed [count: 0 min: 1]. (one_of) combinator failed to parse text 'Good Morning' with input 'eHl'")
}

func TestManyN(t *testing.T) {
t.Parallel()

rem, ext, err := chomp.ManyN(chomp.OneOf("eHl"), 2)("Hello and Good Morning")

require.NoError(t, err)
assert.Equal(t, "o and Good Morning", rem)
require.Len(t, ext, 4)
assert.Equal(t, "H", ext[0])
assert.Equal(t, "e", ext[1])
assert.Equal(t, "l", ext[2])
assert.Equal(t, "l", ext[3])
}

func TestManyNZeroMatches(t *testing.T) {
t.Parallel()

rem, ext, err := chomp.ManyN(chomp.OneOf("eHl"), 0)("Good Morning")

require.NoError(t, err)
assert.Equal(t, "Good Morning", rem)
assert.Empty(t, ext)
}

0 comments on commit 4ac5038

Please sign in to comment.