Skip to content

Commit 4ac5038

Browse files
authored
feat: add a new combinator that will keep parsing input text until it no longer matches (#35)
1 parent e7df717 commit 4ac5038

File tree

3 files changed

+179
-5
lines changed

3 files changed

+179
-5
lines changed

combinator.go

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
"strings"
2828
)
2929

30-
// Result is the expected output from a [Combinator]
30+
// Result is the expected output from a [Combinator].
3131
type Result interface {
3232
string | []string
3333
}
@@ -73,7 +73,7 @@ func (e CombinatorParseError) Error() string {
7373
}
7474

7575
// ParserError defines an error that is raised when a parser
76-
// fails to parse the input text due to a failed [Combinator]
76+
// fails to parse the input text due to a failed [Combinator].
7777
type ParserError struct {
7878
// Err contains the [CombinatorParseError] that caused the parser to fail.
7979
Err error
@@ -91,3 +91,73 @@ func (e ParserError) Error() string {
9191
func (e ParserError) Unwrap() error {
9292
return e.Err
9393
}
94+
95+
// RangedParserError defines an error that is raised when a ranged parser
96+
// fails to parse the input text due to a failed [Combinator] within the
97+
// expected execution range.
98+
type RangedParserError struct {
99+
// Err contains the [CombinatorParseError] that caused the parser to fail.
100+
Err error
101+
102+
// Range contains the execution details of the ranged parser.
103+
Exec RangedParserExec
104+
105+
// Type of [Parser] that failed.
106+
Type string
107+
}
108+
109+
// RangedParserExec details how a ranged [Combinator] was exeucted.
110+
type RangedParserExec struct {
111+
// Min is the minimum number of expected executions.
112+
Min uint
113+
114+
// Max is the maximum number of possible executions.
115+
Max uint
116+
117+
// Count contains the number of executions.
118+
Count uint
119+
}
120+
121+
// String returns a string representation of a [RangedParserExec].
122+
func (e RangedParserExec) String() string {
123+
var buf strings.Builder
124+
buf.WriteString(fmt.Sprintf("[count: %d", e.Count))
125+
if e.Min > 0 {
126+
buf.WriteString(fmt.Sprintf(" min: %d", e.Min))
127+
}
128+
129+
if e.Max > 0 {
130+
buf.WriteString(fmt.Sprintf(" max: %d", e.Max))
131+
}
132+
buf.WriteString("]")
133+
return buf.String()
134+
}
135+
136+
// RangeExecution ...
137+
func RangeExecution(i ...uint) RangedParserExec {
138+
exec := RangedParserExec{}
139+
140+
switch len(i) {
141+
case 1:
142+
exec.Count = i[0]
143+
case 2:
144+
exec.Count = i[0]
145+
exec.Min = i[1]
146+
case 3:
147+
exec.Count = i[0]
148+
exec.Min = i[1]
149+
exec.Max = i[2]
150+
}
151+
152+
return exec
153+
}
154+
155+
// Error returns a friendly string representation of the current error.
156+
func (e RangedParserError) Error() string {
157+
return fmt.Sprintf("(%s) parser failed %s. %v", e.Type, e.Exec, e.Err)
158+
}
159+
160+
// Unwrap returns the inner [CombinatorParseError].
161+
func (e RangedParserError) Unwrap() error {
162+
return e.Err
163+
}

sequence.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ func Repeat[T Result](c Combinator[T], n uint) Combinator[[]string] {
107107
for i := uint(0); i < n; i++ {
108108
var out T
109109
if rem, out, err = c(rem); err != nil {
110-
return rem, nil, ParserError{Err: err, Type: "repeat"}
110+
return rem, nil, RangedParserError{
111+
Err: err,
112+
Exec: RangeExecution(i, n),
113+
Type: "repeat",
114+
}
111115
}
112116
ext = combine(ext, out)
113117
}
@@ -139,9 +143,12 @@ func RepeatRange[T Result](c Combinator[T], n, m uint) Combinator[[]string] {
139143
if i+1 > n {
140144
break
141145
}
142-
return rem, nil, ParserError{Err: err, Type: "repeat_range"}
146+
return rem, nil, RangedParserError{
147+
Err: err,
148+
Exec: RangeExecution(i, n, m),
149+
Type: "repeat_range",
150+
}
143151
}
144-
145152
ext = combine(ext, out)
146153
}
147154

@@ -283,3 +290,53 @@ func All[T Result](c ...Combinator[T]) Combinator[[]string] {
283290
return rem, ext, nil
284291
}
285292
}
293+
294+
// Many will scan the input text and match the [Combinator] a minimum of one
295+
// time. The combinator will repeatedly be executed until the the first failed
296+
// match. This is the equivalent of calling [ManyN] with an argument of 1.
297+
//
298+
// chomp.Many(one.Of("Ho"))("Hello, World!")
299+
// // ("ello, World!", []string{"H"}, nil)
300+
func Many[T Result](c Combinator[T]) Combinator[[]string] {
301+
return func(s string) (string, []string, error) {
302+
return ManyN(c, 1)(s)
303+
}
304+
}
305+
306+
// ManyN will scan the input text and match the [Combinator] a minimum number
307+
// of times. The combinator will repeatedly be executed until the first failed
308+
// match. The minimum number of times must be executed for this combinator to
309+
// be successful.
310+
//
311+
// chomp.ManyN(chomp.OneOf("W"), 0)("Hello, World!")
312+
// // ("Hello, World!", nil, nil)
313+
func ManyN[T Result](c Combinator[T], n uint) Combinator[[]string] {
314+
return func(s string) (string, []string, error) {
315+
var ext []string
316+
var err error
317+
var count uint
318+
319+
rem := s
320+
for {
321+
var out T
322+
var tmpRem string
323+
324+
if tmpRem, out, err = c(rem); err != nil {
325+
break
326+
}
327+
rem = tmpRem
328+
ext = combine(ext, out)
329+
count++
330+
}
331+
332+
if count < n {
333+
return rem, nil, RangedParserError{
334+
Err: err,
335+
Exec: RangeExecution(count, n),
336+
Type: "many_n",
337+
}
338+
}
339+
340+
return rem, ext, nil
341+
}
342+
}

sequence_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,50 @@ func TestAll(t *testing.T) {
146146
assert.Equal(t, " ", ext[1])
147147
assert.Equal(t, "こんにちは、おはよう", ext[2])
148148
}
149+
150+
func TestMany(t *testing.T) {
151+
t.Parallel()
152+
153+
rem, ext, err := chomp.Many(chomp.OneOf("はんにこち"))("こんにちは、おはよう")
154+
155+
require.NoError(t, err)
156+
assert.Equal(t, "、おはよう", rem)
157+
require.Len(t, ext, 5)
158+
assert.Equal(t, "こ", ext[0])
159+
assert.Equal(t, "ん", ext[1])
160+
assert.Equal(t, "に", ext[2])
161+
assert.Equal(t, "ち", ext[3])
162+
assert.Equal(t, "は", ext[4])
163+
}
164+
165+
func TestManyNoMatches(t *testing.T) {
166+
t.Parallel()
167+
168+
_, _, err := chomp.Many(chomp.OneOf("eHl"))("Good Morning")
169+
170+
require.EqualError(t, err, "(many_n) parser failed [count: 0 min: 1]. (one_of) combinator failed to parse text 'Good Morning' with input 'eHl'")
171+
}
172+
173+
func TestManyN(t *testing.T) {
174+
t.Parallel()
175+
176+
rem, ext, err := chomp.ManyN(chomp.OneOf("eHl"), 2)("Hello and Good Morning")
177+
178+
require.NoError(t, err)
179+
assert.Equal(t, "o and Good Morning", rem)
180+
require.Len(t, ext, 4)
181+
assert.Equal(t, "H", ext[0])
182+
assert.Equal(t, "e", ext[1])
183+
assert.Equal(t, "l", ext[2])
184+
assert.Equal(t, "l", ext[3])
185+
}
186+
187+
func TestManyNZeroMatches(t *testing.T) {
188+
t.Parallel()
189+
190+
rem, ext, err := chomp.ManyN(chomp.OneOf("eHl"), 0)("Good Morning")
191+
192+
require.NoError(t, err)
193+
assert.Equal(t, "Good Morning", rem)
194+
assert.Empty(t, ext)
195+
}

0 commit comments

Comments
 (0)