Вложенные тематические классы Scala в / из CSV - PullRequest
0 голосов
/ 03 сентября 2018

Есть много хороших библиотек для записи / чтения кейсов Scala в / из CSV-файлов. Я ищу что-то, что выходит за рамки того, что может обрабатывать вложенных классов дел. Например, здесь Match имеет два Players:

case class Player(name: String, ranking: Int)
case class Match(place: String, winner: Player, loser: Player)

val matches = List(
  Match("London", Player("Jane",7), Player("Fred",23)),
  Match("Rome", Player("Marco",19), Player("Giulia",3)),
  Match("Paris", Player("Isabelle",2), Player("Julien",5))
)

Я бы хотел без особых усилий (без шаблонов!) Написать / прочитать matches в / из этого CSV:

place,winner.name,winner.ranking,loser.name,loser.ranking
London,Jane,7,Fred,23
Rome,Marco,19,Giulia,3
Paris,Isabelle,2,Julien,5

Обратите внимание на строку автоматизированного заголовка, используя точку "." сформировать имя столбца для вложенного поля, например, winner.ranking. Я был бы рад, если бы кто-то смог продемонстрировать простой способ сделать это (скажем, с помощью отражения или Shapeless ).

[Мотивация. Во время анализа данных удобно иметь плоский CSV, с которым можно поиграться, для сортировки, фильтрации и т. Д., Даже если классы вложений вложены. И было бы неплохо, если бы вы могли загрузить вложенные классы дел обратно из таких файлов.]

1 Ответ

0 голосов
/ 03 сентября 2018

Поскольку case-классом является Product, получение значений различных полей относительно просто. Получение имен полей / столбцов требует использования отражения Java. Следующая функция берет список экземпляров класса case и возвращает список строк, каждая из которых представляет собой список строк. Он использует рекурсию для получения значений и заголовков дочерних экземпляров класса case.

def toCsv(p: List[Product]): List[List[String]] = {
  def header(c: Class[_], prefix: String = ""): List[String] = {
    c.getDeclaredFields.toList.flatMap { field =>
      val name = prefix + field.getName
      if (classOf[Product].isAssignableFrom(field.getType)) header(field.getType, name + ".")
      else List(name)
    }
  }

  def flatten(p: Product): List[String] =
    p.productIterator.flatMap {
      case p: Product => flatten(p)
      case v: Any => List(v.toString)
    }.toList

  header(classOf[Match]) :: p.map(flatten)
}

Однако, создание case-классов из CSV является гораздо более сложным процессом, требующим использования отражения для получения типов различных полей, для создания значений из строк CSV и для создания экземпляров case-класса. Для простоты (не говоря о том, что код прост, просто чтобы он не усложнялся), я предполагаю, что порядок столбцов в CSV такой же, как если бы файл был создан с помощью функции toCsv(...), описанной выше. Следующая функция начинается с создания списка «инструкций по обработке одной строки CSV» (эти инструкции также используются для проверки соответствия заголовков столбцов в CSV свойствам case-класса). Затем эти инструкции используются для рекурсивного создания одной строки CSV за раз.

def fromCsv[T <: Product](csv: List[List[String]])(implicit tag: ClassTag[T]): List[T] = {
  trait Instruction {
    val name: String
    val header = true
  }
  case class BeginCaseClassField(name: String, clazz: Class[_]) extends Instruction {
    override val header = false
  }
  case class EndCaseClassField(name: String) extends Instruction {
    override val header = false
  }
  case class IntField(name: String) extends Instruction
  case class StringField(name: String) extends Instruction
  case class DoubleField(name: String) extends Instruction

  def scan(c: Class[_], prefix: String = ""): List[Instruction] = {
    c.getDeclaredFields.toList.flatMap { field =>
      val name = prefix + field.getName
      val fType = field.getType

      if (fType == classOf[Int]) List(IntField(name))
      else if (fType == classOf[Double]) List(DoubleField(name))
      else if (fType == classOf[String]) List(StringField(name))
      else if (classOf[Product].isAssignableFrom(fType)) BeginCaseClassField(name, fType) :: scan(fType, name + ".")
      else throw new IllegalArgumentException(s"Unsupported field type: $fType")
    } :+ EndCaseClassField(prefix)
  }

  def produce(instructions: List[Instruction], row: List[String], argAccumulator: List[Any]): (List[Instruction], List[String], List[Any]) = instructions match {
    case IntField(_) :: tail => produce(tail, row.drop(1), argAccumulator :+ row.head.toString.toInt)
    case StringField(_) :: tail => produce(tail, row.drop(1), argAccumulator :+ row.head.toString)
    case DoubleField(_) :: tail => produce(tail, row.drop(1), argAccumulator :+ row.head.toString.toDouble)
    case BeginCaseClassField(_, clazz) :: tail =>
      val (instructionRemaining, rowRemaining, constructorArgs) = produce(tail, row, List.empty)
      val newCaseClass = clazz.getConstructors.head.newInstance(constructorArgs.map(_.asInstanceOf[AnyRef]): _*)
      produce(instructionRemaining, rowRemaining, argAccumulator :+ newCaseClass)
    case EndCaseClassField(_) :: tail => (tail, row, argAccumulator)
    case Nil if row.isEmpty => (Nil, Nil, argAccumulator)
    case Nil => throw new IllegalArgumentException("Not all values from CSV row were used")
  }

  val instructions = BeginCaseClassField(".", tag.runtimeClass) :: scan(tag.runtimeClass)
  assert(csv.head == instructions.filter(_.header).map(_.name), "CSV header doesn't match target case-class fields")

  csv.drop(1).map(row => produce(instructions, row, List.empty)._3.head.asInstanceOf[T])
}

Я проверял это, используя:

case class Player(name: String, ranking: Int, price: Double)
case class Match(place: String, winner: Player, loser: Player)

val matches = List(
  Match("London", Player("Jane", 7, 12.5), Player("Fred", 23, 11.1)),
  Match("Rome", Player("Marco", 19, 13.54), Player("Giulia", 3, 41.8)),
  Match("Paris", Player("Isabelle", 2, 31.7), Player("Julien", 5, 16.8))
)
val csv = toCsv(matches)
val matchesFromCsv = fromCsv[Match](csv)

assert(matches == matchesFromCsv)

Очевидно, что это должно быть оптимизировано и усилено, если вы когда-нибудь захотите использовать это для производства ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...