Skip to content

Commit

Permalink
TomlParser: Add full support for date and time formats
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
tindzk committed Feb 24, 2018
1 parent bb065e7 commit fd655a1
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 31 deletions.
68 changes: 45 additions & 23 deletions src/main/scala/stoml/TomlParser.scala
@@ -1,11 +1,18 @@
package stoml

import java.time.{
LocalDateTime => JLocalDateTime,
LocalTime => JLocalTime,
LocalDate => JLocalDate,
OffsetDateTime => JOffsetDateTime,
ZoneOffset
}

import java.io.File

import fastparse.all._

import scala.language.{implicitConversions, postfixOps}
import java.util.{Date => JDate}
import java.text.SimpleDateFormat

private[stoml] trait Common {
type Key = String
Expand Down Expand Up @@ -53,7 +60,10 @@ object Toml extends TomlSymbol with Common {

case class Arr(elem: Seq[Elem]) extends AnyVal with Elem

case class Date(elem: JDate) extends AnyVal with Elem
case class Date (elem: JLocalDate ) extends AnyVal with Elem
case class Time (elem: JLocalTime ) extends AnyVal with Elem
case class DateTime (elem: JLocalDateTime ) extends AnyVal with Elem
case class OffsetDateTime(elem: JOffsetDateTime) extends AnyVal with Elem

case class Pair(elem: (String, Elem)) extends AnyVal with Node

Expand All @@ -79,7 +89,6 @@ trait ParserUtil { this: TomlSymbol =>
import Toml.NamedFunction

val Whitespace = NamedFunction(WSChars.contains(_: Char), "Whitespace")
val Digits = NamedFunction('0' to '9' contains (_: Char), "Digits")
val Letters =
NamedFunction((('a' to 'z') ++ ('A' to 'Z')).contains(_: Char), "Letters")
val UntilNewline =
Expand Down Expand Up @@ -109,7 +118,7 @@ trait TomlParser extends ParserUtil with TomlSymbol {

val letters = P(CharsWhile(Letters))
val digit = P(CharIn('0' to '9'))
val digits = P(CharsWhile(Digits))
val digits = P(digit.rep(1))

val skipEscapedDoubleQuote = P("\\" ~ "\"")
val literalChars = NamedFunction(!SingleQuote.contains(_: Char), "LitStr")
Expand Down Expand Up @@ -148,27 +157,40 @@ trait TomlParser extends ParserUtil with TomlSymbol {
val `false` = P("false").map(_ => False)
val boolean: Parser[Bool] = P(`true` | `false`)

lazy val date: Parser[Date] =
rfc3339.opaque("<valid-date-rfc3339>").map { t =>
/* Even though this extra parsing is not necessary,
* it is done just for simplicity, avoiding the use
* of `java.util.Calendar` instances. */
Date(formatter.parse(t))
}
private val TenPowers =
List(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000)

val localTime: Parser[Time] = P(
digit.rep(2).! ~ ":" ~ digit.rep(2).! ~ ":" ~ digit.rep(2).! ~ ("." ~ digit.rep.!).?
).map { case (h, m, s, ns) =>
val nano = ns.map { str =>
val digits = str.length
str.toInt * TenPowers(9 - digits)
}.getOrElse(0)

private val formatter =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
Time(JLocalTime.of(h.toInt, m.toInt, s.toInt, nano))
}

def twice[T](p: Parser[T]) = p ~ p
val localDate: Parser[Date] = P(
digit.rep(4).! ~ "-" ~ digit.rep(2).! ~ "-" ~ digit.rep(2).!
).map { case (y, m, d) =>
Date(JLocalDate.of(y.toInt, m.toInt, d.toInt))
}

def fourTimes[T](p: Parser[T]) = twice(p) ~ twice(p)
val localDateTime: Parser[DateTime] = P(
localDate ~ "T" ~ localTime
).map { case (date, time) =>
DateTime(JLocalDateTime.of(date.elem, time.elem))
}

val offsetDateTime: Parser[OffsetDateTime] = P(
localDateTime ~ ("Z" | (("-" | "+") ~ digit.rep(2) ~ ":" ~ digit.rep(2))).!
).map { case (dateTime, offset) =>
OffsetDateTime(
JOffsetDateTime.of(dateTime.elem, ZoneOffset.of(offset)))
}

val rfc3339: Parser[String] = P {
fourTimes(digit) ~ "-" ~ twice(digit) ~ "-" ~
twice(digit) ~ "T" ~ twice(digit) ~ ":" ~
twice(digit) ~ ":" ~ twice(digit) ~ ("." ~
digit.rep(min = 3, max = 3)).? ~ "Z".?
}.!
val date = P(offsetDateTime | localDateTime | localDate | localTime)

val dashes = P(CharIn(Dashes))
val bareKey = P((letters | digits | dashes).rep(min = 1)).!
Expand Down Expand Up @@ -197,7 +219,7 @@ trait TomlParser extends ParserUtil with TomlSymbol {
}

lazy val elem: Parser[Elem] = P {
WS ~ (string | boolean | double | integer | array | inlineTable | date) ~ WS
WS ~ (date | string | boolean | double | integer | array | inlineTable) ~ WS
}

lazy val node: Parser[Node] = P(WS ~ (pair | table | tableArray) ~ WS)
Expand Down
81 changes: 73 additions & 8 deletions src/test/scala/stoml/DateTomlSpec.scala
@@ -1,9 +1,10 @@
package stoml

import java.time._

import org.scalacheck.Gen
import org.scalatest.prop._
import org.scalatest.{Matchers, PropSpec}
import fastparse.core.Parsed.Success
import org.scalatest.{FunSuite, Matchers, PropSpec}

trait DateTomlGen {
import Gen.chooseNum
Expand All @@ -15,27 +16,27 @@ trait DateTomlGen {

/* This check is not covering all the formats */
val dateFormatGen: Gen[String] = for {
day <- genNum(2, 0, 28)
day <- genNum(2, 1, 28)
month <- genNum(2, 1, 12)
year <- genNum(4, 0, 2200)
hour <- genNum(2, 0, 23)
minute <- genNum(2, 0, 59)
second <- genNum(2, 0, 59)
micro <- genNum(3, 0, 999)
} yield (year + "-" + month + "-" + day +
"T" + hour + ":" + minute + ":" +
second + "." + micro + "Z")
} yield year + "-" + month + "-" + day +
"T" + hour + ":" + minute + ":" +
second + "." + micro + "Z"
}

class DateTomlSpec extends PropSpec
class DateTomlSpec extends PropSpec
with PropertyChecks with Matchers
with DateTomlGen with TomlParser
with TestParserUtil {

property("parse dates following the RFC 3339 spec (`date` parser)") {
forAll(dateFormatGen) {
s =>
shouldBeSuccess(date.parse(s))
shouldBeSuccess(offsetDateTime.parse(s))
}
}

Expand All @@ -46,3 +47,67 @@ class DateTomlSpec extends PropSpec
}
}
}

class DataTomlUnitSpec extends FunSuite
with PropertyChecks with Matchers
with DateTomlGen with TomlParser
with TestParserUtil {

test("Parse local date") {
val toml = "ld = 1979-05-27"
val nodes = testSuccess(toml)
assert(nodes(0) == Toml.Pair("ld", Toml.Date(LocalDate.of(1979, 5, 27))))
}

test("Parse local time") {
val toml =
"""
|lt1 = 07:32:00
|lt2 = 00:32:00.999999
|lt3 = 00:32:00.555
""".stripMargin

val nodes = testSuccess(toml)
assert(nodes(0) == Toml.Pair("lt1", Toml.Time(LocalTime.of(7, 32, 0, 0))))
assert(nodes(1) == Toml.Pair("lt2", Toml.Time(LocalTime.of(0, 32, 0, 999999000))))
assert(nodes(2) == Toml.Pair("lt3", Toml.Time(LocalTime.of(0, 32, 0, 555000000))))
}

test("Parse local date time") {
val toml =
"""
|ldt1 = 1979-05-27T07:32:00
|ldt2 = 1979-05-27T00:32:00.999999
""".stripMargin
val nodes = testSuccess(toml)
assert(nodes(0) == Toml.Pair("ldt1", Toml.DateTime(LocalDateTime.of(
LocalDate.of(1979, 5, 27), LocalTime.of(7, 32, 0, 0)))))
assert(nodes(1) == Toml.Pair("ldt2", Toml.DateTime(LocalDateTime.of(
LocalDate.of(1979, 5, 27), LocalTime.of(0, 32, 0, 999999000)))))
}

test("Parse offset date time") {
val toml =
"""
|odt1 = 1979-05-27T07:32:00Z
|odt2 = 1979-05-27T00:32:00-07:00
|odt3 = 1979-05-27T00:32:00.999999-07:00
""".stripMargin
val nodes = testSuccess(toml)
assert(nodes(0) == Toml.Pair("odt1", Toml.OffsetDateTime(
OffsetDateTime.of(
LocalDateTime.of(
LocalDate.of(1979, 5, 27), LocalTime.of(7, 32, 0)
), ZoneOffset.of("Z")))))
assert(nodes(1) == Toml.Pair("odt2", Toml.OffsetDateTime(
OffsetDateTime.of(
LocalDateTime.of(
LocalDate.of(1979, 5, 27), LocalTime.of(0, 32, 0)
), ZoneOffset.of("-07:00")))))
assert(nodes(2) == Toml.Pair("odt3", Toml.OffsetDateTime(
OffsetDateTime.of(
LocalDateTime.of(
LocalDate.of(1979, 5, 27), LocalTime.of(0, 32, 0, 999999000)
), ZoneOffset.of("-07:00")))))
}
}
6 changes: 6 additions & 0 deletions src/test/scala/stoml/TestParserUtil.scala
Expand Up @@ -7,6 +7,12 @@ import org.scalatest.Matchers
trait TestParserUtil {
this: Matchers =>

def testSuccess(example: String): Seq[Toml.Node] =
TomlParserApi.nodes.parse(example) match {
case Success(v, _) => v
case f: Failure[_, _] => fail(s"Failed to parse `$example`: ${f.msg}")
}

def shouldBeSuccess[T](r: Parsed[T]) = r match {
case s: Success[T, _, _] =>
case f: Failure[_, _] => fail(s"$r is not a Success.")
Expand Down

0 comments on commit fd655a1

Please sign in to comment.