From: PHO Date: Tue, 14 Oct 2014 09:30:52 +0000 (+0900) Subject: The TSV parser is now monadic. X-Git-Url: https://git.cielonegro.org/gitweb.cgi?a=commitdiff_plain;h=c3c60f005928ab2660f79643af25f8380d5ad4db;p=task-reporter.git The TSV parser is now monadic. --- diff --git a/src/main/scala/jp/ymir/taskReporter/core/TSV.scala b/src/main/scala/jp/ymir/taskReporter/core/TSV.scala index 5b2d261..817d950 100644 --- a/src/main/scala/jp/ymir/taskReporter/core/TSV.scala +++ b/src/main/scala/jp/ymir/taskReporter/core/TSV.scala @@ -8,19 +8,8 @@ import scalaz._ import Scalaz._ object TSV { - type Header = Seq[Symbol] - type Field = String - type Record = Map[Symbol, Field] - - // --- - type Failure[ F[_], R] = String => F[R] - type Success[A, F[_], R] = A => F[R] - - trait Parser[A] { - def runParser[F[_], R](kf: Failure[F, R], ks: Success[A, F, R]): F[R] - } - implicit def ParserFunctor = new Functor[Parser] { + implicit val ParserFunctor = new Functor[Parser] { def map[A, B](m: Parser[A])(f: A => B) = new Parser[B] { def runParser[F[_], R](kf: Failure[F, R], ks: Success[B, F, R]): F[R] = { def ks_(a: A) = ks(f(a)) @@ -29,7 +18,7 @@ object TSV { } } - implicit def ParserApplicative = new Applicative[Parser] { + implicit val ParserApplicative = new Applicative[Parser] { def point[A](a: => A) = new Parser[A] { def runParser[F[_], R](kf: Failure[F, R], ks: Success[A, F, R]): F[R] = ks(a) @@ -45,7 +34,7 @@ object TSV { } } - implicit def ParserMonad = new Monad[Parser] { + implicit val ParserMonad = new Monad[Parser] { def point[A](a: => A) = a.point[Parser] def bind[A, B](m: Parser[A])(f: A => Parser[B]) = new Parser[B] { @@ -56,7 +45,7 @@ object TSV { } } - implicit def ParserMonadError = new MonadError[({type λ[String, α] = Parser[α]})#λ, String] { + implicit val ParserMonadError = new MonadError[({type λ[String, α] = Parser[α]})#λ, String] { def point[A](a: => A) = a.point[Parser] def bind[A, B](m: Parser[A])(f: A => Parser[B]) = m.flatMap(f) @@ -72,37 +61,53 @@ object TSV { = kf(e) } } + + implicit def ToMonadErrorOps[A](m: Parser[A]) = new { + def handleError(f: String => Parser[A]): Parser[A] + = implicitly[MonadError[({type λ[String, α] = Parser[α]})#λ, String]].handleError(m)(f) + } + implicit def ToMonadErrorIdOps(e: String) = new { def raiseError[A]: Parser[A] = implicitly[MonadError[({type λ[String, α] = Parser[α]})#λ, String]].raiseError(e) } + type Header = Seq[Symbol] + + case class Field(value: String) { + def parseField[A: FromField] + = implicitly[FromField[A]].parseField(Field(value)) + } + + case class Record(value: Map[Symbol, Field]) { + def parseRecord[A: FromRecord] + = implicitly[FromRecord[A]].parseRecord(Record(value)) + + def lookup[A: FromField](name: Symbol): Parser[A] + = value.get(name).fold(("no field named " + name.name).raiseError[A])(_.parseField[A]) + } + + type Failure[ F[_], R] = String => F[R] + type Success[A, F[_], R] = A => F[R] + + trait Parser[+A] { + def runParser[F[_], R](kf: Failure[F, R], ks: Success[A, F, R]): F[R] + } + def runParser[A](p: Parser[A]): Either[String, A] = { type F[R] = Either[String, R] def left (e: String): F[A] = Left(e) def right(x: A ): F[A] = Right(x) return p.runParser(left, right) } - // --- - - def decode[A: FromRecord](tsv: StringLike[_]): Seq[A] = { - val lines = tsv.split('\n').filter(!_.isEmpty) - val header = lines.head.split('\t').map(Symbol(_)) - val body = lines.tail.map(line => (header zip line.split('\t')).toMap) - return body.map(implicitly[FromRecord[A]].parseRecord) - } trait FromRecord[A] { - def parseRecord(r: Record): A + def parseRecord(r: Record): Parser[A] } trait FromField[A] { def parseField(f: Field): Parser[A] } - implicit def toFromFieldOps(f: Field) = new { - def parseField[A: FromField] - = implicitly[FromField[A]].parseField(f) - } trait ToRecord[A] { def toRecord(a: A): Record @@ -112,11 +117,28 @@ object TSV { def toField(a: A): Field } + class DecodeFailedException private(e: RuntimeException) extends RuntimeException(e) { + def this(msg: String) = this(new RuntimeException(msg)) + def this(msg: String, cause: Throwable) = this(new RuntimeException(msg, cause)) + } + + def decode[A: FromRecord](tsv: StringLike[_]): Seq[A] = { + val lines = tsv.split('\n').filter(!_.isEmpty) + val header = lines.head.split('\t').map(Symbol(_)) + val body = lines.tail.map(line => Record((header zip line.split('\t').map { Field(_) }).toMap)) + return body.map { r => + runParser(r.parseRecord[A]) match { + case Right(a) => a + case Left(e) => throw new DecodeFailedException(e) + } + } + } + // Option[A] implicit def OptionFromField[A: FromField] = new FromField[Option[A]] { def parseField(f: Field): Parser[Option[A]] = { - if (f.isEmpty) { - return (None : Option[A]).point[Parser] + if (f.value.isEmpty) { + return None.point[Parser] } else { f.parseField[A].map { Some(_) } @@ -125,15 +147,15 @@ object TSV { } // String - implicit def StringFromField = new FromField[String] { - def parseField(f: Field): Parser[String] = f.point[Parser] + implicit val StringFromField = new FromField[String] { + def parseField(f: Field): Parser[String] = f.value.point[Parser] } // Int - implicit def IntFromField = new FromField[Int] { + implicit val IntFromField = new FromField[Int] { def parseField(f: Field): Parser[Int] = { try { - f.toInt.point[Parser] + f.value.toInt.point[Parser] } catch { case e: NumberFormatException => diff --git a/src/main/scala/jp/ymir/taskReporter/core/Task.scala b/src/main/scala/jp/ymir/taskReporter/core/Task.scala index 53e813d..7152694 100644 --- a/src/main/scala/jp/ymir/taskReporter/core/Task.scala +++ b/src/main/scala/jp/ymir/taskReporter/core/Task.scala @@ -1,7 +1,10 @@ package jp.ymir.taskReporter.core import java.util.Calendar import java.util.GregorianCalendar +import scala.language.reflectiveCalls import scala.util.matching.Regex +import scalaz._ +import Scalaz._ case class Task( date: Calendar, @@ -18,131 +21,46 @@ object Task { sealed abstract class Status object Status { - case object DoingFine extends Status - case object Lagging extends Status - case object DeadlineChanged extends Status - case object Completed extends Status + case object DoingFine extends Status { override def toString() = "順調" } + case object Lagging extends Status { override def toString() = "遅延" } + case object DeadlineChanged extends Status { override def toString() = "期限変更" } + case object Completed extends Status { override def toString() = "完了" } } - implicit def CalendarFromField = new FromField[Calendar] { + implicit val CalendarFromField = new FromField[Calendar] { val pattern = """^(\d{4})/(\d{2})/(\d{2})$""".r - def parseField(f: Field): Calendar = { - f match { - case pattern(year, month, day) => - new GregorianCalendar(year.toInt, month.toInt, day.toInt) - } - } - } - - implicit def StatusFromField = new FromField[Status] { - def parseField(f: Field): Status = f match { - case "順調" => Status.DoingFine - case "遅延" => Status.Lagging - case "期限変更" => Status.DeadlineChanged - case "完了" => Status.Completed - } - } - - implicit def TaskFromRecord = new FromRecord[Task] { - def parseRecord(r: Record): Task = { - return Task( - date = parseField[Calendar](r('報告日)), - ticketID = parseField[Option[Int]](r('チケットID)), - title = parseField[String](r('作業名)), - expectedCompletionDate = parseField[Calendar](r('作業完了予定年月日)), - deadline = parseField[Option[Calendar]](r('タスク期限)), - status = parseField[Status](r('状態)), - description = parseField[Option[String]](r('説明)) - ) - } - } -} - -/* -class Task(tsvLine: String) { - sealed abstract class Status - object Status { - case object NoProgress extends Status - case object DoingFine extends Status - case object Lagging extends Status - case object WillDelay extends Status - case object DeadlineChanged extends Status - case object Completed extends Status - } - - class InvalidNumberOfColumnsException private(e: RuntimeException) extends RuntimeException(e) { - def this(msg: String) = this(new RuntimeException(msg)) - def this(msg: String, cause: Throwable) = this(new RuntimeException(msg, cause)) - } - - private val cols = tsvLine.split("\\t") - if (cols.length != 7) { - throw new InvalidNumberOfColumnsException(tsvLine) - } - - var date : Calendar = { - val pattern = """^(?:報告日:)?(\d{4})/(\d{2})/(\d{2})$""".r - cols(0) match { + def parseField(f: Field): Parser[Calendar] = f.value match { case pattern(year, month, day) => - new GregorianCalendar(year.toInt, month.toInt, day.toInt) - } - } - - var ticketID : Int = { - val pattern = """^(?:チケットID:)?(\d+)$""".r - cols(1) match { - case pattern(id) => id.toInt - } - } - - var title : String = { - val pattern = """^(?:作業名:)?(.*)$""".r - cols(2) match { - case pattern(title) => title - } - } - - var tentativeDeadline : Option[Calendar] = { - val pattern = """^(?:仮期限:)?(\d{4})/(\d{2})/(\d{2})$""".r - cols(3) match { - case pattern(year, month, day) => - Some(new GregorianCalendar(year.toInt, month.toInt, day.toInt)) + new GregorianCalendar(year.toInt, month.toInt - 1, day.toInt).point[Parser] case _ => - None + ("date not in format yyyy/MM/dd: " + f).raiseError[Calendar] } } - var deadline : Option[Calendar] = { - val pattern = """^(?:期限:)?(\d{4})/(\d{2})/(\d{2})$""".r - cols(4) match { - case pattern(year, month, day) => - Some(new GregorianCalendar(year.toInt, month.toInt, day.toInt)) + implicit val StatusFromField = new FromField[Status] { + def parseField(f: Field): Parser[Status] = f.value match { + case "順調" => (Status.DoingFine: Status).point[Parser] + case "遅延" => (Status.Lagging: Status).point[Parser] + case "期限変更" => (Status.DeadlineChanged: Status).point[Parser] + case "完了" => (Status.Completed: Status).point[Parser] case _ => - None - } - } - - var status : Status = { - val pattern = """^(?:状態:)?(.+)$""".r - cols(5) match { - case pattern(s) => - s match { - case "未作業" => Status.NoProgress - case "順調" => Status.DoingFine - case "悪化" => Status.Lagging - case "遅延" => Status.WillDelay - case "期限変更" => Status.DeadlineChanged - case "完了" => Status.Completed - } + ("invalid status: " + f).raiseError[Status] } } - var supplement : String = { - val pattern = """^(?:補足:)?(.*)$""".r - cols(6) match { - case pattern(title) => title - } + implicit val TaskFromRecord = new FromRecord[Task] { + def parseRecord(r: Record): Parser[Task] = + for { + date <- r.lookup[Calendar ]('報告日) + ticketID <- r.lookup[Option[Int] ]('チケットID) + title <- r.lookup[String ]('作業名) + expectedCompletionDate <- r.lookup[Calendar ]('作業完了予定年月日) + deadline <- r.lookup[Option[Calendar]]('タスク期限) + status <- r.lookup[Status ]('状態) + description <- r.lookup[Option[String] ]('説明).handleError(_ => None.point[Parser]) + } + yield Task(date, ticketID, title, expectedCompletionDate, + deadline, status, description) } } - */