-
Notifications
You must be signed in to change notification settings - Fork 3
/
Window.scala
156 lines (121 loc) · 5.08 KB
/
Window.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package com.rallyhealth.vapors.v1
package data
import cats.data.Ior
import cats.{Order, Show}
import scala.collection.immutable.NumericRange
/**
* Represents an open or closed range with either 1 or 2 boundaries.
*
* A window can be bounded from below or above or both, and it can tell if a value is contained within
* those boundaries.
*/
sealed trait Window[+A] {
def bounds: Option[Ior[Bounded.Above[A], Bounded.Below[A]]]
final def mapBothBounds[B >: A : Order](
fnLowerBound: A => B,
fnUpperBound: A => B,
): Window[B] =
bounds.fold(Window.empty[B]) { known =>
Window.fromBounds {
known.bimap(_.map(fnLowerBound), _.map(fnUpperBound))
}
}
final def mapUpperBound[B >: A : Order](fn: A => B): Window[B] =
mapBothBounds(identity, fn)
final def mapLowerBound[B >: A : Order](fn: A => B): Window[B] =
mapBothBounds(fn, identity)
}
object Window {
import Bounded._
import cats.syntax.order._
@inline final def empty[A]: Window[A] = Empty
private final case object Empty extends Window[Nothing] {
override def bounds: Option[Ior[Above[Nothing], Below[Nothing]]] = None
}
implicit final class IWindowOps[A](private val window: Window[A]) extends AnyVal {
def contains(value: A): Boolean = window match {
case Empty => false
case knownWindow: KnownWindow[A] => knownWindow.contains(value)
}
}
def showWindowWithTerm[A : Show](term: String): Show[Window[A]] = Show.show {
case Empty => "Window.Empty"
case KnownWindow(bounds) =>
val (op1, op2) = bounds
.bimap(
b => (if (b.inclusiveLowerBound) ">=" else ">", ""),
b => ("", if (b.inclusiveUpperBound) "<=" else "<"),
)
.merge
val showA: A => String = Show[A].show
bounds match {
case Ior.Left(lb) => s"$term $op1 ${showA(lb.lowerBound)}"
case Ior.Right(ub) => s"$term $op2 ${showA(ub.upperBound)}"
case Ior.Both(lb, ub) =>
s"Window[$term $op1 ${showA(lb.lowerBound)} and $term $op2 ${showA(ub.upperBound)}]"
}
}
implicit def showWindow[A : Show]: Show[Window[A]] = showWindowWithTerm("x")
def fromBounds[A : Order](bounds: Ior[Bounded.Above[A], Bounded.Below[A]]): Window[A] = KnownWindow(bounds)
def fromRange(range: Range): Window[Int] =
between(range.start, includeMin = true, range.end, includeMax = range.isInclusive)
def fromRange[A : Order](range: NumericRange[A]): Window[A] =
between(range.start, includeMin = true, range.end, includeMax = range.isInclusive)
def equalTo[A : Order](value: A): Window[A] =
betweenInclusive(value, value)
def lessThan[A : Order](
max: A,
inclusive: Boolean,
): Window[A] = KnownWindow(Ior.Right(Below(max, inclusive)))
/** (-infinity, max) */
def lessThan[A : Order](max: A): Window[A] = lessThan(max, inclusive = false)
/** (-infinity, max] */
def lessThanOrEqual[A : Order](max: A): Window[A] = lessThan(max, inclusive = true)
/** Generally defined range with configurable open or lower bound. */
def greaterThan[A : Order](
min: A,
inclusive: Boolean,
): Window[A] = KnownWindow(Ior.Left(Above(min, inclusiveLowerBound = inclusive)))
/** (min, +infinity) */
def greaterThan[A : Order](min: A): Window[A] = greaterThan(min, inclusive = false)
/** [min, +infinity) */
def greaterThanOrEqual[A : Order](min: A): Window[A] = greaterThan(min, inclusive = true)
/** Generally defined bounded range with the ability to set open or closed at each end of the range.*/
def between[A : Order](
min: A,
includeMin: Boolean,
max: A,
includeMax: Boolean,
): Window[A] =
KnownWindow(Ior.Both(Above(min, includeMin), Below(max, includeMax)))
/** [min,max) */
def between[A : Order](
min: A,
max: A,
): Window[A] = between(min, includeMin = true, max, includeMax = false)
/** [min,max] */
def betweenInclusive[A : Order](
min: A,
max: A,
): Window[A] = between(min, includeMin = true, max, includeMax = true)
private final case class KnownWindow[A : Order](knownBounds: Ior[Above[A], Below[A]]) extends Window[A] {
override def bounds: Option[Ior[Above[A], Below[A]]] = Some(knownBounds)
private val checkBetween: A => Boolean = knownBounds match {
case Ior.Left(lb) if lb.inclusiveLowerBound => _ >= lb.lowerBound
case Ior.Left(lb) => _ > lb.lowerBound
case Ior.Right(ub) if ub.inclusiveUpperBound => _ <= ub.upperBound
case Ior.Right(ub) => _ < ub.upperBound
case Ior.Both(lb, ub) if lb.lowerBound == ub.upperBound && (lb.inclusiveLowerBound || ub.inclusiveUpperBound) =>
a => a == lb.lowerBound
case Ior.Both(lb, ub) if lb.inclusiveLowerBound && ub.inclusiveUpperBound =>
a => a >= lb.lowerBound && a <= ub.upperBound
case Ior.Both(lb, ub) if ub.inclusiveUpperBound =>
a => a > lb.lowerBound && a <= ub.upperBound
case Ior.Both(lb, ub) if lb.inclusiveLowerBound =>
a => a >= lb.lowerBound && a < ub.upperBound
case Ior.Both(lb, ub) =>
a => a > lb.lowerBound && a < ub.upperBound
}
def contains(value: A): Boolean = checkBetween(value)
}
}