-
Notifications
You must be signed in to change notification settings - Fork 9
Add example for fs2-data-csv #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5a37fc9
337fd36
35c1234
42bf213
6618846
92a7ddb
af2fd16
fe7e459
4220fb1
095ec12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 .flatMap { (sum, count) =>
IO.println(s"The mean age of the passengers is ${sum / count}")
}?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
zetashift marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
| 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 | ||
|
|
||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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