Skip to content

Commit

Permalink
Added non-empty parser fold variants (#38)
Browse files Browse the repository at this point in the history
* Added foldRight1 and foldLeft1 and fixed bug in tokeniser

* Updated readme
  • Loading branch information
j-mie6 authored Jan 10, 2021
1 parent 90839e7 commit 65f551b
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ hello.runParser("hello world!") // returns Success(())
hello.runParser("hi world!") // returns Success(())
hello.runParser("hey world!") // returns a Failure

val natural: Parsley[Int] = lookAhead(digit) *> digit.foldLeft(0)((n, d) => n * 10 + d.asDigit) // lookahead ensures at least one digit
val natural: Parsley[Int] = digit.foldLeft1(0)((n, d) => n * 10 + d.asDigit)
natural.runParser("0") // returns Success(0)
natural.runParser("123) // returns Success(123)
```
Expand Down
24 changes: 24 additions & 0 deletions src/main/scala/parsley/Parsley.scala
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,30 @@ object Parsley
* @return the result of folding the results of `p` with `f` and `k`
*/
def foldLeft[B](k: B)(f: (B, A) => B): Parsley[B] = Combinator.chainPost(pure(k), map(x => (y: B) => f(y, x)))
/**
* A fold for a parser: `p.foldRight1(k)(f)` will try executing `p` many times until it fails, combining the
* results with right-associative application of `f` with a `k` at the right-most position. It must parse `p`
* at least once.
*
* @example {{{p.foldRight1(Nil)(_::_) == some(p) //some is more efficient, however}}}
*
* @param k base case for iteration
* @param f combining function
* @return the result of folding the results of `p` with `f` and `k`
*/
def foldRight1[B](k: B)(f: (A, B) => B): Parsley[B] = lift2(f, p, foldRight(k)(f))
/**
* A fold for a parser: `p.foldLeft1(k)(f)` will try executing `p` many times until it fails, combining the
* results with left-associative application of `f` with a `k` on the left-most position. It must parse `p`
* at least once.
*
* @example {{{val natural: Parsley[Int] = digit.foldLeft1(0)((x, d) => x * 10 + d.toInt)}}}
*
* @param k base case for iteration
* @param f combining function
* @return the result of folding the results of `p` with `f` and `k`
*/
def foldLeft1[B](k: B)(f: (B, A) => B): Parsley[B] = Combinator.chainPost(map(f(k, _)), map(x => (y: B) => f(y, x)))
/**
* This casts the result of the parser into a new type `B`. If the value returned by the parser
* is castable to type `B`, then this cast is performed. Otherwise the parser fails.
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/parsley/Token.scala
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ final class TokenParser(lang: LanguageDef)
* or "0O". Returns the value of the number.*/
lazy val octal: Parsley[Int] = lexeme('0' *> octal_)

private def number(base: Int, baseDigit: Parsley[Char]): Parsley[Int] = baseDigit.foldLeft(0)((x, d) => base*x + d.asDigit)
private def number(base: Int, baseDigit: Parsley[Char]): Parsley[Int] = baseDigit.foldLeft1(0)((x, d) => base*x + d.asDigit)

// White space & symbols
/**Lexeme parser `symbol(s)` parses `string(s)` and skips trailing white space.*/
Expand Down
26 changes: 25 additions & 1 deletion src/test/scala/parsley/CoreTests.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package parsley

import parsley.Parsley._
import parsley.Char.{char, satisfy}
import parsley.Char.{char, satisfy, digit}
import parsley.Implicits.{charLift, stringLift}

import scala.language.implicitConversions
Expand Down Expand Up @@ -261,6 +261,30 @@ class CoreTests extends ParsleyTest {
p.cast[String].runParser("") shouldBe a [Failure]
}

"foldRight" should "work correctly" in {
val p = 'a'.foldRight[List[Char]](Nil)(_::_)

p.runParser("") should be (Success(Nil))
p.runParser("aaa") should be (Success(List('a', 'a', 'a')))
}
"foldRight1" should "work correctly" in {
val p = 'a'.foldRight1[List[Char]](Nil)(_::_)
p.runParser("") shouldBe a [Failure]
p.runParser("aaa") should be (Success(List('a', 'a', 'a')))
}

"foldLeft" should "work correctly" in {
val p = digit.foldLeft(0)((x, d) => x * 10 + d.asDigit)

p.runParser("") should be (Success(0))
p.runParser("123") should be (Success(123))
}
"foldLeft1" should "work correctly" in {
val p = digit.foldLeft1(0)((x, d) => x * 10 + d.asDigit)
p.runParser("") shouldBe a [Failure]
p.runParser("123") should be (Success(123))
}

"stack overflows" should "not occur" in {
def repeat(n: Int, p: Parsley[Char]): Parsley[Char] = {
if (n > 0) p *> repeat(n-1, p)
Expand Down
16 changes: 16 additions & 0 deletions src/test/scala/parsley/TokeniserTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,22 @@ class TokeniserTests extends ParsleyTest {
tokeniser.integer.runParser("-0xb") should be (Success(-0xb))
}

"decimal" should "parse unsigned integers in the decimal system" in {
tokeniser.decimal.runParser("123") should be (Success(123))
}
it should "not succeed when given no input" in {
tokeniser.decimal.runParser("") shouldBe a [Failure]
}

"hexadecimal" should "parse unsigned hexadecimal integers" in {
tokeniser.hexadecimal.runParser("0xff") should be (Success(255))
}
it should "require at least one digit" in {
tokeniser.hexadecimal.runParser("") shouldBe a [Failure]
tokeniser.hexadecimal.runParser("0") shouldBe a [Failure]
tokeniser.hexadecimal.runParser("0x") shouldBe a [Failure]
}

"unsignedFloat" should "parse unsigned fractional floats" in {
tokeniser.unsignedFloat.runParser("3.142") should be (Success(3.142))
tokeniser.unsignedFloat.runParser("0.23") should be (Success(0.23))
Expand Down

0 comments on commit 65f551b

Please sign in to comment.