I have a Option sequence of dates - from which I need to SAFELY extract the first date after a given date( or else return the given date).
The below seems to be a lot of code for such a simple used case.
Whats the simplest idiom for this.
val dates = Option[Seq[LocalDate]]
val postDates = dates.getOrElse(Seq[LocalDate]()).filter(_.isAfter(inputdate)).headOption
val firstDateOpt = Option(postDates).getOrElse(Seq[LocalDate]()).sorted.headOption
firstDateOpt.getOrElse(inputdate)
Given a Seq[LocalDate]
called dates
(we'll tackle the option later), you can safely and efficiently find the first one after a given LocalDate
as follows:
dates
.view
.filter(_.isAfter(inputDate))
.minOption
minOption
(docs) takes the "minimum" in a sequence (where for LocalDate
s is defined as "the earliest"), possibly returning a None
if the input collection is empty. This is in contrast to min
(docs), which will throw an UnsupportedOperationException
if the collection is empty.view
makes sure that you don't create an intermediate collection with filter
, minimizing waste.IndexedSeq
like a Vector
and not a List
-- without O(1) random access, sorting becomes less efficient), and turn this into something that will always work in O(n) in the size of the input collection.Since the dates
you have is an Option[Seq[LocalDate]]
and you have a function that takes a Seq[LocalDate]
and returns an Option[LocalDate]
you can combine them with a simple flatMap
. Finally, getOrElse
gives you a tool to define the default to return.
Putting it all together, you get the following:
def first(after: LocalDate, dates: Option[Seq[LocalDate]]): LocalDate =
dates.flatMap(_.view.filter(_.isAfter(after)).minOption).getOrElse(after)
If you want to play around with this code you can do so here on Scastie.
minOption
work?If you think about it, minOption
looks a bit magical, in that it "somehow knew" how to tell what "minimum" means in the context of LocalDate
s. If you have a look at the documentation for minOption
that I linked above, you'll notice that there's an implicit
parameter that you didn't have to pass... explicitly. You can read more about implicit (or contextual) parameters here. The brief explanation of what's happening here is that the standard library already makes an Ordering[LocalDate]
available. One interesting thing to notice is that Ordering
expresses the concept of "greater than", which is semantically equivalent to what the isAfter
method does for LocalDate
s.
We can indeed! With the using
clause you can abstract over any type for which there is a given
instance of Ordering
. The result is as follows:
def firstGt[A](a: A, as: Option[Seq[A]])(using ord: Ordering[A]): A =
as.flatMap(_.view.filter(ord.gt(_, a)).minOption).getOrElse(a)
This uses the Scala 3 syntax, but you can achieve the same with Scala 2 as follows:
def firstGt[A](a: A, as: Option[Seq[A]])(implicit ord: Ordering[A]): A =
as.flatMap(_.view.filter(ord.gt(_, a)).minOption).getOrElse(a)
And you can use it transparently on LocalDate
s, Int
s, or even custom type for which you have a given
instance (even custom ones).
import java.time.LocalDate.ofEpochDay as d
assert(
firstGt(d(10), Option(Seq(d(11), d(10), d(12), d(9)))) == d(11)
)
assert(
firstGt(10, Option(Seq(11, 10, 12, 9))) == 11
)
You can play around with this code here on Scastie as well.