From 96acad49529510829a56b0adbdd11b1e9a978f35 Mon Sep 17 00:00:00 2001 From: pgrandjean Date: Wed, 16 Oct 2019 13:54:33 -0400 Subject: [PATCH 1/3] issue #242 implementation of repMN --- .../util/parsing/combinator/Parsers.scala | 51 ++++++ .../scala/util/parsing/combinator/gh242.scala | 149 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 shared/src/test/scala/scala/util/parsing/combinator/gh242.scala 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..2b11c729 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,57 @@ trait Parsers { applyp(in) } + /** A parser generator for a specified range of repetitions interleaved by a + * separator. + * + * `repN(n, m, p, s)` uses `p` at least `m` times and up to `n` times, interleaved + * with separator `s`, to parse the input + * (the result is a `List` of at least `m` consecutive results of `p` and up to `n` results). + * + * @param m minimum number of repetitions + * @param n 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 `m` and up to `n` + * (and that only succeeds if `p` matches at least `n` times). + */ + def repMN[T](m: Int, n: Int, p: Parser[T], sep: Parser[Any]): Parser[List[T]] = Parser { in => + require(0 <= m && m <= n) + val mandatory = if (m == 0) success(Nil) else (p ~ repN(m - 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 == n) 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 a specified range of repetitions. + * + * `repN(n, m, p)` uses `p` at least `m` times and up to `n` times to parse the input + * (the result is a `List` of at least `m` consecutive results of `p` and up to `n` results). + * + * @param m minimum number of repetitions + * @param n maximum number of repetitions + * @param p a `Parser` that is to be applied successively to the input + * @return A parser that returns a list of results produced by repeatedly applying `p` to the input. + * The list has a size between `m` and up to `n` + * (and that only succeeds if `p` matches at least `n` times). + */ + def repMN[T](m: Int, n: Int, p: Parser[T]): Parser[List[T]] = repMN[T](m, n, p, success(())) + /** 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..d3a57187 --- /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]] = repMN(5, 10, 'a', ',') + } + + class TestWithoutSeparator extends Parsers { + type Elem = Char + val csv: Parser[List[Char]] = repMN(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) + } +} From a65cbec4bde41c4ebc17e7374a3dfce9a2a1814b Mon Sep 17 00:00:00 2001 From: Patrick Grandjean Date: Sat, 26 Oct 2019 20:24:50 -0400 Subject: [PATCH 2/3] code review PR #245 --- .../util/parsing/combinator/Parsers.scala | 31 +++++-------------- .../scala/util/parsing/combinator/gh242.scala | 4 +-- 2 files changed, 10 insertions(+), 25 deletions(-) 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 2b11c729..4f50225e 100644 --- a/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala +++ b/shared/src/main/scala/scala/util/parsing/combinator/Parsers.scala @@ -795,27 +795,26 @@ trait Parsers { /** A parser generator for a specified range of repetitions interleaved by a * separator. * - * `repN(n, m, p, s)` uses `p` at least `m` times and up to `n` times, interleaved + * `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 `m` consecutive results of `p` and up to `n` results). + * (the result is a `List` of at least `n` consecutive results of `p` and up to `m` results). * - * @param m minimum number of repetitions - * @param n maximum number of repetitions + * @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 `m` and up to `n` + * 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 repMN[T](m: Int, n: Int, p: Parser[T], sep: Parser[Any]): Parser[List[T]] = Parser { in => - require(0 <= m && m <= n) - val mandatory = if (m == 0) success(Nil) else (p ~ repN(m - 1, sep ~> p)).map { case head ~ tail => head :: tail } + 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 == n) Success(elems.toList, rest) else applyp(rest) + 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) } @@ -829,20 +828,6 @@ trait Parsers { } } - /** A parser generator for a specified range of repetitions. - * - * `repN(n, m, p)` uses `p` at least `m` times and up to `n` times to parse the input - * (the result is a `List` of at least `m` consecutive results of `p` and up to `n` results). - * - * @param m minimum number of repetitions - * @param n maximum number of repetitions - * @param p a `Parser` that is to be applied successively to the input - * @return A parser that returns a list of results produced by repeatedly applying `p` to the input. - * The list has a size between `m` and up to `n` - * (and that only succeeds if `p` matches at least `n` times). - */ - def repMN[T](m: Int, n: Int, p: Parser[T]): Parser[List[T]] = repMN[T](m, n, p, success(())) - /** 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 index d3a57187..2fe21170 100644 --- a/shared/src/test/scala/scala/util/parsing/combinator/gh242.scala +++ b/shared/src/test/scala/scala/util/parsing/combinator/gh242.scala @@ -7,12 +7,12 @@ import scala.util.parsing.input.CharSequenceReader class gh242 { class TestWithSeparator extends Parsers { type Elem = Char - val csv: Parser[List[Char]] = repMN(5, 10, 'a', ',') + val csv: Parser[List[Char]] = repNM(5, 10, 'a', ',') } class TestWithoutSeparator extends Parsers { type Elem = Char - val csv: Parser[List[Char]] = repMN(5, 10, 'a') + val csv: Parser[List[Char]] = repNM(5, 10, 'a') } @Test From 93ec1e08f66c8a42cc6f9a43f0805972eb638a8d Mon Sep 17 00:00:00 2001 From: Patrick Grandjean Date: Fri, 15 Nov 2019 19:10:01 -0500 Subject: [PATCH 3/3] Trigger Travis