scaladatetimecollectionsfunctional-programmingoption-type

Filter an optional sequence


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)

Solution

  • 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
    

    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.

    Wait... how does 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 LocalDates. 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 LocalDates.

    Does this mean we can make it more generic?

    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 LocalDates, Ints, 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.