18

I want to use java.time.LocalDate and java.time.LocalDateTime with an implicit Ordered like:

val date1 = java.time.LocalDate.of(2000, 1, 1)
val date2 = java.time.LocalDate.of(2010, 10, 10)
if (date1 < date2) ...

import scala.math.Ordering.Implicits._ doesn't work, because LocalDate inherits from Comparable<ChronoLocalDate> instead of Comparable<LocalDate>. How can I write my own imlicit Orderd to use <, <=, >, >= operators/methods to compare LocalDate's?

Edit:

I found a way with use of an implicit class:

import java.time.{LocalDate}

object MyDateTimeUtils {
  implicit class MyLocalDateImprovements(val ld: LocalDate)
           extends Ordered[LocalDate] {
    def compare(that: LocalDate): Int = ld.compareTo(that)
  }
}

// Test

import MyDateTimeUtils._

val d1 = LocalDate.of(2016, 1, 1)
val d2 = LocalDate.of(2017, 2, 3)

if (d1 < d2) println("d1 is less than d2")

But I would prefer a way like Scala is doing for all Java classes which implements Comparable<T>. You just have to import scala.math.Ordering.Implicits._ in your code. Scala implements it like:

implicit def ordered[A <% Comparable[A]]: Ordering[A] = new Ordering[A] {
  def compare(x: A, y: A): Int = x compareTo y
}

But unfortunately LocalDate implements Comparable<ChronoLocalDate> instead of Comparable<LocalDate>. I couldn't find a way to modify the above implicit ordered method to fit with LocalDate/Comparable<ChronoLocalDate>. Any idea?

M. Justin
  • 5,160
  • 1
  • 38
  • 65
Michael
  • 206
  • 1
  • 2
  • 6
  • Thank you for pointing our that `import scala.math.Ordering.Implicits._` doesn't work, because `LocalDate` inherits from `Comparable` instead of `Comparable` - I'd been wondering for ages why that wasn't working! – Roberto Tyley May 17 '18 at 15:00
  • Since Scala 2.13.0 this just works without having to define your own `Ordering`. – Jasper-M Dec 10 '19 at 10:33

5 Answers5

25

You can use Ordering.by to create ordering for any type, given a function from that type to something that already has an Ordering - in this case, to Long:

implicit val localDateOrdering: Ordering[LocalDate] = Ordering.by(_.toEpochDay)
Tzach Zohar
  • 35,518
  • 3
  • 75
  • 79
  • 2
    Thank's, but I want to use expressions like `if (date1 < date2)` in my programm code. With java.time.Instant for example it's possible when I `import scala.math.Ordering.Implicits._`, because Instant inherits "Comparable, but LocalDate only Comparable instead of Comparable. – Michael Jun 27 '16 at 17:42
  • also it seems toEpochDay is no longer available – sfosdal Feb 28 '18 at 23:51
22

Comparing by LocalDate.toEpochDay is clear, though maybe relatively slow...

The answer by @tzach-zohar is great, in that it's most obvious what is going on; you're ordering by Epoch Day:

implicit val localDateOrdering: Ordering[LocalDate] = Ordering.by(_.toEpochDay)

However, if you look at the implementation of toEpochDay you'll see that it's relatively involved - 18 lines of code, with 4 divisions, 3 conditionals and a call to isLeapYear() - and the resulting value isn't cached, so it gets recalculated with each comparison, which might be expensive if there were a large number of LocalDates to be sorted.

...making use of LocalDate.compareTo is probably more performant...

The implementation of LocalDate.compareTo is simpler - just 2 conditionals, no division - and it's what you'd be getting with that implicit conversion of java.lang.Comparable to scala.math.Ordering that scala.math.Ordering.Implicits._ offers, if only it worked! But as you said, it doesn't, because LocalDate inherits from Comparable<ChronoLocalDate> instead of Comparable<LocalDate>. One way to take advantage of it might be...

import scala.math.Ordering.Implicits._

implicit val localDateOrdering: Ordering[LocalDate] =
  Ordering.by(identity[ChronoLocalDate])

...which lets you order LocalDates by casting them to ChronoLocalDates, and using the Ordering[ChronoLocalDate] that scala.math.Ordering.Implicits._ gives you!

...and in the end it looks best with the lambda syntax for SAM types

The lambda syntax for SAM types, introduced with Scala 2.12, can make really short work of constructing a new Ordering :

implicit val localDateOrdering: Ordering[LocalDate] = _ compareTo _

...and I think this ends up being my personal favourite! Concise, still fairly clear, and using (I think) the best-performing comparison method.

Roberto Tyley
  • 21,540
  • 9
  • 67
  • 98
3

A slight modification to the implicit ordered should do the trick.

type AsComparable[A] = A => Comparable[_ >: A]

implicit def ordered[A: AsComparable]: Ordering[A] = new Ordering[A] {
  def compare(x: A, y: A): Int = x compareTo y
}

Now every type which is comparable to itself or to a supertype of itself should have an Ordering.

Jasper-M
  • 12,460
  • 1
  • 19
  • 34
  • 1
    Note that view-bounds (` – Roberto Tyley May 17 '18 at 15:49
  • @RobertoTyley You're right. I updated my solution for a world without view bounds. – Jasper-M Nov 30 '18 at 17:43
  • 1
    I think this is the most useful answer. It addresses both `LocalDate` and `LocalDateTime` in one fell swoop, and also sidesteps the performance considerations of `toEpochDay`, and also solves the problems of using only `toEpochSecond` for comparing timestamps that go down to nanoseconds. – Axel Oct 14 '19 at 14:44
  • For the future reader: this modified definition is now part of the standard library since 2.13.0, so this all just works now. – Jasper-M Dec 10 '19 at 10:31
3

Here is the solution that I use:

define two implicits. The first one for making an Ordering[LocalDate] available. And a second one for giving LocalDate a compare method which comes in very handy. I typically put these in package objects in a library I can just include where I need them.

package object net.fosdal.oslo.odatetime {

  implicit val orderingLocalDate: Ordering[LocalDate] = Ordering.by(d => (d.getYear, d.getDayOfYear))

  implicit class LocalDateOps(private val localDate: LocalDate) extends AnyVal with Ordered[LocalDate] {
    override def compare(that: LocalDate): Int = Ordering[LocalDate].compare(localDate, that)
  }
}

with both of these defined you can now do things like:

import net.fosdal.oslo.odatetime._

val bool: Boolean = localDate1 < localDate1

val localDates: Seq[LocalDate] = ...
val sortedSeq = localDates.sorted

Alternatively... you could just use my library (v0.4.3) directly. see: https://github.com/sfosdal/oslo

sfosdal
  • 786
  • 11
  • 23
2

Here's my solution for java.time.LocalDateTime

implicit val localDateTimeOrdering: Ordering[LocalDateTime] =
  Ordering.by(x => x.atZone(ZoneId.of("UTC")).toEpochSecond)
Xavier Guihot
  • 32,132
  • 15
  • 193
  • 118
Alon Catz
  • 2,107
  • 1
  • 16
  • 22