scala-collection-contrib
scala-collection-contrib copied to clipboard
Add `between`, `betweenBy` functions to Seq
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.
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).
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.