]> gitweb @ CieloNegro.org - task-reporter.git/commitdiff
The TSV parser is now monadic.
authorPHO <pho@cielonegro.org>
Tue, 14 Oct 2014 09:30:52 +0000 (18:30 +0900)
committerPHO <pho@cielonegro.org>
Tue, 14 Oct 2014 09:30:52 +0000 (18:30 +0900)
src/main/scala/jp/ymir/taskReporter/core/TSV.scala
src/main/scala/jp/ymir/taskReporter/core/Task.scala

index 5b2d261f5cec1bf49e97a0a303d95b69efcb7301..817d950376ab7a3779ff2aa75e7881a373c9dc3b 100644 (file)
@@ -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 =>
index 53e813d7b0c5079bed38816c010f5cc9be1c02c5..71526941fd34099dc8785f03afe331ba8f5bdeae 100644 (file)
@@ -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)
   }
 }
- */