diff --git a/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala b/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala index efe12579..4f50225e 100644 --- a/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala +++ b/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala @@ -792,6 +792,42 @@ trait Parsers { applyp(in) } + /** A parser generator for a specified range of repetitions interleaved by a + * separator. + * + * `repNM(n, m, p, s)` uses `p` at least `n` times and up to `m` times, interleaved + * with separator `s`, to parse the input + * (the result is a `List` of at least `n` consecutive results of `p` and up to `m` results). + * + * @param n minimum number of repetitions + * @param m maximum number of repetitions + * @param p a `Parser` that is to be applied successively to the input + * @param sep a `Parser` that interleaves with p + * @return A parser that returns a list of results produced by repeatedly applying `p` interleaved + * with `sep` to the input. The list has a size between `n` and up to `m` + * (and that only succeeds if `p` matches at least `n` times). + */ + def repNM[T](n: Int, m: Int, p: Parser[T], sep: Parser[Any] = success(())): Parser[List[T]] = Parser { in => + val mandatory = if (n == 0) success(Nil) else (p ~ repN(n - 1, sep ~> p)).map { case head ~ tail => head :: tail } + val elems = new ListBuffer[T] + + def continue(in: Input): ParseResult[List[T]] = { + val p0 = sep ~> p // avoid repeatedly re-evaluating by-name parser + @tailrec def applyp(in0: Input): ParseResult[List[T]] = p0(in0) match { + case Success(x, rest) => elems += x; if (elems.length == m) Success(elems.toList, rest) else applyp(rest) + case e @ Error(_, _) => e // still have to propagate error + case _ => Success(elems.toList, in0) + } + + applyp(in) + } + + mandatory(in) match { + case Success(x, rest) => elems ++= x; continue(rest) + case ns: NoSuccess => ns + } + } + /** A parser generator for non-empty repetitions. * * `rep1sep(p, q)` repeatedly applies `p` interleaved with `q` to parse the diff --git a/shared/src/test/scala/scala/util/parsing/combinator/gh242.scala b/shared/src/test/scala/scala/util/parsing/combinator/gh242.scala new file mode 100644 index 00000000..2fe21170 --- /dev/null +++ b/shared/src/test/scala/scala/util/parsing/combinator/gh242.scala @@ -0,0 +1,149 @@ +import org.junit.Assert.assertEquals +import org.junit.Test + +import scala.util.parsing.combinator.Parsers +import scala.util.parsing.input.CharSequenceReader + +class gh242 { + class TestWithSeparator extends Parsers { + type Elem = Char + val csv: Parser[List[Char]] = repNM(5, 10, 'a', ',') + } + + class TestWithoutSeparator extends Parsers { + type Elem = Char + val csv: Parser[List[Char]] = repNM(5, 10, 'a') + } + + @Test + def testEmpty(): Unit = { + val tstParsers = new TestWithSeparator + val s = new CharSequenceReader("") + val expectedFailure = """[1.1] failure: end of input + | + | + |^""".stripMargin + assertEquals(expectedFailure, tstParsers.csv(s).toString) + } + + @Test + def testBelowMinimum(): Unit = { + val tstParsers = new TestWithSeparator + val s = new CharSequenceReader("a,a,a,a") + val expectedFailure = """[1.8] failure: end of input + | + |a,a,a,a + | ^""".stripMargin + assertEquals(expectedFailure, tstParsers.csv(s).toString) + } + + @Test + def testMinimum(): Unit = { + val tstParsers = new TestWithSeparator + val s = new CharSequenceReader("a,a,a,a,a") + val expected = List.fill[Char](5)('a') + val actual = tstParsers.csv(s) + assertEquals(9, actual.next.offset) + assert(actual.successful) + assertEquals(expected, actual.get) + } + + @Test + def testInRange(): Unit = { + val tstParsers = new TestWithSeparator + val s = new CharSequenceReader("a,a,a,a,a,a,a,a") + val expected = List.fill[Char](8)('a') + val actual = tstParsers.csv(s) + assertEquals(15, actual.next.offset) + assert(actual.successful) + assertEquals(expected, actual.get) + } + + @Test + def testMaximum(): Unit = { + val tstParsers = new TestWithSeparator + val s = new CharSequenceReader("a,a,a,a,a,a,a,a,a,a") + val expected = List.fill[Char](10)('a') + val actual = tstParsers.csv(s) + assertEquals(19, actual.next.offset) + assert(actual.successful) + assertEquals(expected, actual.get) + } + + @Test + def testAboveMaximum(): Unit = { + val tstParsers = new TestWithSeparator + val s = new CharSequenceReader("a,a,a,a,a,a,a,a,a,a,a,a") + val expected = List.fill[Char](10)('a') + val actual = tstParsers.csv(s) + assertEquals(19, actual.next.offset) + assert(actual.successful) + assertEquals(expected, actual.get) + } + + @Test + def testEmptyWithoutSep(): Unit = { + val tstParsers = new TestWithoutSeparator + val s = new CharSequenceReader("") + val expectedFailure = """[1.1] failure: end of input + | + | + |^""".stripMargin + assertEquals(expectedFailure, tstParsers.csv(s).toString) + } + + @Test + def testBelowMinimumWithoutSep(): Unit = { + val tstParsers = new TestWithoutSeparator + val s = new CharSequenceReader("aaaa") + val expectedFailure = """[1.5] failure: end of input + | + |aaaa + | ^""".stripMargin + assertEquals(expectedFailure, tstParsers.csv(s).toString) + } + + @Test + def testMinimumWithoutSep(): Unit = { + val tstParsers = new TestWithoutSeparator + val s = new CharSequenceReader("aaaaa") + val expected = List.fill[Char](5)('a') + val actual = tstParsers.csv(s) + assertEquals(5, actual.next.offset) + assert(actual.successful) + assertEquals(expected, actual.get) + } + + @Test + def testInRangeWithoutSep(): Unit = { + val tstParsers = new TestWithoutSeparator + val s = new CharSequenceReader("aaaaaaaa") + val expected = List.fill[Char](8)('a') + val actual = tstParsers.csv(s) + assertEquals(8, actual.next.offset) + assert(actual.successful) + assertEquals(expected, actual.get) + } + + @Test + def testMaximumWithoutSep(): Unit = { + val tstParsers = new TestWithoutSeparator + val s = new CharSequenceReader("aaaaaaaaaa") + val expected = List.fill[Char](10)('a') + val actual = tstParsers.csv(s) + assertEquals(10, actual.next.offset) + assert(actual.successful) + assertEquals(expected, actual.get) + } + + @Test + def testAboveMaximumWithoutSep(): Unit = { + val tstParsers = new TestWithoutSeparator + val s = new CharSequenceReader("aaaaaaaaaaaa") + val expected = List.fill[Char](10)('a') + val actual = tstParsers.csv(s) + assertEquals(10, actual.next.offset) + assert(actual.successful) + assertEquals(expected, actual.get) + } +}