diff --git a/TODO.txt b/TODO.txt index 8fb51d9..848c55f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,4 +1,4 @@ -Add repetition parsers to avoid some recursion +Write Repeat tests Think about not requiring so much Pos() when making messages Rename Seq2 to Seq Document Seq diff --git a/gigaparsec.go b/gigaparsec.go index f660679..d6ddc02 100644 --- a/gigaparsec.go +++ b/gigaparsec.go @@ -342,3 +342,30 @@ func Pipe[In, Ignore, Through any](p Parser[In, Ignore]) func(Through) Parser[In }) } } + +// Repeat applies p until p fails, and returns the collected outputs. +// It succeeds if and only if p succeeds at least minCount times. +// It consumes if and only if at least one of the applications of p consumes. +func Repeat[In, Out any](minCount int, p Parser[In, Out]) Parser[In, []Out] { + return func(s State[In]) (Result[In, []Out], error) { + var values []Out + var consumed bool + next := s + for { + result, err := p(s) + if err != nil { + return Result[In, []Out]{}, fmt.Errorf("AtLeastN: %w", err) + } + consumed = consumed || result.Consumed() + if failed, _, msg := result.Failed(); failed { + if len(values) >= minCount { + return Succeed(consumed, values, next, MessageOK(s.Pos())), nil + } + return Fail[In, []Out](consumed, msg), nil + } + var value Out + _, _, value, next, _ = result.Succeeded() + values = append(values, value) + } + } +} diff --git a/parser_test.go b/parser_test.go index 088dda0..707f4d0 100644 --- a/parser_test.go +++ b/parser_test.go @@ -137,3 +137,10 @@ func TestLabel(t *testing.T) { func TestEnd(t *testing.T) { Todo(t) } + +func TestRepeat(t *testing.T) { + t.Run("fails when number of successes is less than minCount", Todo) + t.Run("succeeds when number of successes is greater than minCount", Todo) + t.Run("consumes iff at least one application consumes", Todo) + t.Run("fails on error", Todo) +}