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

harness.csv.CsvDecoder.scala Maven / Gradle / Ivy

package harness.csv

import cats.syntax.either.*
import cats.syntax.option.*
import cats.syntax.traverse.*
import harness.core.{StringDecoder, Zip}

trait CsvDecoder[T] private { self =>

  protected val size: Int
  protected def decodeImpl(line: IArray[Option[String]], lineNo: Int, cell: Int): Either[String, T]

  final def decode(line: IArray[Option[String]], lineNo: Int): Either[String, T] =
    if (line.length != self.size) s"Invalid line length on line $lineNo. Expected $size, but got ${line.length}".asLeft
    else self.decodeImpl(line, lineNo, 0)

  final def optional: CsvDecoder[Option[T]] =
    new CsvDecoder[Option[T]] {
      override protected val size: Int = self.size
      override protected def decodeImpl(line: IArray[Option[String]], lineNo: Int, cell: Int): Either[String, Option[T]] =
        if (cell.until(cell + self.size).forall(line(_).isEmpty)) None.asRight
        else self.decodeImpl(line, lineNo, cell).map(_.some)
    }

  final def ++[T2](other: CsvDecoder[T2])(implicit zip: Zip[T, T2]): CsvDecoder[zip.Out] =
    new CsvDecoder[zip.Out] {
      override protected val size: Int = self.size + other.size
      override protected def decodeImpl(line: IArray[Option[String]], lineNo: Int, cell: Int): Either[String, zip.Out] =
        for {
          t1 <- self.decodeImpl(line, lineNo, cell)
          t2 <- other.decodeImpl(line, lineNo, cell + self.size)
        } yield zip.zip(t1, t2)
    }

  final def apply[T2](implicit other: CsvDecoder[T2], zip: Zip[T, T2]): CsvDecoder[zip.Out] = self ++ other

  final def map[T2](f: T => T2): CsvDecoder[T2] =
    new CsvDecoder[T2] {
      override protected val size: Int = self.size
      override protected def decodeImpl(line: IArray[Option[String]], lineNo: Int, cell: Int): Either[String, T2] =
        self.decodeImpl(line, lineNo, cell).map(f)
    }

  final def emap[T2](f: T => Either[String, T2]): CsvDecoder[T2] =
    new CsvDecoder[T2] {
      override protected val size: Int = self.size
      override protected def decodeImpl(line: IArray[Option[String]], lineNo: Int, cell: Int): Either[String, T2] =
        self.decodeImpl(line, lineNo, cell).flatMap(f)
    }

}
object CsvDecoder {

  inline def apply[T](implicit decoder: CsvDecoder[T]): CsvDecoder[T] = decoder

  def decode[T](csv: String, hasHeaders: Boolean)(implicit decoder: CsvDecoder[T]): Either[String, List[T]] =
    for {
      lines <- Parser.parse(csv)
      (lines, startLineNo) <- (hasHeaders, lines) match {
        case (false, lines)     => (lines, 1).asRight
        case (true, _ :: lines) => (lines, 2).asRight
        case (true, Nil)        => "No header line to ignore".asLeft
      }
      results <- lines.zipWithIndex.traverse { (line, idx) => decoder.decode(line, idx + startLineNo) }
    } yield results

  def cell[T](f: String => Either[String, T]): CsvDecoder[T] =
    new CsvDecoder[T] {
      override protected val size: Int = 1
      override protected def decodeImpl(line: IArray[Option[String]], lineNo: Int, cell: Int): Either[String, T] = {
        line(cell) match {
          case Some(value) => f(value).leftMap(err => s"Error on line $lineNo in cell $cell ${cell + 1} : $err")
          case None        => s"Error on line $lineNo in cell $cell ${cell + 1} : unexpected null".asLeft
        }
      }
    }

  implicit val unitDecoder: CsvDecoder[Unit] =
    new CsvDecoder[Unit] {
      override protected val size: Int = 1
      override protected def decodeImpl(line: IArray[Option[String]], lineNo: Int, cell: Int): Either[String, Unit] = ().asRight
    }

  implicit def optional[T](implicit decoder: CsvDecoder[T]): CsvDecoder[Option[T]] = decoder.optional

  implicit def fromStringDecoder[T](implicit decoder: StringDecoder[T]): CsvDecoder[T] = CsvDecoder.cell(decoder.decode)

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy