Scala按未知数量的字段排序

时间:2016-02-15 10:58:12

标签: scala sorting functional-programming scala-collections

我有N个字段的简单类。

case class Book(a: UUID... z: String)

和功能:

def sort(books:Seq[Book], fields:Seq[SortingFields]) = {...}

其中

case class SortingField(field: String, asc: Boolean)

where field - Book类的一个字段,asc - 一个排序方向。

所以,事先我不知道哪些字段(从0到N)和排序顺序进入我的函数来排序books集合。它可能只是一个ID字段,也可能是特定顺序中所有类的现有字段。

如何实施?

4 个答案:

答案 0 :(得分:4)

我会使用现有的Ordering特征,并使用从Book映射到字段的函数,即Ordering.by[Book, String](_.author)。然后,您只需使用books.sorted(myOrdering)进行排序即可。如果我在Book的伴随对象上定义辅助方法,那么获取这些排序非常简单:

object Book {
  def by[A: Ordering](fun: Book => A): Ordering[Book] = Ordering.by(fun)
}
case class Book(author: String, title: String, year: Int)

val xs = Seq(Book("Deleuze" /* and Guattari */, "A Thousand Plateaus", 1980),
             Book("Deleuze", "Difference and Repetition", 1968),
             Book("Derrida", "Of Grammatology", 1967))

xs.sorted(Book.by(_.title)) // A Thousand, Difference, Of Grammatology
xs.sorted(Book.by(_.year )) // Of Grammatology, Difference, A Thousand

然后,为了通过多个字段链接排序,您可以创建自定义排序,在字段中进行,直到一个比较为非零。例如,我可以将扩展方法andThen添加到Ordering,如下所示:

implicit class OrderingAndThen[A](private val self: Ordering[A]) extends AnyVal {
  def andThen(that: Ordering[A]): Ordering[A] = new Ordering[A] {
    def compare(x: A, y: A): Int = {
      val a = self.compare(x, y)
      if (a != 0) a else that.compare(x, y)
    }
  }
}

所以我可以写:

val ayt = Book.by(_.author) andThen Book.by(_.year) andThen Book.by(_.title)
xs.sorted(ayt)  // Difference, A Thousand, Of Grammatology

答案 1 :(得分:1)

https://angular.io/docs/ts/latest/api/core/Injector-class.html提供了很好的答案,我已经接受了以下内容:

def by[A: Ordering](e: Book => A): Ordering[Book] = Ordering.by(e)

implicit class OrderingAndThen[A](private val self: Ordering[A]) extends AnyVal {
    def andThen(that: Ordering[A]): Ordering[A] = new Ordering[A] {
      def compare(x: A, y: A): Int = {
      val a = self.compare(x, y)
      if (a != 0) a else that.compare(x, y)
    }
  }
}

接下来,我将类字段的名称映射到实际排序的方向

def toOrdering(name: String, r: Boolean): Ordering[Book] = {
    (name match {
      case "id" => Book.by(_.id)
      case "name" =>  Book.by(_.name)
   }) |> (o => if (r) o.reverse else o)
}

使用正向管道运营商:

implicit class PipedObject[A](value: A) {
    def |>[B](f: A => B): B = f(value)
}

最后我将所有顺序与reduce函数结合起来:

val fields = Seq(SortedField("name", true), SortedField("id", false))
val order = fields.map(f => toOrdering(f.field, f.reverse)).reduce(combines(_,_))
coll.sorted(order)

,其中

val combine = (x: Ordering[Book], y: Ordering[Book]) => x andThen y

一种替代方法是使用@tailrec:

def orderingSeq[T](os: Seq[Ordering[T]]): Ordering[T] = new Ordering[T] {
  def compare(x: T, y: T): Int = {
    @tailrec def compare0(rest: Seq[Ordering[T]], result: Int): Int = result match   {
      case 0 if rest.isEmpty => 0
      case 0 => compare0(rest.tail, rest.head.compare(x, y))
      case a => a
    }

    compare0(os, 0)
  }
}

答案 2 :(得分:0)

有可能。但据我所知,你将不得不使用反思。

此外,您必须稍微更改SortingField类,因为scala编译器无法为每个字段找出正确的Ordering类型类。

这是一个简化的例子。

import scala.reflect.ClassTag

/** You should be able to figure out the correct field ordering here. Use `reverse` to decide whether you want to sort ascending or descending. */
case class SortingField[T](field: String, ord: Ordering[T]) { type FieldType = T }
case class Book(a: Int, b: Long, c: String, z: String)

def sort[T](unsorted: Seq[T], fields: Seq[SortingField[_]])(implicit tag: ClassTag[T]): Seq[T] = {
  val bookClazz = tag.runtimeClass

  fields.foldLeft(unsorted) { case (sorted, currentField) =>
    // keep in mind that scala generates a getter method for field 'a'
    val field = bookClazz.getMethod(currentField.field)
    sorted.sortBy[currentField.FieldType](
      field.invoke(_).asInstanceOf[currentField.FieldType]
    )(currentField.ord)
  }
}

但是,对于按多个字段排序,您必须多次对序列进行排序或更好,然后才能正确组合各种排序。

所以这变得越来越复杂了#39;没有任何关于正确性和完整性的保证,但通过一点点测试,它不会失败:

def sort[T](unsorted: Seq[T], fields: Seq[SortingField[_]])(implicit tag: ClassTag[T]): Seq[T] = {
  @inline def invokeGetter[A](field: Method, obj: T): A = field.invoke(obj).asInstanceOf[A]
  @inline def orderingByField[A](field: Method)(implicit ord: Ordering[A]): Ordering[T] = {
    Ordering.by[T, A](invokeGetter[A](field, _))
  }

  val bookClazz = tag.runtimeClass
  if (fields.nonEmpty) {
    val field = bookClazz.getMethod(fields.head.field)

    implicit val composedOrdering: Ordering[T] = fields.tail.foldLeft {
      orderingByField(field)(fields.head.ord)
    } { case (ordering, currentField) =>
      val field = bookClazz.getMethod(currentField.field)
      val subOrdering: Ordering[T] = orderingByField(field)(currentField.ord)

      new Ordering[T] {
        def compare(x: T, y: T): Int = {
          val upperLevelOrderingResult = ordering.compare(x, y)

          if (upperLevelOrderingResult == 0) {
            subOrdering.compare(x, y)
          } else {
            upperLevelOrderingResult
          }
        }
      }
    }

    unsorted.sorted(composedOrdering)
  } else {
    unsorted
  }
}

sort(
  Seq[Book](
    Book(1, 5L, "foo1", "bar1"),
    Book(10, 50L, "foo10", "bar15"),
    Book(2, 3L, "foo3", "bar3"),
    Book(100, 52L, "foo4", "bar6"),
    Book(100, 51L, "foo4", "bar6"),
    Book(100, 51L, "foo3", "bar6"),
    Book(11, 15L, "foo5", "bar7"),
    Book(22, 45L, "foo6", "bar8")
  ),
  Seq(
    SortingField("a", implicitly[Ordering[Int]].reverse),
    SortingField("b", implicitly[Ordering[Long]]),
    SortingField("c", implicitly[Ordering[String]])
  )
)

>> res0: Seq[Book] = List(Book(100,51,foo3,bar6), Book(100,51,foo4,bar6), Book(100,52,foo4,bar6), Book(22,45,foo6,bar8), Book(11,15,foo5,bar7), Book(10,50,foo10,bar15), Book(2,3,foo3,bar3), Book(1,5,foo1,bar1))

答案 3 :(得分:0)

案例类是产品,因此您可以使用instance.productIterator迭代所有字段值。这将按照声明的顺序为您提供字段。您也可以通过索引直接访问它们。据我所知,无法获得字段名称。这必须使用反射或宏来完成。 (也许像Shapeless这样的库可以做到这一点。)

另一种方法是不定义字段以使用名称排序,但使用函数:

case class SortingField[T](field: Book => T, asc: Boolean)(implicit ordering: Ordering[T])

new SortingField(_.fieldName, true)

然后声明排序为:

def sort(books: Seq[Book], fields: Seq[SortingField[_]]) = {...}

使用以下比较方法实现组合排序:

def compare[T](b1: Book, b2: Book, field: SortingField[T]) =
  field.ordering.compare(field.field(b1), field.field(b2))