Skip to content
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ It currently includes:
* [Circe] and http4s integration
* [Decline Effect]
* [Munit Cats Effect]
* [fs2-data-csv]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't this the line you added in another PR? :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's me haha!
But somehow a second fs2-data mention got sneaked in:
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol I've just realized it's a duplicate


[Scala Toolkit]: https://virtuslab.com/blog/scala-toolkit-makes-scala-powerful-straight-out-of-the-box/
[Cats]: https://typelevel.org/cats
Expand All @@ -32,4 +31,3 @@ It currently includes:
[Circe]: https://circe.github.io/circe/
[Decline Effect]: https://ben.kirw.in/decline/effect.html
[Munit Cats Effect]: https://github.com/typelevel/munit-cats-effect
[fs2-data-csv]: https://fs2-data.gnieh.org/documentation/csv/
175 changes: 175 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,184 @@ object Main extends CommandIOApp("mkString", "Concatenates strings from stdin")

@:@

## Parsing and transforming a CSV file

Here, [fs2-data-csv] is used to read and parse a comma separated file.
Manual encoders and decoders are defined for our `Passenger`s to show you how to do everything from scratch.

Let's start with a CSV file that has records of fictious passengers registered for a flight:

```
id,First Name,Age,flight number,destination
1,Seyton,44,WX122,Tanzania
2,Lina,,UX199,Greenland
3,Grogu,,SW999,Singapore
```


@:select(scala-version)

@:choice(scala-3)
```scala mdoc:reset:silent
//> using lib "org.typelevel::toolkit::@VERSION@"

import cats.effect.*
import fs2.text
import fs2.data.csv.*
import fs2.data.csv.generic.semiauto.*
import fs2.io.file.{Path, Flags, Files}
import cats.data.NonEmptyList

case class Passenger(
id: Long,
firstName: String,
age: Either[String, Int],
flightNumber: String,
destination: String
)

object Passenger:
// Here we define a manual decoder for each row in our CSV
given csvRowDecoder: CsvRowDecoder[Passenger, String] with
def apply(row: CsvRow[String]): DecoderResult[Passenger] =
for
id <- row.as[Long]("id")
firstName <- row.as[String]("First Name")
ageOpt <- row.asNonEmpty[Int]("Age")
flightNumber <- row.as[String]("flight number")
destination <- row.as[String]("destination")
yield
val age = ageOpt.toRight[String]("N/A")
Passenger(id, firstName, age, flightNumber, destination)

// Here we define a manual encoder for encoding Passenger classes to a CSV
given csvRowEncoder: CsvRowEncoder[Passenger, String] with
def apply(p: Passenger): CsvRow[String] =
CsvRow.fromNelHeaders(
NonEmptyList.of(
(p.firstName, "first_name"),
(p.age.toString(), "age"),
(p.flightNumber, "flight_number"),
(p.destination, "destination")
)
)

val input = Files[IO]
.readAll(Path("./example.csv"), 1024, Flags.Read)
.through(text.utf8.decode)
.through(decodeUsingHeaders[Passenger]())

object CSVPrinter extends IOApp.Simple:

val run: IO[Unit] =
// Let's do an aggregation of all the values in the age column
val meanIO =
input
.foldMap(p => (p.age.getOrElse(0), 1))
.compile
.lastOrError
.map((sum, count) => sum / count)

input
.evalTap(p =>
IO.println(
s"${p.firstName} is taking flight: ${p.flightNumber} to ${p.destination}"
)
)
.compile
.drain >> meanIO.flatMap(mean =>
IO.println(s"The mean age of the passengers is $mean")
Comment on lines +238 to +253
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is any way to do this in one run I'd love to know!
The only thing I could think of is chucking a IO.println and flatmapping in the foldMap

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit late on that, but can't you put the evalTap right before the foldMap, and then compile and run

.flatMap { (sum, count) =>
  IO.println(s"The mean age of the passengers is ${sum / count}")
}

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@satabin feel free to propose a neater implementation. Now it should be easier as toolkit contains fs2-data-csv too :D

)
```


@:choice(scala-2)
```scala mdoc:reset:silent
//> using lib "org.typelevel::toolkit::@VERSION@"

import cats.effect._
import fs2.text
import fs2.data.csv._
import fs2.data.csv.generic.semiauto._
import fs2.io.file.{Path, Flags, Files}
import cats.data.NonEmptyList

case class Passenger(
id: Long,
firstName: String,
age: Either[String, Int],
flightNumber: String,
destination: String
)

object Passenger {
// Here we define a manual decoder for each row in our CSV
implicit val csvRowDecoder: CsvRowDecoder[Passenger, String] =
new CsvRowDecoder[Passenger, String] {
def apply(row: CsvRow[String]): DecoderResult[Passenger] =
for {
id <- row.as[Long]("id")
firstName <- row.as[String]("First Name")
ageOpt <- row.asNonEmpty[Int]("Age")
flightNumber <- row.as[String]("flight number")
destination <- row.as[String]("destination")
} yield {
val age = ageOpt.toRight[String]("N/A")
Passenger(id, firstName, age, flightNumber, destination)
}
}

// Here we define a manual encoder for encoding Passenger classes to a CSV
implicit val csvRowEncoder: CsvRowEncoder[Passenger, String] =
new CsvRowEncoder[Passenger, String] {
def apply(p: Passenger): CsvRow[String] =
CsvRow.fromNelHeaders(
NonEmptyList.of(
(p.firstName, "first_name"),
(p.age.toString(), "age"),
(p.flightNumber, "flight_number"),
(p.destination, "destination")
)
)
}
}

object CSVPrinter extends IOApp.Simple {
val input = Files[IO]
.readAll(Path("./example.csv"), 1024, Flags.Read)
.through(text.utf8.decode)
.through(decodeUsingHeaders[Passenger]())


val run: IO[Unit] = {
// Let's do an aggregation of all the values in the age column
val meanIO =
input
.foldMap(p => (p.age.getOrElse(0), 1))
.compile
.lastOrError
.map({ case (sum, count) => sum / count})

input
.evalTap(p =>
IO.println(
s"${p.firstName} is taking flight: ${p.flightNumber} to ${p.destination}"
)
)
.compile
.drain >> meanIO.flatMap(mean =>
IO.println(s"The mean age of the passengers is $mean")
)
}
}
```
@:@


[fs2]: https://fs2.io/#/
[fs2-data-csv]: https://fs2-data.gnieh.org/documentation/csv/
[decline]: https://ben.kirw.in/decline/
[scala-native]: https://scala-native.org/en/stable/
[Scala CLI]: https://scala-cli.virtuslab.org/
[Koroeskohr]: https://github.com/Koroeskohr