Skip to content

Instantly share code, notes, and snippets.

@sir-wabbit
Last active June 16, 2020 22:51
Show Gist options
  • Save sir-wabbit/504105d76b824163a0be4c85ea10f1c1 to your computer and use it in GitHub Desktop.
Save sir-wabbit/504105d76b824163a0be4c85ea10f1c1 to your computer and use it in GitHub Desktop.
trait CharReader {
def current(): Int
def advance(): Unit
}
object CharReader {
final val EOF: Int = -1
final val EOB: Int = -2
@silent final class FromString(val text: String, end: Int) extends CharReader {
var index = 0
override def current(): Int = if (index < text.length) text.charAt(index) else end
override def advance(): Unit = if (index < text.length) index += 1
}
}
sealed abstract class Result[+S, +E, +A](final val tag: Int) extends Product with Serializable
object Result {
final val TagOk: Int = 0
final val TagErr: Int = 1
final val TagEOB: Int = 2
final case class Ok[A](value: A) extends Result[Nothing, Nothing, A](TagOk)
final case class Err[E](value: E) extends Result[Nothing, E, Nothing](TagErr)
final case class Cont[S](value: S) extends Result[S, Nothing, Nothing](TagEOB)
}
import Result._
sealed trait ParseIntError extends Product with Serializable
object ParseIntError {
final case object UnexpectedEOF extends ParseIntError
final case object Overflow extends ParseIntError
final case object Empty extends ParseIntError
}
sealed abstract class ParseIntState(final val tag: Int) extends Product with Serializable
object ParseIntState {
final val TagInit: Int = 0
final val TagMain: Int = 1
final case object Init extends ParseIntState(TagInit)
final case class Main(first: Boolean, negative: Boolean, limit: Int, result: Int) extends ParseIntState(TagMain)
}
def readInt(reader: CharReader, state: ParseIntState = ParseIntState.Init): Result[ParseIntState, ParseIntError, Int] = {
@tailrec def main(first: Boolean, negative: Boolean, limit: Int, result: Int): Result[ParseIntState, ParseIntError, Int] = {
val ch = reader.current()
if (ch == CharReader.EOF) {
if (first) Err(ParseIntError.Empty)
else Ok(if (negative) result else -result)
}
else if (ch == CharReader.EOB) Cont(ParseIntState.Main(first, negative, limit, result))
else {
if ('0' <= ch && ch <= '9') {
val digit = ch - '0'
if (digit == 0 && first) {
reader.advance()
Ok(0)
} else {
// Note that we are calculating -VALUE since this allows us to avoid
// problems around Integer.MAX_VALUE.
if (result < limit / 10) Err(ParseIntError.Overflow)
else {
val a = result * 10
if (a < limit + digit) Err(ParseIntError.Overflow)
else {
reader.advance()
main(first = false, negative = negative, limit, a - digit)
}
}
}
} else if (first) Err(ParseIntError.Empty)
else Ok(if (negative) result else -result)
}
}
if (state.tag == ParseIntState.TagInit) {
// Get first char.
val firstChar = reader.current()
if (firstChar == CharReader.EOF) Err(ParseIntError.Empty)
else if (firstChar == CharReader.EOB) Cont(ParseIntState.Init)
else {
// Possible leading "+" or "-"
if (firstChar == '-') {
reader.advance()
main(first = true, negative = true, limit = Integer.MIN_VALUE, 0)
} else if (firstChar == '+') {
reader.advance()
main(first = true, negative = false, limit = -Integer.MAX_VALUE, 0)
}
else main(first = true, negative = false, limit = -Integer.MAX_VALUE, 0)
}
} else {
val s = state.asInstanceOf[ParseIntState.Main] : @silent
main(s.first, s.negative, s.limit, s.result)
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////
property("readInt satisfies its spec") {
import Parsing.Result._
def r(s: String): (String, Result[ParseIntState, ParseIntError, Int]) = {
@tailrec def go(state: Parsing.ParseIntState, rest: String): (String, Result[ParseIntState, ParseIntError, Int]) = {
if (rest.length < 3) {
val reader = new CharReader.FromString(rest, CharReader.EOF)
val r = Parsing.readInt(reader, state)
val l = reader.text.substring(reader.index)
l -> r
} else {
val reader = new CharReader.FromString(rest.substring(0, 3), CharReader.EOB)
Parsing.readInt(reader, state) match {
case r@(Ok(_) | Err(_)) =>
val l = reader.text.substring(reader.index) ++ rest.substring(3)
l -> r
case Cont(s) =>
assert(reader.index == reader.text.length)
go(s, rest.substring(3))
}
}
}
go(ParseIntState.Init, s)
}
assert(r("0") === ("" -> Ok(0)))
assert(r("1") === ("" -> Ok(1)))
assert(r("123456789") === ("" -> Ok(123456789)))
assert(r("123456789 ") === (" " -> Ok(123456789)))
assert(r(" 1") === (" 1" -> Err(ParseIntError.Empty)))
assert(r(" 1 ") === (" 1 " -> Err(ParseIntError.Empty)))
assert(r("- 1") === (" 1" -> Err(ParseIntError.Empty)))
assert(r("+ 1") === (" 1" -> Err(ParseIntError.Empty)))
assert(r(Int.MinValue.toString) === "" -> Ok(Int.MinValue))
assert(r(Int.MaxValue.toString) === "" -> Ok(Int.MaxValue))
assert(r((Int.MaxValue - 1).toString) === "" -> Ok(Int.MaxValue - 1))
assert(r((Int.MinValue + 1).toString) === "" -> Ok(Int.MinValue + 1))
assert(r("") === "" -> Err(ParseIntError.Empty))
assert(r("-") === "" -> Err(ParseIntError.Empty))
assert(r("+") === "" -> Err(ParseIntError.Empty))
assert(r("+-1") === "-1" -> Err(ParseIntError.Empty))
assert(r("-+1") === "+1" -> Err(ParseIntError.Empty))
assert(r("++1") === "+1" -> Err(ParseIntError.Empty))
assert(r("11 1") === " 1" -> Ok(11))
assert(r("123.1") === ".1" -> Ok(123))
assert(r("12-") === "-" -> Ok(12))
// Not an *Int*
assert(r("99999999999999999999999")._2 === Err(ParseIntError.Overflow))
assert(r(Long.MinValue.toString)._2 === Err(ParseIntError.Overflow))
assert(r(Long.MaxValue.toString)._2 === Err(ParseIntError.Overflow))
assert(r((Long.MaxValue - 1).toString)._2 === Err(ParseIntError.Overflow))
assert(r((Long.MinValue + 1).toString)._2 === Err(ParseIntError.Overflow))
assert(r((Int.MaxValue.toLong + 1).toString)._2 === Err(ParseIntError.Overflow))
assert(r((Int.MinValue.toLong - 1).toString)._2 === Err(ParseIntError.Overflow))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment