diff --git a/README.md b/README.md index 69ebd4a..f639ebb 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,11 @@ Itto-CSV is a pure scala library for working with the CSV format -`libraryDependencies += "com.github.gekomad" %% "itto-csv" % "2.1.0"` +The latest version of the library is available for Scala 2.12, 2.13, 3 and Scala-js. -Scala 2.12 and 2.13 +`libraryDependencies += "com.github.gekomad" %% "itto-csv" % "2.1.1"` + +Scala 2 ------- View [microsite](https://gekomad.github.io/itto-csv/v1/docs/) for more information. diff --git a/build.sbt b/build.sbt index a83f5e6..641e4b6 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,9 @@ - -lazy val root = (project in file(".")).aggregate(scala2, scala3) +lazy val `itto-csv` = (project in file(".")).aggregate(scala2, scala3, scala2Js, scala3Js) lazy val scala2 = project in file("scala2") lazy val scala3 = project in file("scala3") +lazy val scala2Js = project in file("scala2_js") + +lazy val scala3Js = project in file("scala3_js") diff --git a/project/plugins.sbt b/project/plugins.sbt index 064aaa1..08bf0a9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,5 @@ addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.21.1") \ No newline at end of file diff --git a/scala2/build.sbt b/scala2/build.sbt index 6832e1b..e618e0a 100644 --- a/scala2/build.sbt +++ b/scala2/build.sbt @@ -1,8 +1,8 @@ name := "itto-csv" -version := "2.1.0" +version := "2.1.1" organization := "com.github.gekomad" -//scalaVersion := "2.12.20" scalaVersion := "2.13.15" +//scalaVersion := "2.12.20" val fs2Version = "3.11.0" scalacOptions ++= { if (scalaVersion.value.startsWith("2.12")) { @@ -32,28 +32,20 @@ scalacOptions ++= Seq( "-Xfatal-warnings" ) -//cats - -libraryDependencies += "co.fs2" %% "fs2-core" % fs2Version -libraryDependencies += "co.fs2" %% "fs2-io" % fs2Version +libraryDependencies += "co.fs2" %% "fs2-core" % fs2Version +libraryDependencies += "co.fs2" %% "fs2-io" % fs2Version +libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.12" +libraryDependencies += "com.github.gekomad" %% "scala-regex-collection" % "2.0.1" +libraryDependencies += "com.storm-enroute" %% "scalameter" % "0.19" % Test +libraryDependencies += "org.scalameta" %% "munit" % "1.0.2" % Test +libraryDependencies += "org.apache.commons" % "commons-csv" % "1.12.0" % Test +libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.18.1" % Test -//shapeless -libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.12" - -//scala-regex-collection -libraryDependencies += "com.github.gekomad" %% "scala-regex-collection" % "2.0.0" - -//test -libraryDependencies += "com.storm-enroute" %% "scalameter" % "0.21" % Test -libraryDependencies += "org.scalameta" %% "munit" % "1.0.2" % Test -libraryDependencies += "org.apache.commons" % "commons-csv" % "1.12.0" % Test -libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.18.1" % Test -testFrameworks += new TestFramework("munit.Framework") Test / testOptions += Tests.Argument(TestFrameworks.ScalaCheck, "-minSuccessfulTests", "1000") //sonatype publishTo := sonatypePublishToBundle.value -logLevel := Level.Debug + pomExtra := diff --git a/scala2/src/test/scala/StringToCsvFieldMeter.scala b/scala2/src/test/scala/StringToCsvFieldMeter.scala index 87394da..57fce17 100644 --- a/scala2/src/test/scala/StringToCsvFieldMeter.scala +++ b/scala2/src/test/scala/StringToCsvFieldMeter.scala @@ -1,7 +1,7 @@ import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} import org.apache.commons.csv.CSVFormat import org.scalacheck.Gen -import org.scalameter.{config, Key, KeyValue, Warmer} +import org.scalameter.{Key, Warmer, config} class StringToCsvFieldMeter extends munit.FunSuite { @@ -10,24 +10,24 @@ class StringToCsvFieldMeter extends munit.FunSuite { implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default val standardConfig = config( - KeyValue((Key.exec.minWarmupRuns, 20)), - KeyValue((Key.exec.maxWarmupRuns, 100000)), - KeyValue((Key.exec.benchRuns, 200)), - KeyValue((Key.verbose, false)) - ).withWarmer(new Warmer.Default) + Key.exec.minWarmupRuns -> 20, + Key.exec.maxWarmupRuns -> 100000, + Key.exec.benchRuns -> 200, + Key.verbose -> false + ) withWarmer new Warmer.Default val asciiStringGen = Gen.asciiPrintableStr.map(_.mkString.take(20)) val l = Gen.listOfN(100, asciiStringGen).sample.get val apacheTime = { implicit val _a = CSVFormat.DEFAULT - standardConfig.measure(l.foreach(ApacheCommonCsvHelper.fildParser)) + standardConfig measure l.foreach(ApacheCommonCsvHelper.fildParser) } { - val ittoTime = standardConfig.measure(l.foreach(StringToCsvField.stringToCsvField)) + val ittoTime = standardConfig measure l.foreach(StringToCsvField.stringToCsvField) printf(f"** itto-csv time: ${ittoTime.value}%1.2f apache-csv time: ${apacheTime.value}%1.2f**\n") } } -} +} \ No newline at end of file diff --git a/scala2/src/test/scala/TokenizeCsvMeter.scala b/scala2/src/test/scala/TokenizeCsvMeter.scala index 39d5125..f2976a4 100644 --- a/scala2/src/test/scala/TokenizeCsvMeter.scala +++ b/scala2/src/test/scala/TokenizeCsvMeter.scala @@ -1,6 +1,7 @@ import com.github.gekomad.ittocsv.parser.IttoCSVFormat import org.scalacheck.Gen -import org.scalameter.{Key, KeyValue, Warmer, config} +import org.scalameter.{Key, Warmer, config} + class TokenizeCsvMeter extends munit.FunSuite { @@ -9,11 +10,12 @@ class TokenizeCsvMeter extends munit.FunSuite { implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default import com.github.gekomad.ittocsv.util.StringUtils._ val standardConfig = config( - KeyValue((Key.exec.minWarmupRuns, 20)), - KeyValue((Key.exec.maxWarmupRuns, 100000)), - KeyValue((Key.exec.benchRuns, 200)), - KeyValue((Key.verbose, false)) - ).withWarmer(new Warmer.Default) + Key.exec.minWarmupRuns -> 20, + Key.exec.maxWarmupRuns -> 100000, + Key.exec.benchRuns -> 1000, + Key.verbose -> false + ) withWarmer new Warmer.Default + val asciiStringGen = Gen.asciiPrintableStr.map(_.mkString.take(40)) val l = Gen.listOfN(1000, asciiStringGen).sample.get @@ -24,4 +26,4 @@ class TokenizeCsvMeter extends munit.FunSuite { } printf(f"** tokenizeCsvMeter time: ${time.value}%1.2f **\n") } -} +} \ No newline at end of file diff --git a/scala2_js/build.sbt b/scala2_js/build.sbt new file mode 100644 index 0000000..e0f3246 --- /dev/null +++ b/scala2_js/build.sbt @@ -0,0 +1,59 @@ +name := "itto-csv" +publishTo := sonatypePublishTo.value + +import org.scalajs.linker.interface.{ESVersion, ModuleSplitStyle} + +lazy val scala2Js = project + .in(file(".")) + .enablePlugins(ScalaJSPlugin) + .settings( + version := "2.1.1", + scalaVersion := "2.13.15", + //scalaVersion := "2.12.20", + organization := "com.github.gekomad", + scalaJSUseMainModuleInitializer := false, + scalaJSLinkerConfig ~= (_.withESFeatures(_.withESVersion(ESVersion.ES2018))), + scalaJSLinkerConfig ~= { + _.withModuleKind(ModuleKind.ESModule) + .withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("scala2Js"))) + }, + + scalacOptions ++= { + if (scalaVersion.value.startsWith("2.12")) { + Seq("-Ypartial-unification", "-Xfatal-warnings") + } else { + Seq("-Xfatal-warnings") + } + }, + libraryDependencies += "co.fs2" %%% "fs2-core" % "3.11.0", + libraryDependencies += "co.fs2" %%% "fs2-io" % "3.11.0", + libraryDependencies += "com.chuusai" %%% "shapeless" % "2.3.12", + libraryDependencies += "com.github.gekomad" %%% "scala-regex-collection" % "2.0.1", + libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.0", + libraryDependencies += "org.scalameta" %%% "munit" % "1.0.2" % Test, + libraryDependencies += "org.scalacheck" %%% "scalacheck" % "1.18.1" % Test + ) + +//sonatype +publishTo := sonatypePublishToBundle.value + +pomExtra := + + + Apache 2 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + gekomad + Giuseppe Cannella + https://github.com/gekomad + + + + https://github.com/gekomad/itto-csv + scm:git:https://github.com/gekomad/itto-csv + + https://github.com/gekomad/itto-csv diff --git a/scala2_js/project/build.properties b/scala2_js/project/build.properties new file mode 100644 index 0000000..db1723b --- /dev/null +++ b/scala2_js/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.5 diff --git a/scala2_js/project/plugins.sbt b/scala2_js/project/plugins.sbt new file mode 100644 index 0000000..721393f --- /dev/null +++ b/scala2_js/project/plugins.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.5") +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Conversions.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Conversions.scala new file mode 100644 index 0000000..fc9b271 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Conversions.scala @@ -0,0 +1,75 @@ +package com.github.gekomad.ittocsv.core + +import java.util.UUID +import com.github.gekomad.ittocsv.util.TryTo.tryToEither + +/** Converts a string to type + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + * @see + * See test code for more information + * @see + * See [[https://github.com/gekomad/itto-csv/blob/master/README.md]] for more information. + */ +object Conversions { + + trait ConvertTo[A] { + def to(a: String): Either[ParseFailure, A] + } + + implicit val toInts: ConvertTo[Int] = (a: String) => tryToEither(a.toInt)(ParseFailure(s"$a is not Int")) + + implicit val toDoubles: ConvertTo[Double] = (a: String) => tryToEither(a.toDouble)(ParseFailure(s"$a is not Double")) + + implicit val toBytes: ConvertTo[Byte] = (a: String) => tryToEither(a.toByte)(ParseFailure(s"$a is not Byte")) + + implicit val toShorts: ConvertTo[Short] = (a: String) => tryToEither(a.toShort)(ParseFailure(s"$a is not Short")) + + implicit val toFloats: ConvertTo[Float] = (a: String) => tryToEither(a.toFloat)(ParseFailure(s"$a is not Float")) + + implicit val toLongs: ConvertTo[Long] = (a: String) => tryToEither(a.toLong)(ParseFailure(s"$a is not Long")) + + implicit val toChars: ConvertTo[Char] = (a: String) => + tryToEither(if (a.length == 1) a(0) else throw new Exception)(ParseFailure(s"$a is not Char")) + + implicit val toBooleans: ConvertTo[Boolean] = (a: String) => + tryToEither(a.toBoolean)(ParseFailure(s"$a is not Boolean")) + + implicit val toUUIDS: ConvertTo[UUID] = (a: String) => + tryToEither(UUID.fromString(a))(ParseFailure(s"$a is not UUID")) + + import java.time._ + import java.time.format.DateTimeFormatter._ + + implicit def fromGenericOption[A](implicit + f: String => Either[ParseFailure, A] + ): String => Either[ParseFailure, Option[A]] = + s => if (s == "") Right(None) else f(s).map(Some(_)) + + implicit val fromStringToLocalDateTime: String => Either[ParseFailure, LocalDateTime] = { s => + tryToEither(LocalDateTime.parse(s, ISO_LOCAL_DATE_TIME))(ParseFailure(s"Not a LocalDataTime $s")) + } + + implicit val fromStringToLocalDate: String => Either[ParseFailure, LocalDate] = { s => + tryToEither(LocalDate.parse(s, ISO_LOCAL_DATE))(ParseFailure(s"Not a LocalDate $s")) + } + + implicit val fromStringToLocalTime: String => Either[ParseFailure, LocalTime] = + (s: String) => tryToEither(LocalTime.parse(s, ISO_LOCAL_TIME))(ParseFailure(s"Not a LocalTime $s")) + + implicit val fromStringToOffsetDateTime: String => Either[ParseFailure, OffsetDateTime] = + (s: String) => tryToEither(OffsetDateTime.parse(s, ISO_OFFSET_DATE_TIME))(ParseFailure(s"Not a OffsetDateTime $s")) + + implicit val fromStringToOffsetTime: String => Either[ParseFailure, OffsetTime] = + (s: String) => tryToEither(OffsetTime.parse(s, ISO_OFFSET_TIME))(ParseFailure(s"Not a OffsetTime $s")) + + implicit val fromStringToZonedDateTime: String => Either[ParseFailure, ZonedDateTime] = + (s: String) => tryToEither(ZonedDateTime.parse(s, ISO_ZONED_DATE_TIME))(ParseFailure(s"Not a ZonedDateTime $s")) + + implicit val fromStringInstant: String => Either[ParseFailure, Instant] = + (s: String) => tryToEither(Instant.parse(s))(ParseFailure(s"Not a Instant $s")) + + def convert[A](s: String)(implicit f: ConvertTo[A]): Either[ParseFailure, A] = f.to(s) +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Convert.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Convert.scala new file mode 100644 index 0000000..1e0324b --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Convert.scala @@ -0,0 +1,80 @@ +package com.github.gekomad.ittocsv.core + +import cats.{Applicative, Id} +import cats.data.{Validated, ValidatedNel} +import com.github.gekomad.ittocsv.core.Conversions.ConvertTo +import com.github.gekomad.ittocsv.core.Types.Validate +import com.github.gekomad.ittocsv.parser.IttoCSVFormat +import com.github.gekomad.ittocsv.util.TryTo._ +import com.github.gekomad.ittocsv.core.Conversions.convert +trait Convert[V] { + def parse(input: String): ValidatedNel[ParseFailure, V] +} +import scala.language.higherKinds +object Convert { + import cats.implicits._ + + def to[V](input: String)(implicit C: Convert[V]): ValidatedNel[ParseFailure, V] = C.parse(input) + + def instance[V](body: String => ValidatedNel[ParseFailure, V]): Convert[V] = (input: String) => body(input) + + def f[A: ConvertTo, F[_]: Applicative](implicit csvFormat: IttoCSVFormat): Convert[F[List[A]]] = + Convert.instance( + _.split(csvFormat.delimeter.toString, -1).toList + .map(s => tryToValidate(convert[A](s).getOrElse(throw new Exception))(ParseFailure(s"Bad type on $s"))) + .sequence + .map(Applicative[F].pure(_)) + ) + + implicit def optionLists[A: ConvertTo](implicit csvFormat: IttoCSVFormat): Convert[Option[List[A]]] = f[A, Option] + + implicit def lists[A: ConvertTo](implicit csvFormat: IttoCSVFormat): Convert[List[A]] = f[A, Id] + + implicit def genericValidator[A](implicit csvFormat: IttoCSVFormat, validator: Validate[A]): Convert[A] = + Convert.instance(validator.validate(_).toValidatedNel) + + implicit def generic[A](implicit f: String => Either[ParseFailure, A]): Convert[A] = + Convert.instance(f(_).toValidatedNel) + + implicit val optionBoolean: Convert[Option[Boolean]] = Convert.instance { + case "" => Validated.valid(None) + case s => tryToValidate(Some(s.toBoolean))(ParseFailure(s"Not a Boolean for input string: $s")) + } + + implicit val optionShort: Convert[Option[Short]] = Convert.instance { + case "" => Validated.valid(None) + case s => tryToValidate(Some(s.toShort))(ParseFailure(s"Not a Short for input string: $s")) + } + + implicit val optionByte: Convert[Option[Byte]] = Convert.instance { + case "" => Validated.valid(None) + case s => tryToValidate(Some(s.toByte))(ParseFailure(s"Not a Byte for input string: $s")) + } + + implicit val optionChar: Convert[Option[Char]] = Convert.instance { + case "" => Validated.valid(None) + case s => + tryToValidate(if (s.length == 1) Some(s(0)) else throw new Exception)( + ParseFailure(s"Not a Char for input string: $s") + ) + } + + implicit val optionString: Convert[Option[String]] = Convert.instance { + case "" => Validated.valid(None) + case s => Validated.valid(Some(s)) + } + + implicit val optionDouble: Convert[Option[Double]] = Convert.instance { + case "" => Validated.valid(None) + case s => tryToValidate(Some(s.toDouble))(ParseFailure(s"Not a Double for input string: $s")) + } + + implicit val optionInt: Convert[Option[Int]] = Convert.instance { + case "" => Validated.valid(None) + case s => tryToValidate(Some(s.toInt))(ParseFailure(s"Not a Int for input string: $s")) + } + + implicit def gen[A](implicit conv: ConvertTo[A]): Convert[A] = Convert.instance(conv.to(_).toValidatedNel) + + implicit val strings: Convert[String] = Convert.instance(_.validNel) +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/FromCsv.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/FromCsv.scala new file mode 100644 index 0000000..6885a2d --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/FromCsv.scala @@ -0,0 +1,95 @@ +package com.github.gekomad.ittocsv.core + +/** Converts a CSV to type + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + * @see + * See test code for more information + * @see + * See [[https://github.com/gekomad/itto-csv/blob/master/README.md]] for more information + */ +object FromCsv { + + import cats.data.NonEmptyList + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.Header.{FieldNames, fieldNames} + import com.github.gekomad.ittocsv.util.StringUtils.tokenizeCsvLine + + /** @param csv + * is the string to parse. It might contain record separator + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * `List[Either[NonEmptyList[ParseFailure], A]]` based on the parsing of `csv`, any errors are reported + * {{{ + * import com.github.gekomad.ittocsv.core.FromCsv._ + * implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + * + * case class Bar(a: String, b: Int) + * + * assert(fromCsv[Bar]("abc,42\r\nfoo,24") == List(Right(Bar("abc", 42)), Right(Bar("foo", 24)))) + * assert(fromCsv[Bar]("abc,hi") == List(Left(cats.data.NonEmptyList(com.github.gekomad.ittocsv.core.FromCsv.ParseFailure("Not a Int hi"), Nil)))) + * + * case class Foo(v: String, a: List[Int]) + * assert(fromCsv[Foo]("abc,\"1,2,3\"") == List(Right(Foo("abc", List(1, 2, 3))))) + * }}} + */ + def fromCsv[A: FieldNames: Schema]( + csv: String + )(implicit csvFormat: IttoCSVFormat): List[Either[NonEmptyList[ParseFailure], A]] = + fromCsv(csv.split(csvFormat.recordSeparator, -1).toList) + + import com.github.gekomad.ittocsv.core.Conversions._ + + /** @param csv + * is the string to parse. It might contain record separator + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * `List[Either[ParseFailure, A]]` based on the parsing of `csv`, any errors are reported + * {{{ + * import com.github.gekomad.ittocsv.core.FromCsv._ + * + * implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + * + * assert(fromCsvL[Double]("1.1,2.1,3.1") == List(Right(1.1), Right(2.1), Right(3.1))) + * assert(fromCsvL[Double]("1.1,abc,3.1") == List(Right(1.1), Left(com.github.gekomad.ittocsv.core.FromCsv.ParseFailure("Not a Double abc")), Right(3.1))) + * }}} + */ + def fromCsvL[A: ConvertTo](csv: String)(implicit csvFormat: IttoCSVFormat): List[Either[ParseFailure, A]] = + csv.split(csvFormat.delimeter.toString, -1).toList.map(convert[A]) + + /** @param csvList + * is the List[String] to parse + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * `List[Either[NonEmptyList[ParseFailure], A]]` based on the parsing of `csvList` any errors are reported + * {{{ + * import com.github.gekomad.ittocsv.parser.IttoCSVFormat + * import com.github.gekomad.ittocsv.core.FromCsv._ + * case class Foo(a: Int, b: Double, c: String, d: Boolean) + * implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + * val p1 = fromCsv[Foo](List("1,3.14,foo,true", "2,3.14,bar,false")) + * assert(p1 == List(Right(Foo(1, 3.14, "foo", true)), Right(Foo(2, 3.14, "bar", false)))) + * }}} + */ + def fromCsv[A: FieldNames: Schema]( + csvList: List[String] + )(implicit csvFormat: IttoCSVFormat): List[Either[NonEmptyList[ParseFailure], A]] = csvList match { + case Nil => Nil + case l => + l collect { + case row if !row.isEmpty || !csvFormat.ignoreEmptyLines => + tokenizeCsvLine(row) match { + case None => Left(NonEmptyList(ParseFailure(s"$csvList is not a valid csv string"), Nil)) + case Some(t) => + val schema = Schema.of[A] + val p: Map[String, String] = fieldNames[A].zip(t).toMap + schema.readFrom(p).toEither + } + } + } +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Header.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Header.scala new file mode 100644 index 0000000..9c0e4da --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Header.scala @@ -0,0 +1,31 @@ +package com.github.gekomad.ittocsv.core + +import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + +object Header { + + import shapeless._ + import shapeless.ops.record._ + import shapeless.ops.hlist.ToTraversable + + trait FieldNames[T] { + def apply(): List[String] + } + + implicit def toNames[T, Repr <: HList, KeysRepr <: HList](implicit + gen: LabelledGeneric.Aux[T, Repr], + keys: Keys.Aux[Repr, KeysRepr], + traversable: ToTraversable.Aux[KeysRepr, List, Symbol] + ): FieldNames[T] = () => keys().toList.map(_.name) + + /** @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * the string with class's fields name encoded according with csvFormat + */ + def csvHeader[T](implicit h: FieldNames[T], csvFormat: IttoCSVFormat): String = + h().map(StringToCsvField.stringToCsvField).mkString(csvFormat.delimeter.toString) + + def fieldNames[T](implicit h: FieldNames[T]): List[String] = h() + +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/ParseFailure.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/ParseFailure.scala new file mode 100644 index 0000000..8f9f216 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/ParseFailure.scala @@ -0,0 +1,3 @@ +package com.github.gekomad.ittocsv.core + +final case class ParseFailure(error: String) diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Schema.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Schema.scala new file mode 100644 index 0000000..810f0f7 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Schema.scala @@ -0,0 +1,43 @@ +package com.github.gekomad.ittocsv.core + +import cats._ +import implicits._ +import data.ValidatedNel +import shapeless._ +import labelled._ + +sealed trait Schema[A] { + def readFrom(input: Map[String, String]): ValidatedNel[ParseFailure, A] +} + +object Schema { + def of[A](implicit s: Schema[A]): Schema[A] = s + + private def instance[A](body: Map[String, String] => ValidatedNel[ParseFailure, A]): Schema[A] = new Schema[A] { + def readFrom(input: Map[String, String]): ValidatedNel[ParseFailure, A] = body(input) + } + + implicit val noOp: Schema[HNil] = new Schema[HNil] { + def readFrom(input: Map[String, String]): ValidatedNel[Nothing, HNil.type] = HNil.validNel + } + + implicit def parsing[K <: Symbol, V: Convert, T <: HList](implicit + key: Witness.Aux[K], + next: Schema[T] + ): Schema[FieldType[K, V] :: T] = Schema.instance { input => + ( + input + .get(key.value.name) + .fold(ParseFailure(s"${key.value.name} is missing").invalidNel: ValidatedNel[ParseFailure, V])(entry => + Convert.to[V](entry) + ) + .map(field[K](_)), + next.readFrom(input) + ).mapN(_ :: _) + } + + implicit def classes[A, R <: HList](implicit repr: LabelledGeneric.Aux[A, R], schema: Schema[R]): Schema[A] = + Schema.instance { input => + schema.readFrom(input).map(repr.from) + } +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/ToCsv.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/ToCsv.scala new file mode 100644 index 0000000..ef08efc --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/ToCsv.scala @@ -0,0 +1,178 @@ +package com.github.gekomad.ittocsv.core + +import java.time.Instant +import java.util.UUID + +import com.github.gekomad.ittocsv.core.Header._ +import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} +import shapeless.{::, Generic, HList, HNil, Lazy} + +trait CsvStringEncoder[A] { + def encode(value: A): String +} + +/** Converts the type A to CSV + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + * @see + * See test code for more information + * @see + * See [[https://github.com/gekomad/itto-csv/blob/master/README.md]] for more information. + */ +object ToCsv { + + def createEncoder[A](func: A => String): CsvStringEncoder[A] = (value: A) => func(value) + + val csvConverter = StringToCsvField + + implicit def stringEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[String] = + createEncoder(t => csvConverter.stringToCsvField(t)) + + implicit def intEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Int] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def longEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Long] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def doubleEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Double] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def booleanEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Boolean] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def byteEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Byte] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def uuidEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[UUID] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def shortEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Short] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def floatEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Float] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def charEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Char] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + import java.time.LocalDateTime + import java.time.LocalDate + import java.time.LocalTime + import java.time.OffsetDateTime + import java.time.OffsetTime + import java.time.ZonedDateTime + + implicit def localDateEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[LocalDate] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def localDateTimeEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[LocalDateTime] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def localTimeEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[LocalTime] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def offsetDateTimeEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[OffsetDateTime] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def offsetTimeEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[OffsetTime] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def zonedDateTimeEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[ZonedDateTime] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit def instantEncoder(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Instant] = + createEncoder(t => csvConverter.stringToCsvField(t.toString)) + + implicit val hnilEncoder: CsvStringEncoder[HNil] = (_: HNil) => "" + + implicit def genericEncoder[A, R](implicit + gen: Generic.Aux[A, R], + rEncoder: Lazy[CsvStringEncoder[R]] + ): CsvStringEncoder[A] = createEncoder(value => rEncoder.value.encode(gen.to(value))) + + private def header[A: FieldNames](implicit enc: CsvStringEncoder[A], csvFormat: IttoCSVFormat): String = + if (csvFormat.printHeader) csvHeader[A] + csvFormat.recordSeparator else "" + + /** @param a + * is the element to convert + * @param printRecordSeparator + * if true, appends the record separator to end of string + * @param enc + * the [[com.github.gekomad.ittocsv.core.CsvStringEncoder]] encoder + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * the CSV string encoded + * {{{ + * import com.github.gekomad.ittocsv.core.ToCsv._ + * implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + * + * case class Bar(a: String, b: Int) + * assert(toCsv(Bar("侍", 42)) == "侍,42") + * case class Baz(x: String) + * case class Foo(a: Int, c: Baz) + * case class Xyz(a: String, b: Int, c: Foo) + * + * assert(toCsv(Xyz("hello", 3, Foo(1, Baz("hi, dude")))) == "hello,3,1,\"hi, dude\"") + * }}} + */ + def toCsv[A]( + a: A, + printRecordSeparator: Boolean = false + )(implicit enc: CsvStringEncoder[A], csvFormat: IttoCSVFormat): String = + (if (printRecordSeparator) csvFormat.recordSeparator else "") + enc.encode(a) + + /** @param a + * is the List of elements to convert + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * the CSV string encoded + * {{{ + * import com.github.gekomad.ittocsv.core.ToCsv._ + * implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + * case class Bar(a: String, b: Int) + * assert(toCsv(List(Bar("abc", 42), Bar("def", 24))) == "abc,42,def,24") + * }}} + */ + def toCsv[A](a: Seq[A])(implicit enc: CsvStringEncoder[A], csvFormat: IttoCSVFormat): String = + a.map(toCsv(_)).mkString(csvFormat.delimeter.toString) + + /** @param a + * is the List of elements to convert + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * {{{ + * import com.github.gekomad.ittocsv.core.ToCsv._ + * implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + * case class Bar(a: String, b: Int) + * assert(toCsvL(List(Bar("abc", 42), Bar("def", 24))) == "a,b\r\nabc,42\r\ndef,24") + * }}} + */ + def toCsvL[A: FieldNames](a: Seq[A])(implicit enc: CsvStringEncoder[A], csvFormat: IttoCSVFormat): String = + header + a.map(toCsv(_)).mkString(csvFormat.recordSeparator) + + implicit def hlistEncoder[H, T <: HList](implicit + hEncoder: CsvStringEncoder[H], + tEncoder: CsvStringEncoder[T], + csvFormat: IttoCSVFormat + ): CsvStringEncoder[H :: T] = createEncoder { + case h :: HNil => hEncoder.encode(h) + case h :: Nil :: HNil => hEncoder.encode(h) + case h :: t => hEncoder.encode(h) ++ csvFormat.delimeter.toString + tEncoder.encode(t) + } + + import shapeless.{:+:, CNil, Coproduct, Inl, Inr} + + implicit val cnilEncoder: CsvStringEncoder[CNil] = createEncoder(_ => throw new Exception("Inconceivable!")) + + implicit def coproductEncoder[H, T <: Coproduct](implicit + hEncoder: Lazy[CsvStringEncoder[H]], + tEncoder: CsvStringEncoder[T] + ): CsvStringEncoder[H :+: T] = createEncoder { + case Inl(h) => hEncoder.value.encode(h) + case Inr(t) => tEncoder.encode(t) + } +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Types.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Types.scala new file mode 100644 index 0000000..5a3fbc6 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/core/Types.scala @@ -0,0 +1,422 @@ +package com.github.gekomad.ittocsv.core + +object Types { + + trait Validate[A] { + def validate(value: String): Either[ParseFailure, A] + } + + type Cons[A] = String => A + + final case class Validator[A](regex: String, typeName: String)(implicit apply: Cons[A]) extends Validate[A] { + implicit val validator: com.github.gekomad.regexcollection.Collection.Validator[A] = + com.github.gekomad.regexcollection.Collection.Validator[A](regex) + def validate(value: String): Either[ParseFailure, A] = + com.github.gekomad.regexcollection.Validate + .validate[A](value) + .map(_ => Right(apply(value))) + .getOrElse(Left(ParseFailure(s"Not a $typeName $value")): Either[ParseFailure, A]) + } + + object implicits { + final case class Youtube(value: String) + final case class Facebook(value: String) + final case class Twitter(value: String) + + implicit val apply0: Cons[Youtube] = Youtube + implicit val apply1: Cons[Facebook] = Facebook + implicit val apply2: Cons[Twitter] = Twitter + + implicit val validatorYoutube: Validator[Youtube] = + Validator[Youtube](com.github.gekomad.regexcollection.Collection.validatorYoutube.regexp, Youtube.toString) + implicit val validatorFacebook: Validator[Facebook] = + Validator[Facebook](com.github.gekomad.regexcollection.Collection.validatorFacebook.regexp, Facebook.toString) + implicit val validatorTwitter: Validator[Twitter] = + Validator[Twitter](com.github.gekomad.regexcollection.Collection.validatorTwitter.regexp, Twitter.toString) + + // MACAddressOps + final case class MACAddress(value: String) + implicit val apply3: Cons[MACAddress] = MACAddress + + implicit val validatorMACAddress: Validator[MACAddress] = Validator[MACAddress]( + com.github.gekomad.regexcollection.Collection.validatorMACAddress.regexp, + MACAddress.toString + ) + + // EmailOps + + implicit val apply4: Cons[Email] = Email + implicit val apply5: Cons[EmailSimple] = EmailSimple + implicit val applye1: Cons[Email1] = Email1 + + final case class Email(email: String) + final case class Email1(email: String) + final case class EmailSimple(emailSimple: String) + implicit val validatorEmail1: Validator[Email1] = + Validator[Email1](com.github.gekomad.regexcollection.Collection.validatorEmail1.regexp, Email1.toString) + implicit val validatorEmail: Validator[Email] = + Validator[Email](com.github.gekomad.regexcollection.Collection.validatorEmail.regexp, Email.toString) + implicit val validatorEmailSimple: Validator[EmailSimple] = Validator[EmailSimple]( + com.github.gekomad.regexcollection.Collection.validatorEmailSimple.regexp, + EmailSimple.toString + ) + + // HexOps + final case class HEX(value: String) + final case class HEX1(value: String) + final case class HEX2(value: String) + final case class HEX3(value: String) + + implicit val apply6: Cons[HEX] = HEX + implicit val apply7: Cons[HEX1] = HEX1 + implicit val apply8: Cons[HEX2] = HEX2 + implicit val apply9: Cons[HEX3] = HEX3 + + implicit val validatorHEX: Validator[HEX] = + Validator[HEX](com.github.gekomad.regexcollection.Collection.validatorHEX.regexp, HEX.toString) + implicit val validatorHEX1: Validator[HEX1] = + Validator[HEX1](com.github.gekomad.regexcollection.Collection.validatorHEX1.regexp, HEX1.toString) + implicit val validatorHEX2: Validator[HEX2] = + Validator[HEX2](com.github.gekomad.regexcollection.Collection.validatorHEX2.regexp, HEX2.toString) + implicit val validatorHEX3: Validator[HEX3] = + Validator[HEX3](com.github.gekomad.regexcollection.Collection.validatorHEX3.regexp, HEX3.toString) + + // UrlOps + final case class URL(value: String) + final case class URL1(value: String) + final case class URL2(value: String) + final case class URL3(value: String) + final case class FTP(value: String) + final case class FTP1(value: String) + final case class FTP2(value: String) + final case class Domain(value: String) + + implicit val apply10: Cons[URL] = URL + implicit val apply11: Cons[URL1] = URL1 + implicit val apply12: Cons[URL2] = URL2 + implicit val apply13: Cons[URL3] = URL3 + implicit val apply14: Cons[FTP] = FTP + implicit val apply15: Cons[FTP1] = FTP1 + implicit val apply16: Cons[FTP2] = FTP2 + implicit val apply17: Cons[Domain] = Domain + + implicit val validatorURL: Validator[URL] = + Validator[URL](com.github.gekomad.regexcollection.Collection.validatorURL.regexp, URL.toString) + implicit val validatorURL1: Validator[URL1] = + Validator[URL1](com.github.gekomad.regexcollection.Collection.validatorURL1.regexp, URL1.toString) + implicit val validatorURL2: Validator[URL2] = + Validator[URL2](com.github.gekomad.regexcollection.Collection.validatorURL2.regexp, URL2.toString) + implicit val validatorURL3: Validator[URL3] = + Validator[URL3](com.github.gekomad.regexcollection.Collection.validatorURL3.regexp, URL3.toString) + implicit val validatorFTP: Validator[FTP] = + Validator[FTP](com.github.gekomad.regexcollection.Collection.validatorFTP.regexp, FTP.toString) + implicit val validatorFTP1: Validator[FTP1] = + Validator[FTP1](com.github.gekomad.regexcollection.Collection.validatorFTP1.regexp, FTP1.toString) + implicit val validatorFTP2: Validator[FTP2] = + Validator[FTP2](com.github.gekomad.regexcollection.Collection.validatorFTP2.regexp, FTP2.toString) + implicit val validatorDomain: Validator[Domain] = + Validator[Domain](com.github.gekomad.regexcollection.Collection.validatorDomain.regexp, Domain.toString) + + // MD5Ops + implicit val apply18: Cons[MD5] = MD5 + final case class MD5(url: String) + implicit val validatorMD5: Validator[MD5] = + Validator[MD5](com.github.gekomad.regexcollection.Collection.validatorMD5.regexp, MD5.toString) + + // SHAOps + implicit val apply19: Cons[SHA1] = SHA1 + implicit val apply20: Cons[SHA256] = SHA256 + + final case class SHA1(value: String) + final case class SHA256(value: String) + + implicit val validatorSHA1: Validator[SHA1] = + Validator[SHA1](com.github.gekomad.regexcollection.Collection.validatorSHA1.regexp, SHA1.toString) + implicit val validatorSHA256: Validator[SHA256] = + Validator[SHA256](com.github.gekomad.regexcollection.Collection.validatorSHA256.regexp, SHA256.toString) + + // IPOps + implicit val apply21: Cons[IP] = IP + implicit val apply22: Cons[IP6] = IP6 + + final case class IP(value: String) + final case class IP6(value: String) + + implicit val validatorIP: Validator[IP] = + Validator[IP](com.github.gekomad.regexcollection.Collection.validatorIP.regexp, IP.toString) + implicit val validatorIP6: Validator[IP6] = + Validator[IP6](com.github.gekomad.regexcollection.Collection.validatorIP_6.regexp, IP6.toString) + + // BitcoinAddOps + implicit def apply23: Cons[BitcoinAdd] = BitcoinAdd + + final case class BitcoinAdd(value: String) + + implicit val validatorBitcoinAdd: Validator[BitcoinAdd] = Validator[BitcoinAdd]( + com.github.gekomad.regexcollection.Collection.validatorBitcoinAdd.regexp, + BitcoinAdd.toString + ) + + // PhonesOps + implicit val apply24: Cons[USphoneNumber] = USphoneNumber + implicit val apply25: Cons[ItalianMobilePhone] = ItalianMobilePhone + implicit val apply26: Cons[ItalianPhone] = ItalianPhone + + final case class USphoneNumber(value: String) + final case class ItalianMobilePhone(value: String) + final case class ItalianPhone(value: String) + + implicit val validatorUSphoneNumber: Validator[USphoneNumber] = Validator[USphoneNumber]( + com.github.gekomad.regexcollection.Collection.validatorUSphoneNumber.regexp, + USphoneNumber.toString + ) + implicit val validatorItalianMobilePhone: Validator[ItalianMobilePhone] = + Validator[ItalianMobilePhone]( + com.github.gekomad.regexcollection.Collection.validatorItalianMobilePhone.regexp, + ItalianMobilePhone.toString + ) + implicit val validatorItalianPhone: Validator[ItalianPhone] = Validator[ItalianPhone]( + com.github.gekomad.regexcollection.Collection.validatorItalianPhone.regexp, + ItalianPhone.toString + ) + + // TimeOps + implicit val apply27: Cons[Time24] = Time24 + implicit val apply28: Cons[MDY] = MDY + implicit val apply29: Cons[MDY2] = MDY2 + implicit val apply30: Cons[MDY3] = MDY3 + implicit val apply31: Cons[MDY4] = MDY4 + implicit val apply32: Cons[DMY] = DMY + implicit val apply33: Cons[DMY2] = DMY2 + implicit val apply34: Cons[DMY3] = DMY3 + implicit val apply35: Cons[DMY4] = DMY4 + implicit val apply36: Cons[Time] = Time + + final case class Time24(value: String) + final case class MDY(value: String) + final case class MDY2(value: String) + final case class MDY3(value: String) + final case class MDY4(value: String) + final case class DMY(value: String) + final case class DMY2(value: String) + final case class DMY3(value: String) + final case class DMY4(value: String) + final case class Time(value: String) + + implicit val validatorTime24: Validator[Time24] = + Validator[Time24](com.github.gekomad.regexcollection.Collection.validatorTime24.regexp, Time24.toString) + implicit val validatorMDY: Validator[MDY] = + Validator[MDY](com.github.gekomad.regexcollection.Collection.validatorMDY.regexp, MDY.toString) + implicit val validatorMDY2: Validator[MDY2] = + Validator[MDY2](com.github.gekomad.regexcollection.Collection.validatorMDY2.regexp, MDY2.toString) + implicit val validatorMDY3: Validator[MDY3] = + Validator[MDY3](com.github.gekomad.regexcollection.Collection.validatorMDY3.regexp, MDY3.toString) + implicit val validatorMDY4: Validator[MDY4] = + Validator[MDY4](com.github.gekomad.regexcollection.Collection.validatorMDY4.regexp, MDY4.toString) + implicit val validatorDMY: Validator[DMY] = + Validator[DMY](com.github.gekomad.regexcollection.Collection.validatorDMY.regexp, DMY.toString) + implicit val validatorDMY2: Validator[DMY2] = + Validator[DMY2](com.github.gekomad.regexcollection.Collection.validatorDMY2.regexp, DMY2.toString) + implicit val validatorDMY3: Validator[DMY3] = + Validator[DMY3](com.github.gekomad.regexcollection.Collection.validatorDMY3.regexp, DMY3.toString) + implicit val validatorDMY4: Validator[DMY4] = + Validator[DMY4](com.github.gekomad.regexcollection.Collection.validatorDMY4.regexp, DMY4.toString) + implicit val validatorTime: Validator[Time] = + Validator[Time](com.github.gekomad.regexcollection.Collection.validatorTime.regexp, Time.toString) + + // CrontabOps + final case class Cron(value: String) + implicit val apply37: Cons[Cron] = Cron + implicit val validatorCron: Validator[Cron] = + Validator[Cron](com.github.gekomad.regexcollection.Collection.validatorCron.regexp, Cron.toString) + + // CodesOps + + final case class ItalianFiscalCode(value: String) + final case class ItalianVAT(value: String) + final case class ItalianIban(value: String) + final case class USstates(value: String) + final case class USstates1(value: String) + final case class USZipCode(value: String) + final case class ItalianZipCode(value: String) + final case class USstreets(value: String) + final case class USstreetNumber(value: String) + final case class GermanStreet(value: String) + + implicit val apply38: Cons[ItalianFiscalCode] = ItalianFiscalCode + implicit val apply39: Cons[ItalianVAT] = ItalianVAT + implicit val apply40: Cons[ItalianIban] = ItalianIban + implicit val apply401: Cons[USstates] = USstates + implicit val apply402: Cons[USstates1] = USstates1 + implicit val apply53: Cons[USZipCode] = USZipCode + implicit val apply54: Cons[ItalianZipCode] = ItalianZipCode + implicit val apply541: Cons[USstreets] = USstreets + implicit val apply542: Cons[USstreetNumber] = USstreetNumber + implicit val apply543: Cons[GermanStreet] = GermanStreet + + implicit val validatorItalianFiscalCode: Validator[ItalianFiscalCode] = + Validator[ItalianFiscalCode]( + com.github.gekomad.regexcollection.Collection.validatorItalianFiscalCode.regexp, + ItalianFiscalCode.toString + ) + implicit val validatorItalianVAT: Validator[ItalianVAT] = Validator[ItalianVAT]( + com.github.gekomad.regexcollection.Collection.validatorItalianVAT.regexp, + ItalianVAT.toString + ) + implicit val validatorItalianIban: Validator[ItalianIban] = Validator[ItalianIban]( + com.github.gekomad.regexcollection.Collection.validatorItalianIban.regexp, + ItalianIban.toString + ) + implicit val validatorUSstates: Validator[USstates] = + Validator[USstates](com.github.gekomad.regexcollection.Collection.validatorUSstates.regexp, USstates.toString) + implicit val validatorUSstates1: Validator[USstates1] = + Validator[USstates1](com.github.gekomad.regexcollection.Collection.validatorUSstates1.regexp, USstates1.toString) + implicit val validatorUSZipCode: Validator[USZipCode] = + Validator[USZipCode](com.github.gekomad.regexcollection.Collection.validatorUSZipCode.regexp, USZipCode.toString) + implicit val validatorItalianZipCode: Validator[ItalianZipCode] = Validator[ItalianZipCode]( + com.github.gekomad.regexcollection.Collection.validatorItalianZipCode.regexp, + ItalianZipCode.toString + ) + implicit val validatorUSstreets: Validator[USstreets] = + Validator[USstreets](com.github.gekomad.regexcollection.Collection.validatorUSstreets.regexp, USstreets.toString) + implicit val validatorUSstreetNumber: Validator[USstreetNumber] = Validator[USstreetNumber]( + com.github.gekomad.regexcollection.Collection.validatorUSstreetNumber.regexp, + USstreetNumber.toString + ) + implicit val validatorGermanStreet: Validator[GermanStreet] = Validator[GermanStreet]( + com.github.gekomad.regexcollection.Collection.validatorGermanStreet.regexp, + GermanStreet.toString + ) + + // ConcurrencyOps + final case class UsdCurrency(value: String) + final case class EurCurrency(value: String) + final case class YenCurrency(value: String) + + implicit val apply41: Cons[UsdCurrency] = UsdCurrency + implicit val apply42: Cons[EurCurrency] = EurCurrency + implicit val apply43: Cons[YenCurrency] = YenCurrency + + implicit val validatorUsdCurrency: Validator[UsdCurrency] = Validator[UsdCurrency]( + com.github.gekomad.regexcollection.Collection.validatorUsdCurrency.regexp, + UsdCurrency.toString + ) + implicit val validatorEurCurrency: Validator[EurCurrency] = Validator[EurCurrency]( + com.github.gekomad.regexcollection.Collection.validatorEurCurrency.regexp, + EurCurrency.toString + ) + implicit val validatorYenCurrency: Validator[YenCurrency] = Validator[YenCurrency]( + com.github.gekomad.regexcollection.Collection.validatorYenCurrency.regexp, + YenCurrency.toString + ) + + // StringsOps + final case class NotASCII(value: String) + final case class SingleChar(value: String) + final case class AZString(value: String) + final case class StringAndNumber(value: String) + final case class AsciiString(value: String) + + implicit val apply44: Cons[NotASCII] = NotASCII + implicit val apply441: Cons[SingleChar] = SingleChar + implicit val apply442: Cons[AZString] = AZString + implicit val apply443: Cons[StringAndNumber] = StringAndNumber + implicit val apply444: Cons[AsciiString] = AsciiString + + implicit val validatorNotASCII: Validator[NotASCII] = + Validator[NotASCII](com.github.gekomad.regexcollection.Collection.validatorNotASCII.regexp, NotASCII.toString) + implicit val validatorSingleChar: Validator[SingleChar] = Validator[SingleChar]( + com.github.gekomad.regexcollection.Collection.validatorSingleChar.regexp, + SingleChar.toString + ) + implicit val validatorAZString: Validator[AZString] = + Validator[AZString](com.github.gekomad.regexcollection.Collection.validatorAZString.regexp, AZString.toString) + implicit val validatorAsciiString: Validator[AsciiString] = Validator[AsciiString]( + com.github.gekomad.regexcollection.Collection.validatorAsciiString.regexp, + AsciiString.toString + ) + implicit val validatorStringAndNumber: Validator[StringAndNumber] = + Validator[StringAndNumber]( + com.github.gekomad.regexcollection.Collection.validatorStringAndNumber.regexp, + StringAndNumber.toString + ) + + // LogsOps + final case class ApacheError(value: String) + implicit val apply45: Cons[ApacheError] = ApacheError + + implicit val validatorApacheError: Validator[ApacheError] = Validator[ApacheError]( + com.github.gekomad.regexcollection.Collection.validatorApacheError.regexp, + ApacheError.toString + ) + + // NumbersOps + final case class Number1(value: String) + final case class Unsigned32(value: String) + final case class Signed(value: String) + final case class Percentage(value: String) + final case class Scientific(value: String) + final case class SingleNumber(value: String) + final case class Celsius(value: String) + final case class Fahrenheit(value: String) + + implicit val apply46: Cons[Number1] = Number1 + implicit val apply47: Cons[Unsigned32] = Unsigned32 + implicit val apply48: Cons[Signed] = Signed + implicit val apply49: Cons[Percentage] = Percentage + implicit val apply491: Cons[Scientific] = Scientific + implicit val apply492: Cons[SingleNumber] = SingleNumber + implicit val apply493: Cons[Celsius] = Celsius + implicit val apply494: Cons[Fahrenheit] = Fahrenheit + + implicit val validatorNumber1: Validator[Number1] = + Validator[Number1](com.github.gekomad.regexcollection.Collection.validatorNumber1.regexp, Number1.toString) + implicit val validatorUnsigned32: Validator[Unsigned32] = Validator[Unsigned32]( + com.github.gekomad.regexcollection.Collection.validatorUnsigned32.regexp, + Unsigned32.toString + ) + implicit val validatorSigned: Validator[Signed] = + Validator[Signed](com.github.gekomad.regexcollection.Collection.validatorSigned.regexp, Signed.toString) + implicit val validatorPercentage: Validator[Percentage] = Validator[Percentage]( + com.github.gekomad.regexcollection.Collection.validatorPercentage.regexp, + Percentage.toString + ) + implicit val validatorScientific: Validator[Scientific] = Validator[Scientific]( + com.github.gekomad.regexcollection.Collection.validatorScientific.regexp, + Scientific.toString + ) + implicit val validatorSingleNumber: Validator[SingleNumber] = Validator[SingleNumber]( + com.github.gekomad.regexcollection.Collection.validatorSingleNumber.regexp, + SingleNumber.toString + ) + implicit val validatorCelsius: Validator[Celsius] = + Validator[Celsius](com.github.gekomad.regexcollection.Collection.validatorCelsius.regexp, Celsius.toString) + implicit val validatorFahrenheit: Validator[Fahrenheit] = Validator[Fahrenheit]( + com.github.gekomad.regexcollection.Collection.validatorFahrenheit.regexp, + Fahrenheit.toString + ) + + // CoordinatesOps + final case class Coordinate(value: String) + final case class Coordinate1(value: String) + final case class Coordinate2(value: String) + + implicit val apply50: Cons[Coordinate] = Coordinate + implicit val apply51: Cons[Coordinate1] = Coordinate1 + implicit val apply52: Cons[Coordinate2] = Coordinate2 + + implicit val validatorCoordinate: Validator[Coordinate] = Validator[Coordinate]( + com.github.gekomad.regexcollection.Collection.validatorCoordinate.regexp, + Coordinate.toString + ) + implicit val validatorCoordinate1: Validator[Coordinate1] = Validator[Coordinate1]( + com.github.gekomad.regexcollection.Collection.validatorCoordinate1.regexp, + Coordinate1.toString + ) + implicit val validatorCoordinate2: Validator[Coordinate2] = Validator[Coordinate2]( + com.github.gekomad.regexcollection.Collection.validatorCoordinate2.regexp, + Coordinate2.toString + ) + + } +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/Constants.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/Constants.scala new file mode 100644 index 0000000..5aee252 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/Constants.scala @@ -0,0 +1,20 @@ +package com.github.gekomad.ittocsv.parser + +/** @author + * Giuseppe Cannella + * @since 0.0.1 + */ +object Constants { + val COMMA = ',' + val SEMICOLON = ';' + val COMMENT = '#' + val CR = "\r" + val CR_char = '\r' + val CRLF = "\r\n" + val DOUBLE_QUOTE: Char = '"' + val LF = "\n" + val LF_char = '\n' + val SP = ' ' + val PIPE = '|' + val TAB = '\t' +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/CsvFieldToString.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/CsvFieldToString.scala new file mode 100644 index 0000000..10147f5 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/CsvFieldToString.scala @@ -0,0 +1,78 @@ +package com.github.gekomad.ittocsv.parser + +/** Trasforms a single CSV field to string + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + * @see + * See test code for more information + * @see + * See [[https://github.com/gekomad/itto-csv/blob/master/README.md]] for more information. + */ +object CsvFieldToString { + + /** @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * trims the string according to csvFormat + */ + def trim(field: String)(implicit csvFormat: IttoCSVFormat): String = if (csvFormat.trim) field.trim else field + + /** @return + * trasforms a CSV field to string + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @param field + * the string to trasform + * {{{ + * csvFieldToString("\"\"\",\"\"\"") // "\",\"" + * csvFieldToString("\"aa\na\"") // "aa\na" + * csvFieldToString("\"\"\"\"\"\"") // "\"\"" + * csvFieldToString("\"\"\"\"") // "\"" + * csvFieldToString("\",\"") // "," + * csvFieldToString("\"\"") // "" + * csvFieldToString("\"\"\"a\"\"\"") // "\"a\"" + * csvFieldToString("\" \"") // " " + * csvFieldToString("aaa") // "aaa" + * csvFieldToString("\"aa\"\"b\"") // "aa\"b" + * csvFieldToString("\"aa\"\"\"\"b\"") // "aa\"\"b" + * csvFieldToString("\"aa,a\"") // "aa,a" + * csvFieldToString("\"aa,\"\"b\"") // "aa,\"b" + * csvFieldToString("\"aa,\"\"b\"") // "aa,\"b" + * }}} + */ + def csvFieldToString(field: String)(implicit csvFormat: IttoCSVFormat): String = { + + /* + * @param csvFormat the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @param field the string to trasform + * {{{ + * "aaaa,bbbb" => aaaa,bbbb + * "aaaa\nbbbb" => aaaa\nbbbb + * "" => { } + * "aaaabbbb " => {aaaabbbb } + * "#aaaabbbb" => #aaaabbbb + * }}} + * + */ + def parseBorders(a: String)(implicit csvFormat: IttoCSVFormat): String = { + val q = csvFormat.quote.toString + if (a.length > 1 && a.startsWith(q) && a.endsWith(q)) a.init.drop(1) else a + } + + /* + * @param csvFormat the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @param field the string to trasform + * {{{ + * "aaaa""bbbb" => aaaa"bbbb + * "aaaa""""bbbb" => aaaa""bbbb + * }}} + * + */ + def parseQuote(a: String)(implicit csvFormat: IttoCSVFormat): String = + a.replace(s"${csvFormat.quote}${csvFormat.quote}", s"${csvFormat.quote}") + + parseQuote(parseBorders(trim(field))) + } +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/IttoCSVFormat.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/IttoCSVFormat.scala new file mode 100644 index 0000000..c95c4e9 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/IttoCSVFormat.scala @@ -0,0 +1,129 @@ +package com.github.gekomad.ittocsv.parser + +import Constants._ + +/** The formatter determines how CSV will be generated, two formatters are available: + * + * | Method | Description | Default Formatter | Tab formatter | + * |:-------------------------------|:----------------------------:|------------------:|--------------:| + * | withDelimiter(c: Char) | the separator between fields | , | \t | + * | withQuote(c: Char) | the quoteChar character | " | " | + * | withQuoteEmpty(c: Boolean) | quotes field if empty | false | false | + * | withForceQuote(c: Boolean) | quotes all fields | false | false | + * | withPrintHeader(c: Boolean) | if true prints the header | false | false | + * | withTrim(c: Boolean) | trims the field | false | false | + * | withRecordSeparator(c: String) | the rows separator | \r\n | \r\n | + * + * it's possible to create custom foramtters editing the default ones, example: + * + * {{{ + * implicit val newFormatter = default.withForceQuote(true).withRecordSeparator("\n").with..... + * }}} + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + * @param delimeter + * the separator between fields + * @param quote + * the quoteChar character + * @param recordSeparator + * the record separator + * @param quoteEmpty + * if true quotes the empty field + * @param forceQuote + * if true quotes all fields + * @param printHeader + * if true prints the header + * @param trim + * if true trims the fields + * @param ignoreEmptyLines + * if true skip empty lines + * @param quoteLowerChar + * if true quotes lower chars + * @see + * See test code for more information + * @see + * See [[https://github.com/gekomad/itto-csv/blob/master/README.md]] for more information. + */ +final case class IttoCSVFormat( + delimeter: Char, + quote: Char, + recordSeparator: String, + quoteEmpty: Boolean, + forceQuote: Boolean, + printHeader: Boolean, + trim: Boolean, + ignoreEmptyLines: Boolean, + quoteLowerChar: Boolean +) { + def withDelimiter(c: Char): IttoCSVFormat = this.copy(delimeter = c) + + def withQuote(c: Char): IttoCSVFormat = this.copy(quote = c) + + def withQuoteEmpty(c: Boolean): IttoCSVFormat = this.copy(quoteEmpty = c) + + def withForceQuote(c: Boolean): IttoCSVFormat = this.copy(forceQuote = c) + + def withPrintHeader(c: Boolean): IttoCSVFormat = this.copy(printHeader = c) + + def withTrim(c: Boolean): IttoCSVFormat = this.copy(trim = c) + + def withRecordSeparator(c: String): IttoCSVFormat = this.copy(recordSeparator = c) + + def withIgnoreEmptyLines(c: Boolean): IttoCSVFormat = this.copy(ignoreEmptyLines = c) + + def withQuoteLowerChar(c: Boolean): IttoCSVFormat = this.copy(quoteLowerChar = c) +} + +object IttoCSVFormat { + + /** | Method | Descrizione | default | + * |:---------------------------------|:-----------------------------------------:|--------:| + * | withDelimiter(c: Char) | the separator between fields | , | + * | withQuote(c: Char) | the quoteChar character | " | + * | withQuoteEmpty(c: Boolean) | quotes field if empty | false | + * | withForceQuote(c: Boolean) | quotes all fields | false | + * | withPrintHeader(c: Boolean) | if true prints the header (method toCsvL) | false | + * | withTrim(c: Boolean) | trims the field | false | + * | withRecordSeparator(c: String) | the rows separator | \r\n | + * | withIgnoreEmptyLines(c: Boolean) | skips empty lines false | | + * | withQuoteLowerChar(c: Boolean) | quotes lower chars | false | + */ + val default: IttoCSVFormat = IttoCSVFormat( + quote = DOUBLE_QUOTE, + delimeter = COMMA, + recordSeparator = CRLF, + quoteEmpty = false, + forceQuote = false, + printHeader = true, + trim = false, + ignoreEmptyLines = false, + quoteLowerChar = false + ) + + /* + * | Method | Descrizione | default| + * |----------|:-------------:|------:|-:| + * | withDelimiter(c: Char) | the separator between fields |\t| + * | withQuote(c: Char) | the quoteChar character |"| + * | withQuoteEmpty(c: Boolean) | quotes field if empty |false| + * | withForceQuote(c: Boolean) | quotes all fields |false| + * | withPrintHeader(c: Boolean) | if true prints the header (method toCsvL) |false| + * | withTrim(c: Boolean) | trims the field | false| + * | withRecordSeparator(c: String) | the rows separator |\r\n| + * | withIgnoreEmptyLines(c: Boolean) | skips empty lines | false | + * | withQuoteLowerChar(c: Boolean) | quotes lower chars| false | + */ + val tab: IttoCSVFormat = IttoCSVFormat( + quote = DOUBLE_QUOTE, + delimeter = TAB, + recordSeparator = CRLF, + quoteEmpty = false, + forceQuote = false, + printHeader = true, + trim = false, + ignoreEmptyLines = false, + quoteLowerChar = false + ) +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/StringToCsvField.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/StringToCsvField.scala new file mode 100644 index 0000000..545b284 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/StringToCsvField.scala @@ -0,0 +1,67 @@ +package com.github.gekomad.ittocsv.parser + +import Constants._ + +/** + * Trasforms a string to CSV field + * + * @author Giuseppe Cannella + * @since 0.0.1 + * @see See test code for more information + * @see See [[https://github.com/gekomad/itto-csv/blob/master/README.md]] for more information + */ +object StringToCsvField { + + /** + * @return trasforms a string to CSV field + * @param csvFormat the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @param field the string to trasform + * {{{ + * stringToCsvField("\"") // "\"\"\"\"" + * stringToCsvField(",") // "\",\"" + * stringToCsvField("\",\"") // "\"\"\",\"\"\"" + * stringToCsvField("\"a\"") // "\"\"\"a\"\"\"" + * stringToCsvField("") // "\"\"" + * stringToCsvField("aa") // "\"aa\"" + * stringToCsvField(" ") // "\" \"" + * stringToCsvField("aaa") // "\"aaa\"" + * stringToCsvField("aa\na") // "\"aa\na\"" + * stringToCsvField("aa\"b") // "\"aa\"\"b\"" + * stringToCsvField("aa\"\"b") // "\"aa\"\"\"\"b\"" + * stringToCsvField("aa,a") // "\"aa,a\"" + * stringToCsvField("aa,\"b") // "\"aa,\"\"b\"" + * stringToCsvField("aa,\"b") // "\"aa,\"\"b\"" + * }}} + * + */ + def stringToCsvField(field: String)(implicit csvFormat: IttoCSVFormat): String = { + + def trim(s: String): String = if (csvFormat.trim) s.trim else s + + def parseQuote(string: String)(implicit csvFormatter: IttoCSVFormat): String = { + val q = csvFormat.quote + string match { + case x if x == s"$q$q" => s"$q$q$q$q$q$q" + case x if x == s"$q" => s"$q$q$q$q" + case "" if csvFormatter.quoteEmpty || csvFormatter.forceQuote => s"$q$q" + case "" => string + case s => + var c = 0 + var containsQuote = false + + do { + if (s(c) == q || s(c) == csvFormat.delimeter || s(c) == CR_char || s(c) == LF_char) containsQuote = true + c = c + 1 + } while (!containsQuote && c < s.length) + + val p = if (containsQuote) s.replace(csvFormat.quote.toString, s"$q$q") else s + + if (containsQuote || csvFormat.forceQuote || csvFormat.quoteLowerChar && (s(s.length - 1) <= SP || s(0) <= COMMENT)) + s"${csvFormat.quote}$p${csvFormat.quote}" + else p + } + } + + parseQuote(trim(field)) + } +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/io/FromFile.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/io/FromFile.scala new file mode 100644 index 0000000..d586e55 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/io/FromFile.scala @@ -0,0 +1,61 @@ +//package com.github.gekomad.ittocsv.parser.io +// +//import com.github.gekomad.ittocsv.core.FromCsv.fromCsv +//import com.github.gekomad.ittocsv.core.ParseFailure +//import fs2.io.file.Files +//import cats.effect.unsafe.implicits.global +//import scala.util.{Failure, Success, Try} +// +///** Reads a CSV file +// * +// * @author +// * Giuseppe Cannella +// * @since 1.0.1 +// * @see +// * See test code for more information +// * @see +// * See [[https://github.com/gekomad/itto-csv/blob/master/README.md]] for more information +// */ +//object FromFile { +// +// import cats.data.NonEmptyList +// import com.github.gekomad.ittocsv.core.Header.FieldNames +// import com.github.gekomad.ittocsv.core.Schema +// import com.github.gekomad.ittocsv.parser.IttoCSVFormat +// +// /** @param filePath +// * the file path of file to read +// * @param skipHeader +// * if true doesn't read first row +// * @param csvFormat +// * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter +// * @return +// * `Try[List[Either[NonEmptyList[FromCsvImpl.ParseFailure], A]]]` +// */ +// def csvFromFileUnsafe[A: FieldNames: Schema](filePath: String, skipHeader: Boolean)(implicit +// csvFormat: IttoCSVFormat +// ): Try[List[Either[NonEmptyList[ParseFailure], A]]] = +// csvFromFileStream(filePath, skipHeader).compile.toList.attempt.unsafeRunSync() match { +// case Left(e) => Failure(e) +// case Right(value) => Success(value) +// } +// +// import java.nio.file.Paths +// +// import cats.effect.IO +// import fs2.{Stream, text} +// +// def csvFromFileStream[A: FieldNames: Schema](filePath: String, skipHeader: Boolean)(implicit +// csvFormat: IttoCSVFormat +// ): Stream[IO, Either[NonEmptyList[ParseFailure], A]] = { +// val x: Stream[IO, Either[NonEmptyList[ParseFailure], A]] = +// Files[IO] +// .readAll(fs2.io.file.Path(filePath)) +// .through(text.utf8.decode) +// .through(text.lines) +// .map(line => fromCsv[A](line).head) +// +// if (skipHeader) x.drop(1) else x +// +// } +//} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/io/ToFile.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/io/ToFile.scala new file mode 100644 index 0000000..a458243 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/parser/io/ToFile.scala @@ -0,0 +1,67 @@ +//package com.github.gekomad.ittocsv.parser.io +// +//import cats.effect.{ExitCode, IO} +//import com.github.gekomad.ittocsv.core.CsvStringEncoder +//import com.github.gekomad.ittocsv.util.ListUtils +//import fs2.{Chunk, Pure} +// +///** Write to CSV file +// * +// * @author +// * Giuseppe Cannella +// * @since 0.0.1 +// * @see +// * See test code for more information +// * @see +// * See [[https://github.com/gekomad/itto-csv/blob/master/README.md]] for more information +// */ +//object ToFile { +// +// import com.github.gekomad.ittocsv.core.Header.FieldNames +// import com.github.gekomad.ittocsv.parser.IttoCSVFormat +// +// /** @param list +// * the list to write +// * @param filePath +// * the file path of file to write +// * @param csvFormat +// * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter +// * @return +// * the filePath into a ` fs2.Stream[IO, Unit]` +// */ +// def csvToFile[A: FieldNames]( +// list: Seq[A], +// filePath: String +// )(implicit csvFormat: IttoCSVFormat, enc: CsvStringEncoder[A]): IO[ExitCode] = { +// import com.github.gekomad.ittocsv.core.ToCsv._ +// import com.github.gekomad.ittocsv.core.Header._ +// +// val l: List[String] = csvHeader[A] :: list.map(toCsv(_, printRecordSeparator = true)).toList +// +// ListUtils.writeFile(l, filePath, false) +// +// } +// +// /** @param stream +// * the stream to write +// * @param filePath +// * the file path of file to write +// * @param csvFormat +// * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter +// * @return +// * the filePath into a ` fs2.Stream[IO, Unit]` +// */ +// def csvToFileStream[A: FieldNames]( +// stream: fs2.Stream[Pure, A], +// filePath: String +// )(implicit csvFormat: IttoCSVFormat, enc: CsvStringEncoder[A]): IO[ExitCode] = { +// import com.github.gekomad.ittocsv.core.ToCsv._ +// import com.github.gekomad.ittocsv.core.Header._ +// +// val l: fs2.Stream[Pure, String] = stream.map(toCsv(_, printRecordSeparator = true)).cons(Chunk(csvHeader[A])) +// +// ListUtils.writeFileStream(l, filePath, false) +// +// } +// +//} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/ListUtils.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/ListUtils.scala new file mode 100644 index 0000000..73d94d7 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/ListUtils.scala @@ -0,0 +1,58 @@ +package com.github.gekomad.ittocsv.util +import cats.effect.{ExitCode, IO} +import cats.implicits._ +import fs2.io.file.Files +import fs2.{Stream, text} + +import java.nio.file.Paths +import scala.concurrent.ExecutionContext.Implicits.global + +/** Utils for lists + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + */ +object ListUtils { + + /** @param list + * the list to write + * @param filePath + * the file path of file to write + * @param addLineSeparator + * if true add a line separator, default = true + * @return + * the filePath into `Stream[IO, Unit]` + */ + def writeFile(list: List[String], filePath: String, addLineSeparator: Boolean = true): IO[ExitCode] = { + val a: Stream[IO, String] = Stream.emits(list) + val b: Stream[IO, String] = if (addLineSeparator) a.map(_ + System.lineSeparator) else a + b.through(text.utf8.encode) + .through(Files[IO].writeAll(fs2.io.file.Path(filePath))) + .compile + .drain + .as(ExitCode.Success) + } + + /** @param stream + * the stream to write + * @param filePath + * the file path of file to write + * @param addLineSeparator + * if true add a line separator, default = true + * @return + * the filePath into `Stream[IO, Unit]` + */ + def writeFileStream( + stream: fs2.Stream[IO, String], + filePath: String, + addLineSeparator: Boolean = true + ): IO[ExitCode] = { + val b: Stream[IO, String] = if (addLineSeparator) stream.map(_ + System.lineSeparator) else stream + b.through(text.utf8.encode) + .through(Files[IO].writeAll(fs2.io.file.Path(filePath))) + .compile + .drain + .as(ExitCode.Success) + } +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/StringUtils.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/StringUtils.scala new file mode 100644 index 0000000..1a56753 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/StringUtils.scala @@ -0,0 +1,77 @@ +package com.github.gekomad.ittocsv.util + +import com.github.gekomad.ittocsv.parser.IttoCSVFormat + +import scala.annotation.tailrec + +/** Utils for strings + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + */ +object StringUtils { + + /** @return + * splits the string at positions of separators + * @param string + * the string to split + */ + def split(string: String, separators: List[Int]): List[String] = { + + def sp(s: String, sep: List[Int]): List[String] = sep match { + case Nil => List(s) + + case x0 :: x1 :: xs => + val a = s.substring(x0 + 1, x1) + val b = sp(s, x1 :: xs) + a :: b + case x0 :: _ => List(s.substring(x0 + 1, s.length)) + } + + sp(string, -1 :: separators) + } + + def getDelimiter(s: String): Char = { + @tailrec + def go(s: String, delimiter: Int): Int = if (s.contains(delimiter.toChar)) go(s, delimiter - 1) else delimiter + + go(s, 0xffff).toChar + } + + /** @return + * trasforms a CSV string to List of strings + * @param csvFormat + * the CSV formatter + * @param csv + * the string to trasform + * {{{ + * tokenizeCsvLine("""1,"foo,bar",y,"2,e,","2ne","a""bc""z"""") // List("1", "foo,bar", "y", "2,e,", "2ne", "a\"bc\"z")) + * tokenizeCsvLine("1,foo") // Some(List("1", "foo")) + * tokenizeCsvLine("1,\"foo") // None + * }}} + */ + def tokenizeCsvLine(csv: String)(implicit csvFormat: IttoCSVFormat): Option[List[String]] = csv match { + case _ if !csv.contains(csvFormat.quote) => Some(csv.split(csvFormat.delimeter.toString, -1).toList) + case _ if csv.count(_ == csvFormat.quote) % 2 != 0 => None + case _ => + val delimiter = getDelimiter(csv) + val string = csv.replace(s"${csvFormat.quote}${csvFormat.quote}", delimiter.toString) + + var inside = false + val arr = string.toCharArray + val commas = scala.collection.mutable.ListBuffer.empty[Int] + + var c = 0 + while (c < arr.length) { + if (arr(c) == csvFormat.quote) inside = !inside + else if (!inside && arr(c) == csvFormat.delimeter) commas += c + c = c + 1 + } + + val l: List[String] = split(arr.mkString, commas.toList) + val p: List[String] = l.map(_.replace(s"${csvFormat.quote}", "").replace(delimiter, csvFormat.quote)) + Some(p) + } + +} diff --git a/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/TryTo.scala b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/TryTo.scala new file mode 100644 index 0000000..a877f32 --- /dev/null +++ b/scala2_js/src/main/scala/com/github/gekomad/ittocsv/util/TryTo.scala @@ -0,0 +1,16 @@ +package com.github.gekomad.ittocsv.util +import cats.data.{NonEmptyList, Validated} +import scala.util.{Success, Try} + +object TryTo { + def tryToEither[A, B](a: => A)(b: B): Either[B, A] = Try(a) match { + case Success(value) => Right(value) + case _ => Left(b) + } + + def tryToValidate[A, B](a: => A)(b: B): Validated[NonEmptyList[B], A] = Try(a) match { + case Success(value) => Validated.valid(value) + case _ => Validated.invalidNel(b) + } + +} diff --git a/scala2_js/src/test/resources/csv_with_error.csv b/scala2_js/src/test/resources/csv_with_error.csv new file mode 100644 index 0000000..6b5f351 --- /dev/null +++ b/scala2_js/src/test/resources/csv_with_error.csv @@ -0,0 +1,5 @@ +id name date +1CC3CCBB-C749-3078-E050-1AACBE064651 bob 2018-11-20T09:10:25 +3CC3CCBB-C749-3078-E050-1AACBE064653 alice 2018-11-20T10:12:24 +xxx jim 2018-11-20T11:18:17 +5CC3CCBB-C749-3078-E050-1AACBE064655 tom 2018-11-20T11:36:04 \ No newline at end of file diff --git a/scala2_js/src/test/resources/csv_with_header.csv b/scala2_js/src/test/resources/csv_with_header.csv new file mode 100644 index 0000000..83d159a --- /dev/null +++ b/scala2_js/src/test/resources/csv_with_header.csv @@ -0,0 +1,5 @@ +id name date +1CC3CCBB-C749-3078-E050-1AACBE064651 bob 2018-11-20T09:10:25 +3CC3CCBB-C749-3078-E050-1AACBE064653 alice 2018-11-20T10:12:24 +4CC3CCBB-C749-3078-E050-1AACBE064654 jim 2018-11-20T11:18:17 +5CC3CCBB-C749-3078-E050-1AACBE064655 tom 2018-11-20T11:36:04 \ No newline at end of file diff --git a/scala2_js/src/test/resources/csv_without_header.csv b/scala2_js/src/test/resources/csv_without_header.csv new file mode 100644 index 0000000..a05cdbe --- /dev/null +++ b/scala2_js/src/test/resources/csv_without_header.csv @@ -0,0 +1,4 @@ +1CC3CCBB-C749-3078-E050-1AACBE064651 bob 2018-11-20T09:10:25 +3CC3CCBB-C749-3078-E050-1AACBE064653 alice 2018-11-20T10:12:24 +4CC3CCBB-C749-3078-E050-1AACBE064654 jim 2018-11-20T11:18:17 +5CC3CCBB-C749-3078-E050-1AACBE064655 tom 2018-11-20T11:36:04 \ No newline at end of file diff --git a/scala2_js/src/test/resources/empty_file.csv b/scala2_js/src/test/resources/empty_file.csv new file mode 100644 index 0000000..e69de29 diff --git a/scala2_js/src/test/scala/CsvFieldTest.scala b/scala2_js/src/test/scala/CsvFieldTest.scala new file mode 100644 index 0000000..b27ea3d --- /dev/null +++ b/scala2_js/src/test/scala/CsvFieldTest.scala @@ -0,0 +1,108 @@ +class CsvFieldTest extends munit.FunSuite { + test("stringToCsvField trim") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withTrim(true) + + assert(StringToCsvField.stringToCsvField("a ") == "a") + assert(StringToCsvField.stringToCsvField(" a ") == "a") + assert(StringToCsvField.stringToCsvField(" ") == "") + } + + test("stringToCsvField force quote") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withForceQuote(true) + + assert(StringToCsvField.stringToCsvField("\"") == "\"\"\"\"") + assert(StringToCsvField.stringToCsvField(",") == "\",\"") + assert(StringToCsvField.stringToCsvField("\",\"") == "\"\"\",\"\"\"") + assert(StringToCsvField.stringToCsvField("\"a\"") == "\"\"\"a\"\"\"") + assert(StringToCsvField.stringToCsvField("") == "\"\"") + assert(StringToCsvField.stringToCsvField("aa") == "\"aa\"") + assert(StringToCsvField.stringToCsvField(" ") == "\" \"") + assert(StringToCsvField.stringToCsvField("aaa") == "\"aaa\"") + assert(StringToCsvField.stringToCsvField("aa\na") == "\"aa\na\"") + assert(StringToCsvField.stringToCsvField("aa\"b") == "\"aa\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa\"\"b") == "\"aa\"\"\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,a") == "\"aa,a\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + } + + test("stringToCsvField 0") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withQuoteLowerChar(true) + assert(StringToCsvField.stringToCsvField(" ") == "\" \"") + } + + test("stringToCsvField 1") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withQuoteLowerChar(true) + + assert(StringToCsvField.stringToCsvField("\"") == "\"\"\"\"") + assert(StringToCsvField.stringToCsvField(",") == "\",\"") + assert(StringToCsvField.stringToCsvField("\",\"") == "\"\"\",\"\"\"") + assert(StringToCsvField.stringToCsvField("\"a\"") == "\"\"\"a\"\"\"") + assert(StringToCsvField.stringToCsvField("") == "") + assert(StringToCsvField.stringToCsvField(" ") == "\" \"") + assert(StringToCsvField.stringToCsvField("aaa") == "aaa") + assert(StringToCsvField.stringToCsvField("aa\na") == "\"aa\na\"") + assert(StringToCsvField.stringToCsvField("aa\"b") == "\"aa\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa\"\"b") == "\"aa\"\"\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,a") == "\"aa,a\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + } + + test("stringToCsvField 2") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withQuoteEmpty(true).withQuoteLowerChar(true) + + assert(StringToCsvField.stringToCsvField("\"") == "\"\"\"\"") + assert(StringToCsvField.stringToCsvField(",") == "\",\"") + assert(StringToCsvField.stringToCsvField("\",\"") == "\"\"\",\"\"\"") + assert(StringToCsvField.stringToCsvField("\"a\"") == "\"\"\"a\"\"\"") + assert(StringToCsvField.stringToCsvField("") == "\"\"") + assert(StringToCsvField.stringToCsvField(" ") == "\" \"") + assert(StringToCsvField.stringToCsvField("aaa") == "aaa") + assert(StringToCsvField.stringToCsvField("aa\na") == "\"aa\na\"") + assert(StringToCsvField.stringToCsvField("aa\"b") == "\"aa\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa\"\"b") == "\"aa\"\"\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,a") == "\"aa,a\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + } + + test("csvFieldToString") { + import com.github.gekomad.ittocsv.parser.CsvFieldToString.csvFieldToString + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withQuoteEmpty(true) + + assert(csvFieldToString("\"\"\",\"\"\"") == "\",\"") + assert(csvFieldToString("\"aa\na\"") == "aa\na") + assert(csvFieldToString("\"\"\"\"\"\"") == "\"\"") + + assert(csvFieldToString("\"\"\"\"") == "\"") + assert(csvFieldToString("\",\"") == ",") + assert(csvFieldToString("\"\"") == "") + assert(csvFieldToString("\"\"\"a\"\"\"") == "\"a\"") + + assert(csvFieldToString("\" \"") == " ") + assert(csvFieldToString("aaa") == "aaa") + + assert(csvFieldToString("\"aa\"\"b\"") == "aa\"b") + assert(csvFieldToString("\"aa\"\"\"\"b\"") == "aa\"\"b") + assert(csvFieldToString("\"aa,a\"") == "aa,a") + assert(csvFieldToString("\"aa,\"\"b\"") == "aa,\"b") + assert(csvFieldToString("\"aa,\"\"b\"") == "aa,\"b") + } + + test("both 1") { + import com.github.gekomad.ittocsv.parser.CsvFieldToString.csvFieldToString + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withDelimiter('A') + val orig = "aAc" + val csv = StringToCsvField.stringToCsvField(orig) + val string = csvFieldToString(csv) + assert(orig == string) + } +} diff --git a/scala2_js/src/test/scala/CsvLineTest.scala b/scala2_js/src/test/scala/CsvLineTest.scala new file mode 100644 index 0000000..ecfc909 --- /dev/null +++ b/scala2_js/src/test/scala/CsvLineTest.scala @@ -0,0 +1,28 @@ +import com.github.gekomad.ittocsv.parser.IttoCSVFormat + +class CsvLineTest extends munit.FunSuite { + + test("csv string to list") { + + implicit val csvFormat: IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + import com.github.gekomad.ittocsv.util.StringUtils._ + { + val csvString = """1,"foo,bar",y,"2,e,","2ne","a""bc""z"""" + + assert(tokenizeCsvLine(csvString) == Some(List("1", "foo,bar", "y", "2,e,", "2ne", "a\"bc\"z"))) + } + + { + val csvString = "1,foo" + + assert(tokenizeCsvLine(csvString) == Some(List("1", "foo"))) + } + + { + val csvString = "1,\"foo" + + assert(tokenizeCsvLine(csvString) == None) + } + } + +} diff --git a/scala2_js/src/test/scala/FromCsvTest.scala b/scala2_js/src/test/scala/FromCsvTest.scala new file mode 100644 index 0000000..9b4d64c --- /dev/null +++ b/scala2_js/src/test/scala/FromCsvTest.scala @@ -0,0 +1,1198 @@ +import cats.Id +import cats.data.Validated.{Invalid, Valid} +import cats.data.{NonEmptyList, ValidatedNel} + +import com.github.gekomad.ittocsv.core.{Convert, ParseFailure, Schema} + +class FromCsvTest extends munit.FunSuite { + + test("csv string to type - 1") { + + import cats.data.Validated.{Invalid, Valid} + import cats.data.NonEmptyList + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Header._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(a: Int, b: Double, c: String, d: Option[Boolean]) + + val schema = Schema.of[Foo] + val fields: List[String] = fieldNames[Foo] + val csv: List[String] = List("1", "3.14", "foo", "true") + val p: Map[String, String] = fields.zip(csv).toMap + assert(schema.readFrom(p) == Valid(Foo(1, 3.14, "foo", Some(true)))) + + def e: Map[String, String] = Map("c" -> "true", "b" -> "xx", "d" -> "true") + + assert( + schema.readFrom(e) == Invalid(NonEmptyList(ParseFailure("a is missing"), List(ParseFailure("xx is not Double")))) + ) + + } + + test("csv string to type - 2") { + + import cats.data.Validated.{Invalid, Valid} + import cats.data.NonEmptyList + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Header._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Foo( + a: Int, + b: Double, + c: String, + d: Option[Boolean], + e: Option[String], + f: Option[String], + e1: Option[Double], + f1: Option[Double], + e2: Option[Int], + f2: Option[Int] + ) + + val schema = Schema.of[Foo] + val fields: List[String] = fieldNames[Foo] + val csv: List[String] = List("1", "3.14", "foo", "", "", "hi", "", "3.3", "", "100") + val p: Map[String, String] = fields.zip(csv).toMap + assert(schema.readFrom(p) == Valid(Foo(1, 3.14, "foo", None, None, Some("hi"), None, Some(3.3), None, Some(100)))) + + } + + test("csv string to types 3") { + + import cats.data.Validated.{Invalid, Valid} + import cats.data.NonEmptyList + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Header._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(a: Int, b: Char, c: String, d: Option[Boolean]) + + val schema = Schema.of[Foo] + val fields: List[String] = fieldNames[Foo] + val csv: List[String] = List("1", "λ", "foo", "baz") + val p: Map[String, String] = fields.zip(csv).toMap + assert(schema.readFrom(p) == Invalid(NonEmptyList(ParseFailure("Not a Boolean for input string: baz"), List()))) + + } + + test("tokenizeCsvLine to types ok") { + import cats.data.NonEmptyList + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Header._ + import cats.data.Validated.Valid + import com.github.gekomad.ittocsv.util.StringUtils._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(a: Int, b: Double, c: String, d: Boolean) + + val fields: List[String] = fieldNames[Foo] + val csv: Option[List[String]] = tokenizeCsvLine("1,3.14,foo,true") + csv match { + case None => assert(false) + case Some(g) => + assert(g == List("1", "3.14", "foo", "true")) + val schema = Schema.of[Foo] + val p: Map[String, String] = fields.zip(g).toMap + assert(schema.readFrom(p) == Valid(Foo(1, 3.14, "foo", true))) + } + } + + test("tokenizeCsvLine to types boolean ko") { + import cats.data.NonEmptyList + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Header._ + import cats.data.Validated.Invalid + import com.github.gekomad.ittocsv.util.StringUtils._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(a: Int, b: Double, c: String, d: Boolean) + + val fields: List[String] = fieldNames[Foo] + val csv: Option[List[String]] = tokenizeCsvLine("1,3.14,foo,bar") + csv match { + case None => assert(false) + case Some(g) => + assert(g == List("1", "3.14", "foo", "bar")) + val schema = Schema.of[Foo] + val p: Map[String, String] = fields.zip(g).toMap + assert(schema.readFrom(p) == Invalid(NonEmptyList(ParseFailure("bar is not Boolean"), Nil))) + } + } + + test("tokenizeCsvLine to types Option[Double] ko") { + import cats.data.NonEmptyList + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Header._ + import cats.data.Validated.Invalid + import com.github.gekomad.ittocsv.util.StringUtils._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(a: Int, b: Double, c: String, d: Option[Double]) + + val fields: List[String] = fieldNames[Foo] + val csv: Option[List[String]] = tokenizeCsvLine("1,3.14,foo,bar") + csv match { + case None => assert(false) + case Some(g) => + assert(g == List("1", "3.14", "foo", "bar")) + val schema = Schema.of[Foo] + val p: Map[String, String] = fields.zip(g).toMap + assert(schema.readFrom(p) == Invalid(NonEmptyList(ParseFailure("""Not a Double for input string: bar"""), Nil))) + } + } + + test("from csv SHA1") { + import cats.data.NonEmptyList + import com.github.gekomad.ittocsv.core.Types.implicits.SHA1 + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.ParseFailure + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(a: String, b: SHA1) + assert( + fromCsv[Bar]("abc,1c18da5dbf74e3fc1820469cf1f54355b7eec92d") == List( + Right(Bar("abc", SHA1("1c18da5dbf74e3fc1820469cf1f54355b7eec92d"))) + ) + ) + + assert(fromCsv[Bar]("abc,hi") == List(Left(NonEmptyList(ParseFailure("Not a SHA1 hi"), Nil)))) + } + + test("List[Int] ok") { + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + val a: ValidatedNel[ParseFailure, Id[List[Int]]] = Convert.f[Int, Id].parse("1,2,3") + assert(a == Valid(List(1, 2, 3))) + } + + test("List[Int] ko") { + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + val a: ValidatedNel[ParseFailure, Id[List[Int]]] = Convert.f[Int, Id].parse("foo,bar,3") + assert(a == Invalid(NonEmptyList(ParseFailure("Bad type on foo"), List(ParseFailure("Bad type on bar"))))) + } + + test("from csv SHA256") { + import com.github.gekomad.ittocsv.core.Types.implicits.SHA256 + import com.github.gekomad.ittocsv.core.FromCsv._ + + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(a: String, b: SHA256) + assert( + fromCsv[Bar]("abc,000020f89134d831f48541b2d8ec39397bc99fccf4cc86a3861257dbe6d819d1") == List( + Right(Bar("abc", SHA256("000020f89134d831f48541b2d8ec39397bc99fccf4cc86a3861257dbe6d819d1"))) + ) + ) + + assert(fromCsv[Bar]("abc,hi") == List(Left(NonEmptyList(ParseFailure("Not a SHA256 hi"), Nil)))) + + } + + test("from csv IP") { + import com.github.gekomad.ittocsv.core.Types.implicits.IP + import com.github.gekomad.ittocsv.core.FromCsv._ + + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(a: String, b: IP) + assert(fromCsv[Bar]("abc,10.192.168.1") == List(Right(Bar("abc", IP("10.192.168.1"))))) + + assert(fromCsv[Bar]("abc,hi") == List(Left(NonEmptyList(ParseFailure("Not a IP hi"), Nil)))) + } + + test("from csv IP6") { + import com.github.gekomad.ittocsv.core.Types.implicits.IP6 + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(a: String, b: IP6) + assert(fromCsv[Bar]("abc,2001:db8:a0b:12f0::1") == List(Right(Bar("abc", IP6("2001:db8:a0b:12f0::1"))))) + + assert(fromCsv[Bar]("abc,hi") == List(Left(NonEmptyList(ParseFailure("Not a IP6 hi"), Nil)))) + } + + test("from csv MD5") { + import com.github.gekomad.ittocsv.core.Types.implicits.MD5 + import com.github.gekomad.ittocsv.core.FromCsv._ + + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(a: String, b: MD5) + assert( + fromCsv[Bar]("abc,23f8e84c1f4e7c8814634267bd456194") == List( + Right(Bar("abc", MD5("23f8e84c1f4e7c8814634267bd456194"))) + ) + ) + + assert(fromCsv[Bar]("abc,hi") == List(Left(NonEmptyList(ParseFailure("Not a MD5 hi"), Nil)))) + } + + test("from csv uuid") { + import java.util.UUID + import com.github.gekomad.ittocsv.core.FromCsv._ + + import com.github.gekomad.ittocsv.core.Conversions.toUUIDS + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(a: String, b: UUID) + + assert( + fromCsv[Bar]("abc,487d414d-67a6-4c2f-b95b-d811561ccd75") == List( + Right(Bar("abc", UUID.fromString("487d414d-67a6-4c2f-b95b-d811561ccd75"))) + ) + ) + + assert( + fromCsv[Bar]("abc,xxc586e2-7cc3-4d39-a449-") == List( + Left(NonEmptyList(ParseFailure("xxc586e2-7cc3-4d39-a449- is not UUID"), Nil)) + ) + ) + + } + + test("from csv url") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(a: String, b: URL, c: URL1, d: URL2, e: URL3) + + assert( + fromCsv[Bar]( + "abc,http://abc.def.com,http://www.aaa.com,http://www.aaa.com,https://www.google.com:8080/url?" + ) == List( + Right( + Bar( + "abc", + URL("http://abc.def.com"), + URL1("http://www.aaa.com"), + URL2("http://www.aaa.com"), + URL3("https://www.google.com:8080/url?") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("abc,www.aaa.com,abc.def.com,abc.def.com,abc.def.com") == List( + Left( + NonEmptyList( + ParseFailure("Not a URL www.aaa.com"), + List( + ParseFailure("Not a URL1 abc.def.com"), + ParseFailure("Not a URL2 abc.def.com"), + ParseFailure("Not a URL3 abc.def.com") + ) + ) + ) + ) + ) + } + + test("from csv ftp domain") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(a: String, b: FTP, c: FTP1, d: FTP2, e: Domain) + + assert( + fromCsv[Bar]("abc,ftp://aaa.com,ftp://aaa.com,ftps://aaa.com,plus.google.com") == List( + Right( + Bar("abc", FTP("ftp://aaa.com"), FTP1("ftp://aaa.com"), FTP2("ftps://aaa.com"), Domain("plus.google.com")) + ) + ) + ) + + assert( + fromCsv[Bar]("abc,www.aaa.com,abc.def.com,abc.def.com,abc") == List( + Left( + NonEmptyList( + ParseFailure("Not a FTP www.aaa.com"), + List( + ParseFailure("Not a FTP1 abc.def.com"), + ParseFailure("Not a FTP2 abc.def.com"), + ParseFailure("Not a Domain abc") + ) + ) + ) + ) + ) + } + + test("HEX") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(a: HEX, b: HEX1, c: HEX2, d: HEX3) + + assert( + fromCsv[Bar]("F0F0F0,#F0F0F0,0xF0F0F0,0xF0F0F0") == + List(Right(Bar(HEX("F0F0F0"), HEX1("#F0F0F0"), HEX2("0xF0F0F0"), HEX3("0xF0F0F0")))) + ) + + assert( + fromCsv[Bar]("aa,bb,cc,dd") == List( + Left( + NonEmptyList( + ParseFailure("Not a HEX aa"), + List(ParseFailure("Not a HEX1 bb"), ParseFailure("Not a HEX2 cc"), ParseFailure("Not a HEX3 dd")) + ) + ) + ) + ) + } + + test("GermanStreet") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: GermanStreet) + + assert(fromCsv[Bar]("Mühlenstr. 33") == List(Right(Bar(GermanStreet("Mühlenstr. 33"))))) + + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a GermanStreet aa"), Nil)))) + } + + test("SingleChar") { + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Types.implicits._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(b: SingleChar) + assert(fromCsv[Bar]("a") == List(Right(Bar(SingleChar("a"))))) + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a SingleChar aa"), Nil)))) + } + + test("AZString") { + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Types.implicits._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(b: AZString) + assert(fromCsv[Bar]("aA") == List(Right(Bar(AZString("aA"))))) + assert(fromCsv[Bar]("1") == List(Left(NonEmptyList(ParseFailure("Not a AZString 1"), Nil)))) + } + + test("StringAndNumber") { + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Types.implicits._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(b: StringAndNumber) + assert(fromCsv[Bar]("aA1") == List(Right(Bar(StringAndNumber("aA1"))))) + assert(fromCsv[Bar]("$") == List(Left(NonEmptyList(ParseFailure("Not a StringAndNumber $"), Nil)))) + } + + test("AsciiString") { + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Types.implicits._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(b: AsciiString) + assert(fromCsv[Bar]("aA1%") == List(Right(Bar(AsciiString("aA1%"))))) + assert(fromCsv[Bar]("テ") == List(Left(NonEmptyList(ParseFailure("Not a AsciiString テ"), Nil)))) + } + + test("SingleNumber") { + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Types.implicits._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Bar(b: SingleNumber) + assert(fromCsv[Bar]("1") == List(Right(Bar(SingleNumber("1"))))) + assert(fromCsv[Bar]("11") == List(Left(NonEmptyList(ParseFailure("Not a SingleNumber 11"), Nil)))) + } + + test("MACAddress") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: MACAddress) + + assert(fromCsv[Bar]("fE:dC:bA:98:76:54") == List(Right(Bar(MACAddress("fE:dC:bA:98:76:54"))))) + + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a MACAddress aa"), Nil)))) + } + + test("Phones") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(a: USphoneNumber, b: ItalianMobilePhone, c: ItalianPhone) + + assert( + fromCsv[Bar]("555-555-5555,+393471234561,02 645566") == List( + Right(Bar(USphoneNumber("555-555-5555"), ItalianMobilePhone("+393471234561"), ItalianPhone("02 645566"))) + ) + ) + + assert( + fromCsv[Bar]("aa,bb,cc") == List( + Left( + NonEmptyList( + ParseFailure("Not a USphoneNumber aa"), + List(ParseFailure("Not a ItalianMobilePhone bb"), ParseFailure("Not a ItalianPhone cc")) + ) + ) + ) + ) + + } + + test("Time") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar( + a1: MDY, + a2: MDY2, + a3: MDY3, + a4: MDY4, + a11: DMY, + a21: DMY2, + a31: DMY3, + a41: DMY4, + b: Time, + c: Time24 + ) + + assert( + fromCsv[Bar]( + "1/12/1902,1-12-1902,01/01/1900,01-12-1902,1/12/1902,1-12-1902,01/12/1902,01-12-1902,8am,23:50:00" + ) == List( + Right( + Bar( + MDY("1/12/1902"), + MDY2("1-12-1902"), + MDY3("01/01/1900"), + MDY4("01-12-1902"), + DMY("1/12/1902"), + DMY2("1-12-1902"), + DMY3("01/12/1902"), + DMY4("01-12-1902"), + Time("8am"), + Time24("23:50:00") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("1,2,3,4,5,6,7,8,9,10") == List( + Left( + NonEmptyList( + ParseFailure("Not a MDY 1"), + List( + ParseFailure("Not a MDY2 2"), + ParseFailure("Not a MDY3 3"), + ParseFailure("Not a MDY4 4"), + ParseFailure("Not a DMY 5"), + ParseFailure("Not a DMY2 6"), + ParseFailure("Not a DMY3 7"), + ParseFailure("Not a DMY4 8"), + ParseFailure("Not a Time 9"), + ParseFailure("Not a Time24 10") + ) + ) + ) + ) + ) + + } + + test("Coordinates") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default.withQuote('|') + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(a: Coordinate, b: Coordinate1, c: Coordinate2) + + assert( + fromCsv[Bar]("""N90.00.00 E180.00.00,45°23'36.0" N 10°33'48.0" E,12:12:12.223546"N""") == + List( + Right( + Bar( + Coordinate("N90.00.00 E180.00.00"), + Coordinate1("""45°23'36.0" N 10°33'48.0" E"""), + Coordinate2("""12:12:12.223546"N""") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("a,b,c") == List( + Left( + NonEmptyList( + ParseFailure("Not a Coordinate a"), + List(ParseFailure("Not a Coordinate1 b"), ParseFailure("Not a Coordinate2 c")) + ) + ) + ) + ) + } + + test("Zip code") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(a: USZipCode, b: ItalianZipCode) + + assert( + fromCsv[Bar]("43802,23887") == + List(Right(Bar(USZipCode("43802"), ItalianZipCode("23887")))) + ) + + assert( + fromCsv[Bar]("a,b") == List( + Left(NonEmptyList(ParseFailure("Not a USZipCode a"), List(ParseFailure("Not a ItalianZipCode b")))) + ) + ) + } + + test("Numbers") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(a: Number1, b: Signed, c: Unsigned32, d: Percentage, e: Scientific) + + assert( + fromCsv[Bar]("99.99,-10,4294967295,10%,-2.384E-03") == + List( + Right( + Bar(Number1("99.99"), Signed("-10"), Unsigned32("4294967295"), Percentage("10%"), Scientific("-2.384E-03")) + ) + ) + ) + + assert( + fromCsv[Bar]("a,b,c,d,e") == List( + Left( + NonEmptyList( + ParseFailure("Not a Number1 a"), + List( + ParseFailure("Not a Signed b"), + ParseFailure("Not a Unsigned32 c"), + ParseFailure("Not a Percentage d"), + ParseFailure("Not a Scientific e") + ) + ) + ) + ) + ) + } + + test("Codes") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar( + a: ItalianFiscalCode, + b: ItalianVAT, + c: ItalianIban, + d: USstates, + e: USstates1, + f: USstreets, + g: USstreetNumber + ) + + assert( + fromCsv[Bar]( + """BDAPPP14A01A001R,13297040362,IT28 W800 0000 2921 0064 5211 151,CA,Florida,"123 Park Ave Apt 123 New York City, NY 10002",P.O. Box 432""" + ) == + List( + Right( + Bar( + ItalianFiscalCode("BDAPPP14A01A001R"), + ItalianVAT("13297040362"), + ItalianIban("IT28 W800 0000 2921 0064 5211 151"), + USstates("CA"), + USstates1("Florida"), + USstreets("123 Park Ave Apt 123 New York City, NY 10002"), + USstreetNumber("P.O. Box 432") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("a,b,c,d,e,f,g") == List( + Left( + NonEmptyList( + ParseFailure("Not a ItalianFiscalCode a"), + List( + ParseFailure("Not a ItalianVAT b"), + ParseFailure("Not a ItalianIban c"), + ParseFailure("Not a USstates d"), + ParseFailure("Not a USstates1 e"), + ParseFailure("Not a USstreets f"), + ParseFailure("Not a USstreetNumber g") + ) + ) + ) + ) + ) + } + + test("BitcoinAdd") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: BitcoinAdd) + + assert( + fromCsv[Bar]("3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v") == List( + Right(Bar(BitcoinAdd("3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v"))) + ) + ) + + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a BitcoinAdd aa"), Nil)))) + } + + test("Celsius") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: Celsius) + + assert(fromCsv[Bar]("+2 °C") == List(Right(Bar(Celsius("+2 °C"))))) + + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a Celsius aa"), Nil)))) + } + + test("Fahrenheit") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: Fahrenheit) + + assert(fromCsv[Bar]("+2 °F") == List(Right(Bar(Fahrenheit("+2 °F"))))) + + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a Fahrenheit aa"), Nil)))) + } + + test("ApacheError") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: ApacheError) + + assert( + fromCsv[Bar]("[Fri Dec 16 02:25:55 2005] [error] [client 1.2.3.4] Client sent malformed Host header") == List( + Right(Bar(ApacheError("[Fri Dec 16 02:25:55 2005] [error] [client 1.2.3.4] Client sent malformed Host header"))) + ) + ) + + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a ApacheError aa"), Nil)))) + } + + test("Concurrency") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(a: UsdCurrency, b: EurCurrency, c: YenCurrency) + + assert( + fromCsv[Bar]("$1.00,\"133,89 EUR\",¥1.00") == List( + Right(Bar(UsdCurrency("$1.00"), EurCurrency("133,89 EUR"), YenCurrency("¥1.00"))) + ) + ) + + assert( + fromCsv[Bar]("a,b,c") == List( + Left( + NonEmptyList( + ParseFailure("Not a UsdCurrency a"), + List(ParseFailure("Not a EurCurrency b"), ParseFailure("Not a YenCurrency c")) + ) + ) + ) + ) + } + + test("NotASCII") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: NotASCII) + + assert(fromCsv[Bar]("テスト。") == List(Right(Bar(NotASCII("テスト。"))))) + + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a NotASCII aa"), Nil)))) + } + + test("Crontab") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: Cron) + + assert(fromCsv[Bar]("5 4 * * *") == List(Right(Bar(Cron("5 4 * * *"))))) + + assert(fromCsv[Bar]("aa") == List(Left(NonEmptyList(ParseFailure("Not a Cron aa"), Nil)))) + } + + test("from csv social") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits._ + + final case class Bar(b: Youtube, c: Facebook, d: Twitter) + + assert( + fromCsv[Bar]( + "https://www.youtube.com/watch?v=9bZkp7q19f0,https://www.facebook.com/pages/,https://twitter.com/rtpharry/" + ) == List( + Right( + Bar( + Youtube("https://www.youtube.com/watch?v=9bZkp7q19f0"), + Facebook("https://www.facebook.com/pages/"), + Twitter("https://twitter.com/rtpharry/") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("aa,bb,cc") == List( + Left( + NonEmptyList( + ParseFailure("Not a Youtube aa"), + List(ParseFailure("Not a Facebook bb"), ParseFailure("Not a Twitter cc")) + ) + ) + ) + ) + } + + test("decode custom type") { + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import scala.util.Try + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class MyType(a: Int) + final case class Foo(a: MyType, b: Int) + + import com.github.gekomad.ittocsv.core.FromCsv._ + + implicit def _l(implicit csvFormat: IttoCSVFormat): String => Either[ParseFailure, MyType] = { + str: String => + if (str.startsWith("[") && str.endsWith("]")) + Try(str.substring(1, str.length - 1).toInt) + .map(f => Right(MyType(f))) + .getOrElse(Left(ParseFailure(s"Not a MyType $str"))) + else Left(ParseFailure(s"Wrong format $str")) + + } + + assert(fromCsv[Foo]("[42],99") == List(Right(Foo(MyType(42), 99)))) + assert(fromCsv[Foo]("[x],99") == List(Left(NonEmptyList(ParseFailure("Not a MyType [x]"), Nil)))) + assert(fromCsv[Foo]("42,99") == List(Left(NonEmptyList(ParseFailure("Wrong format 42"), Nil)))) + + } + + test("from csv url with custom parser") { + + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.URL + + final case class Bar(a: String, b: URL) + + assert(fromCsv[Bar]("abc,http://abc.def.com") == List(Right(Bar("abc", URL("http://abc.def.com"))))) + assert(fromCsv[Bar]("abc,https://abc.def.com") == List(Right(Bar("abc", URL("https://abc.def.com"))))) + assert(fromCsv[Bar]("abc,www.aaa.com") == List(Left(NonEmptyList(ParseFailure("Not a URL www.aaa.com"), Nil)))) + + { + import com.github.gekomad.ittocsv.core.Types.Validator + implicit val _l: Validator[URL] = + com.github.gekomad.ittocsv.core.Types.implicits.validatorURL + .copy(regex = """[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?""") + + assert(fromCsv[Bar]("abc,www.aaa.com") == List(Right(Bar("abc", URL("www.aaa.com"))))) + } + } + + test("from csv email") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.Email + + final case class Bar(a: String, b: Email) + + assert(fromCsv[Bar]("abc,aaa@aai.sss") == List(Right(Bar("abc", Email("aaa@aai.sss"))))) + assert(fromCsv[Bar]("abc,$aaa@aai.sss") == List(Right(Bar("abc", Email("$aaa@aai.sss"))))) + assert(fromCsv[Bar]("abc,a@i.d") == List(Right(Bar("abc", Email("a@i.d"))))) + assert(fromCsv[Bar]("abc,a@%.d") == List(Left(NonEmptyList(ParseFailure("Not a Email a@%.d"), Nil)))) + assert(fromCsv[Bar]("abc,a @i.d") == List(Left(NonEmptyList(ParseFailure("Not a Email a @i.d"), Nil)))) + assert(fromCsv[Bar]("abc,hi") == List(Left(NonEmptyList(ParseFailure("Not a Email hi"), Nil)))) + assert(fromCsv[Bar]("abc,hi@") == List(Left(NonEmptyList(ParseFailure("Not a Email hi@"), Nil)))) + assert(fromCsv[Bar]("abc,@") == List(Left(NonEmptyList(ParseFailure("Not a Email @"), Nil)))) + assert(fromCsv[Bar]("abc,@.com") == List(Left(NonEmptyList(ParseFailure("Not a Email @.com"), Nil)))) + assert(fromCsv[Bar]("abc,hi@g.") == List(Left(NonEmptyList(ParseFailure("Not a Email hi@g."), Nil)))) + assert(fromCsv[Bar]("abc,hi@.d") == List(Left(NonEmptyList(ParseFailure("Not a Email hi@.d"), Nil)))) + assert(fromCsv[Bar]("abc,") == List(Left(NonEmptyList(ParseFailure("Not a Email "), Nil)))) + assert(fromCsv[Bar]("abc, ") == List(Left(NonEmptyList(ParseFailure("Not a Email "), Nil)))) + } + + test("from csv emailSimple") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.EmailSimple + + final case class Bar(a: String, b: EmailSimple) + + assert(fromCsv[Bar]("abc,aaa@aai.sss") == List(Right(Bar("abc", EmailSimple("aaa@aai.sss"))))) + assert(fromCsv[Bar]("abc,a@i.d") == List(Right(Bar("abc", EmailSimple("a@i.d"))))) + assert(fromCsv[Bar]("abc,a@%.d") == List(Right(Bar("abc", EmailSimple("a@%.d"))))) + + } + + test("from csv email1") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.Email1 + + final case class Bar(a: String, b: Email1) + + assert(fromCsv[Bar]("abc,aaa@ai.sss") == List(Right(Bar("abc", Email1("aaa@ai.sss"))))) + assert(fromCsv[Bar]("abc,a@i.da") == List(Right(Bar("abc", Email1("a@i.da"))))) + assert(fromCsv[Bar]("abc,a@%.da") == List(Left(NonEmptyList(ParseFailure("Not a Email1 a@%.da"), Nil)))) + + } + + test("from csv email with custom parser") { + + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + import com.github.gekomad.ittocsv.core.Types.implicits.Email + + final case class Bar(a: String, b: Email) + import com.github.gekomad.ittocsv.core.Types.Validator + implicit val _l: Validator[Email] = + com.github.gekomad.ittocsv.core.Types.implicits.validatorEmail.copy(regex = """.+@.+\..+""") + + assert(fromCsv[Bar]("abc,a@%.d") == List(Right(Bar("abc", Email("a@%.d"))))) + } + + test("from csv to type") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + final case class Bar(a: String, b: Int) + assert(fromCsv[Bar]("abc,42") == List(Right(Bar("abc", 42)))) + assert(fromCsv[Bar]("abc,hi") == List(Left(NonEmptyList(ParseFailure("hi is not Int"), Nil)))) + } + + test("from csv to List of type") { + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + final case class Bar(a: String, b: Int) + assert(fromCsv[Bar]("abc,42\r\nfoo,24") == List(Right(Bar("abc", 42)), Right(Bar("foo", 24)))) + } + + test("tokenizeCsvLine to types complete") { + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + + final case class Foo(a: Int, b: Double, c: Option[String], d: Boolean) + + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + val o = fromCsv[Foo]("1,3.14,foo,true") + + assert(o == List(Right(Foo(1, 3.14, Some("foo"), true)))) + + } + + test("list of csv string to list of type") { + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + final case class Foo(a: Int, b: Double, c: String, d: Boolean) + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + val o = fromCsv[Foo](List("1,3.14,foo,true", "2,3.14,bar,false")) // List[Either[NonEmptyList[ParseFailure], Foo]] + assert(o == List(Right(Foo(1, 3.14, "foo", true)), Right(Foo(2, 3.14, "bar", false)))) + } + + test("list of csv string to list of type with empty string and ignoreEmptyLines false") { + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + final case class Foo(a: Int) + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + val o = fromCsv[Foo](List("1", "")) // List[Either[NonEmptyList[ParseFailure], Foo]] + assert(o == List(Right(Foo(1)), Left(NonEmptyList(ParseFailure(" is not Int"), Nil)))) + } + + test("list of csv string to list of type with empty string and ignoreEmptyLines true") { + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + final case class Foo(a: Int) + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withIgnoreEmptyLines(true) + val o = fromCsv[Foo](List("1", "", "2")) // List[Either[NonEmptyList[ParseFailure], Foo]] + assert(o == List(Right(Foo(1)), Right(Foo(2)))) + } + + test("decode Option[List[Int]]") { + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + + import com.github.gekomad.ittocsv.core.FromCsv._ + + final case class Foo(v: String, a: Option[List[Int]]) + + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsv[Foo]("abc,\"1,2,3\"") == List(Right(Foo("abc", Some(List(1, 2, 3)))))) + assert(fromCsv[Foo]("abc,\"1,xy,3\"") == List(Left(cats.data.NonEmptyList(ParseFailure("Bad type on xy"), Nil)))) + + } + + test("decode List[Char]") { + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + + import com.github.gekomad.ittocsv.core.FromCsv._ + + final case class Foo(v: String, a: List[Char]) + + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsv[Foo]("abc,\"a,b,c\"") == List(Right(Foo("abc", List('a', 'b', 'c'))))) + assert(fromCsv[Foo]("abc,\"1,xy,3\"") == List(Left(cats.data.NonEmptyList(ParseFailure("Bad type on xy"), Nil)))) + + } + + test("decode List[Boolean]") { + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + + import com.github.gekomad.ittocsv.core.FromCsv._ + + final case class Foo(a: List[Boolean]) + + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsv[Foo]("\"true,false\"") == List(Right(Foo(List(true, false))))) + assert(fromCsv[Foo]("\"abc,false\"") == List(Left(cats.data.NonEmptyList(ParseFailure("Bad type on abc"), Nil)))) + + } + + test("decode List[Int]") { + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + import com.github.gekomad.ittocsv.core.FromCsv._ + + final case class Foo(v: String, a: List[Int]) + + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsv[Foo]("abc,\"1,2,3\"") == List(Right(Foo("abc", List(1, 2, 3))))) + assert(fromCsv[Foo]("abc,\"1,xy,3\"") == List(Left(cats.data.NonEmptyList(ParseFailure("Bad type on xy"), Nil)))) + + } + + test("decode List[Double]") { + + import com.github.gekomad.ittocsv.core.FromCsv._ + + implicit val csvFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + assert(fromCsvL[Double]("1.1,2.1,3.1") == List(Right(1.1), Right(2.1), Right(3.1))) + assert(fromCsvL[Double]("1.1,abc,3.1") == List(Right(1.1), Left(ParseFailure("abc is not Double")), Right(3.1))) + assert(fromCsvL[Double]("") == List(Left(ParseFailure(" is not Double")))) + + } + + test("decode LocalDateTime") { + + import java.time.LocalDateTime + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Conversions._ + + final case class Foo(a: Int, b: LocalDateTime) + + implicit val csvFormat = IttoCSVFormat.default + + { + val o = fromCsv[Foo]("1,2000-12-31T11:21:19") + assert( + o == List( + Right( + Foo(1, LocalDateTime.parse("2000-12-31T11:21:19", java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + ) + ) + ) + } + + } + + test("decode Option[LocalDate]") { + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE + import java.time.LocalDate + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Conversions._ + + final case class Foo(a: Int, b: Option[LocalDate]) + + implicit val csvFormat = IttoCSVFormat.default + + { + val o = fromCsv[Foo]("1,2000-12-31") + assert(o == List(Right(Foo(1, Some(LocalDate.parse("2000-12-31", ISO_LOCAL_DATE)))))) + } + + } + + test("decode Option[LocalDateTime]") { + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME + import java.time.LocalDateTime + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Conversions._ + + final case class Foo(a: Int, b: Option[LocalDateTime]) + + implicit val csvFormat = IttoCSVFormat.default + + { + val o = fromCsv[Foo]("1,2000-12-31T11:21:19") + assert(o == List(Right(Foo(1, Some(LocalDateTime.parse("2000-12-31T11:21:19", ISO_LOCAL_DATE_TIME)))))) + } + + } + + test("decode date and time") { + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME + import java.time.LocalDateTime + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.FromCsv._ + import com.github.gekomad.ittocsv.core.Conversions._ + import java.time.{LocalDate, LocalTime, OffsetDateTime} + import java.time.format.DateTimeFormatter + import java.time.ZonedDateTime + import java.time.Instant + + final case class Foo( + i: Instant, + t: LocalTime, + d: LocalDate, + dt: OffsetDateTime, + z: ZonedDateTime, + ldt: LocalDateTime + ) + + implicit val csvFormat = IttoCSVFormat.default + + { + val o = fromCsv[Foo]( + "2019-11-30T18:35:24.00Z,11:15:30,2019-12-27,2012-12-03T10:15:30+01:00,2019-04-01T17:24:11.252+05:30[Asia/Calcutta],2000-12-31T11:21:19" + ) + assert( + o == List( + Right( + Foo( + Instant.parse("2019-11-30T18:35:24.00Z"), + LocalTime.parse("11:15:30", DateTimeFormatter.ISO_LOCAL_TIME), + LocalDate.parse("2019-12-27"), + OffsetDateTime.parse("2012-12-03T10:15:30+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME), + ZonedDateTime.parse("2019-04-01T17:24:11.252+05:30[Asia/Calcutta]"), + LocalDateTime.parse("2000-12-31T11:21:19", ISO_LOCAL_DATE_TIME) + ) + ) + ) + ) + } + + } + + test("decode custom Option[LocalDateTime]") { + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + import com.github.gekomad.ittocsv.core.FromCsv._ + + final case class Foo(a: Int, b: Option[java.time.LocalDateTime]) + + import scala.util.Try + import java.time.LocalDateTime + import java.time.format.DateTimeFormatter + + implicit val csvFormat = IttoCSVFormat.default + + implicit def localDateTimeToCsv: String => Either[ParseFailure, Option[LocalDateTime]] = { + case "" => Right(None) + case s => + Try { + val x = LocalDateTime.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) + Right(Some(x)) + }.getOrElse(Left(ParseFailure(s"Not a LocalDataTime $s"))) + + } + + { + val l = LocalDateTime.parse("2000-11-11 11:11:11.0", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) + val o = fromCsv[Foo]("1,2000-11-11 11:11:11.0") + assert(o == List(Right(Foo(1, Some(l))))) + } + + { + val o = fromCsv[Foo]("1,daigoro-xx-11 11:11:11.0") + assert(o == List(Left(cats.data.NonEmptyList(ParseFailure("Not a LocalDataTime daigoro-xx-11 11:11:11.0"), Nil)))) + } + } + +} diff --git a/scala2_js/src/test/scala/ScalaCheckToCsvFromCsv.scala b/scala2_js/src/test/scala/ScalaCheckToCsvFromCsv.scala new file mode 100644 index 0000000..741ed31 --- /dev/null +++ b/scala2_js/src/test/scala/ScalaCheckToCsvFromCsv.scala @@ -0,0 +1,42 @@ +import com.github.gekomad.ittocsv.parser.Constants._ +import com.github.gekomad.ittocsv.parser.CsvFieldToString._ +import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} +import org.scalacheck.{Arbitrary, Gen, Prop, Properties} + +/** from string to csv and to string again, the strings should be the same + */ +object ScalaCheckToCsvFromCsv extends Properties("Scalacheck - from string to csv and to string again") { + + import org.scalacheck.Prop.forAll + + val generators = List(Gen.asciiPrintableStr, Gen.asciiStr, Arbitrary.arbitrary[String]) + val csvFormats = List(IttoCSVFormat.default.withQuoteLowerChar(true), IttoCSVFormat.tab.withQuoteLowerChar(true)) + + val delimiters = List(COMMA, SEMICOLON, PIPE) + val recordSeparators = List(LF, CRLF) + val quotes = List(PIPE, DOUBLE_QUOTE) + + def doubleTrasformation(format: IttoCSVFormat, gen: Gen[String]): Prop = forAll(gen) { orig => + val csv = StringToCsvField.stringToCsvField(orig)(format) + val string = csvFieldToString(csv)(format) + orig == string + } + + for { + generator <- generators + format1 <- csvFormats + } yield doubleTrasformation(format1, generator) + + for { + generator <- generators + format1 <- csvFormats + delimiter <- delimiters + recordSeparator <- recordSeparators + quote <- quotes + if quote != delimiter + } yield property("csvFieldToString") = doubleTrasformation( + format1.withDelimiter(delimiter).withRecordSeparator(recordSeparator).withQuote(quote), + generator + ) + +} diff --git a/scala2_js/src/test/scala/StringUtilTest.scala b/scala2_js/src/test/scala/StringUtilTest.scala new file mode 100644 index 0000000..a65c929 --- /dev/null +++ b/scala2_js/src/test/scala/StringUtilTest.scala @@ -0,0 +1,49 @@ +class StringUtilTest extends munit.FunSuite { + + test("split") { + import com.github.gekomad.ittocsv.util.StringUtils._ + { + val string = "a;b;c" + val separators = List(1, 3) + val res = split(string, separators) + assert(res == List("a", "b", "c")) + } + + { + val string = "a;;c" + val separators = List(1, 2) + val res = split(string, separators) + assert(res == List("a", "", "c")) + } + + { + val string = "a;b;" + val separators = List(1, 3) + val res = split(string, separators) + assert(res == List("a", "b", "")) + } + + { + val string = ";;" + val separators = List(0, 1) + val res = split(string, separators) + assert(res == List("", "", "")) + } + + { + val string = "" + val separators = List() + val res = split(string, separators) + assert(res == List("")) + } + + { + val string = """1,"foo,bar",y,"2,e,","2ne","a""bc""z"""" + val separators = List(1, 11, 13, 20, 26) + val res = split(string, separators) + assert(res == List("1", "\"foo,bar\"", "y", "\"2,e,\"", "\"2ne\"", "\"a\"\"bc\"\"z\"")) + } + + } + +} diff --git a/scala2_js/src/test/scala/ToCsvTest.scala b/scala2_js/src/test/scala/ToCsvTest.scala new file mode 100644 index 0000000..66c9b9e --- /dev/null +++ b/scala2_js/src/test/scala/ToCsvTest.scala @@ -0,0 +1,748 @@ +import java.time.format.DateTimeFormatter +import java.time.{Instant, LocalDate, LocalTime, OffsetDateTime, ZonedDateTime} + +import com.github.gekomad.ittocsv.parser.IttoCSVFormat + +class ToCsvTest extends munit.FunSuite { + + test("email") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, email: Email) + + assert(toCsv(Bar(1, Email("daigoro@itto.com"))) == "1,daigoro@itto.com") + } + + test("email1") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, email: Email1) + + assert(toCsv(Bar(1, Email1("daigoro@itto.com"))) == "1,daigoro@itto.com") + } + + test("emailSimple") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, email: EmailSimple) + + assert(toCsv(Bar(1, EmailSimple("daigoro@itto.com"))) == "1,daigoro@itto.com") + } + + test("url") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, url: URL, url1: URL1, url2: URL2, url3: URL3) + + assert( + toCsv( + Bar( + 1, + URL("http://aaa.ccc.com"), + URL1("http://www.aaa.com"), + URL2("http://www.aaa.com"), + URL3("https://www.google.com:8080/url?") + ) + ) == + "1,http://aaa.ccc.com,http://www.aaa.com,http://www.aaa.com,https://www.google.com:8080/url?" + ) + } + + test("ftp domain") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, b: FTP, c: FTP1, d: FTP2, e: Domain) + + assert( + toCsv(Bar(1, FTP("ftp://aaa.com"), FTP1("ftp://aaa.com"), FTP2("ftps://aaa.com"), Domain("plus.google.com"))) == + "1,ftp://aaa.com,ftp://aaa.com,ftps://aaa.com,plus.google.com" + ) + } + + test("social") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(b: Youtube, c: Facebook, d: Twitter) + + assert( + toCsv( + Bar( + Youtube("https://www.youtube.com/watch?v=9bZkp7q19f0"), + Facebook("http://www.facebook.com/thesimpsons"), + Twitter("http://twitter.com/rtpharry/") + ) + ) == + "https://www.youtube.com/watch?v=9bZkp7q19f0,http://www.facebook.com/thesimpsons,http://twitter.com/rtpharry/" + ) + } + + test("MACAddress") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, a: MACAddress) + + assert(toCsv(Bar(1, MACAddress("fE:dC:bA:98:76:54"))) == "1,fE:dC:bA:98:76:54") + } + + test("Phones") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: USphoneNumber, b: ItalianMobilePhone, c: ItalianPhone) + + assert( + toCsv( + Bar(USphoneNumber("555-555-5555"), ItalianMobilePhone("+393471234561"), ItalianPhone("02 645566")) + ) == "555-555-5555,+393471234561,02 645566" + ) + } + + test("BitcoinAdd") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, a: BitcoinAdd) + + assert(toCsv(Bar(1, BitcoinAdd("3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v"))) == "1,3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v") + } + + test("Codes") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar( + a: ItalianFiscalCode, + b: ItalianVAT, + c: ItalianIban, + d: USstates, + e: USstates1, + f: USstreets, + g: USstreetNumber + ) + + assert( + toCsv( + Bar( + ItalianFiscalCode("BDAPPP14A01A001R"), + ItalianVAT("13297040362"), + ItalianIban("IT28 W800 0000 2921 0064 5211 151"), + USstates("CA"), + USstates1("Florida"), + USstreets("123 Park Ave Apt 123 New York City, NY 10002"), + USstreetNumber("P.O. Box 432") + ) + ) == """BDAPPP14A01A001R,13297040362,IT28 W800 0000 2921 0064 5211 151,CA,Florida,"123 Park Ave Apt 123 New York City, NY 10002",P.O. Box 432""" + ) + } + + test("Coordinates") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default.withQuote('|') + + final case class Bar(a: Coordinate, b: Coordinate1, c: Coordinate2) + + assert( + toCsv( + Bar( + Coordinate("N90.00.00 E180.00.00"), + Coordinate1("""45°23'36.0" N 10°33'48.0" E"""), + Coordinate2("""12:12:12.223546"N""") + ) + ) == """N90.00.00 E180.00.00,45°23'36.0" N 10°33'48.0" E,12:12:12.223546"N""" + ) + } + + test("Numbers") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: Number1, b: Signed, c: Unsigned32, d: Percentage, e: Scientific) + + assert( + toCsv( + Bar(Number1("99.99"), Signed("-10"), Unsigned32("4294967295"), Percentage("10%"), Scientific("-2.384E-03")) + ) == "99.99,-10,4294967295,10%,-2.384E-03" + ) + } + + test("Zip code") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: USZipCode, b: ItalianZipCode) + + assert(toCsv(Bar(USZipCode("43802"), ItalianZipCode("23887"))) == "43802,23887") + } + + test("GermanStreet") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: GermanStreet) + + assert(toCsv(Bar(GermanStreet("Mühlenstr. 33"))) == "Mühlenstr. 33") + } + + test("SingleChar") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: SingleChar) + assert(toCsv(Bar(SingleChar("a"))) == "a") + } + + test("AZString") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: AZString) + assert(toCsv(Bar(AZString("aA"))) == "aA") + } + + test("Celsius") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: Celsius) + assert(toCsv(Bar(Celsius("+2 °C"))) == "+2 °C") + } + + test("Fahrenheit") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: Fahrenheit) + assert(toCsv(Bar(Fahrenheit("+2 °F"))) == "+2 °F") + } + + test("StringAndNumber") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: StringAndNumber) + assert(toCsv(Bar(StringAndNumber("a1"))) == "a1") + } + + test("AsciiString") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: AsciiString) + assert(toCsv(Bar(AsciiString("a$"))) == "a$") + } + + test("SingleNumber") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: SingleNumber) + assert(toCsv(Bar(SingleNumber("3"))) == "3") + } + + test("Concurrency") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: UsdCurrency, b: EurCurrency, c: YenCurrency) + + assert( + toCsv(Bar(UsdCurrency("$1.00"), EurCurrency("133,89 EUR"), YenCurrency("¥1.00"))) == "$1.00,\"133,89 EUR\",¥1.00" + ) + } + + test("Crontab") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, a: Cron) + + assert(toCsv(Bar(1, Cron("5 4 * * *"))) == "1,5 4 * * *") + } + + test("ApacheError") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(b: ApacheError) + + assert( + toCsv( + Bar(ApacheError("[Fri Dec 16 02:25:55 2005] [error] [client 1.2.3.4] Client sent malformed Host header")) + ) == "[Fri Dec 16 02:25:55 2005] [error] [client 1.2.3.4] Client sent malformed Host header" + ) + } + + test("NotASCII") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(b: NotASCII) + + assert(toCsv(Bar(NotASCII("テスト。"))) == "テスト。") + } + + test("Time") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar( + a1: MDY, + a2: MDY2, + a3: MDY3, + a4: MDY4, + a11: DMY, + a21: DMY2, + a31: DMY3, + a41: DMY4, + b: Time, + c: Time24 + ) + + assert( + toCsv( + Bar( + MDY("1/12/1902"), + MDY2("1-12-1902"), + MDY3("01/01/1900"), + MDY4("01-12-1902"), + DMY("1/12/1902"), + DMY2("1-12-1902"), + DMY3("01/12/1902"), + DMY4("01-12-1902"), + Time("8am"), + Time24("23:50:00") + ) + ) == + "1/12/1902,1-12-1902,01/01/1900,01-12-1902,1/12/1902,1-12-1902,01/12/1902,01-12-1902,8am,23:50:00" + ) + } + + test("HEX") { + import com.github.gekomad.ittocsv.core.Types.implicits._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: HEX, b: HEX1, c: HEX2, d: HEX3) + + assert( + toCsv( + Bar(HEX("F0F0F0"), HEX1("#F0F0F0"), HEX2("0xF0F0F0"), HEX3("0xF0F0F0")) + ) == "F0F0F0,#F0F0F0,0xF0F0F0,0xF0F0F0" + ) + } + + test("IP") { + import com.github.gekomad.ittocsv.core.Types.implicits.IP + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, a: IP) + + assert(toCsv(Bar(1, IP("10.168.1.108"))) == "1,10.168.1.108") + } + + test("IP6") { + import com.github.gekomad.ittocsv.core.Types.implicits.IP6 + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, a: IP6) + + assert(toCsv(Bar(1, IP6("2001:db8:a0b:12f0::1"))) == "1,2001:db8:a0b:12f0::1") + } + + test("SHA1") { + import com.github.gekomad.ittocsv.core.Types.implicits.SHA1 + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, a: SHA1) + + assert( + toCsv(Bar(1, SHA1("1c18da5dbf74e3fc1820469cf1f54355b7eec92d"))) == "1,1c18da5dbf74e3fc1820469cf1f54355b7eec92d" + ) + + } + + test("SHA256") { + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + import com.github.gekomad.ittocsv.core.Types.implicits.SHA256 + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, a: SHA256) + + assert( + toCsv( + Bar(1, SHA256("000020f89134d831f48541b2d8ec39397bc99fccf4cc86a3861257dbe6d819d1")) + ) == "1,000020f89134d831f48541b2d8ec39397bc99fccf4cc86a3861257dbe6d819d1" + ) + + } + + test("UUID") { + import java.util.UUID + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(i: Int, a: UUID) + + assert( + toCsv(Bar(1, UUID.fromString("1CC3CCBB-C749-3078-E050-1AACBE064651"))) == "1,1cc3ccbb-c749-3078-e050-1aacbe064651" + ) + + } + + test("type to csv string") { + + { + import com.github.gekomad.ittocsv.core.ToCsv._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + implicit val csvFormat: IttoCSVFormat = + IttoCSVFormat.default.withDelimiter('.') + + final case class Bar(i: Int, salary: Double) + + assert(toCsv(Bar(1, 33003.3)) == "1.\"33003.3\"") + + } + + { // use default formatter + import com.github.gekomad.ittocsv.core.CsvStringEncoder + import com.github.gekomad.ittocsv.core.ToCsv._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + implicit val csvFormat: IttoCSVFormat = + IttoCSVFormat.default.withPrintHeader(false) + + final case class Bar(name: String, date: java.util.Date, salary: Double) + + implicit val dateEncoder: CsvStringEncoder[java.util.Date] = + (value: java.util.Date) => value.toString + + val d = new java.util.Date(0).toString + + assert(toCsv(Bar("Bo,b", new java.util.Date(0), 33003.3)) == s""""Bo,b",$d,33003.3""") + + assert( + toCsv(List(Bar("Bob", new java.util.Date(0), 1111.3), Bar("Jim", new java.util.Date(0), 2222.2))) == + s"Bob,$d,1111.3,Jim,$d,2222.2" + ) + } + + { // use tab formatter + import com.github.gekomad.ittocsv.core.CsvStringEncoder + import com.github.gekomad.ittocsv.core.ToCsv._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + implicit val csvFormat: IttoCSVFormat = + IttoCSVFormat.tab.withRecordSeparator("\n") + implicit val dateEncoder: CsvStringEncoder[java.util.Date] = + (value: java.util.Date) => value.toString + + final case class Bar(name: String, date: java.util.Date, salary: Double) + val d = new java.util.Date(0).toString + assert( + toCsv(Bar("Bo,b", new java.util.Date(0), 33003.3)) == + s"Bo,b\t$d\t33003.3" + ) + + assert( + toCsv(List(Bar("Bob", new java.util.Date(0), 1111.3), Bar("Jim", new java.util.Date(0), 2222.2))) == + s"Bob\t$d\t1111.3\tJim\t$d\t2222.2" + ) + } + + { + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + final case class Baz(x: String) + final case class Foo(a: Int, c: Baz) + final case class Bar(a: String, b: Int, c: Foo) + + assert(toCsv(Bar("Bar", 3, Foo(1, Baz("hi, dude")))) == "Bar,3,1,\"hi, dude\"") + } + } + + test("encode custom type") { + + import com.github.gekomad.ittocsv.core.CsvStringEncoder + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class MyType(a: Int) + final case class Foo(a: MyType, b: Int) + + // encode + import com.github.gekomad.ittocsv.core.ToCsv._ + + implicit def _f(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[MyType] = createEncoder { node => + csvConverter.stringToCsvField(s"[${node.a}]") + } + + assert(toCsv(Foo(MyType(42), 99)) == "[42],99") + + } + + test("ToCsvT") { + + object ToCsvT { + + import com.github.gekomad.ittocsv.core.Header._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.core.CsvStringEncoder + import com.github.gekomad.ittocsv.core.ToCsv.toCsv + + def toCsvT[A: FieldNames](csvT: (A, Long))(implicit enc: CsvStringEncoder[A], csvFormat: IttoCSVFormat): String = + (if (csvT._2 == 0) csvHeader[A] else "") + toCsv(csvT._1, true) + + } + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + implicit val csvFormat: IttoCSVFormat = + IttoCSVFormat.default.withDelimiter(';').withRecordSeparator("\n") + + import com.github.gekomad.ittocsv.core.ToCsv._ + import ToCsvT._ + + final case class Foo(name: String) + + val l = for { + c <- 0 to 5 + } yield toCsvT((Foo("id" + c), c)) + + assert(l.mkString == "name\nid0\nid1\nid2\nid3\nid4\nid5") + + } + + test("type to csv with Instant") { + import com.github.gekomad.ittocsv.core.ToCsv._ + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME + import java.time.LocalDateTime + + implicit val csvFormat: IttoCSVFormat = + com.github.gekomad.ittocsv.parser.IttoCSVFormat.default.withPrintHeader(false) + + val instant1 = Instant.parse("2010-11-30T18:35:24.00Z") + val instant2 = Instant.parse("2011-11-30T18:35:24.00Z") + val instant3 = Instant.parse("2012-11-30T18:35:24.00Z") + val instant4 = Instant.parse("2013-11-30T18:35:24.00Z") + + final case class Bar(i: Option[Instant], e: Instant) + val l: List[Bar] = List(Bar(Some(instant1), instant2), Bar(Some(instant3), instant4)) + assert(toCsv(l) == "2010-11-30T18:35:24Z,2011-11-30T18:35:24Z,2012-11-30T18:35:24Z,2013-11-30T18:35:24Z") + } + + test("type to csv with date and time") { + import com.github.gekomad.ittocsv.core.ToCsv._ + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME + import java.time.LocalDateTime + + implicit val csvFormat: IttoCSVFormat = + com.github.gekomad.ittocsv.parser.IttoCSVFormat.default.withPrintHeader(false) + + val localDateTime = LocalDateTime.parse("2000-12-31T12:13:14", ISO_LOCAL_DATE_TIME) + val localTime = LocalTime.parse("11:15:30", DateTimeFormatter.ISO_LOCAL_TIME) + val localDate = LocalDate.parse("2019-12-27") + val offsetDateTime = OffsetDateTime.parse("2012-12-03T10:15:30+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME) + val zonedDateTime = ZonedDateTime.parse("2019-04-01T17:24:11.252+05:30[Asia/Calcutta]") + + final case class Bar( + a: LocalDateTime, + b: LocalTime, + c: Option[LocalDate], + e: Option[OffsetDateTime], + f: ZonedDateTime + ) + val l: List[Bar] = List(Bar(localDateTime, localTime, Some(localDate), Some(offsetDateTime), zonedDateTime)) + assert( + toCsv( + l + ) == "2000-12-31T12:13:14,11:15:30,2019-12-27,2012-12-03T10:15:30+01:00,2019-04-01T17:24:11.252+05:30[Asia/Calcutta]" + ) + } + + test("type to csv with custom localDateTime") { + import com.github.gekomad.ittocsv.core.ToCsv._ + import com.github.gekomad.ittocsv.core.CsvStringEncoder + import java.time.LocalDateTime + import java.time.format.DateTimeFormatter + implicit val csvFormat: IttoCSVFormat = + com.github.gekomad.ittocsv.parser.IttoCSVFormat.default.withPrintHeader(false) + + implicit def localDateTimeEncoder(implicit + csvFormat: com.github.gekomad.ittocsv.parser.IttoCSVFormat + ): CsvStringEncoder[LocalDateTime] = + (value: LocalDateTime) => value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) + + val localDateTime = + LocalDateTime.parse("2000-11-11 11:11:11.0", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) + + final case class Bar(a: String, b: Long, c: LocalDateTime, e: Option[Int]) + val l: List[Bar] = List(Bar("Yel,low", 3L, localDateTime, Some(1)), Bar("eee", 7L, localDateTime, None)) + assert(toCsv(l) == "\"Yel,low\",3,2000-11-11 11:11:11.0,1,eee,7,2000-11-11 11:11:11.0,") + } + + test("from type to csv") { + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + final case class Bar(a: String, b: Int) + assert(toCsv(Bar("Bar", 42)) == "Bar,42") + } + + test("from list of type to csv") { + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + final case class Bar(a: String, b: Int) + assert(toCsv(List(Bar("abc", 42), Bar("def", 24))) == "abc,42,def,24") + } + + test("from list of type to List of csv") { + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + final case class Bar(a: String, b: Int) + assert(toCsvL(List(Bar("abc", 42), Bar("def", 24))) == "a,b\r\nabc,42\r\ndef,24") + } + + test("serialize List[Type]") { + + implicit val csvFormat: IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + final case class Bar(c: String, a: Int) + import com.github.gekomad.ittocsv.core.ToCsv._ + + val x = List(Bar("abc", 1), Bar("def", 2)) + assert(toCsv(x) == "abc,1,def,2") + assert(x.map(toCsv(_)) == List("abc,1", "def,2")) + } + + test("serialize List[Double]") { + + implicit val csvFormat: IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.ToCsv._ + + assert(toCsv(List(1.1, 2.1, 3.1)) == "1.1,2.1,3.1") + + } + + test("serialize with record separator") { + + final case class Foo(a: String, b: String) + implicit val csvFormat: IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + import com.github.gekomad.ittocsv.core.ToCsv._ + + assert(toCsv(Foo("aaa", "bbb"), true) == "\r\naaa,bbb") + + } + + test("write Csv with header") { + import com.github.gekomad.ittocsv.core.CsvStringEncoder + import com.github.gekomad.ittocsv.core.Header._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Bar(name: String, date: java.util.Date, salary: Double) + + import com.github.gekomad.ittocsv.core.ToCsv._ + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.tab + + implicit val dateEncoder: CsvStringEncoder[java.util.Date] = + (value: java.util.Date) => value.toString + + def g[A: FieldNames](a: A)(implicit enc: CsvStringEncoder[A]): String = + toCsv(a) + + val d = new java.util.Date(0).toString + assert(g(Bar("Bo,b", new java.util.Date(0), 33003.3)) == s"Bo,b\t$d\t33003.3") + + } + + test("get header 1") { + import com.github.gekomad.ittocsv.core.Header._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Record(i: Int, d: Double, s: String, b: Boolean) + + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + def g[A: FieldNames]: String = csvHeader[A] + + val header = g[Record] + assert(header == "i,d,s,b") + } + + test("get header 2") { + import com.github.gekomad.ittocsv.core.Header._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Record(X: Int, d: Double, s: String, b: Boolean) + implicit val csvFormat: IttoCSVFormat = + IttoCSVFormat.default.withDelimiter('X') + + val header = csvHeader[Record] + + assert(header == "\"X\"XdXsXb") + } +} diff --git a/scala2_js/src/test/scala/TreeTest.scala b/scala2_js/src/test/scala/TreeTest.scala new file mode 100644 index 0000000..1ea4d9a --- /dev/null +++ b/scala2_js/src/test/scala/TreeTest.scala @@ -0,0 +1,86 @@ +import com.github.gekomad.ittocsv.core.ParseFailure + +import scala.util.matching.Regex + +class TreeTest extends munit.FunSuite { + + test("encode/decode Tree[Int]") { + object OTree { + + // thanks to amitayh https://gist.github.com/amitayh/373f512c50222e15550869e2ff539b25 + final case class Tree[A](value: A, left: Option[Tree[A]] = None, right: Option[Tree[A]] = None) + + object Serializer { + val pattern: Regex = """^(\d+)\((.*)\)$""".r + val treeOpen: Char = '(' + val treeClose: Char = ')' + val separator: Char = ',' + val separatorLength: Int = 1 + + def serialize[A](nodeOption: Option[Tree[A]]): String = nodeOption match { + case Some(Tree(value, left, right)) => + val leftStr = serialize(left) + val rightStr = serialize(right) + s"$value$treeOpen$leftStr$separator$rightStr$treeClose" + + case None => "" + } + + def deserialize[A](str: String, f: String => A): Option[Tree[A]] = str match { + case pattern(value, inner) => + val (left, right) = splitInner(inner) + Some(Tree(f(value), deserialize(left, f), deserialize(right, f))) + case _ => None + } + + def splitInner(inner: String): (String, String) = { + var balance = 0 + val left = inner.takeWhile { + case `treeOpen` => balance += 1; true + case `treeClose` => balance -= 1; true + case `separator` if balance == 0 => false + case _ => true + } + + val right = inner.drop(left.length + separatorLength) + + (left, right) + } + } + + } + import com.github.gekomad.ittocsv.core.CsvStringEncoder + import OTree.Serializer._ + import OTree._ + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + implicit val csvFormat: IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(v: String, a: Tree[Int]) + + // encode + import com.github.gekomad.ittocsv.core.ToCsv._ + + implicit def _f(implicit csvFormat: IttoCSVFormat): CsvStringEncoder[Tree[Int]] = createEncoder { node => + csvConverter.stringToCsvField(serialize(Some(node))) + } + + val tree: Tree[Int] = Tree(1, Some(Tree(2, Some(Tree(3)))), Some(Tree(4, Some(Tree(5)), Some(Tree(6))))) + + val serialized: String = toCsv(Foo("abc", tree)) + + assert(serialized == "abc,\"1(2(3(,),),4(5(,),6(,)))\"") + + // decode + import com.github.gekomad.ittocsv.core.FromCsv._ + implicit def _l(implicit csvFormat: IttoCSVFormat): String => Either[ParseFailure, Tree[Int]] = + (str: String) => + deserialize(str, _.toInt) match { + case None => Left(ParseFailure(s"Not a Node[Short] $str")) + case Some(a) => Right(a) + } + + assert(fromCsv[Foo](serialized) == List(Right(Foo("abc", tree)))) + + } +} diff --git a/scala3/build.sbt b/scala3/build.sbt index df8fa1b..a7faf50 100644 --- a/scala3/build.sbt +++ b/scala3/build.sbt @@ -1,13 +1,13 @@ name := "itto-csv" -version := "2.1.0" +version := "2.1.1" organization := "com.github.gekomad" scalaVersion := "3.5.2" val fs2Version = "3.11.0" -libraryDependencies += "com.github.gekomad" %% "scala-regex-collection" % "2.0.0" +libraryDependencies += "com.github.gekomad" %% "scala-regex-collection" % "2.0.1" libraryDependencies += "co.fs2" %% "fs2-core" % fs2Version libraryDependencies += "co.fs2" %% "fs2-io" % fs2Version libraryDependencies += "org.apache.commons" % "commons-csv" % "1.12.0" % Test @@ -31,12 +31,10 @@ scalacOptions ++= Seq( "-Xfatal-warnings" ) -testFrameworks += new TestFramework("munit.Framework") - //sonatype publishTo := sonatypePublishToBundle.value -logLevel := Level.Debug + pomExtra := diff --git a/scala3_js/build.sbt b/scala3_js/build.sbt new file mode 100644 index 0000000..4668f50 --- /dev/null +++ b/scala3_js/build.sbt @@ -0,0 +1,49 @@ +name := "itto-csv" + +import org.scalajs.linker.interface.{ESVersion, ModuleSplitStyle} + +lazy val scala3Js = project + .in(file(".")) + .enablePlugins(ScalaJSPlugin) + .settings( + version := "2.1.1", + scalaVersion := "3.5.2", + organization := "com.github.gekomad", + scalaJSUseMainModuleInitializer := false, + scalaJSLinkerConfig ~= (_.withESFeatures(_.withESVersion(ESVersion.ES2018))), + scalaJSLinkerConfig ~= { + _.withModuleKind(ModuleKind.ESModule) + .withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("scala3Js"))) + }, + scalacOptions ++= Seq("-Xfatal-warnings"), + libraryDependencies += "co.fs2" %%% "fs2-core" % "3.11.0", + libraryDependencies += "co.fs2" %%% "fs2-io" % "3.11.0", + libraryDependencies += "com.github.gekomad" %%% "scala-regex-collection" % "2.0.1", + libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.0", + libraryDependencies += "org.scalameta" %%% "munit" % "1.0.2" % Test, + libraryDependencies += "org.scalacheck" %%% "scalacheck" % "1.18.1" % Test + ) + +//sonatype +publishTo := sonatypePublishToBundle.value + +pomExtra := + + + Apache 2 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + gekomad + Giuseppe Cannella + https://github.com/gekomad + + + + https://github.com/gekomad/itto-csv + scm:git:https://github.com/gekomad/itto-csv + + https://github.com/gekomad/itto-csv diff --git a/scala3_js/project/build.properties b/scala3_js/project/build.properties new file mode 100644 index 0000000..db1723b --- /dev/null +++ b/scala3_js/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.5 diff --git a/scala3_js/project/plugins.sbt b/scala3_js/project/plugins.sbt new file mode 100644 index 0000000..b3f2697 --- /dev/null +++ b/scala3_js/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/FromCsv.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/FromCsv.scala new file mode 100644 index 0000000..3343e85 --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/FromCsv.scala @@ -0,0 +1,264 @@ +package com.github.gekomad.ittocsv.core + +import com.github.gekomad.ittocsv.core.Types.RegexValidator +import com.github.gekomad.ittocsv.core.Types.implicits.* +import com.github.gekomad.ittocsv.parser.IttoCSVFormat +import com.github.gekomad.ittocsv.util.StringUtils.tokenizeCsvLine +import com.github.gekomad.regexcollection.Collection.* +import java.time.* +import java.time.format.DateTimeFormatter.* +import scala.util.Try + +object FromCsv: + + import scala.deriving.Mirror + + trait Decoder[A, B] extends (A => Either[List[String], B]) + + object Decoder: + given Decoder[String, String] = Right(_) + + given Decoder[String, java.util.UUID] = a => + Try(java.util.UUID.fromString(a)).toOption.toRight(List(s"$a value is not valid UUID")) + given Decoder[String, LocalDateTime] = a => + Try(LocalDateTime.parse(a, ISO_LOCAL_DATE_TIME)).toOption.toRight(List(s"$a value is not valid LocalDateTime")) + given Decoder[String, LocalDate] = a => + Try(LocalDate.parse(a, ISO_LOCAL_DATE)).toOption.toRight(List(s"$a value is not valid LocalDate")) + given Decoder[String, LocalTime] = a => + Try(LocalTime.parse(a, ISO_LOCAL_TIME)).toOption.toRight(List(s"$a value is not valid LocalTime")) + given Decoder[String, OffsetDateTime] = a => + Try(OffsetDateTime.parse(a, ISO_OFFSET_DATE_TIME)).toOption.toRight(List(s"$a value is not valid OffsetDateTime")) + given Decoder[String, OffsetTime] = a => + Try(OffsetTime.parse(a, ISO_OFFSET_TIME)).toOption.toRight(List(s"$a value is not valid OffsetTime")) + given Decoder[String, ZonedDateTime] = a => + Try(ZonedDateTime.parse(a, ISO_ZONED_DATE_TIME)).toOption.toRight(List(s"$a value is not valid ZonedDateTime")) + given Decoder[String, Instant] = a => Try(Instant.parse(a)).toOption.toRight(List(s"$a value is not valid Instant")) + + given Decoder[String, Boolean] = a => + if (a.toLowerCase == "true") Right(true) + else if (a.toLowerCase == "false") Right(false) + else Left(List(s"$a value is not valid Boolean")) + + given Decoder[String, Int] = a => a.toIntOption.toRight(List(s"$a value is not valid Int")) + + given Decoder[String, Char] = a => { + if (a.length == 1) a.headOption.toRight(List(s"$a value is not valid Char")) + else + Left(List(s"$a value is not valid Char")) + } + + given Decoder[String, Double] = a => a.toDoubleOption.toRight(List(s"$a value is not valid Double")) + + given Decoder[String, SHA1] = a => RegexValidator[SHA1](validatorSHA1.regexp).validate(a) + + given Decoder[String, SHA256] = a => RegexValidator[SHA256](validatorSHA256.regexp).validate(a) + + given Decoder[String, IP] = a => RegexValidator[IP](validatorIP.regexp).validate(a) + + given Decoder[String, IP6] = a => RegexValidator[IP6](validatorIP_6.regexp).validate(a) + + given Decoder[String, BitcoinAdd] = a => RegexValidator[BitcoinAdd](validatorBitcoinAdd.regexp).validate(a) + + given Decoder[String, USphoneNumber] = a => RegexValidator[USphoneNumber](validatorUSphoneNumber.regexp).validate(a) + + given Decoder[String, ItalianMobilePhone] = a => + RegexValidator[ItalianMobilePhone](validatorItalianMobilePhone.regexp).validate(a) + + given Decoder[String, ItalianPhone] = a => RegexValidator[ItalianPhone](validatorItalianPhone.regexp).validate(a) + + given Decoder[String, Time24] = a => RegexValidator[Time24](validatorTime24.regexp).validate(a) + + given Decoder[String, MDY] = a => RegexValidator[MDY](validatorMDY.regexp).validate(a) + + given Decoder[String, MDY2] = a => RegexValidator[MDY2](validatorMDY2.regexp).validate(a) + + given Decoder[String, MDY3] = a => RegexValidator[MDY3](validatorMDY3.regexp).validate(a) + + given Decoder[String, MDY4] = a => RegexValidator[MDY4](validatorMDY4.regexp).validate(a) + + given Decoder[String, DMY] = a => RegexValidator[DMY](validatorDMY.regexp).validate(a) + + given Decoder[String, DMY2] = a => RegexValidator[DMY2](validatorDMY2.regexp).validate(a) + + given Decoder[String, DMY3] = a => RegexValidator[DMY3](validatorDMY3.regexp).validate(a) + + given Decoder[String, DMY4] = a => RegexValidator[DMY4](validatorDMY4.regexp).validate(a) + + given Decoder[String, Time] = a => RegexValidator[Time](validatorTime.regexp).validate(a) + + given Decoder[String, Cron] = a => RegexValidator[Cron](validatorCron.regexp).validate(a) + + given Decoder[String, ItalianFiscalCode] = a => + RegexValidator[ItalianFiscalCode](validatorItalianFiscalCode.regexp).validate(a) + + given Decoder[String, ItalianVAT] = a => RegexValidator[ItalianVAT](validatorItalianVAT.regexp).validate(a) + + given Decoder[String, ItalianIban] = a => RegexValidator[ItalianIban](validatorItalianIban.regexp).validate(a) + + given Decoder[String, USstates] = a => RegexValidator[USstates](validatorUSstates.regexp).validate(a) + + given Decoder[String, USstates1] = a => RegexValidator[USstates1](validatorUSstates1.regexp).validate(a) + + given Decoder[String, USZipCode] = a => RegexValidator[USZipCode](validatorUSZipCode.regexp).validate(a) + + given Decoder[String, ItalianZipCode] = a => + RegexValidator[ItalianZipCode](validatorItalianZipCode.regexp).validate(a) + + given Decoder[String, USstreets] = a => RegexValidator[USstreets](validatorUSstreets.regexp).validate(a) + + given Decoder[String, USstreetNumber] = a => + RegexValidator[USstreetNumber](validatorUSstreetNumber.regexp).validate(a) + + given Decoder[String, GermanStreet] = a => RegexValidator[GermanStreet](validatorGermanStreet.regexp).validate(a) + + given Decoder[String, UsdCurrency] = a => RegexValidator[UsdCurrency](validatorUsdCurrency.regexp).validate(a) + + given Decoder[String, EurCurrency] = a => RegexValidator[EurCurrency](validatorEurCurrency.regexp).validate(a) + + given Decoder[String, YenCurrency] = a => RegexValidator[YenCurrency](validatorYenCurrency.regexp).validate(a) + + given Decoder[String, NotASCII] = a => RegexValidator[NotASCII](validatorNotASCII.regexp).validate(a) + + given Decoder[String, SingleChar] = a => RegexValidator[SingleChar](validatorSingleChar.regexp).validate(a) + + given Decoder[String, AZString] = a => RegexValidator[AZString](validatorAZString.regexp).validate(a) + + given Decoder[String, AsciiString] = a => RegexValidator[AsciiString](validatorAsciiString.regexp).validate(a) + + given Decoder[String, StringAndNumber] = a => + RegexValidator[StringAndNumber](validatorStringAndNumber.regexp).validate(a) + + given Decoder[String, ApacheError] = a => RegexValidator[ApacheError](validatorApacheError.regexp).validate(a) + + given Decoder[String, Number1] = a => RegexValidator[Number1](validatorNumber1.regexp).validate(a) + + given Decoder[String, Unsigned32] = a => RegexValidator[Unsigned32](validatorUnsigned32.regexp).validate(a) + + given Decoder[String, Signed] = a => RegexValidator[Signed](validatorSigned.regexp).validate(a) + + given Decoder[String, Percentage] = a => RegexValidator[Percentage](validatorPercentage.regexp).validate(a) + + given Decoder[String, Scientific] = a => RegexValidator[Scientific](validatorScientific.regexp).validate(a) + + given Decoder[String, SingleNumber] = a => RegexValidator[SingleNumber](validatorSingleNumber.regexp).validate(a) + + given Decoder[String, Celsius] = a => RegexValidator[Celsius](validatorCelsius.regexp).validate(a) + + given Decoder[String, Fahrenheit] = a => RegexValidator[Fahrenheit](validatorFahrenheit.regexp).validate(a) + + given Decoder[String, Coordinate] = a => RegexValidator[Coordinate](validatorCoordinate.regexp).validate(a) + + given Decoder[String, Coordinate1] = a => RegexValidator[Coordinate1](validatorCoordinate1.regexp).validate(a) + + given Decoder[String, Coordinate2] = a => RegexValidator[Coordinate2](validatorCoordinate2.regexp).validate(a) + + given Decoder[String, Youtube] = a => RegexValidator[Youtube](validatorYoutube.regexp).validate(a) + + given Decoder[String, Facebook] = a => RegexValidator[Facebook](validatorFacebook.regexp).validate(a) + + given Decoder[String, Twitter] = a => RegexValidator[Twitter](validatorTwitter.regexp).validate(a) + + given Decoder[String, MACAddress] = a => RegexValidator[MACAddress](validatorMACAddress.regexp).validate(a) + + given Decoder[String, Email1] = a => RegexValidator[Email1](validatorEmail1.regexp).validate(a) + + given Decoder[String, Email] = a => RegexValidator[Email](validatorEmail.regexp).validate(a) + + given Decoder[String, EmailSimple] = a => RegexValidator[EmailSimple](validatorEmailSimple.regexp).validate(a) + + given Decoder[String, HEX] = a => RegexValidator[HEX](validatorHEX.regexp).validate(a) + + given Decoder[String, HEX1] = a => RegexValidator[HEX1](validatorHEX1.regexp).validate(a) + + given Decoder[String, HEX2] = a => RegexValidator[HEX2](validatorHEX2.regexp).validate(a) + + given Decoder[String, HEX3] = a => RegexValidator[HEX3](validatorHEX3.regexp).validate(a) + + given Decoder[String, URL] = a => RegexValidator[URL](validatorURL.regexp).validate(a) + + given Decoder[String, URL1] = a => RegexValidator[URL1](validatorURL1.regexp).validate(a) + + given Decoder[String, URL2] = a => RegexValidator[URL2](validatorURL2.regexp).validate(a) + + given Decoder[String, URL3] = a => RegexValidator[URL3](validatorURL3.regexp).validate(a) + + given Decoder[String, FTP] = a => RegexValidator[FTP](validatorFTP.regexp).validate(a) + + given Decoder[String, FTP1] = a => RegexValidator[FTP1](validatorFTP1.regexp).validate(a) + + given Decoder[String, FTP2] = a => RegexValidator[FTP2](validatorFTP2.regexp).validate(a) + + given Decoder[String, Domain] = a => RegexValidator[Domain](validatorDomain.regexp).validate(a) + + given Decoder[String, MD5] = a => RegexValidator[MD5](validatorMD5.regexp).validate(a) + + given [A](using f: Decoder[String, A]): Decoder[String, Option[A]] = s => + if (s == "") Right(None) else f(s).map(Some(_)) + + given [A](using f: Decoder[String, A], csvFormat: IttoCSVFormat): Decoder[String, List[A]] = s => + val a = s.split(csvFormat.delimeter.toString, -1).toList.map(f(_)) + val (l, rights) = a.partitionMap(identity) + val lefts = l.flatten + if (lefts.isEmpty) Right(rights) else Left(lefts) + + given Decoder[List[String], EmptyTuple] = + case Nil => Right(EmptyTuple) + case s => Left(List(s"$s empty list")) + + given [H, T <: Tuple](using dh: Decoder[String, H], dt: Decoder[List[String], T]): Decoder[List[String], H *: T] = + case h :: t => + (dh(h), dt(t)) match + case (Right(a), Right(b)) => Right(a *: b) + case (Left(e), Left(e2)) => Left(e ::: e2) + case (Left(e), _) => Left(e) + case (_, Left(e)) => Left(e) + case Nil => Left(List("empty list")) + + def list2Product[A]( + xs: List[String] + )(using m: Mirror.ProductOf[A], d: Decoder[List[String], m.MirroredElemTypes]): Either[List[String], A] = + d(xs) match + case Right(r) => Right(m.fromProduct(r)) + case Left(l) => Left(l) + + /** @param csvList + * is the List[String] to parse + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * `List[Either[List[String], A]]` based on the parsing of `csvList` any errors are reported + */ + def fromCsv[A]( + csvList: List[String] + )(using + m: Mirror.ProductOf[A], + d: Decoder[List[String], m.MirroredElemTypes], + csvFormat: IttoCSVFormat + ): List[Either[List[String], A]] = csvList match + case Nil => Nil + case l => + l.collect { + case row if row.nonEmpty || !csvFormat.ignoreEmptyLines => + tokenizeCsvLine(row) match + case None => Left(List(s"$csvList is not a valid csv string")) + case Some(t) => + list2Product[A](t).match + case Right(a) => Right(a) + case Left(a) => Left(a) + } + + def fromCsv[A](csv: String)(using + m: Mirror.ProductOf[A], + d: Decoder[List[String], m.MirroredElemTypes], + csvFormat: IttoCSVFormat + ): List[Either[List[String], A]] = fromCsv(csv.split(csvFormat.recordSeparator, -1).toList) + + def fromCsvL[A]( + x: String + )(using dec: Decoder[String, A], csvFormat: IttoCSVFormat): List[Either[String, A]] = + x.split(csvFormat.delimeter.toString, -1).toList.map(dec(_)).map { + case Left(a) => Left(a.head) + case Right(a) => Right(a) + } + +end FromCsv diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/Header.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/Header.scala new file mode 100644 index 0000000..4535655 --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/Header.scala @@ -0,0 +1,28 @@ +package com.github.gekomad.ittocsv.core + +import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + +object Header: + + import scala.compiletime.{constValue, erasedValue} + import scala.deriving._ + + private inline def toNames[T <: Tuple]: List[String] = + inline erasedValue[T] match + case _: (head *: tail) => + (inline constValue[head] match + case str: String => str + ) :: toNames[tail] + case _ => Nil + + private inline def fieldNames[P](using mirror: Mirror.Of[P]): List[String] = toNames[mirror.MirroredElemLabels] + + /** @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * the string with class's fields name encoded according to csvFormat + */ + inline def csvHeader[T](using mirror: Mirror.Of[T], csvFormat: IttoCSVFormat): String = + fieldNames[T].map(StringToCsvField.stringToCsvField).mkString(csvFormat.delimeter.toString) + +end Header diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/ToCsv.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/ToCsv.scala new file mode 100644 index 0000000..ccc73e4 --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/ToCsv.scala @@ -0,0 +1,196 @@ +package com.github.gekomad.ittocsv.core + +import com.github.gekomad.ittocsv.core.Header._ +import com.github.gekomad.ittocsv.core.Types.implicits._ +import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.OffsetTime +import java.time.ZonedDateTime +import java.util.UUID +import scala.deriving.Mirror + +object ToCsv: + trait FieldEncoder[A]: + def encodeField(a: A)(using csvFormat: IttoCSVFormat): String + + trait RowEncoder[A]: + def encodeRow(a: A)(using csvFormat: IttoCSVFormat): List[String] + + def customFieldEncoder[A](f: A => String): FieldEncoder[A] = + new FieldEncoder[A]: + def encodeField(x: A)(using csvFormat: IttoCSVFormat): String = + StringToCsvField.stringToCsvField(f(x)) + + given FieldEncoder[Int] = customFieldEncoder[Int](_.toString) + given FieldEncoder[Boolean] = customFieldEncoder[Boolean](x => if x then "true" else "false") + given FieldEncoder[UUID] = customFieldEncoder[UUID](_.toString) + given FieldEncoder[String] = customFieldEncoder[String](identity) + given FieldEncoder[Long] = customFieldEncoder[Long](_.toString) + given FieldEncoder[Double] = customFieldEncoder[Double](_.toString) + given FieldEncoder[Byte] = customFieldEncoder[Byte](_.toString) + given FieldEncoder[Short] = customFieldEncoder[Short](_.toString) + given FieldEncoder[Float] = customFieldEncoder[Float](_.toString) + given FieldEncoder[Char] = customFieldEncoder[Char](_.toString) + given FieldEncoder[LocalDate] = customFieldEncoder[LocalDate](_.toString) + given FieldEncoder[LocalDateTime] = customFieldEncoder[LocalDateTime](_.toString) + given FieldEncoder[LocalTime] = customFieldEncoder[LocalTime](_.toString) + given FieldEncoder[OffsetDateTime] = customFieldEncoder[OffsetDateTime](_.toString) + given FieldEncoder[OffsetTime] = customFieldEncoder[OffsetTime](_.toString) + given FieldEncoder[ZonedDateTime] = customFieldEncoder[ZonedDateTime](_.toString) + given FieldEncoder[Instant] = customFieldEncoder[Instant](_.toString) + given FieldEncoder[SHA1] = customFieldEncoder[SHA1](_.value) + given FieldEncoder[SHA256] = customFieldEncoder[SHA256](_.value) + given FieldEncoder[IP] = customFieldEncoder[IP](_.value) + given FieldEncoder[IP6] = customFieldEncoder[IP6](_.value) + given FieldEncoder[BitcoinAdd] = customFieldEncoder[BitcoinAdd](_.value) + given FieldEncoder[USphoneNumber] = customFieldEncoder[USphoneNumber](_.value) + given FieldEncoder[ItalianMobilePhone] = customFieldEncoder[ItalianMobilePhone](_.value) + given FieldEncoder[ItalianPhone] = customFieldEncoder[ItalianPhone](_.value) + given FieldEncoder[Time24] = customFieldEncoder[Time24](_.value) + given FieldEncoder[MDY] = customFieldEncoder[MDY](_.value) + given FieldEncoder[MDY2] = customFieldEncoder[MDY2](_.value) + given FieldEncoder[MDY3] = customFieldEncoder[MDY3](_.value) + given FieldEncoder[MDY4] = customFieldEncoder[MDY4](_.value) + given FieldEncoder[DMY] = customFieldEncoder[DMY](_.value) + given FieldEncoder[DMY2] = customFieldEncoder[DMY2](_.value) + given FieldEncoder[DMY3] = customFieldEncoder[DMY3](_.value) + given FieldEncoder[DMY4] = customFieldEncoder[DMY4](_.value) + given FieldEncoder[Time] = customFieldEncoder[Time](_.value) + given FieldEncoder[Cron] = customFieldEncoder[Cron](_.value) + given FieldEncoder[ItalianFiscalCode] = customFieldEncoder[ItalianFiscalCode](_.value) + given FieldEncoder[ItalianVAT] = customFieldEncoder[ItalianVAT](_.value) + given FieldEncoder[ItalianIban] = customFieldEncoder[ItalianIban](_.value) + given FieldEncoder[USstates] = customFieldEncoder[USstates](_.value) + given FieldEncoder[USstates1] = customFieldEncoder[USstates1](_.value) + given FieldEncoder[USZipCode] = customFieldEncoder[USZipCode](_.value) + given FieldEncoder[ItalianZipCode] = customFieldEncoder[ItalianZipCode](_.value) + given FieldEncoder[USstreets] = customFieldEncoder[USstreets](_.value) + given FieldEncoder[USstreetNumber] = customFieldEncoder[USstreetNumber](_.value) + given FieldEncoder[GermanStreet] = customFieldEncoder[GermanStreet](_.value) + given FieldEncoder[UsdCurrency] = customFieldEncoder[UsdCurrency](_.value) + given FieldEncoder[EurCurrency] = customFieldEncoder[EurCurrency](_.value) + given FieldEncoder[YenCurrency] = customFieldEncoder[YenCurrency](_.value) + given FieldEncoder[NotASCII] = customFieldEncoder[NotASCII](_.value) + given FieldEncoder[SingleChar] = customFieldEncoder[SingleChar](_.value) + given FieldEncoder[AZString] = customFieldEncoder[AZString](_.value) + given FieldEncoder[AsciiString] = customFieldEncoder[AsciiString](_.value) + given FieldEncoder[StringAndNumber] = customFieldEncoder[StringAndNumber](_.value) + given FieldEncoder[ApacheError] = customFieldEncoder[ApacheError](_.value) + given FieldEncoder[Number1] = customFieldEncoder[Number1](_.value) + given FieldEncoder[Unsigned32] = customFieldEncoder[Unsigned32](_.value) + given FieldEncoder[Signed] = customFieldEncoder[Signed](_.value) + given FieldEncoder[Percentage] = customFieldEncoder[Percentage](_.value) + given FieldEncoder[Scientific] = customFieldEncoder[Scientific](_.value) + given FieldEncoder[SingleNumber] = customFieldEncoder[SingleNumber](_.value) + given FieldEncoder[Celsius] = customFieldEncoder[Celsius](_.value) + given FieldEncoder[Fahrenheit] = customFieldEncoder[Fahrenheit](_.value) + given FieldEncoder[Coordinate] = customFieldEncoder[Coordinate](_.value) + given FieldEncoder[Coordinate1] = customFieldEncoder[Coordinate1](_.value) + given FieldEncoder[Coordinate2] = customFieldEncoder[Coordinate2](_.value) + given FieldEncoder[Youtube] = customFieldEncoder[Youtube](_.value) + given FieldEncoder[Facebook] = customFieldEncoder[Facebook](_.value) + given FieldEncoder[Twitter] = customFieldEncoder[Twitter](_.value) + given FieldEncoder[MACAddress] = customFieldEncoder[MACAddress](_.value) + given FieldEncoder[Email1] = customFieldEncoder[Email1](_.value) + given FieldEncoder[Email] = customFieldEncoder[Email](_.value) + given FieldEncoder[EmailSimple] = customFieldEncoder[EmailSimple](_.value) + given FieldEncoder[HEX] = customFieldEncoder[HEX](_.value) + given FieldEncoder[HEX1] = customFieldEncoder[HEX1](_.value) + given FieldEncoder[HEX2] = customFieldEncoder[HEX2](_.value) + given FieldEncoder[HEX3] = customFieldEncoder[HEX3](_.value) + given FieldEncoder[URL] = customFieldEncoder[URL](_.value) + given FieldEncoder[URL1] = customFieldEncoder[URL1](_.value) + given FieldEncoder[URL2] = customFieldEncoder[URL2](_.value) + given FieldEncoder[URL3] = customFieldEncoder[URL3](_.value) + given FieldEncoder[FTP] = customFieldEncoder[FTP](_.value) + given FieldEncoder[FTP1] = customFieldEncoder[FTP1](_.value) + given FieldEncoder[FTP2] = customFieldEncoder[FTP2](_.value) + given FieldEncoder[Domain] = customFieldEncoder[Domain](_.value) + given FieldEncoder[MD5] = customFieldEncoder[MD5](_.value) + + given [A](using enc: FieldEncoder[A]): FieldEncoder[Option[A]] = + new FieldEncoder[Option[A]]: + def encodeField(x: Option[A])(using csvFormat: IttoCSVFormat): String = + x match + case Some(xx) => enc.encodeField(xx) + case _ => StringToCsvField.stringToCsvField("") + + given [A](using enc: FieldEncoder[A]): FieldEncoder[List[A]] = + new FieldEncoder[List[A]]: + def encodeField(x: List[A])(using csvFormat: IttoCSVFormat): String = + x match + case x :: Nil => enc.encodeField(x) + case x :: xs => enc.encodeField(x) + csvFormat.recordSeparator + encodeField(xs) + case _ => StringToCsvField.stringToCsvField("") + + given RowEncoder[EmptyTuple] with + def encodeRow(empty: EmptyTuple)(using csvFormat: IttoCSVFormat): List[String] = List.empty + + given [H: FieldEncoder, T <: Tuple: RowEncoder]: RowEncoder[H *: T] with + def encodeRow(tuple: H *: T)(using csvFormat: IttoCSVFormat): List[String] = + summon[FieldEncoder[H]].encodeField(tuple.head) :: summon[RowEncoder[T]].encodeRow(tuple.tail) + +// private def tupleToCsv[A <: Tuple: RowEncoder](tuple: A)(using csvFormat: IttoCSVFormat): List[String] = +// summon[RowEncoder[A]].encodeRow(tuple) + + inline def header[A](using mirror: Mirror.Of[A], csvFormat: IttoCSVFormat): String = + if (csvFormat.printHeader) csvHeader[A] + csvFormat.recordSeparator else "" + + /** @param a + * is the element to convert + * @param printRecordSeparator + * if true, appends the record separator to end of string + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * the CSV string encoded + */ + def toCsv[A <: Product]( + a: A, + printRecordSeparator: Boolean = false + )(using m: scala.deriving.Mirror.ProductOf[A], e: RowEncoder[m.MirroredElemTypes], csvFormat: IttoCSVFormat): String = + (if (printRecordSeparator) csvFormat.recordSeparator else "") + toCsv(a) + + inline def toCsvL[A <: Product](a: Seq[A])(using + m: scala.deriving.Mirror.ProductOf[A], + e: RowEncoder[m.MirroredElemTypes], + csvFormat: IttoCSVFormat + ): String = header + a.map(toCsv).mkString(csvFormat.recordSeparator) + + def toCsv[A <: Product](a: Seq[A])(using + m: scala.deriving.Mirror.ProductOf[A], + e: RowEncoder[m.MirroredElemTypes], + csvFormat: IttoCSVFormat + ): String = a.map(toCsv).mkString(csvFormat.delimeter.toString) + + def toCsv[A](a: Seq[A])(using + enc: FieldEncoder[A], + csvFormat: IttoCSVFormat + ): String = a.map(toCsv).mkString(csvFormat.delimeter.toString) + + def toCsv[A <: Product](t: A)(using + m: scala.deriving.Mirror.ProductOf[A], + e: RowEncoder[m.MirroredElemTypes], + csvFormat: IttoCSVFormat + ): String = e.encodeRow(Tuple.fromProductTyped(t)).mkString(csvFormat.delimeter.toString) + + def toCsv[A](t: A)(using + enc: FieldEncoder[A], + csvFormat: IttoCSVFormat + ): String = enc.encodeField(t) + + def toCsvFlat[A <: Product](a: A)(using m: scala.deriving.Mirror.ProductOf[A], csvFormat: IttoCSVFormat): String = { + + def flatTuple(any: Any): Tuple = any match + case p: Product => p.productIterator.map(flatTuple).foldLeft(EmptyTuple: Tuple)(_ ++ _) + case a => Tuple1(a) + + val tuple = flatTuple(Tuple.fromProductTyped(a)).toList + tuple.map(a => StringToCsvField.stringToCsvField(a.toString)).mkString(csvFormat.delimeter.toString) + } + +end ToCsv diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/Types.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/Types.scala new file mode 100644 index 0000000..9e4d3de --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/core/Types.scala @@ -0,0 +1,301 @@ +package com.github.gekomad.ittocsv.core + +import scala.deriving.Mirror +import com.github.gekomad.regexcollection.Collection.Validator + +object Types: + + trait Validate[A]: + def validate(value: String): Either[List[String], A] + + private type Cons[A] = String => A + + final case class RegexValidator[A](regex: String)(using apply: Cons[A], m: Mirror.Of[A]) extends Validate[A] { + given Validator[A] = Validator[A](regex) + + def validate(value: String): Either[List[String], A] = + com.github.gekomad.regexcollection.Validate + .validate[A](value) + .map(_ => Right(apply(value))) + .getOrElse(Left(List(s"$value value is not valid ${m.toString}"))) + } + + object implicits: + final case class Youtube(value: String) + + final case class Facebook(value: String) + + final case class ApacheError(value: String) + + final case class Twitter(value: String) + + final case class UsdCurrency(value: String) + + final case class EurCurrency(value: String) + + final case class YenCurrency(value: String) + + final case class NotASCII(value: String) + + final case class SingleChar(value: String) + + final case class AZString(value: String) + + final case class StringAndNumber(value: String) + + final case class AsciiString(value: String) + + final case class Number1(value: String) + + final case class Unsigned32(value: String) + + final case class Signed(value: String) + + final case class Percentage(value: String) + + final case class Scientific(value: String) + + final case class SingleNumber(value: String) + + final case class Celsius(value: String) + + final case class Fahrenheit(value: String) + + final case class Coordinate(value: String) + + final case class Coordinate1(value: String) + + final case class Coordinate2(value: String) + + final case class MACAddress(value: String) + + final case class Email(value: String) + + final case class Email1(value: String) + + final case class EmailSimple(value: String) + + final case class HEX(value: String) + + final case class HEX1(value: String) + + final case class HEX2(value: String) + + final case class HEX3(value: String) + + final case class URL(value: String) + + final case class URL1(value: String) + + final case class URL2(value: String) + + final case class URL3(value: String) + + final case class FTP(value: String) + + final case class FTP1(value: String) + + final case class FTP2(value: String) + + final case class Domain(value: String) + + final case class MD5(value: String) + + final case class SHA1(value: String) + + final case class SHA256(value: String) + + final case class IP(value: String) + + final case class BitcoinAdd(value: String) + + final case class IP6(value: String) + + final case class USphoneNumber(value: String) + + final case class ItalianMobilePhone(value: String) + + final case class ItalianPhone(value: String) + + final case class Time24(value: String) + + final case class MDY(value: String) + + final case class MDY2(value: String) + + final case class MDY3(value: String) + + final case class MDY4(value: String) + + final case class DMY(value: String) + + final case class DMY2(value: String) + + final case class DMY3(value: String) + + final case class DMY4(value: String) + + final case class Time(value: String) + + final case class ItalianFiscalCode(value: String) + + final case class ItalianVAT(value: String) + + final case class ItalianIban(value: String) + + final case class USstates(value: String) + + final case class USstates1(value: String) + + final case class USZipCode(value: String) + + final case class ItalianZipCode(value: String) + + final case class USstreets(value: String) + + final case class USstreetNumber(value: String) + + final case class GermanStreet(value: String) + + final case class Cron(value: String) + + given Cons[Youtube] = Youtube.apply + + given Cons[Facebook] = Facebook.apply + + given Cons[Twitter] = Twitter.apply + + given Cons[MACAddress] = MACAddress.apply + + given Cons[Email] = Email.apply + + given Cons[EmailSimple] = EmailSimple.apply + + given Cons[Email1] = Email1.apply + + given Cons[HEX] = HEX.apply + + given Cons[HEX1] = HEX1.apply + + given Cons[HEX2] = HEX2.apply + + given Cons[HEX3] = HEX3.apply + + given Cons[URL] = URL.apply + + given Cons[URL1] = URL1.apply + + given Cons[URL2] = URL2.apply + + given Cons[URL3] = URL3.apply + + given Cons[FTP] = FTP.apply + + given Cons[FTP1] = FTP1.apply + + given Cons[FTP2] = FTP2.apply + + given Cons[Domain] = Domain.apply + + given Cons[MD5] = MD5.apply + + given Cons[SHA1] = SHA1.apply + + given Cons[SHA256] = SHA256.apply + + given Cons[IP] = IP.apply + + given Cons[IP6] = IP6.apply + + given Cons[BitcoinAdd] = BitcoinAdd.apply + + given Cons[USphoneNumber] = USphoneNumber.apply + + given Cons[ItalianMobilePhone] = ItalianMobilePhone.apply + + given Cons[ItalianPhone] = ItalianPhone.apply + + given Cons[Time24] = Time24.apply + + given Cons[MDY] = MDY.apply + + given Cons[MDY2] = MDY2.apply + + given Cons[MDY3] = MDY3.apply + + given Cons[MDY4] = MDY4.apply + + given Cons[DMY] = DMY.apply + + given Cons[DMY2] = DMY2.apply + + given Cons[DMY3] = DMY3.apply + + given Cons[DMY4] = DMY4.apply + + given Cons[Time] = Time.apply + + given Cons[Cron] = Cron.apply + + given Cons[ItalianFiscalCode] = ItalianFiscalCode.apply + + given Cons[ItalianVAT] = ItalianVAT.apply + + given Cons[ItalianIban] = ItalianIban.apply + + given Cons[USstates] = USstates.apply + + given Cons[USstates1] = USstates1.apply + + given Cons[USZipCode] = USZipCode.apply + + given Cons[ItalianZipCode] = ItalianZipCode.apply + + given Cons[USstreets] = USstreets.apply + + given Cons[USstreetNumber] = USstreetNumber.apply + + given Cons[GermanStreet] = GermanStreet.apply + + given Cons[UsdCurrency] = UsdCurrency.apply + + given Cons[EurCurrency] = EurCurrency.apply + + given Cons[YenCurrency] = YenCurrency.apply + + given Cons[NotASCII] = NotASCII.apply + + given Cons[SingleChar] = SingleChar.apply + + given Cons[AZString] = AZString.apply + + given Cons[StringAndNumber] = StringAndNumber.apply + + given Cons[AsciiString] = AsciiString.apply + + given Cons[ApacheError] = ApacheError.apply + + given Cons[Number1] = Number1.apply + + given Cons[Unsigned32] = Unsigned32.apply + + given Cons[Signed] = Signed.apply + + given Cons[Percentage] = Percentage.apply + + given Cons[Scientific] = Scientific.apply + + given Cons[SingleNumber] = SingleNumber.apply + + given Cons[Celsius] = Celsius.apply + + given Cons[Fahrenheit] = Fahrenheit.apply + + given Cons[Coordinate] = Coordinate.apply + + given Cons[Coordinate1] = Coordinate1.apply + + given Cons[Coordinate2] = Coordinate2.apply + + end implicits +end Types diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/Constants.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/Constants.scala new file mode 100644 index 0000000..16d2edb --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/Constants.scala @@ -0,0 +1,20 @@ +package com.github.gekomad.ittocsv.parser + +/** @author + * Giuseppe Cannella + * @since 0.0.1 + */ +object Constants: + val COMMA = ',' + val SEMICOLON = ';' + val COMMENT = '#' + val CR = "\r" + val CR_char = '\r' + val CRLF = "\r\n" + val DOUBLE_QUOTE: Char = '"' + val LF = "\n" + val LF_char = '\n' + val SP = ' ' + val PIPE = '|' + val TAB = '\t' +end Constants diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/CsvFieldToString.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/CsvFieldToString.scala new file mode 100644 index 0000000..f1c7ed2 --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/CsvFieldToString.scala @@ -0,0 +1,74 @@ +package com.github.gekomad.ittocsv.parser + +/** Trasforms a single CSV field to string + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + */ +object CsvFieldToString: + + /** @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @return + * trims the string according to csvFormat + */ + private def trim(field: String)(using csvFormat: IttoCSVFormat): String = if (csvFormat.trim) field.trim else field + + /** @return + * trasforms a CSV field to string + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @param field + * the string to trasform + * {{{ + * csvFieldToString("\"\"\",\"\"\"") // "\",\"" + * csvFieldToString("\"aa\na\"") // "aa\na" + * csvFieldToString("\"\"\"\"\"\"") // "\"\"" + * csvFieldToString("\"\"\"\"") // "\"" + * csvFieldToString("\",\"") // "," + * csvFieldToString("\"\"") // "" + * csvFieldToString("\"\"\"a\"\"\"") // "\"a\"" + * csvFieldToString("\" \"") // " " + * csvFieldToString("aaa") // "aaa" + * csvFieldToString("\"aa\"\"b\"") // "aa\"b" + * csvFieldToString("\"aa\"\"\"\"b\"") // "aa\"\"b" + * csvFieldToString("\"aa,a\"") // "aa,a" + * csvFieldToString("\"aa,\"\"b\"") // "aa,\"b" + * csvFieldToString("\"aa,\"\"b\"") // "aa,\"b" + * }}} + */ + def csvFieldToString(field: String)(using csvFormat: IttoCSVFormat): String = { + + /* + * @param csvFormat the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @param field the string to trasform + * {{{ + * "aaaa,bbbb" => aaaa,bbbb + * "aaaa\nbbbb" => aaaa\nbbbb + * "" => { } + * "aaaabbbb " => {aaaabbbb } + * "#aaaabbbb" => #aaaabbbb + * }}} + * + */ + def parseBorders(a: String)(using csvFormat: IttoCSVFormat): String = { + val q = csvFormat.quote.toString + if (a.length > 1 && a.startsWith(q) && a.endsWith(q)) a.init.drop(1) else a + } + + /* + * @param csvFormat the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @param field the string to trasform + * {{{ + * "aaaa""bbbb" => aaaa"bbbb + * "aaaa""""bbbb" => aaaa""bbbb + * }}} + * + */ + def parseQuote(a: String)(using csvFormat: IttoCSVFormat): String = + a.replace(s"${csvFormat.quote}${csvFormat.quote}", s"${csvFormat.quote}") + + parseQuote(parseBorders(trim(field))) + } +end CsvFieldToString diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/IttoCSVFormat.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/IttoCSVFormat.scala new file mode 100644 index 0000000..85c7fc8 --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/IttoCSVFormat.scala @@ -0,0 +1,125 @@ +package com.github.gekomad.ittocsv.parser + +import Constants._ + +/** The formatter determines how CSV will be generated, two formatters are available: + * + * | Method | Description | Default Formatter | Tab formatter | + * |:-------------------------------|:----------------------------:|------------------:|--------------:| + * | withDelimiter(c: Char) | the separator between fields | , | \t | + * | withQuote(c: Char) | the quoteChar character | " | " | + * | withQuoteEmpty(c: Boolean) | quotes field if empty | false | false | + * | withForceQuote(c: Boolean) | quotes all fields | false | false | + * | withPrintHeader(c: Boolean) | if true prints the header | false | false | + * | withTrim(c: Boolean) | trims the field | false | false | + * | withRecordSeparator(c: String) | the rows separator | \r\n | \r\n | + * + * it's possible to create custom foramtters editing the default ones, example: + * + * {{{ + * given IttoCSVFormat = IttoCSVFormat.default.withForceQuote(true).withRecordSeparator("\n").with..... + * }}} + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + * @param delimeter + * the separator between fields + * @param quote + * the quoteChar character + * @param recordSeparator + * the record separator + * @param quoteEmpty + * if true quotes the empty field + * @param forceQuote + * if true quotes all fields + * @param printHeader + * if true prints the header + * @param trim + * if true trims the fields + * @param ignoreEmptyLines + * if true skip empty lines + * @param quoteLowerChar + * if true quotes lower chars + */ +final case class IttoCSVFormat( + delimeter: Char, + quote: Char, + recordSeparator: String, + quoteEmpty: Boolean, + forceQuote: Boolean, + printHeader: Boolean, + trim: Boolean, + ignoreEmptyLines: Boolean, + quoteLowerChar: Boolean +) { + def withDelimiter(c: Char): IttoCSVFormat = this.copy(delimeter = c) + + def withQuote(c: Char): IttoCSVFormat = this.copy(quote = c) + + def withQuoteEmpty(c: Boolean): IttoCSVFormat = this.copy(quoteEmpty = c) + + def withForceQuote(c: Boolean): IttoCSVFormat = this.copy(forceQuote = c) + + def withPrintHeader(c: Boolean): IttoCSVFormat = this.copy(printHeader = c) + + def withTrim(c: Boolean): IttoCSVFormat = this.copy(trim = c) + + def withRecordSeparator(c: String): IttoCSVFormat = this.copy(recordSeparator = c) + + def withIgnoreEmptyLines(c: Boolean): IttoCSVFormat = this.copy(ignoreEmptyLines = c) + + def withQuoteLowerChar(c: Boolean): IttoCSVFormat = this.copy(quoteLowerChar = c) +} + +object IttoCSVFormat: + + /** | Method | Descrizione | default | + * |:---------------------------------|:-----------------------------------------:|--------:| + * | withDelimiter(c: Char) | the separator between fields | , | + * | withQuote(c: Char) | the quoteChar character | " | + * | withQuoteEmpty(c: Boolean) | quotes field if empty | false | + * | withForceQuote(c: Boolean) | quotes all fields | false | + * | withPrintHeader(c: Boolean) | if true prints the header (method toCsvL) | false | + * | withTrim(c: Boolean) | trims the field | false | + * | withRecordSeparator(c: String) | the rows separator | \r\n | + * | withIgnoreEmptyLines(c: Boolean) | skips empty lines false | | + * | withQuoteLowerChar(c: Boolean) | quotes lower chars | false | + */ + val default: IttoCSVFormat = IttoCSVFormat( + quote = DOUBLE_QUOTE, + delimeter = COMMA, + recordSeparator = CRLF, + quoteEmpty = false, + forceQuote = false, + printHeader = true, + trim = false, + ignoreEmptyLines = false, + quoteLowerChar = false + ) + + /* + * | Method | Descrizione | default| + * |----------|:-------------:|------:|-:| + * | withDelimiter(c: Char) | the separator between fields |\t| + * | withQuote(c: Char) | the quoteChar character |"| + * | withQuoteEmpty(c: Boolean) | quotes field if empty |false| + * | withForceQuote(c: Boolean) | quotes all fields |false| + * | withPrintHeader(c: Boolean) | if true prints the header (method toCsvL) |false| + * | withTrim(c: Boolean) | trims the field | false| + * | withRecordSeparator(c: String) | the rows separator |\r\n| + * | withIgnoreEmptyLines(c: Boolean) | skips empty lines | false | + * | withQuoteLowerChar(c: Boolean) | quotes lower chars| false | + */ + val tab: IttoCSVFormat = IttoCSVFormat( + quote = DOUBLE_QUOTE, + delimeter = TAB, + recordSeparator = CRLF, + quoteEmpty = false, + forceQuote = false, + printHeader = true, + trim = false, + ignoreEmptyLines = false, + quoteLowerChar = false + ) +end IttoCSVFormat diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/StringToCsvField.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/StringToCsvField.scala new file mode 100644 index 0000000..1e574db --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/parser/StringToCsvField.scala @@ -0,0 +1,70 @@ +package com.github.gekomad.ittocsv.parser + +import com.github.gekomad.ittocsv.parser.Constants._ + +/** Trasforms a string to CSV field + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + */ +object StringToCsvField: + + /** @return + * trasforms a string to CSV field + * @param csvFormat + * the [[com.github.gekomad.ittocsv.parser.IttoCSVFormat]] formatter + * @param field + * the string to trasform + * {{{ + * stringToCsvField("\"") // "\"\"\"\"" + * stringToCsvField(",") // "\",\"" + * stringToCsvField("\",\"") // "\"\"\",\"\"\"" + * stringToCsvField("\"a\"") // "\"\"\"a\"\"\"" + * stringToCsvField("") // "\"\"" + * stringToCsvField("aa") // "\"aa\"" + * stringToCsvField(" ") // "\" \"" + * stringToCsvField("aaa") // "\"aaa\"" + * stringToCsvField("aa\na") // "\"aa\na\"" + * stringToCsvField("aa\"b") // "\"aa\"\"b\"" + * stringToCsvField("aa\"\"b") // "\"aa\"\"\"\"b\"" + * stringToCsvField("aa,a") // "\"aa,a\"" + * stringToCsvField("aa,\"b") // "\"aa,\"\"b\"" + * stringToCsvField("aa,\"b") // "\"aa,\"\"b\"" + * }}} + */ + def stringToCsvField(field: String)(using csvFormat: IttoCSVFormat): String = { + + def trim(s: String): String = if (csvFormat.trim) s.trim else s + + def parseQuote(string: String)(using csvFormatter: IttoCSVFormat): String = { + val q = csvFormat.quote + string match + case x if x == s"$q$q" => s"$q$q$q$q$q$q" + case x if x == s"$q" => s"$q$q$q$q" + case "" if csvFormatter.quoteEmpty || csvFormatter.forceQuote => s"$q$q" + case "" => string + case s => + var c = 0 + var containsQuote = false + while + if (s(c) == q || s(c) == csvFormat.delimeter || s(c) == CR_char || s(c) == LF_char) containsQuote = true + c = c + 1 + !containsQuote && c < s.length + do () + + val p = if (containsQuote) s.replace(csvFormat.quote.toString, s"$q$q") else s + + if ( + containsQuote || csvFormat.forceQuote || csvFormat.quoteLowerChar && (s(s.length - 1) <= SP || s( + 0 + ) <= COMMENT) + ) + s"${csvFormat.quote}$p${csvFormat.quote}" + else p + } + + parseQuote(trim(field)) + } + +end StringToCsvField diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/util/ListUtils.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/util/ListUtils.scala new file mode 100644 index 0000000..8d82bca --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/util/ListUtils.scala @@ -0,0 +1,56 @@ +package com.github.gekomad.ittocsv.util + +import cats.effect.{ExitCode, IO} +import fs2.io.file.Files +import fs2.{text, Stream} + +/** Utils for lists + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + */ +object ListUtils: + + /** @param list + * the list to write + * @param filePath + * the file path of file to write + * @param addLineSeparator + * if true add a line separator, default = true + * @return + * IO[ExitCode] + */ + def writeFile(list: List[String], filePath: String, addLineSeparator: Boolean = true): IO[ExitCode] = { + val a: Stream[IO, String] = Stream.emits(list) + val b: Stream[IO, String] = if (addLineSeparator) a.map(_ + System.lineSeparator) else a + b.through(text.utf8.encode) + .through(Files[IO].writeAll(fs2.io.file.Path(filePath))) + .compile + .drain + .as(ExitCode.Success) + } + + /** @param stream + * the stream to write + * @param filePath + * the file path of file to write + * @param addLineSeparator + * if true add a line separator, default = true + * @return + * IO[ExitCode] + */ + def writeFileStream( + stream: fs2.Stream[IO, String], + filePath: String, + addLineSeparator: Boolean = true + ): IO[ExitCode] = { + val b: Stream[IO, String] = if (addLineSeparator) stream.map(_ + System.lineSeparator) else stream + b.through(text.utf8.encode) + .through(Files[IO].writeAll(fs2.io.file.Path(filePath))) + .compile + .drain + .as(ExitCode.Success) + } + +end ListUtils diff --git a/scala3_js/src/main/scala/com/github/gekomad/ittocsv/util/StringUtils.scala b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/util/StringUtils.scala new file mode 100644 index 0000000..2a7a101 --- /dev/null +++ b/scala3_js/src/main/scala/com/github/gekomad/ittocsv/util/StringUtils.scala @@ -0,0 +1,68 @@ +package com.github.gekomad.ittocsv.util + +import com.github.gekomad.ittocsv.parser.IttoCSVFormat +import scala.annotation.tailrec + +/** Utils for strings + * + * @author + * Giuseppe Cannella + * @since 0.0.1 + */ +object StringUtils: + + /** @return + * splits the string at positions of separators + * @param string + * the string to split + */ + def split(string: String, separators: List[Int]): List[String] = { + + def sp(s: String, sep: List[Int]): List[String] = sep match + case Nil => List(s) + case x0 :: x1 :: xs => + val a = s.substring(x0 + 1, x1) + val b = sp(s, x1 :: xs) + a :: b + case x0 :: _ => List(s.substring(x0 + 1, s.length)) + + sp(string, -1 :: separators) + } + + def getDelimiter(s: String): Char = { + @tailrec + def go(s: String, delimiter: Int): Int = if (s.contains(delimiter.toChar)) go(s, delimiter - 1) else delimiter + + go(s, 0xffff).toChar + } + + /** @return + * trasforms a CSV string to List of strings + * @param csvFormat + * the CSV formatter + * @param csv + * the string to trasform + */ + def tokenizeCsvLine(csv: String)(using csvFormat: IttoCSVFormat): Option[List[String]] = csv match + case _ if !csv.contains(csvFormat.quote) => Some(csv.split(csvFormat.delimeter.toString, -1).toList) + case _ if csv.count(_ == csvFormat.quote) % 2 != 0 => None + case _ => + val delimiter = getDelimiter(csv) + val string = csv.replace(s"${csvFormat.quote}${csvFormat.quote}", delimiter.toString) + + var inside = false + val arr = string.toCharArray + val commas = scala.collection.mutable.ListBuffer.empty[Int] + + var c = 0 + while (c < arr.length) { + if (arr(c) == csvFormat.quote) inside = !inside + else if (!inside && arr(c) == csvFormat.delimeter) commas += c + c = c + 1 + } + + val l: List[String] = split(arr.mkString, commas.toList) + val p: List[String] = l.map(_.replace(s"${csvFormat.quote}", "").replace(delimiter, csvFormat.quote)) + Some(p) + +end StringUtils diff --git a/scala3_js/src/test/resources/csv_with_error.csv b/scala3_js/src/test/resources/csv_with_error.csv new file mode 100644 index 0000000..6b5f351 --- /dev/null +++ b/scala3_js/src/test/resources/csv_with_error.csv @@ -0,0 +1,5 @@ +id name date +1CC3CCBB-C749-3078-E050-1AACBE064651 bob 2018-11-20T09:10:25 +3CC3CCBB-C749-3078-E050-1AACBE064653 alice 2018-11-20T10:12:24 +xxx jim 2018-11-20T11:18:17 +5CC3CCBB-C749-3078-E050-1AACBE064655 tom 2018-11-20T11:36:04 \ No newline at end of file diff --git a/scala3_js/src/test/resources/csv_with_header.csv b/scala3_js/src/test/resources/csv_with_header.csv new file mode 100644 index 0000000..83d159a --- /dev/null +++ b/scala3_js/src/test/resources/csv_with_header.csv @@ -0,0 +1,5 @@ +id name date +1CC3CCBB-C749-3078-E050-1AACBE064651 bob 2018-11-20T09:10:25 +3CC3CCBB-C749-3078-E050-1AACBE064653 alice 2018-11-20T10:12:24 +4CC3CCBB-C749-3078-E050-1AACBE064654 jim 2018-11-20T11:18:17 +5CC3CCBB-C749-3078-E050-1AACBE064655 tom 2018-11-20T11:36:04 \ No newline at end of file diff --git a/scala3_js/src/test/resources/csv_without_header.csv b/scala3_js/src/test/resources/csv_without_header.csv new file mode 100644 index 0000000..a05cdbe --- /dev/null +++ b/scala3_js/src/test/resources/csv_without_header.csv @@ -0,0 +1,4 @@ +1CC3CCBB-C749-3078-E050-1AACBE064651 bob 2018-11-20T09:10:25 +3CC3CCBB-C749-3078-E050-1AACBE064653 alice 2018-11-20T10:12:24 +4CC3CCBB-C749-3078-E050-1AACBE064654 jim 2018-11-20T11:18:17 +5CC3CCBB-C749-3078-E050-1AACBE064655 tom 2018-11-20T11:36:04 \ No newline at end of file diff --git a/scala3_js/src/test/resources/empty_file.csv b/scala3_js/src/test/resources/empty_file.csv new file mode 100644 index 0000000..e69de29 diff --git a/scala3_js/src/test/scala/CsvFieldTest.scala b/scala3_js/src/test/scala/CsvFieldTest.scala new file mode 100644 index 0000000..8a1604c --- /dev/null +++ b/scala3_js/src/test/scala/CsvFieldTest.scala @@ -0,0 +1,110 @@ +class CsvFieldTest extends munit.FunSuite: + + test("stringToCsvFieldTrim") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + given IttoCSVFormat = IttoCSVFormat.default.withTrim(true) + + assert(StringToCsvField.stringToCsvField("a ") == "a") + assert(StringToCsvField.stringToCsvField(" a ") == "a") + assert(StringToCsvField.stringToCsvField(" ") == "") + } + + test("stringToCsvFieldForceQuote") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + given IttoCSVFormat = IttoCSVFormat.default.withForceQuote(true) + + assert(StringToCsvField.stringToCsvField("\"") == "\"\"\"\"") + assert(StringToCsvField.stringToCsvField(",") == "\",\"") + assert(StringToCsvField.stringToCsvField("\",\"") == "\"\"\",\"\"\"") + assert(StringToCsvField.stringToCsvField("\"a\"") == "\"\"\"a\"\"\"") + assert(StringToCsvField.stringToCsvField("") == "\"\"") + assert(StringToCsvField.stringToCsvField("aa") == "\"aa\"") + assert(StringToCsvField.stringToCsvField(" ") == "\" \"") + assert(StringToCsvField.stringToCsvField("aaa") == "\"aaa\"") + assert(StringToCsvField.stringToCsvField("aa\na") == "\"aa\na\"") + assert(StringToCsvField.stringToCsvField("aa\"b") == "\"aa\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa\"\"b") == "\"aa\"\"\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,a") == "\"aa,a\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + } + + test("stringToCsvField0") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + given IttoCSVFormat = IttoCSVFormat.default.withQuoteLowerChar(true) + assert(StringToCsvField.stringToCsvField(" ") == "\" \"") + } + + test("stringToCsvField1") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + given IttoCSVFormat = IttoCSVFormat.default.withQuoteLowerChar(true) + + assert(StringToCsvField.stringToCsvField("\"") == "\"\"\"\"") + assert(StringToCsvField.stringToCsvField(",") == "\",\"") + assert(StringToCsvField.stringToCsvField("\",\"") == "\"\"\",\"\"\"") + assert(StringToCsvField.stringToCsvField("\"a\"") == "\"\"\"a\"\"\"") + assert(StringToCsvField.stringToCsvField("") == "") + assert(StringToCsvField.stringToCsvField(" ") == "\" \"") + assert(StringToCsvField.stringToCsvField("aaa") == "aaa") + assert(StringToCsvField.stringToCsvField("aa\na") == "\"aa\na\"") + assert(StringToCsvField.stringToCsvField("aa\"b") == "\"aa\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa\"\"b") == "\"aa\"\"\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,a") == "\"aa,a\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + } + + test("stringToCsvField2") { + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + given IttoCSVFormat = IttoCSVFormat.default.withQuoteEmpty(true).withQuoteLowerChar(true) + + assert(StringToCsvField.stringToCsvField("\"") == "\"\"\"\"") + assert(StringToCsvField.stringToCsvField(",") == "\",\"") + assert(StringToCsvField.stringToCsvField("\",\"") == "\"\"\",\"\"\"") + assert(StringToCsvField.stringToCsvField("\"a\"") == "\"\"\"a\"\"\"") + assert(StringToCsvField.stringToCsvField("") == "\"\"") + assert(StringToCsvField.stringToCsvField(" ") == "\" \"") + assert(StringToCsvField.stringToCsvField("aaa") == "aaa") + assert(StringToCsvField.stringToCsvField("aa\na") == "\"aa\na\"") + assert(StringToCsvField.stringToCsvField("aa\"b") == "\"aa\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa\"\"b") == "\"aa\"\"\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,a") == "\"aa,a\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + assert(StringToCsvField.stringToCsvField("aa,\"b") == "\"aa,\"\"b\"") + } + + test("csvFieldToStringTest") { + import com.github.gekomad.ittocsv.parser.CsvFieldToString.csvFieldToString + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + given IttoCSVFormat = IttoCSVFormat.default.withQuoteEmpty(true) + + assert(csvFieldToString("\"\"\",\"\"\"") == "\",\"") + assert(csvFieldToString("\"aa\na\"") == "aa\na") + assert(csvFieldToString("\"\"\"\"\"\"") == "\"\"") + + assert(csvFieldToString("\"\"\"\"") == "\"") + assert(csvFieldToString("\",\"") == ",") + assert(csvFieldToString("\"\"") == "") + assert(csvFieldToString("\"\"\"a\"\"\"") == "\"a\"") + + assert(csvFieldToString("\" \"") == " ") + assert(csvFieldToString("aaa") == "aaa") + + assert(csvFieldToString("\"aa\"\"b\"") == "aa\"b") + assert(csvFieldToString("\"aa\"\"\"\"b\"") == "aa\"\"b") + assert(csvFieldToString("\"aa,a\"") == "aa,a") + assert(csvFieldToString("\"aa,\"\"b\"") == "aa,\"b") + assert(csvFieldToString("\"aa,\"\"b\"") == "aa,\"b") + } + + test("both1") { + import com.github.gekomad.ittocsv.parser.CsvFieldToString.csvFieldToString + import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + given IttoCSVFormat = IttoCSVFormat.default.withDelimiter('A') + val orig = "aAc" + val csv = StringToCsvField.stringToCsvField(orig) + val string = csvFieldToString(csv) + assert(orig == string) + } + +end CsvFieldTest diff --git a/scala3_js/src/test/scala/CsvLineTest.scala b/scala3_js/src/test/scala/CsvLineTest.scala new file mode 100644 index 0000000..6c90596 --- /dev/null +++ b/scala3_js/src/test/scala/CsvLineTest.scala @@ -0,0 +1,27 @@ +import com.github.gekomad.ittocsv.parser.IttoCSVFormat + +class CsvLineTest extends munit.FunSuite: + test("csvStringToList") { + + given IttoCSVFormat = com.github.gekomad.ittocsv.parser.IttoCSVFormat.default + import com.github.gekomad.ittocsv.util.StringUtils.* + { + val csvString = """1,"foo,bar",y,"2,e,","2ne","a""bc""z"""" + + assert(tokenizeCsvLine(csvString) == Some(List("1", "foo,bar", "y", "2,e,", "2ne", "a\"bc\"z"))) + } + + { + val csvString = "1,foo" + + assert(tokenizeCsvLine(csvString) == Some(List("1", "foo"))) + } + + { + val csvString = "1,\"foo" + + assert(tokenizeCsvLine(csvString).isEmpty) + } + } + +end CsvLineTest diff --git a/scala3_js/src/test/scala/FromCsvTest.scala b/scala3_js/src/test/scala/FromCsvTest.scala new file mode 100644 index 0000000..663b73f --- /dev/null +++ b/scala3_js/src/test/scala/FromCsvTest.scala @@ -0,0 +1,1184 @@ +import com.github.gekomad.ittocsv.core.FromCsv.{list2Product, Decoder} +import com.github.gekomad.ittocsv.parser.IttoCSVFormat + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class FromCsvTest extends munit.FunSuite: + + test("csv_string_to_type_1") { + + { + final case class Foo(a: Int, b: Double, c: String, d: Option[Boolean]) + val csv: List[String] = List("1", "3.14", "foo", "true") + val a = list2Product[Foo](csv) + assert(a == Right(Foo(1, 3.14, "foo", Some(true)))) + } + + { + final case class Foo(a: Int, b: Double, c: String, d: Boolean) + val csv: List[String] = List("1", "3.14", "foo", "False") + val a = list2Product[Foo](csv) + assert(a == Right(Foo(1, 3.14, "foo", false))) + } + + } + + test("csv_string_to_type_2") { + + final case class Foo( + a: Int, + b: Double, + c: String, + d: Option[Boolean], + e: Option[String], + f: Option[String], + e1: Option[Double], + f1: Option[Double], + e2: Option[Int], + f2: Option[Int] + ) + + val csv: List[String] = List("1", "3.14", "foo", "", "", "hi", "", "3.3", "", "100") + val a = list2Product[Foo](csv) + assert(a == Right(Foo(1, 3.14, "foo", None, None, Some("hi"), None, Some(3.3), None, Some(100)))) + } + + test("csv_string_to_type_3") { + + final case class Foo(a: Int, b: Char, c: String, d: Option[Boolean]) + + { + val csv: List[String] = List("1", "λ", "foo", "true") + assert(list2Product[Foo](csv) == Right(Foo(1, 'λ', "foo", Some(true)))) + } + + { + val csv: List[String] = List("1", "λ", "foo", "baz") + assert(list2Product[Foo](csv) == Left(List("baz value is not valid Boolean"))) + } + + } + + test("tokenizeCsvLine_to_types_ok") { + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.util.StringUtils.* + + given IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(a: Int, b: Double, c: String, d: Boolean) + + val csv: Option[List[String]] = tokenizeCsvLine("1,3.14,foo,true") + csv match + case None => assert(false) + case Some(g) => + assert(g == List("1", "3.14", "foo", "true")) + val a = list2Product[Foo](g) + assert(a == Right(Foo(1, 3.14, "foo", true))) + } + + test("tokenizeCsvLine_to_types_boolean_ko") { + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.util.StringUtils.* + given IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(a: Int, b: Double, c: String, d: Boolean) + + val csv: Option[List[String]] = tokenizeCsvLine("1,3.14,foo,bar") + csv match + case None => assert(false) + case Some(g) => + assert(g == List("1", "3.14", "foo", "bar")) + val a = list2Product[Foo](g) + assert(a == Left(List("bar value is not valid Boolean"))) + } + + test("tokenizeCsvLine_to_types_Option_Double__ko") { + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + import com.github.gekomad.ittocsv.util.StringUtils.* + given IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(a: Int, b: Double, c: String, d: Option[Double]) + + val csv: Option[List[String]] = tokenizeCsvLine("1,3.14,foo,bar") + csv match + case None => assert(false) + case Some(g) => + assert(g == List("1", "3.14", "foo", "bar")) + val a = list2Product[Foo](g) + assert(a == Left(List("bar value is not valid Double"))) + } + + test("fromCsvSha1") { + + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.SHA1 + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: String, b: SHA1) + assert( + fromCsv[Bar]("abc,1c18da5dbf74e3fc1820469cf1f54355b7eec92d") == List( + Right(Bar("abc", SHA1("1c18da5dbf74e3fc1820469cf1f54355b7eec92d"))) + ) + ) + + assert(fromCsv[Bar]("abc,hi") == List(Left(List("hi value is not valid SHA1")))) + } + + test("fromCsvShaKO") { + + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.SHA1 + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: SHA1, b: SHA1) + val x = fromCsv[Bar]("abc,hi") + assert(x == List(Left(List("abc value is not valid SHA1", "hi value is not valid SHA1")))) + + } + + test("List_Int_ok") { + given IttoCSVFormat = IttoCSVFormat.default + import com.github.gekomad.ittocsv.core.FromCsv.fromCsvL + val a = fromCsvL[Int]("1,2,3") + assert(a == List(Right(1), Right(2), Right(3))) + } + + test("List_Int_ko") { + given IttoCSVFormat = IttoCSVFormat.default + import com.github.gekomad.ittocsv.core.FromCsv.fromCsvL + val a = fromCsvL[Int]("foo,bar,3") + assert(a == List(Left("foo value is not valid Int"), Left("bar value is not valid Int"), Right(3))) + } + + test("from_csv_SHA256") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.SHA256 + + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: String, b: SHA256) + assert( + fromCsv[Bar]("abc,000020f89134d831f48541b2d8ec39397bc99fccf4cc86a3861257dbe6d819d1") == List( + Right(Bar("abc", SHA256("000020f89134d831f48541b2d8ec39397bc99fccf4cc86a3861257dbe6d819d1"))) + ) + ) + + assert(fromCsv[Bar]("abc,hi") == List(Left(List("hi value is not valid SHA256")))) + + } + + test("from_csv_IP") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.IP + + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: String, b: IP) + assert(fromCsv[Bar]("abc,10.192.168.1") == List(Right(Bar("abc", IP("10.192.168.1"))))) + + assert(fromCsv[Bar]("abc,hi") == List(Left(List("hi value is not valid IP")))) + } + + test("from_csv_IP6") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.IP6 + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: String, b: IP6) + assert(fromCsv[Bar]("abc,2001:db8:a0b:12f0::1") == List(Right(Bar("abc", IP6("2001:db8:a0b:12f0::1"))))) + + assert(fromCsv[Bar]("abc,hi") == List(Left(List("hi value is not valid IP6")))) + } + + test("from_csv_MD5") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.MD5 + + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: String, b: MD5) + assert( + fromCsv[Bar]("abc,23f8e84c1f4e7c8814634267bd456194") == List( + Right(Bar("abc", MD5("23f8e84c1f4e7c8814634267bd456194"))) + ) + ) + assert(fromCsv[Bar]("abc,hi") == List(Left(List("hi value is not valid MD5")))) + } + + test("from_csv_UUID") { + import com.github.gekomad.ittocsv.core.FromCsv.* + + import java.util.UUID + + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(a: String, b: UUID) + + assert( + fromCsv[Bar]("abc,487d414d-67a6-4c2f-b95b-d811561ccd75") == List( + Right(Bar("abc", UUID.fromString("487d414d-67a6-4c2f-b95b-d811561ccd75"))) + ) + ) + + assert( + fromCsv[Bar]("abc,xxc586e2-7cc3-4d39-a449-") == List( + Left(List("xxc586e2-7cc3-4d39-a449- value is not valid UUID")) + ) + ) + + } + + test("from_csv_URL") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(a: String, b: URL, c: URL1, d: URL2, e: URL3) + + assert( + fromCsv[Bar]( + "abc,http://abc.def.com,http://www.aaa.com,http://www.aaa.com,https://www.google.com:8080/url?" + ) == List( + Right( + Bar( + "abc", + URL("http://abc.def.com"), + URL1("http://www.aaa.com"), + URL2("http://www.aaa.com"), + URL3("https://www.google.com:8080/url?") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("abc,www.aaa.com,abc.def.com,abc.def.com,abc.def.com") == List( + Left( + List( + "www.aaa.com value is not valid URL", + "abc.def.com value is not valid URL1", + "abc.def.com value is not valid URL2", + "abc.def.com value is not valid URL3" + ) + ) + ) + ) + } + + test("from_csv_domain") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(a: String, b: FTP, c: FTP1, d: FTP2, e: Domain) + + assert( + fromCsv[Bar]("abc,ftp://aaa.com,ftp://aaa.com,ftps://aaa.com,plus.google.com") == List( + Right( + Bar("abc", FTP("ftp://aaa.com"), FTP1("ftp://aaa.com"), FTP2("ftps://aaa.com"), Domain("plus.google.com")) + ) + ) + ) + + assert( + fromCsv[Bar]("abc,www.aaa.com,abc.def.com,abc.def.com,abc") == List( + Left( + List( + "www.aaa.com value is not valid FTP", + "abc.def.com value is not valid FTP1", + "abc.def.com value is not valid FTP2", + "abc value is not valid Domain" + ) + ) + ) + ) + } + + test("hex") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(a: HEX, b: HEX1, c: HEX2, d: HEX3) + + assert( + fromCsv[Bar]("F0F0F0,#F0F0F0,0xF0F0F0,0xF0F0F0") == + List(Right(Bar(HEX("F0F0F0"), HEX1("#F0F0F0"), HEX2("0xF0F0F0"), HEX3("0xF0F0F0")))) + ) + + assert( + fromCsv[Bar]("aa,bb,cc,dd") == List( + Left( + List( + "aa value is not valid HEX", + "bb value is not valid HEX1", + "cc value is not valid HEX2", + "dd value is not valid HEX3" + ) + ) + ) + ) + } + + test("germanStreet") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: GermanStreet) + + assert(fromCsv[Bar]("Mühlenstr. 33") == List(Right(Bar(GermanStreet("Mühlenstr. 33"))))) + + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid GermanStreet")))) + } + + test("singleChar") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.* + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(b: SingleChar) + assert(fromCsv[Bar]("a") == List(Right(Bar(SingleChar("a"))))) + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid SingleChar")))) + } + + test("azString") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.* + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(b: AZString) + assert(fromCsv[Bar]("aA") == List(Right(Bar(AZString("aA"))))) + assert(fromCsv[Bar]("1") == List(Left(List("1 value is not valid AZString")))) + } + + test("stringAndNumber") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.* + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(b: StringAndNumber) + assert(fromCsv[Bar]("aA1") == List(Right(Bar(StringAndNumber("aA1"))))) + assert(fromCsv[Bar]("$") == List(Left(List("$ value is not valid StringAndNumber")))) + } + + test("asciiString") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.* + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(b: AsciiString) + assert(fromCsv[Bar]("aA1%") == List(Right(Bar(AsciiString("aA1%"))))) + assert(fromCsv[Bar]("テ") == List(Left(List("テ value is not valid AsciiString")))) + } + + test("singleNumber") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.* + given IttoCSVFormat = IttoCSVFormat.default + + final case class Bar(b: SingleNumber) + assert(fromCsv[Bar]("1") == List(Right(Bar(SingleNumber("1"))))) + assert(fromCsv[Bar]("11") == List(Left(List("11 value is not valid SingleNumber")))) + } + + test("macAddress") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: MACAddress) + + assert(fromCsv[Bar]("fE:dC:bA:98:76:54") == List(Right(Bar(MACAddress("fE:dC:bA:98:76:54"))))) + + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid MACAddress")))) + } + + test("phines") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(a: USphoneNumber, b: ItalianMobilePhone, c: ItalianPhone) + + assert( + fromCsv[Bar]("555-555-5555,+393471234561,02 645566") == List( + Right(Bar(USphoneNumber("555-555-5555"), ItalianMobilePhone("+393471234561"), ItalianPhone("02 645566"))) + ) + ) + + assert( + fromCsv[Bar]("aa,bb,cc") == List( + Left( + List( + "aa value is not valid USphoneNumber", + "bb value is not valid ItalianMobilePhone", + "cc value is not valid ItalianPhone" + ) + ) + ) + ) + + } + + test("time") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar( + a1: MDY, + a2: MDY2, + a3: MDY3, + a4: MDY4, + a11: DMY, + a21: DMY2, + a31: DMY3, + a41: DMY4, + b: Time, + c: Time24 + ) + + assert( + fromCsv[Bar]( + "1/12/1902,1-12-1902,01/01/1900,01-12-1902,1/12/1902,1-12-1902,01/12/1902,01-12-1902,8am,23:50:00" + ) == List( + Right( + Bar( + MDY("1/12/1902"), + MDY2("1-12-1902"), + MDY3("01/01/1900"), + MDY4("01-12-1902"), + DMY("1/12/1902"), + DMY2("1-12-1902"), + DMY3("01/12/1902"), + DMY4("01-12-1902"), + Time("8am"), + Time24("23:50:00") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("1,2,3,4,5,6,7,8,9,10") == List( + Left( + List( + "1 value is not valid MDY", + "2 value is not valid MDY2", + "3 value is not valid MDY3", + "4 value is not valid MDY4", + "5 value is not valid DMY", + "6 value is not valid DMY2", + "7 value is not valid DMY3", + "8 value is not valid DMY4", + "9 value is not valid Time", + "10 value is not valid Time24" + ) + ) + ) + ) + + } + + test("coordinates") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default.withQuote('|') + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(a: Coordinate, b: Coordinate1, c: Coordinate2) + + assert( + fromCsv[Bar]("""N90.00.00 E180.00.00,45°23'36.0" N 10°33'48.0" E,12:12:12.223546"N""") == + List( + Right( + Bar( + Coordinate("N90.00.00 E180.00.00"), + Coordinate1("""45°23'36.0" N 10°33'48.0" E"""), + Coordinate2("""12:12:12.223546"N""") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("a,b,c") == List( + Left( + List( + "a value is not valid Coordinate", + "b value is not valid Coordinate1", + "c value is not valid Coordinate2" + ) + ) + ) + ) + } + + test("zipCode") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(a: USZipCode, b: ItalianZipCode) + + assert( + fromCsv[Bar]("43802,23887") == + List(Right(Bar(USZipCode("43802"), ItalianZipCode("23887")))) + ) + + assert( + fromCsv[Bar]("a,b") == List(Left(List("a value is not valid USZipCode", "b value is not valid ItalianZipCode"))) + ) + } + + test("numbers") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(a: Number1, b: Signed, c: Unsigned32, d: Percentage, e: Scientific) + + assert( + fromCsv[Bar]("99.99,-10,4294967295,10%,-2.384E-03") == + List( + Right( + Bar(Number1("99.99"), Signed("-10"), Unsigned32("4294967295"), Percentage("10%"), Scientific("-2.384E-03")) + ) + ) + ) + + assert( + fromCsv[Bar]("a,b,c,d,e") == List( + Left( + List( + "a value is not valid Number1", + "b value is not valid Signed", + "c value is not valid Unsigned32", + "d value is not valid Percentage", + "e value is not valid Scientific" + ) + ) + ) + ) + } + + test("codes") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar( + a: ItalianFiscalCode, + b: ItalianVAT, + c: ItalianIban, + d: USstates, + e: USstates1, + f: USstreets, + g: USstreetNumber + ) + + assert( + fromCsv[Bar]( + """BDAPPP14A01A001R,13297040362,IT28 W800 0000 2921 0064 5211 151,CA,Florida,"123 Park Ave Apt 123 New York City, NY 10002",P.O. Box 432""" + ) == + List( + Right( + Bar( + ItalianFiscalCode("BDAPPP14A01A001R"), + ItalianVAT("13297040362"), + ItalianIban("IT28 W800 0000 2921 0064 5211 151"), + USstates("CA"), + USstates1("Florida"), + USstreets("123 Park Ave Apt 123 New York City, NY 10002"), + USstreetNumber("P.O. Box 432") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("a,b,c,d,e,f,g") == List( + Left( + List( + "a value is not valid ItalianFiscalCode", + "b value is not valid ItalianVAT", + "c value is not valid ItalianIban", + "d value is not valid USstates", + "e value is not valid USstates1", + "f value is not valid USstreets", + "g value is not valid USstreetNumber" + ) + ) + ) + ) + } + + test("bitcoinAdd") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: BitcoinAdd) + + assert( + fromCsv[Bar]("3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v") == List( + Right(Bar(BitcoinAdd("3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v"))) + ) + ) + + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid BitcoinAdd")))) + } + + test("celsius") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: Celsius) + + assert(fromCsv[Bar]("+2 °C") == List(Right(Bar(Celsius("+2 °C"))))) + + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid Celsius")))) + } + + test("fahrenheit") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: Fahrenheit) + + assert(fromCsv[Bar]("+2 °F") == List(Right(Bar(Fahrenheit("+2 °F"))))) + + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid Fahrenheit")))) + } + + test("apacheError") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: ApacheError) + + assert( + fromCsv[Bar]("[Fri Dec 16 02:25:55 2005] [error] [client 1.2.3.4] Client sent malformed Host header") == List( + Right(Bar(ApacheError("[Fri Dec 16 02:25:55 2005] [error] [client 1.2.3.4] Client sent malformed Host header"))) + ) + ) + + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid ApacheError")))) + } + + test("concurrency") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(a: UsdCurrency, b: EurCurrency, c: YenCurrency) + + assert( + fromCsv[Bar]("$1.00,\"133,89 EUR\",¥1.00") == List( + Right(Bar(UsdCurrency("$1.00"), EurCurrency("133,89 EUR"), YenCurrency("¥1.00"))) + ) + ) + + assert( + fromCsv[Bar]("a,b,c") == List( + Left( + List( + "a value is not valid UsdCurrency", + "b value is not valid EurCurrency", + "c value is not valid YenCurrency" + ) + ) + ) + ) + } + + test("notAscii") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: NotASCII) + + assert(fromCsv[Bar]("テスト。") == List(Right(Bar(NotASCII("テスト。"))))) + + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid NotASCII")))) + } + + test("crontab") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: Cron) + + assert(fromCsv[Bar]("5 4 * * *") == List(Right(Bar(Cron("5 4 * * *"))))) + + assert(fromCsv[Bar]("aa") == List(Left(List("aa value is not valid Cron")))) + } + + test("fromCsvSocial") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.* + + final case class Bar(b: Youtube, c: Facebook, d: Twitter) + + assert( + fromCsv[Bar]( + "https://www.youtube.com/watch?v=9bZkp7q19f0,https://www.facebook.com/pages/,https://twitter.com/rtpharry/" + ) == List( + Right( + Bar( + Youtube("https://www.youtube.com/watch?v=9bZkp7q19f0"), + Facebook("https://www.facebook.com/pages/"), + Twitter("https://twitter.com/rtpharry/") + ) + ) + ) + ) + + assert( + fromCsv[Bar]("aa,bb,cc") == List( + Left( + List( + "aa value is not valid Youtube", + "bb value is not valid Facebook", + "cc value is not valid Twitter" + ) + ) + ) + ) + } + + test("decodeCustomType") { + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + import scala.util.Try + given IttoCSVFormat = IttoCSVFormat.default + + final case class MyType(a: Int) + final case class Foo(a: MyType, b: Int) + + import com.github.gekomad.ittocsv.core.FromCsv.* + + given Decoder[String, MyType] = a => + (if (a.startsWith("[") && a.endsWith("]")) + Try(a.substring(1, a.length - 1).toInt) + .map(f => Some(MyType(f))) + .getOrElse(None) + else None).toRight(List(s"$a value is not valid MyType")) + + assert(fromCsv[Foo]("[42],99") == List(Right(Foo(MyType(42), 99)))) + assert(fromCsv[Foo]("[x],99") == List(Left(List("[x] value is not valid MyType")))) + assert(fromCsv[Foo]("42,99") == List(Left(List("42 value is not valid MyType")))) + + } + + test("from_csv_email_with_custom_parser") { + + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.core.Types.implicits.Email + given IttoCSVFormat = IttoCSVFormat.default + import com.github.gekomad.ittocsv.core.Types.RegexValidator + + final case class Bar(a: String, b: Email) + + assert(fromCsv[Bar]("abc,a@%.d") == List(Left(List("a@%.d value is not valid Email")))) + + { + given Decoder[String, Email] = (a: String) => RegexValidator[Email](""".+@.+\..+""").validate(a) + assert(fromCsv[Bar]("abc,a@%.d") == List(Right(Bar("abc", Email("a@%.d"))))) + } + } + + test("from_csv_url_with_custom_parser") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.URL + + final case class Bar(a: String, b: URL) + assert(fromCsv[Bar]("abc,http://abc.def.com") == List(Right(Bar("abc", URL("http://abc.def.com"))))) + assert(fromCsv[Bar]("abc,https://abc.def.com") == List(Right(Bar("abc", URL("https://abc.def.com"))))) + assert(fromCsv[Bar]("abc,www.aaa.com") == List(Left(List("www.aaa.com value is not valid URL")))) + + { + import com.github.gekomad.ittocsv.core.Types.RegexValidator + + given Decoder[String, URL] = (a: String) => + RegexValidator[URL]("""[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?""") + .validate(a) + + assert(fromCsv[Bar]("abc,www.aaa.com") == List(Right(Bar("abc", URL("www.aaa.com"))))) + } + } + + test("from_csv_email") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.Email + + final case class Bar(a: String, b: Email) + + assert(fromCsv[Bar]("abc,aaa@aai.sss") == List(Right(Bar("abc", Email("aaa@aai.sss"))))) + assert(fromCsv[Bar]("abc,$aaa@aai.sss") == List(Right(Bar("abc", Email("$aaa@aai.sss"))))) + assert(fromCsv[Bar]("abc,a@i.d") == List(Right(Bar("abc", Email("a@i.d"))))) + assert(fromCsv[Bar]("abc,a@%.d") == List(Left(List("a@%.d value is not valid Email")))) + assert(fromCsv[Bar]("abc,a @i.d") == List(Left(List("a @i.d value is not valid Email")))) + assert(fromCsv[Bar]("abc,hi") == List(Left(List("hi value is not valid Email")))) + assert(fromCsv[Bar]("abc,hi@") == List(Left(List("hi@ value is not valid Email")))) + assert(fromCsv[Bar]("abc,@") == List(Left(List("@ value is not valid Email")))) + assert(fromCsv[Bar]("abc,@.com") == List(Left(List("@.com value is not valid Email")))) + assert(fromCsv[Bar]("abc,hi@g.") == List(Left(List("hi@g. value is not valid Email")))) + assert(fromCsv[Bar]("abc,hi@.d") == List(Left(List("hi@.d value is not valid Email")))) + assert(fromCsv[Bar]("abc,") == List(Left(List(" value is not valid Email")))) + assert(fromCsv[Bar]("abc, ") == List(Left(List(" value is not valid Email")))) + } + + test("from_csv_email_simple") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.EmailSimple + + final case class Bar(a: String, b: EmailSimple) + + assert(fromCsv[Bar]("abc,aaa@aai.sss") == List(Right(Bar("abc", EmailSimple("aaa@aai.sss"))))) + assert(fromCsv[Bar]("abc,a@i.d") == List(Right(Bar("abc", EmailSimple("a@i.d"))))) + assert(fromCsv[Bar]("abc,a@%.d") == List(Right(Bar("abc", EmailSimple("a@%.d"))))) + + } + + test("from_csv_email1") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + + import com.github.gekomad.ittocsv.core.Types.implicits.Email1 + + final case class Bar(a: String, b: Email1) + + assert(fromCsv[Bar]("abc,aaa@ai.sss") == List(Right(Bar("abc", Email1("aaa@ai.sss"))))) + assert(fromCsv[Bar]("abc,a@i.da") == List(Right(Bar("abc", Email1("a@i.da"))))) + assert(fromCsv[Bar]("abc,a@%.da") == List(Left(List("a@%.da value is not valid Email1")))) + + } + + test("from_csv_to_type") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + final case class Bar(a: String, b: Int) + assert(fromCsv[Bar]("abc,42") == List(Right(Bar("abc", 42)))) + assert(fromCsv[Bar]("abc,hi") == List(Left(List("hi value is not valid Int")))) + } + + test("from_csv_to_list_of_type") { + import com.github.gekomad.ittocsv.core.FromCsv.* + given IttoCSVFormat = IttoCSVFormat.default + final case class Bar(a: String, b: Int) + assert(fromCsv[Bar]("abc,42\r\nfoo,24") == List(Right(Bar("abc", 42)), Right(Bar("foo", 24)))) + } + + test("tokenizeCsvLine_to_types_complete") { + + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + final case class Foo(a: Int, b: Double, c: Option[String], d: Boolean) + + given IttoCSVFormat = IttoCSVFormat.default + + val o = fromCsv[Foo]("1,3.14,foo,true") + + assert(o == List(Right(Foo(1, 3.14, Some("foo"), true)))) + + } + + test("list_of_csv_string_to_list_of_type") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Foo(a: Int, b: Double, c: String, d: Boolean) + given IttoCSVFormat = IttoCSVFormat.default + val o = fromCsv[Foo](List("1,3.14,foo,true", "2,3.14,bar,false")) // List[Either[List[ParseFailure], Foo]] + assert(o == List(Right(Foo(1, 3.14, "foo", true)), Right(Foo(2, 3.14, "bar", false)))) + } + + test("list_of_csv_string_to_list_of_type_with_empty_string_and_ignoreEmptyLines_false") { + + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Foo(a: Int) + given IttoCSVFormat = IttoCSVFormat.default + val o = fromCsv[Foo](List("1", "")) // List[Either[List[ParseFailure], Foo]] + assert(o == List(Right(Foo(1)), Left(List(" value is not valid Int")))) + } + + test("list_of_csv_string_to_list_of_type_with_empty_string_and_ignoreEmptyLines_true") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Foo(a: Int) + given IttoCSVFormat = IttoCSVFormat.default.withIgnoreEmptyLines(true) + val o = fromCsv[Foo](List("1", "", "2")) + assert(o == List(Right(Foo(1)), Right(Foo(2)))) + } + + test("decode_List_char") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + final case class Foo(v: String, a: List[Char]) + + given IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsv[Foo]("abc,\"a,b,c\"") == List(Right(Foo("abc", List('a', 'b', 'c'))))) + assert(fromCsv[Foo]("abc,\"a,λ,c\"") == List(Right(Foo("abc", List('a', 'λ', 'c'))))) + assert(fromCsv[Foo]("abc,\"1,xy,3\"") == List(Left(List("xy value is not valid Char")))) + } + + test("decodeOption_List_int") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + final case class Foo(v: String, a: Option[List[Int]]) + + given IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsv[Foo]("abc,\"1,2,3\"") == List(Right(Foo("abc", Some(List(1, 2, 3)))))) + assert(fromCsv[Foo]("abc,\"1,xy,3\"") == List(Left(List("xy value is not valid Int")))) + } + + test("decode_List_bool") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + final case class Foo(a: List[Boolean]) + + given IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsv[Foo]("\"true,false\"") == List(Right(Foo(List(true, false))))) + assert(fromCsv[Foo]("\"abc,false\"") == List(Left(List("abc value is not valid Boolean")))) + + } + + test("decode_List_int") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + final case class Foo(v: String, a: List[Int]) + + given IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsv[Foo]("abc,\"1,2,3\"") == List(Right(Foo("abc", List(1, 2, 3))))) + assert(fromCsv[Foo]("abc,\"1,xy,y\"") == List(Left(List("xy value is not valid Int", "y value is not valid Int")))) + + } + + test("decode_List_double") { + import com.github.gekomad.ittocsv.core.FromCsv.fromCsvL + + given IttoCSVFormat = IttoCSVFormat.default + + assert(fromCsvL[Double]("1.1,2.1,3.1") == List(Right(1.1), Right(2.1), Right(3.1))) + assert(fromCsvL[Double]("1.1,abc,3.1") == List(Right(1.1), Left("abc value is not valid Double"), Right(3.1))) + assert( + fromCsvL[Double]("1.1,abc,foo") == List( + Right(1.1), + Left("abc value is not valid Double"), + Left("foo value is not valid Double") + ) + ) + assert(fromCsvL[Double]("") == List(Left(" value is not valid Double"))) + + } + + test("decode_LocalDateTime") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + import java.time.LocalDateTime + + final case class Foo(a: Int, b: LocalDateTime) + + given IttoCSVFormat = IttoCSVFormat.default + + val o = fromCsv[Foo]("1,2000-12-31T11:21:19") + assert( + o == List( + Right( + Foo(1, LocalDateTime.parse("2000-12-31T11:21:19", java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + ) + ) + ) + + } + + test("decode_Option_LocalDate") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + import java.time.LocalDate + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE + + final case class Foo(a: Int, b: Option[LocalDate]) + + given IttoCSVFormat = IttoCSVFormat.default + + val o = fromCsv[Foo]("1,2000-12-31") + assert(o == List(Right(Foo(1, Some(LocalDate.parse("2000-12-31", ISO_LOCAL_DATE)))))) + + } + + test("decode_LocalDateTime2") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + import java.time.LocalDateTime + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME + + final case class Foo(a: Int, b: Option[LocalDateTime]) + + given IttoCSVFormat = IttoCSVFormat.default + + { + val o = fromCsv[Foo]("1,2000-12-31T11:21:19") + assert(o == List(Right(Foo(1, Some(LocalDateTime.parse("2000-12-31T11:21:19", ISO_LOCAL_DATE_TIME)))))) + } + + } + test("decode_date_and_time") { + + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + import java.time.format.DateTimeFormatter + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME + import java.time.{Instant, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, ZonedDateTime} + + final case class Foo( + i: Instant, + t: LocalTime, + d: LocalDate, + dt: OffsetDateTime, + z: ZonedDateTime, + ldt: LocalDateTime + ) + + given IttoCSVFormat = IttoCSVFormat.default + + { + val o = fromCsv[Foo]( + "2019-11-30T18:35:24.00Z,11:15:30,2019-12-27,2012-12-03T10:15:30+01:00,2019-04-01T17:24:11.252+05:30[Asia/Calcutta],2000-12-31T11:21:19" + ) + assert( + o == List( + Right( + Foo( + Instant.parse("2019-11-30T18:35:24.00Z"), + LocalTime.parse("11:15:30", DateTimeFormatter.ISO_LOCAL_TIME), + LocalDate.parse("2019-12-27"), + OffsetDateTime.parse("2012-12-03T10:15:30+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME), + ZonedDateTime.parse("2019-04-01T17:24:11.252+05:30[Asia/Calcutta]"), + LocalDateTime.parse("2000-12-31T11:21:19", ISO_LOCAL_DATE_TIME) + ) + ) + ) + ) + } + } + + test("CSVtoListOftTypeWithCustomLocalDateTime") { + import com.github.gekomad.ittocsv.core.FromCsv.* + + given IttoCSVFormat = IttoCSVFormat.default + + case class Foo(a: Int, b: java.time.LocalDateTime) + + given Decoder[String, LocalDateTime] = { s => + scala.util + .Try { + Right(LocalDateTime.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))) + } + .getOrElse(Left(List(s"Not a LocalDataTime $s"))) + } + + val l = fromCsv[Foo]("1,2000-12-31 11:21:19.0") + assert( + l == List( + Right( + Foo(1, LocalDateTime.parse("2000-12-31 11:21:19.0", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))) + ) + ) + ) + } + + test("decode_custom_option_LocalDateTime") { + import com.github.gekomad.ittocsv.core.FromCsv.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + final case class Foo(a: Int, b: Option[java.time.LocalDateTime]) + + import java.time.LocalDateTime + import java.time.format.DateTimeFormatter + import scala.util.Try + + given IttoCSVFormat = IttoCSVFormat.default + + val pattern = "yyyy-MM-dd HH:mm:ss.0" + given Decoder[String, Option[LocalDateTime]] = { + case "" => Right(None) + case s => + Try { + val x = LocalDateTime.parse(s, DateTimeFormatter.ofPattern(pattern)) + Right(Some(x)) + }.getOrElse(Left(List(s"Not a LocalDataTime $s"))) + } + +// given (String => Either[String, Option[LocalDateTime]]) = { +// case "" => Right(None) +// case s => +// Try { +// val x = LocalDateTime.parse(s, DateTimeFormatter.ofPattern(pattern)) +// Right(Some(x)) +// }.getOrElse(Left(s"Not a LocalDataTime $s")) +// } + + { + val l = LocalDateTime.parse("2000-11-11 11:11:11.0", DateTimeFormatter.ofPattern(pattern)) + val o = fromCsv[Foo]("1,2000-11-11 11:11:11.0") + assert(o == List(Right(Foo(1, Some(l))))) + } + + { + val o = fromCsv[Foo]("1,daigoro-xx-11 11:11:11.0") + assert(o == List(Left(List("Not a LocalDataTime daigoro-xx-11 11:11:11.0")))) + } + } + +end FromCsvTest diff --git a/scala3_js/src/test/scala/GetHeader.scala b/scala3_js/src/test/scala/GetHeader.scala new file mode 100644 index 0000000..627be42 --- /dev/null +++ b/scala3_js/src/test/scala/GetHeader.scala @@ -0,0 +1,20 @@ +import com.github.gekomad.ittocsv.parser.IttoCSVFormat + +class GetHeader extends munit.FunSuite: + + test("GetHeader") { + case class Foo(i: Int, d: Double, s: Option[String], b: Boolean) + import com.github.gekomad.ittocsv.core.Header.* + + { + given IttoCSVFormat = IttoCSVFormat.default + assert(csvHeader[Foo] == "i,d,s,b") + } + + { + given IttoCSVFormat = IttoCSVFormat.default.withDelimiter('|').withForceQuote(true) + assert(csvHeader[Foo] == """"i"|"d"|"s"|"b"""") + } + + } +end GetHeader diff --git a/scala3_js/src/test/scala/ListToCSV.scala b/scala3_js/src/test/scala/ListToCSV.scala new file mode 100644 index 0000000..85e4329 --- /dev/null +++ b/scala3_js/src/test/scala/ListToCSV.scala @@ -0,0 +1,18 @@ +import com.github.gekomad.ittocsv.parser.IttoCSVFormat + +class ListToCSV extends munit.FunSuite: + + test("listToCsv") { + + import com.github.gekomad.ittocsv.core.ToCsv.* + given IttoCSVFormat = IttoCSVFormat.default + import com.github.gekomad.ittocsv.core.Header.* + case class Bar(a: String, b: Int) + val a = List(Bar("Bar", 42), Bar("Foo", 24)) + + val aa = toCsvL(a) + + assert(aa == "a,b\r\nBar,42\r\nFoo,24") + } + +end ListToCSV diff --git a/scala3_js/src/test/scala/StringUtilTest.scala b/scala3_js/src/test/scala/StringUtilTest.scala new file mode 100644 index 0000000..9717cef --- /dev/null +++ b/scala3_js/src/test/scala/StringUtilTest.scala @@ -0,0 +1,49 @@ +class StringUtilTest extends munit.FunSuite: + + test("split1") { + import com.github.gekomad.ittocsv.util.StringUtils.* + { + val string = "a;b;c" + val separators = List(1, 3) + val res = split(string, separators) + assert(res == List("a", "b", "c")) + } + + { + val string = "a;;c" + val separators = List(1, 2) + val res = split(string, separators) + assert(res == List("a", "", "c")) + } + + { + val string = "a;b;" + val separators = List(1, 3) + val res = split(string, separators) + assert(res == List("a", "b", "")) + } + + { + val string = ";;" + val separators = List(0, 1) + val res = split(string, separators) + assert(res == List("", "", "")) + } + + { + val string = "" + val separators = List() + val res = split(string, separators) + assert(res == List("")) + } + + { + val string = """1,"foo,bar",y,"2,e,","2ne","a""bc""z"""" + val separators = List(1, 11, 13, 20, 26) + val res = split(string, separators) + assert(res == List("1", "\"foo,bar\"", "y", "\"2,e,\"", "\"2ne\"", "\"a\"\"bc\"\"z\"")) + } + + } + +end StringUtilTest diff --git a/scala3_js/src/test/scala/ToCsvTest.scala b/scala3_js/src/test/scala/ToCsvTest.scala new file mode 100644 index 0000000..d84e055 --- /dev/null +++ b/scala3_js/src/test/scala/ToCsvTest.scala @@ -0,0 +1,563 @@ +import com.github.gekomad.ittocsv.core.ToCsv +import com.github.gekomad.ittocsv.core.ToCsv.{given, *} +import com.github.gekomad.ittocsv.core.Types.implicits.* +import com.github.gekomad.ittocsv.parser.{IttoCSVFormat, StringToCsvField} + +import java.time.format.DateTimeFormatter +import java.time.* +import java.util.UUID +import scala.deriving.Mirror + +class ToCsvTest extends munit.FunSuite: + given IttoCSVFormat = IttoCSVFormat.default + test("toCsvSHA1") { + + final case class Bar(i: Int, a: SHA1) + val x = Bar(1, SHA1("1c18da5dbf74e3fc1820469cf1f54355b7eec92d")) + + assert(toCsv(x) == "1,1c18da5dbf74e3fc1820469cf1f54355b7eec92d") + } + + test("email") { + + final case class Bar(i: Int, email: Email) + + assert(toCsv(Bar(1, Email("daigoro@itto.com"))) == "1,daigoro@itto.com") + } + + test("email1") { + + final case class Bar(i: Int, email: Email1) + + assert(toCsv(Bar(1, Email1("daigoro@itto.com"))) == "1,daigoro@itto.com") + } + + test("emailSimple") { + + final case class Bar(i: Int, email: EmailSimple) + + assert(toCsv(Bar(1, EmailSimple("daigoro@itto.com"))) == "1,daigoro@itto.com") + } + + test("url") { + + final case class Bar(i: Int, url: URL, url1: URL1, url2: URL2, url3: URL3) + + assert( + toCsv( + Bar( + 1, + URL("http://aaa.ccc.com"), + URL1("http://www.aaa.com"), + URL2("http://www.aaa.com"), + URL3("https://www.google.com:8080/url?") + ) + ) == + "1,http://aaa.ccc.com,http://www.aaa.com,http://www.aaa.com,https://www.google.com:8080/url?" + ) + } + + test("ftpDomain") { + + final case class Bar(i: Int, b: FTP, c: FTP1, d: FTP2, e: Domain) + + assert( + toCsv(Bar(1, FTP("ftp://aaa.com"), FTP1("ftp://aaa.com"), FTP2("ftps://aaa.com"), Domain("plus.google.com"))) == + "1,ftp://aaa.com,ftp://aaa.com,ftps://aaa.com,plus.google.com" + ) + } + + test("social1") { + + final case class Bar(b: Youtube, c: Facebook, d: Twitter) + + assert( + toCsv( + Bar( + Youtube("https://www.youtube.com/watch?v=9bZkp7q19f0"), + Facebook("http://www.facebook.com/thesimpsons"), + Twitter("http://twitter.com/rtpharry/") + ) + ) == + "https://www.youtube.com/watch?v=9bZkp7q19f0,http://www.facebook.com/thesimpsons,http://twitter.com/rtpharry/" + ) + } + + test("macAddress") { + + final case class Bar(i: Int, a: MACAddress) + + assert(toCsv(Bar(1, MACAddress("fE:dC:bA:98:76:54"))) == "1,fE:dC:bA:98:76:54") + } + + test("phones") { + + final case class Bar(a: USphoneNumber, b: ItalianMobilePhone, c: ItalianPhone) + + assert( + toCsv( + Bar(USphoneNumber("555-555-5555"), ItalianMobilePhone("+393471234561"), ItalianPhone("02 645566")) + ) == "555-555-5555,+393471234561,02 645566" + ) + } + + test("bitcoinAdd") { + + final case class Bar(i: Int, a: BitcoinAdd) + + assert(toCsv(Bar(1, BitcoinAdd("3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v"))) == "1,3Nxwenay9Z8Lc9JBiywExpnEFiLp6Afp8v") + } + + test("codes") { + + final case class Bar( + a: ItalianFiscalCode, + b: ItalianVAT, + c: ItalianIban, + d: USstates, + e: USstates1, + f: USstreets, + g: USstreetNumber + ) + + assert( + toCsv( + Bar( + ItalianFiscalCode("BDAPPP14A01A001R"), + ItalianVAT("13297040362"), + ItalianIban("IT28 W800 0000 2921 0064 5211 151"), + USstates("CA"), + USstates1("Florida"), + USstreets("123 Park Ave Apt 123 New York City, NY 10002"), + USstreetNumber("P.O. Box 432") + ) + ) == """BDAPPP14A01A001R,13297040362,IT28 W800 0000 2921 0064 5211 151,CA,Florida,"123 Park Ave Apt 123 New York City, NY 10002",P.O. Box 432""" + ) + } + + test("coordinates") { + final case class Bar(a: Coordinate, b: Coordinate1, c: Coordinate2) + given IttoCSVFormat = IttoCSVFormat.default.withQuote('|') + + assert( + toCsv( + Bar( + Coordinate("N90.00.00 E180.00.00"), + Coordinate1("""45°23'36.0" N 10°33'48.0" E"""), + Coordinate2("""12:12:12.223546"N""") + ) + ) == """N90.00.00 E180.00.00,45°23'36.0" N 10°33'48.0" E,12:12:12.223546"N""" + ) + } + + test("numbers") { + + final case class Bar(a: Number1, b: Signed, c: Unsigned32, d: Percentage, e: Scientific) + + assert( + toCsv( + Bar(Number1("99.99"), Signed("-10"), Unsigned32("4294967295"), Percentage("10%"), Scientific("-2.384E-03")) + ) == "99.99,-10,4294967295,10%,-2.384E-03" + ) + } + + test("zipCode") { + + final case class Bar(a: USZipCode, b: ItalianZipCode) + + assert(toCsv(Bar(USZipCode("43802"), ItalianZipCode("23887"))) == "43802,23887") + } + + test("germanStreet") { + + final case class Bar(a: GermanStreet) + + assert(toCsv(Bar(GermanStreet("Mühlenstr. 33"))) == "Mühlenstr. 33") + } + + test("singleChar") { + + final case class Bar(a: SingleChar) + assert(toCsv(Bar(SingleChar("a"))) == "a") + } + + test("azString") { + + final case class Bar(a: AZString) + assert(toCsv(Bar(AZString("aA"))) == "aA") + } + + test("celsius1") { + + final case class Bar(a: Celsius) + assert(toCsv(Bar(Celsius("+2 °C"))) == "+2 °C") + } + + test("fahrenheit") { + + final case class Bar(a: Fahrenheit) + assert(toCsv(Bar(Fahrenheit("+2 °F"))) == "+2 °F") + } + + test("stringAndNumber") { + + final case class Bar(a: StringAndNumber) + assert(toCsv(Bar(StringAndNumber("a1"))) == "a1") + } + + test("asciiString") { + + final case class Bar(a: AsciiString) + assert(toCsv(Bar(AsciiString("a$"))) == "a$") + } + + test("singleNumber") { + + final case class Bar(a: SingleNumber) + assert(toCsv(Bar(SingleNumber("3"))) == "3") + } + + test("concurrency") { + + final case class Bar(a: UsdCurrency, b: EurCurrency, c: YenCurrency) + + assert( + toCsv(Bar(UsdCurrency("$1.00"), EurCurrency("133,89 EUR"), YenCurrency("¥1.00"))) == "$1.00,\"133,89 EUR\",¥1.00" + ) + } + + test("crontab") { + + final case class Bar(i: Int, a: Cron) + + assert(toCsv(Bar(1, Cron("5 4 * * *"))) == "1,5 4 * * *") + } + + test("apacheError") { + + final case class Bar(b: ApacheError) + + assert( + toCsv( + Bar(ApacheError("[Fri Dec 16 02:25:55 2005] [error] [client 1.2.3.4] Client sent malformed Host header")) + ) == "[Fri Dec 16 02:25:55 2005] [error] [client 1.2.3.4] Client sent malformed Host header" + ) + } + + test("noAscii") { + + final case class Bar(b: NotASCII) + + assert(toCsv(Bar(NotASCII("テスト。"))) == "テスト。") + } + + test("time") { + + final case class Bar( + a1: MDY, + a2: MDY2, + a3: MDY3, + a4: MDY4, + a11: DMY, + a21: DMY2, + a31: DMY3, + a41: DMY4, + b: Time, + c: Time24 + ) + + assert( + toCsv( + Bar( + MDY("1/12/1902"), + MDY2("1-12-1902"), + MDY3("01/01/1900"), + MDY4("01-12-1902"), + DMY("1/12/1902"), + DMY2("1-12-1902"), + DMY3("01/12/1902"), + DMY4("01-12-1902"), + Time("8am"), + Time24("23:50:00") + ) + ) == + "1/12/1902,1-12-1902,01/01/1900,01-12-1902,1/12/1902,1-12-1902,01/12/1902,01-12-1902,8am,23:50:00" + ) + } + + test("hex") { + + final case class Bar(a: HEX, b: HEX1, c: HEX2, d: HEX3) + + assert( + toCsv( + Bar(HEX("F0F0F0"), HEX1("#F0F0F0"), HEX2("0xF0F0F0"), HEX3("0xF0F0F0")) + ) == "F0F0F0,#F0F0F0,0xF0F0F0,0xF0F0F0" + ) + } + + test("ip") { + + final case class Bar(i: Int, a: IP) + + assert(toCsv(Bar(1, IP("10.168.1.108"))) == "1,10.168.1.108") + } + + test("ip6") { + + final case class Bar(i: Int, a: IP6) + + assert(toCsv(Bar(1, IP6("2001:db8:a0b:12f0::1"))) == "1,2001:db8:a0b:12f0::1") + } + + test("sha256") { + + final case class Bar(i: Int, a: SHA256) + + assert( + toCsv( + Bar(1, SHA256("000020f89134d831f48541b2d8ec39397bc99fccf4cc86a3861257dbe6d819d1")) + ) == "1,000020f89134d831f48541b2d8ec39397bc99fccf4cc86a3861257dbe6d819d1" + ) + + } + + test("uuid") { + final case class Bar(i: Int, a: UUID) + + assert( + toCsv(Bar(1, UUID.fromString("1CC3CCBB-C749-3078-E050-1AACBE064651"))) == "1,1cc3ccbb-c749-3078-e050-1aacbe064651" + ) + + } + + test("type_to_csv_string") { + { + given IttoCSVFormat = IttoCSVFormat.default.withDelimiter('.') + final case class Bar(i: Int, salary: Double) + + assert(toCsv(Bar(1, 33003.3)) == "1.\"33003.3\"") + + } + + { // use default formatter + + given IttoCSVFormat = IttoCSVFormat.default.withPrintHeader(false) + + final case class Bar(name: String, date: java.util.Date, salary: Double) + given FieldEncoder[java.util.Date] = customFieldEncoder[java.util.Date](_.toString) + + val d = new java.util.Date(0).toString + + assert(toCsv(Bar("Bo,b", new java.util.Date(0), 33003.3)) == s""""Bo,b",$d,33003.3""") + + assert( + toCsv(List(Bar("Bob", new java.util.Date(0), 1111.3), Bar("Jim", new java.util.Date(0), 2222.2))) == + s"Bob,$d,1111.3,Jim,$d,2222.2" + ) + } + + { // use tab formatter + + given IttoCSVFormat = IttoCSVFormat.tab.withRecordSeparator("\n") + given FieldEncoder[java.util.Date] = customFieldEncoder[java.util.Date](_.toString) + + final case class Bar(name: String, date: java.util.Date, salary: Double) + val d = new java.util.Date(0).toString + assert( + toCsv(Bar("Bo,b", new java.util.Date(0), 33003.3)) == + s"Bo,b\t$d\t33003.3" + ) + + assert( + toCsv(List(Bar("Bob", new java.util.Date(0), 1111.3), Bar("Jim", new java.util.Date(0), 2222.2))) == + s"Bob\t$d\t1111.3\tJim\t$d\t2222.2" + ) + } + + { + + final case class Baz(x: String) + final case class Foo(a: Int, c: Baz) + final case class Bar(a: String, b: Int, c: Foo) + val a = toCsvFlat(Bar("hello", 3, Foo(1, Baz("hi, dude")))) + + assert(a == "hello,3,1,\"hi, dude\"") + } + } + + test("encode_custom_type") { + final case class MyType(a: Int) + final case class Foo(a: MyType, b: Int) + + // encode + import com.github.gekomad.ittocsv.core.ToCsv.* + given FieldEncoder[MyType] = customFieldEncoder[MyType](x => s"[${x.a}]") + + assert(toCsv(Foo(MyType(42), 99)) == "[42],99") + + } + + test("toCsvTest") { + + object ToCsvT: + + import com.github.gekomad.ittocsv.core.Header.* + + inline def toCsvT[A <: Product]( + csvT: (A, Long) + )(using m: Mirror.ProductOf[A], e: RowEncoder[m.MirroredElemTypes]): String = + (if (csvT._2 == 0) csvHeader[A] else "") + toCsv(csvT._1, true) + end ToCsvT + + given IttoCSVFormat = IttoCSVFormat.default.withDelimiter(';').withRecordSeparator("\n") + import ToCsvT.* + + final case class Foo(name: String) + + val l = for { + c <- 0 to 5 + } yield toCsvT((Foo("id" + c), c)) + + assert(l.mkString == "name\nid0\nid1\nid2\nid3\nid4\nid5") + + } + + test("type_to_csv_with_Instant") { + + given IttoCSVFormat = IttoCSVFormat.default.withPrintHeader(false) + + val instant1 = Instant.parse("2010-11-30T18:35:24.00Z") + val instant2 = Instant.parse("2011-11-30T18:35:24.00Z") + val instant3 = Instant.parse("2012-11-30T18:35:24.00Z") + val instant4 = Instant.parse("2013-11-30T18:35:24.00Z") + + final case class Bar(i: Option[Instant], e: Instant) + val l: List[Bar] = List(Bar(Some(instant1), instant2), Bar(Some(instant3), instant4)) + assert(toCsv(l) == "2010-11-30T18:35:24Z,2011-11-30T18:35:24Z,2012-11-30T18:35:24Z,2013-11-30T18:35:24Z") + } + + test("type_to_csv_with_date_and_time") { + + import java.time.LocalDateTime + import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME + + given IttoCSVFormat = IttoCSVFormat.default.withPrintHeader(false) + + val localDateTime = LocalDateTime.parse("2000-12-31T12:13:14", ISO_LOCAL_DATE_TIME) + val localTime = LocalTime.parse("11:15:30", DateTimeFormatter.ISO_LOCAL_TIME) + val localDate = LocalDate.parse("2019-12-27") + val offsetDateTime = OffsetDateTime.parse("2012-12-03T10:15:30+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME) + val zonedDateTime = ZonedDateTime.parse("2019-04-01T17:24:11.252+05:30[Asia/Calcutta]") + + final case class Bar( + a: LocalDateTime, + b: LocalTime, + c: Option[LocalDate], + e: Option[OffsetDateTime], + f: ZonedDateTime + ) + val l: List[Bar] = List(Bar(localDateTime, localTime, Some(localDate), Some(offsetDateTime), zonedDateTime)) + assert( + toCsv( + l + ) == "2000-12-31T12:13:14,11:15:30,2019-12-27,2012-12-03T10:15:30+01:00,2019-04-01T17:24:11.252+05:30[Asia/Calcutta]" + ) + } + + test("type_to_csv_with_custom_localDateTime") { + + import java.time.LocalDateTime + import java.time.format.DateTimeFormatter + given IttoCSVFormat = IttoCSVFormat.default.withPrintHeader(false) + given FieldEncoder[LocalDateTime] = + customFieldEncoder[LocalDateTime](_.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))) + + val localDateTime = + LocalDateTime.parse("2000-11-11 11:11:11.0", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) + + final case class Bar(a: String, b: Long, c: LocalDateTime, e: Option[Int]) + val l: List[Bar] = List(Bar("Yel,low", 3L, localDateTime, Some(1)), Bar("eee", 7L, localDateTime, None)) + val x = toCsv(l) + assert(x == "\"Yel,low\",3,2000-11-11 11:11:11.0,1,eee,7,2000-11-11 11:11:11.0,") + } + + test("from_type_to_csv") { + final case class Bar(a: String, b: Int) + assert(toCsv(Bar("Bar", 42)) == "Bar,42") + } + + test("from_list_of_type_to_csv") { + final case class Bar(a: String, b: Int) + assert(toCsv(List(Bar("abc", 42), Bar("def", 24))) == "abc,42,def,24") + } + + test("from_list_of_type_to_List_of_csv") { + final case class Bar(a: String, b: Int) + assert(toCsvL(List(Bar("abc", 42), Bar("def", 24))) == "a,b\r\nabc,42\r\ndef,24") + } + + test("serialize_List_Type") { + + final case class Bar(c: String, a: Int) + + val x = List(Bar("abc", 1), Bar("def", 2)) + + assert(toCsv(x) == "abc,1,def,2") + assert(x.map(toCsv(_)) == List("abc,1", "def,2")) + } + + test("serialize_List_Double") { + + assert(toCsv(List(1.1, 2.1, 3.1)) == "1.1,2.1,3.1") + + } + + test("serialize_with_record_separator") { + final case class Foo(a: String, b: String) + + assert(toCsv(Foo("aaa", "bbb"), true) == "\r\naaa,bbb") + + } + + test("write_Csv_with_header") { + + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Bar(name: String, date: java.util.Date, salary: Double) + + import com.github.gekomad.ittocsv.core.ToCsv.* + given IttoCSVFormat = IttoCSVFormat.tab + given FieldEncoder[java.util.Date] = customFieldEncoder[java.util.Date](_.toString) + + def g[A <: Product](a: A)(using m: scala.deriving.Mirror.ProductOf[A], e: RowEncoder[m.MirroredElemTypes]): String = + toCsv(a) + + val d = new java.util.Date(0).toString + assert(g(Bar("Bo,b", new java.util.Date(0), 33003.3)) == s"Bo,b\t$d\t33003.3") + + } + + test("get_header1") { + import com.github.gekomad.ittocsv.core.Header.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Record(i: Int, d: Double, s: String, b: Boolean) + + given IttoCSVFormat = IttoCSVFormat.default + + inline def g[A](using mirror: Mirror.Of[A]): String = csvHeader[A] + + val header = g[Record] + assert(header == "i,d,s,b") + } + + test("get_header2") { + import com.github.gekomad.ittocsv.core.Header.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + final case class Record(X: Int, d: Double, s: String, b: Boolean) + given IttoCSVFormat = IttoCSVFormat.default.withDelimiter('X') + + val header = csvHeader[Record] + + assert(header == "\"X\"XdXsXb") + } +end ToCsvTest diff --git a/scala3_js/src/test/scala/TreeTest.scala b/scala3_js/src/test/scala/TreeTest.scala new file mode 100644 index 0000000..2fdb62e --- /dev/null +++ b/scala3_js/src/test/scala/TreeTest.scala @@ -0,0 +1,79 @@ +import com.github.gekomad.ittocsv.core.FromCsv.* +import com.github.gekomad.ittocsv.core.ToCsv +import com.github.gekomad.ittocsv.core.ToCsv.given + +import scala.util.matching.Regex + +class TreeTest extends munit.FunSuite: + test("encode_decodeTree_Int") { + + object OTree: + + // thanks to amitayh https://gist.github.com/amitayh/373f512c50222e15550869e2ff539b25 + final case class Tree[A](value: A, left: Option[Tree[A]] = None, right: Option[Tree[A]] = None) + + object Serializer: + val pattern: Regex = """^(\d+)\((.*)\)$""".r + val treeOpen: Char = '(' + val treeClose: Char = ')' + val separator: Char = ',' + val separatorLength: Int = 1 + + def serialize[A](nodeOption: Option[Tree[A]]): String = nodeOption match + case Some(Tree(value, left, right)) => + val leftStr = serialize(left) + val rightStr = serialize(right) + s"$value$treeOpen$leftStr$separator$rightStr$treeClose" + + case None => "" + + def deserialize[A](str: String, f: String => A): Option[Tree[A]] = str match + case pattern(value, inner) => + val (left, right) = splitInner(inner) + Some(Tree(f(value), deserialize(left, f), deserialize(right, f))) + case _ => None + + def splitInner(inner: String): (String, String) = { + var balance = 0 + val left = inner.takeWhile { + case `treeOpen` => balance += 1; true + case `treeClose` => balance -= 1; true + case `separator` if balance == 0 => false + case _ => true + } + + val right = inner.drop(left.length + separatorLength) + + (left, right) + } + end Serializer + + end OTree + import OTree.* + import OTree.Serializer.* + import com.github.gekomad.ittocsv.parser.IttoCSVFormat + + given IttoCSVFormat = IttoCSVFormat.default + + final case class Foo(v: String, a: Tree[Int]) + + // encode + import com.github.gekomad.ittocsv.core.ToCsv.* + given FieldEncoder[Tree[Int]] = customFieldEncoder[Tree[Int]](x => serialize(Some(x))) + + val tree: Tree[Int] = Tree(1, Some(Tree(2, Some(Tree(3)))), Some(Tree(4, Some(Tree(5)), Some(Tree(6))))) + + val serialized: String = toCsv(Foo("abc", tree)) + + assert(serialized == "abc,\"1(2(3(,),),4(5(,),6(,)))\"") + + // decode + given Decoder[String, Tree[Int]] = str => { + deserialize(str, _.toInt) match + case None => Left(List(s"Not a Node[Short] $str")) + case Some(a) => Right(a) + } + + assert(fromCsv[Foo](serialized) == List(Right(Foo("abc", tree)))) + } +end TreeTest diff --git a/scala3_js/src/test/scala/TypeToCSV.scala b/scala3_js/src/test/scala/TypeToCSV.scala new file mode 100644 index 0000000..9e5afc0 --- /dev/null +++ b/scala3_js/src/test/scala/TypeToCSV.scala @@ -0,0 +1,25 @@ +import com.github.gekomad.ittocsv.core.* +import com.github.gekomad.ittocsv.core.ToCsv.* +import com.github.gekomad.ittocsv.parser.IttoCSVFormat + +import java.util.UUID + +class TypeToCSV extends munit.FunSuite: + given IttoCSVFormat = IttoCSVFormat.default + test("CSVtoList1") { + + case class Bar(a: String, b: Int) + val x = Bar("侍", 42) + val a = toCsv(x) + assert(a == "侍,42") + } + + test("CSVtoList2") { + + case class Bar(a: String, b: UUID) + val x = Bar("侍", UUID.fromString("889bd28a-00b6-45ab-9481-92060ba1ce7b")) + val a = toCsv(x) + + assert(a == "侍,889bd28a-00b6-45ab-9481-92060ba1ce7b") + } +end TypeToCSV diff --git a/scala3_js/src/test/scala/Util.scala b/scala3_js/src/test/scala/Util.scala new file mode 100644 index 0000000..c786ae1 --- /dev/null +++ b/scala3_js/src/test/scala/Util.scala @@ -0,0 +1,9 @@ +object Util: + def deleteFile(fileName: String): Unit = { + import java.io.File + val file = File(fileName) + if (file.isFile && file.exists) { + file.delete() + } + } +end Util