scala-collection-contrib icon indicating copy to clipboard operation
scala-collection-contrib copied to clipboard

Add `between`, `betweenBy` functions to Seq

Open VladKopanev opened this issue 3 years ago • 2 comments
trafficstars

It would be nice to have a function similar to SQL BETWEEN operator that can filter elements from a Seq that are in some range. This is not an uncommon task when working with statistics, time series, location etc. We can implement it as an extension for Seq, roughly like this:

extension [T: Ordering](seq: Seq[T])
  def between(a: T, b: T): Seq[T] = seq.filter(e => Ordering[T].gt(e, a) && Ordering[T].lt(e, b))

and then use it like so:

Seq(1, 2, 3, 4, 5, 6 ,7, 7, 9, 10).between(4, 7) //will result in Seq(5, 6)

We could also make an extension that would allow filtering sequences of arbitrary product types that don't have an ordering, but have some fields that we can use for comparison, similar to sortBy function, for example:

extension [T](seq: Seq[T])
  def betweenBy[U: Ordering](a: U, b: U)(f: T => U): Seq[T] = seq.filter { e => 
    Ordering[U].gt(f(e), a) && Ordering[U].lt(f(e), b) 
}

locationDataList.betweenBy(hourAgoInstant, Instant.now)(_.timestamp)

IMHO this is much more intuitive, less prone to errors, and nicer to read than using a simple filter function.

P.S. naming is debatable here because I also can suggest using between on numeric types to check if they are in some range, e.g. 1.between(-1, 5) // returns true that would be useful addition too, but might cause confusion if paired with between on Sequences.

VladKopanev avatar Nov 29 '21 09:11 VladKopanev

Hi, thank you for writing this up! It seems to me that the gain would be relatively low, though.

If I understand correctly, the current way of achieving the same is:

xs.filter(x => x > 0 && x < 42)
xs.filter(x => x.foo.bar > 0 && x.foo.bar < 42)

And with your proposal, that would look like the following:

xs.between(0, 42)
xs.betweenBy(0, 42)(_.foo.bar)

Also, here we assume that between uses strict bounds, but often you want some of the bounds to be non-strict. You have this flexibility with filter: xs.filter(x => x >= 0 && x < 42).

julienrf avatar Nov 29 '21 11:11 julienrf

Hi @julienrf, yes, the function is very simple, it will not add anything that you couldn't do on your own with filter, but I would argue that it's more descriptive for the code reader. It can be just, you know, that one nice little thing that library has here for you if you want it there and could maybe lift the level of satisfaction overall, but that's IMO. For non-strict bounds, we can either have a different version of it like betweenInclusive or just let the user resort to filter if they are hitting some corner cases.

VladKopanev avatar Nov 29 '21 13:11 VladKopanev