All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kuyfi.TZDBParser.scala Maven / Gradle / Ivy

The newest version!
package kuyfi

import java.time.{DayOfWeek, LocalTime, Month}
import java.time.format.TextStyle
import java.util.Locale

import atto.ParseResult.{Done, Fail}
import better.files.File
import shapeless.{Coproduct, _}
import shapeless.ops.coproduct.Inject

import scalaz.effect.IO

/**
  * Model of the TimeZone Database
  */
object TZDB {

  /**
    * Definition of timestamps
    */
  sealed trait At extends Product with Serializable {
    def time: LocalTime
  }
  case class AtWallTime(time: LocalTime) extends At
  case class AtStandardTime(time: LocalTime) extends At
  case class AtUniversalTime(time: LocalTime) extends At

  /**
    * Model for Zone entries on TZDB
    */
  case class GmtOffset(h: Int, m: Int, s: Int)
  case class Until(y: Int, m: Option[Month], d: Option[DayOfTheMonth], at: Option[At])
  case class ZoneTransition(at: GmtOffset, rules: String, format: String, until: Option[Until])
  case class Zone(name: String, transitions: List[ZoneTransition])  extends Product with Serializable

  /**
    * Model for Rule Entries
    */
  case class Letter(letter: String)
  case class Save(time: LocalTime)

  sealed trait On extends Product with Serializable
  case class DayOfTheMonth(i: Int) extends On
  case class LastWeekday(d: DayOfWeek) extends On
  case class AfterWeekday(d: DayOfWeek, day: Int) extends On
  case class BeforeWeekday(d: DayOfWeek, day: Int) extends On

  sealed trait Year extends Product with Serializable
  case class GivenYear(year: Int) extends Year
  case object Minimum extends Year
  case object Maximum extends Year
  case object Only extends Year
  case class Rule(name: String, from: Year, to: Year, month: Month, on: On, at: At, save: Save, letter: Letter) extends Product with Serializable

  /**
    * Model for Link entries
    */
  case class Link(from: String, to: String) extends Product with Serializable

  /**
    * Comments and blank lines
    */
  case class Comment(comment: String) extends Product with Serializable
  case class BlankLine(line: String) extends Product with Serializable

  /**
    * Coproduct for the content of lines on the parsed files
    */
  type Row = Comment :+: BlankLine :+: Link :+: Rule :+: Zone :+: CNil

  implicit class ToCoproduct[A](val a: A) extends AnyVal {
    def liftC[C <: Coproduct](implicit inj: Inject[C, A]): C = Coproduct[C](a)
  }

}

/**
  * Defines atto parsers to read tzdb files
  */
object TZDBParser {
  import TZDB._
  import scalaz._
  import scalaz.effect._
  import Scalaz._
  import atto._, Atto.{char => chr, _}, compat.scalaz._
  import better.files._

  // Useful Monoid
  implicit def parserListMonoid[A]: Monoid[ParseResult[List[A]]] =
    Monoid.instance[ParseResult[List[A]]]((a, b) => (a, b) match {
      case (Done(u, x), Done(v, y)) => Done(u + v, x ::: y)
      case _                        => Fail("", Nil, "Can only handle full response ")
    }, Done("", List.empty[A]))

  implicit class Parser2Coproduct[A](val a: Parser[A]) extends AnyVal {
    def liftC[C <: shapeless.Coproduct](implicit inj: shapeless.ops.coproduct.Inject[C, A]): Parser[C] = a.map(_.liftC[C])
  }

  private val space = chr(' ')
  private val semicolon = chr(':')
  private val tab = chr('\t')
  private val nl = chr('\n')
  private val identifier = stringOf1(noneOf(" \t\n"))

  private val whitespace: Parser[Char] = tab | space
  private val linkSeparator: Parser[List[Char]] = many(whitespace)

  private val months: List[(String, Month)] = Month.values().map(m => (m.getDisplayName(TextStyle.SHORT, Locale.ENGLISH), m)).toList
  private val days: List[(String, DayOfWeek)] = DayOfWeek.values().map(m => (m.getDisplayName(TextStyle.SHORT, Locale.ENGLISH), m)).toList

  val from: Parser[String] =
    stringOf1(digit) |
    string("minimum") |
    string("maximum")

  val fromParser: Parser[Year] = {
    stringOf1(digit).map(y => GivenYear(y.toInt)) |
    string("minimum").map(_ => Minimum: Year) |
    string("maximum").map(_ => Maximum: Year) |
    string("max").map(_ => Maximum: Year) |
    string("min").map(_ => Minimum: Year)
  }

  val toParser: Parser[Year] = {
    stringOf1(digit).map(y => GivenYear(y.toInt)) |
    string("minimum").map(_ => Minimum: Year) |
    string("maximum").map(_ => Maximum: Year) |
    string("max").map(_ => Maximum: Year) |
    string("min").map(_ => Minimum: Year) |
    string("only").map(_ => Only: Year)
  }

  def parseOneOf[A](items: List[(String, A)], msg: String): Parser[A] = {
    val p = items.map {
      case (i, v) => string(i).map(_ => v)
    }

    p.foldRight(err[A](msg))(_ | _)
  }

  val monthParser: Parser[Month] = parseOneOf(months, "unknown month")

  val dayParser: Parser[DayOfWeek] = parseOneOf(days, "unknown day")

  val afterWeekdayParser: Parser[On] =
    for {
      d <- opt(space) ~> dayParser <~ string(">=")
      a <- int
    } yield AfterWeekday(d, a)

  val beforeWeekdayParser: Parser[On] =
    for {
      d <- opt(space) ~> dayParser <~ string("<=")
      a <- int
    } yield BeforeWeekday(d, a)

  val lastWeekdayParser: Parser[On] =
    for {
      _ <- string("last")
      d <- dayParser
    } yield LastWeekday(d)

  val onParser: Parser[On] =
    (opt(space) ~> int.map(DayOfTheMonth.apply)) |
    afterWeekdayParser |
    beforeWeekdayParser |
    lastWeekdayParser

  val timePartParser: Parser[Char] =
    digit | semicolon

  val hourMinParser: Parser[(Int, Int)] =
    for {
      _ <- opt(space)
      h <- int <~ semicolon
      m <- int
    } yield (h, m)

  val hourMinParserLT: Parser[LocalTime] = hourMinParser.map {
    case (h, m) => LocalTime.of(fixHourRange(h), m)
  }

  val hourMinParserOf: Parser[GmtOffset] = hourMinParser.map {
    case (h, m) => GmtOffset(h, m, 0)
  }

  val hourMinSecParser: Parser[(Boolean, Int, Int, Int)] =
    for {
      n <- opt(chr('-'))
      _ <- opt(space)
      h <- int <~ semicolon
      m <- int <~ semicolon
      s <- int
    } yield (n.isDefined, h, m, s)

  val hourMinSecParserLT: Parser[LocalTime] = hourMinSecParser.map {
      case (_, h, m, s) => LocalTime.of(fixHourRange(h), m, s)
    }

  val hourMinSecParserOf: Parser[GmtOffset] = hourMinSecParser.map {
      case (n, h, m, s) => GmtOffset(n ? -h | h, (n && h === 0) ? -m | m, s)
    }

  val timeParser: Parser[LocalTime] =
    opt(many(whitespace)) ~>
    hourMinSecParserLT |
    hourMinParserLT |
    int.map(h => LocalTime.of(fixHourRange(h), 0))

  private def fixHourRange(h: Int) = (h === 24) ? 0 | h

  val gmtOffsetParser: Parser[GmtOffset] =
    hourMinSecParserOf |
    hourMinParserOf |
    int.map(h => GmtOffset(h, 0, 0))

  val atParser: Parser[At] =
    (timeParser ~ chr('w')).map(x => AtWallTime(x._1): At) |
    (timeParser ~ chr('s')).map(x => AtStandardTime(x._1): At) |
    (timeParser ~ chr('u')).map(x => AtUniversalTime(x._1): At) |
    timeParser.map(x => AtWallTime(x): At)

  val saveParser: Parser[Save] =
    timeParser.map(x => Save(x))

  val toEndLine: Parser[String] = takeWhile(_ =/= '\n') <~ opt(nl)

  val letterParser: Parser[Letter] =
    for {
      l <- takeWhile(c => c.isUpper || c === '-')
      _ <- toEndLine
    } yield Letter(l)

  val ruleParser: Parser[Rule] =
    for {
      _      <- string("Rule") <~ whitespace
      name   <- identifier <~ whitespace
      from   <- fromParser <~ whitespace
      to     <- toParser <~ whitespace
      _      <- chr('-') <~ whitespace
      month  <- monthParser <~ whitespace
      on     <- onParser <~ whitespace
      at     <- atParser <~ whitespace
      save   <- saveParser <~ whitespace
      letter <- letterParser
    } yield Rule(name, from, to, month, on, at, save, letter)

  val linkParser: Parser[Link] =
    for {
      _    <- string("Link") <~ linkSeparator
      from <- identifier <~ linkSeparator
      to   <- identifier
      _    <- toEndLine
    } yield Link(from, to)

  val untilParser: Parser[Until] =
    for {
      year  <- int <~ opt(whitespace)
      month <- opt(monthParser) <~ many(whitespace)
      day   <- opt(int.map(DayOfTheMonth.apply)) <~ many(whitespace)
      at    <- opt(atParser)
      _     <- toEndLine
    } yield Until(year, month, day, at)

  val commentParser: Parser[Comment] =
    chr('#') ~> toEndLine.map(Comment.apply)

  val zoneTransitionParser: Parser[ZoneTransition] =
    for {
      gmtOff <- many(whitespace) ~> gmtOffsetParser <~ whitespace
      rules  <- identifier <~ many(whitespace)
      format <- identifier <~ many(whitespace)
      until  <- opt(untilParser)
      _      <- opt(many(commentParser))
    } yield ZoneTransition(gmtOff, rules, format, until)

  val continuationZoneTransitionParser: Parser[ZoneTransition] =
    for {
      _      <- manyN(3, whitespace)
      gmtOff <- gmtOffsetParser <~ whitespace
      rules  <- opt(whitespace) ~> identifier <~ many(whitespace)
      format <- identifier <~ many(whitespace)
      until  <- opt(untilParser)
      _      <- opt(many(many(whitespace) ~> commentParser))
    } yield ZoneTransition(gmtOff, rules, format, until)

  val zoneTransitionListParser: Parser[List[ZoneTransition]] =
    (zoneTransitionParser ~ many(continuationZoneTransitionParser)).map { case (a, b) => a :: b }

  val zoneParser: Parser[Zone] =
    for {
      _      <- string("Zone") <~ whitespace
      name   <- identifier <~ whitespace
      trans  <- zoneTransitionListParser
    } yield Zone(name, trans)

  val blankLine: Parser[BlankLine] =
    nl.map(_ => BlankLine(""))
    //many(whitespace <~ nl).map(c => BlankLine(c.mkString))

  val fileParser: Parser[List[Row]] =
    for {
      c <- many(commentParser.liftC[Row] | ruleParser.liftC[Row] | zoneParser.liftC[Row] | linkParser.liftC[Row] | blankLine.liftC[Row])
    } yield c

  def parseFile(text: String): ParseResult[List[Row]] = {
    fileParser parseOnly text
  }

  val tzdbFiles: List[String] = List(
    "africa",
    "antarctica",
    "asia",
    "australasia",
    "backward",
    "etcetera",
    "europe",
    "northamerica",
    "pacificnew",
    "southamerica",
    "systemv"
  )

  /**
    * Entry point. Takes a dir with the TZDB files and parses them into Rows
    */
  def parseAll(dir: File): IO[List[Row]] = IO {
    dir match {
      case File.Type.SymbolicLink(_) => Nil
      case File.Type.Directory(files) =>
        val parsed = files.filter(f => tzdbFiles.contains(f.name)).map(f => parseFile(f.contentAsString))
        parsed.toList.suml match {
          case Done(_, v) => v
          case _          => Nil
        }
      case File.Type.RegularFile(_) => Nil
      case _ => Nil
    }
  }
}

object TZDBCodeGenerator {
  import TZDB._
  import treehugger.forest._, definitions._, treehuggerDSL._

  private val autoGeneratedCommend = "Auto-generated code from TZDB definitions, don't edit"

  // Typeclass of code generator
  trait TreeGenerator[A] {
    def generateTree(a: A): Tree
  }

  implicit class TreeGeneratorOps[A](val a: A) extends AnyVal {
    def toTree(implicit t: TreeGenerator[A]): Tree = t.generateTree(a)
  }

  object TreeGenerator {
    // "Summoner" method
    def apply[A](implicit enc: TreeGenerator[A]): TreeGenerator[A] = enc

    // "Constructor" method
    def instance[A](func: A => Tree): TreeGenerator[A] =
      new TreeGenerator[A] {
        def generateTree(value: A): Tree =
          func(value)
    }

    // Globally visible type class instances
    implicit def intInstance: TreeGenerator[Int] = instance(LIT.apply)
    implicit def stringInstance: TreeGenerator[String] = instance(LIT.apply)

    // Encoders for products
    implicit def cnilInstance: TreeGenerator[CNil] = instance(_ => throw new Exception("Cannot happen"))
    implicit def coproductInstance[H, T <: Coproduct](
      implicit
      hInstance: Lazy[TreeGenerator[H]], // wrap in Lazy
      tInstance: TreeGenerator[T]
    ): TreeGenerator[H :+: T] = instance {
      case Inl(h) => hInstance.value.generateTree(h)
      case Inr(t) => tInstance.generateTree(t)
    }

    implicit val zoneInstance: TreeGenerator[Zone] =
      instance(z => LIT(z.name))

    implicit val linkInstance: TreeGenerator[Link] =
      instance(l => TUPLE(l.to.toTree, l.from.toTree))

    implicit val zoneListInstance: TreeGenerator[List[Zone]] =
      instance( z =>
        LAZYVAL("allZones", "List[String]") := LIST(z.map(_.toTree))
      )

    implicit val linkInstances: TreeGenerator[List[Link]] =
      instance( l =>
        LAZYVAL("zoneLinks", "Map[String, String]") := MAKE_MAP(l.map(_.toTree): _*)
      )
  }

  // Go over the links and remove links whose source is unknown
  def cleanLinks(rows: List[Row]): List[Row] ={
    val zoneNames = rows.flatMap(_.select[Zone]).map(_.name)
    rows.filter {
      case r if r.select[Link].isDefined => r.select[Link].exists(l => zoneNames.contains(l.from))
      case _                             => true
    }
  }

  def exportAll(dir: java.io.File, to: java.io.File): IO[Unit] = {
    import better.files._
    for {
      rows      <- TZDBParser.parseAll(File(dir.toURI))
      cleanrows <- IO(cleanLinks(rows))
      tree      <- IO(exportAll(cleanrows))
      _         <- IO(File(to.toURI).write(treeToString(tree)))
    } yield ()
  }

  def exportAll(rows: List[Row]): Tree = {
    BLOCK (
      OBJECTDEF("tzdb") := BLOCK(rows.flatMap(_.select[Zone]).toTree, rows.flatMap(_.select[Link]).toTree)
    ) inPackage "tzdb" withComment autoGeneratedCommend
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy