🥰

ゆめみ社コーディングテストを Scala の fs2 でとく

2 min read

この記事の例題がファイルのread/write, データの基本的な加工を扱っていて良い感じだったので Scala fs2 を使って解いてみた.

https://zenn.dev/foolishell/articles/589536ec741ddd

fs2 は functional stream for scala の略称兼ライブラリ名で、Scala で関数型ライブラリと組み合わせて Stream を扱える便利なライブラリ. 書き捨てコードなので ammonite の scala script を使って書く.

https://fs2.io/
ranking.sc

// scala 2.13.6
import $ivy.`org.typelevel::cats-effect:3.2.5`
import $ivy.`co.fs2::fs2-core:3.1.1`
import $ivy.`co.fs2::fs2-io:3.1.1`

import fs2.io.file._
import fs2.{Stream,text}
import cats.effect.{IO}
import cats.effect.unsafe.implicits.global

@main
def main(arg: String) = readCSV(arg).unsafeRunSync()

private def collectRecord(
    acc: Map[String, Seq[Int]],
    record: String
): Map[String, Seq[Int]] =
  record.split(",").toList match {
    case timestamp :: playerId :: score :: Nil =>
      acc.updatedWith(playerId) {
        case Some(scores) => Some(score.toInt +: scores)
        case None         => Some(Seq(score.toInt))
      }
    case _ => acc
  }
private def collectRanking(result: Map[Int, Seq[String]], entry: (String, Seq[Int])) = {
  val (key, value) = entry
  result.updatedWith(math.round(value.sum / value.length)) {
    case Some(playerIds) => Some(key +: playerIds)
    case None            => Some(Seq(key))
  }
}

private def readCSV(filename: String) = {
  val header = "rank,player_id,mean_score"
  val body = Files[IO]
    .readAll(Path(filename), 4096, Flags.Read)
    .through(text.utf8.decode)
    .through(text.lines)
    .drop(1)
    .fold(Map.empty[String, Seq[Int]])(collectRecord)
    .map(_.foldLeft(Map.empty[Int, Seq[String]])(collectRanking))
    .map(averageScoreToPlayerIds =>
      averageScoreToPlayerIds.toList.sortBy(-_._1).take(10)
    )
    // format ranking
    .map(list =>
      list.foldLeft((1, List.empty[String])) {
        case ((ranking, acc), (averageScore, playerIds)) if ranking <= 10 =>
          (
            ranking + playerIds.length,
            playerIds
              .map(id => s"$ranking,$id,$averageScore")
              .toList ::: acc
          )
	case otherwise => otherwise._1 
      }
    )
    .map(_._2.reverse)
    .compile
    .toList
    .map(_.head)
  body.map(header :: _).map(_.mkString("\n"))
}

Discussion

ログインするとコメントできます