Intervals
An interval is defined by two points {a-, a+}, representing its start and end on a linear domain D. An interval is a convex set that contains all elements between a- and a+.
For each element of the domain D, there is only one successor and predecessor, with no other elements in-between.
A pair {a-, a+} ∈ D × D corresponds to a point on a two-dimensional plane.
Using the successor, succ(), and predecessor, pred() functions to get the next and previous elements, we define different types of intervals:
- Closed interval
[a-, a+]is represented as[a-, a+]. - Right-open interval
[a-, a+)is represented as[a-, pred(a+)]. - Left-open interval
(a-, a+]is represented as[succ(a-), a+]. - Open interval
(a-, a+)is represented as[succ(a-), pred(a+)].
We consider an interval to be in canonical form when represented as a closed interval.
Classification
A pair {a-, a+} represents a non-empty interval if a- ≤ a+; otherwise, the interval is empty.
If the left boundary a- is equal to the right boundary a+, forming a single value, it is called a degenerate interval or point.
A non-empty interval is proper if a- < a+.
A proper interval is bounded if it is both left- and right-bounded, and unbounded otherwise.
On the diagram above, proper intervals are represented as points above the line a+ = a-, point intervals lie on the line a+ = a-, and empty intervals are below the line.
- Empty:
a- > a+ - Point:
{x} = {x | a- = x = a+} - Proper:
a- < a+- Bounded:
- Open:
(a-, a+) = {x | a- < x < a+} - Closed:
[a-, a+] = {x | a- ≤ x ≤ a+} - Left-closed, right-open:
[a-, a+) = {x | a- ≤ x < a+} - Left-open, right-closed:
(a-, a+] = {x | a- < x ≤ a+}
- Open:
- Left-bounded, right-unbounded:
- Left-open:
(a-, +∞) = {x | x > a-} - Left-closed:
[a-, +∞) = {x | x ≥ a-}
- Left-open:
- Left-unbounded, right-bounded:
- Right-open:
(-∞, a+) = {x | x < a+} - Right-closed:
(-∞, a+] = {x | x ≤ a+}
- Right-open:
- Unbounded:
(-∞, +∞)
- Bounded:
Creation
To create an interval, use one of the factory methods:
Interval.empty // ∅ = (+∞, -∞)
Interval.point(5) // {5}
Interval.open(1, 5) // (1, 5)
Interval.closed(1, 5) // [1, 5]
Interval.leftClosedRightOpen(1, 5) // [1, 5)
Interval.leftOpenRightClosed(1, 5) // (1, 5]
Interval.leftOpen(1) // (1, +∞)
Interval.leftClosed(5) // [5, +∞)
Interval.rightOpen(1) // (-∞, 1)
Interval.rightClosed(5) // (-∞, 5]
Interval.unbounded // (-∞, +∞)
Interval.open(Some(1), Some(5)) // (1, 5)
Interval.open(Some(1), None) // (1, +∞)
Interval.open(None, Some(5)) // (-∞, 5)
Interval.open(None, None) // (-∞, +∞)
Interval.closed(Some(1), Some(5)) // [1, 5]
Interval.closed(Some(1), None) // [1, +∞)
Interval.closed(None, Some(5)) // (-∞, 5]
Interval.closed(None, None) // (-∞, +∞)
Operations
Given an interval a, you can check its type using:
a.isEmpty: check if the interval is empty.a.isPoint: check if the interval is a point.a.isProper: check if the interval is proper.
Alternatively, use the negations a.nonEmpty, a.nonPoint, and a.nonProper.
Interval.open(1, 5).isEmpty // false
Interval.open(1, 5).isProper // true
Interval.open(1, 5).isPoint // false
Canonical
a.canonical produces the canonical form of an interval, where both boundaries are closed.
A closed interval remains unchanged in its canonical form.
Interval.open(1, 5).canonical // (1, 5) -> [2, 4]
Interval.closed(1, 5).canonical // [1, 5] -> [1, 5]
Swap
a.swap swaps the left and right boundaries of an interval, converting a non-empty interval into an empty interval or vice versa.
Interval.closed(1, 5).swap // [1, 5] -> [5, 1]
Inflate
a.inflate inflates an interval, extending its size: [a-, a+] -> [pred(a-), succ(a+)].
Interval.closed(1, 2).inflate // [1, 2] -> [0, 3]
In addition, you can use a.inflateLeft and a.inflateRight to extend the left and right boundaries independently.
Deflate
a.deflate reduces the size of an interval: [a-, a+] -> [succ(a-), pred(a+)].
Note: After deflation, an interval may become empty.
Interval.closed(1, 2).deflate // [1, 2] -> [2, 1]
Similarly, a.deflateLeft and a.deflateRight reduce the left and right boundaries independently.
Show
Use .toString to represent an interval in a human-readable form:
val a = Interval.empty
val b = Interval.point(5)
val c = Interval.proper(None, true, Some(2), false)
a.toString // ∅
b.toString // {5}
c.toString // [-∞,2)
Display
A collection of intervals can be displayed using ASCII or Mermaid diagrams.
ASCII
val a = Interval.closed(3, 7)
val b = Interval.closed(10, 15)
val c = Interval.closed(12, 20)
val renderer = AsciiRenderer.make()
val diagram = Diagram
.empty
.withSection
renderer.render(diagram)
println(renderer.result)
The output will be:
[*******] | [3,7] : a
[**********] | [10,15] : b
[***************] | [12,20] : c
--+-------+-----+----+-----+---------+-- |
3 7 10 12 15 20 |
Canvas size and theme can be customized. See the examples directory for more details.
Mermaid
Only Date/Time intervals are supported for Mermaid diagrams.
val t1 = LocalTime.parse("04:00")
val t2 = LocalTime.parse("10:00")
val t3 = LocalTime.parse("08:00")
val t4 = LocalTime.parse("20:00")
val a = Interval.closed(t1, t2)
val b = Interval.closed(t3, t4)
val renderer = MermaidRenderer.make
val diagram = Diagram
.empty
.withTitle("My Mermaid Diagram")
.withSection
renderer.render(diagram)
println(renderer.result)
This will produce a Mermaid diagram:
gantt
title My Mermaid Diagram
dateFormat HH:mm:ss.SSS
axisFormat %H:%M:%S
section My Section
a :04:00:00.000, 10:00:00.000
b :08:00:00.000, 20:00:00.000
This can be rendered using the Mermaid Live Editor.
Domain
To work with intervals, a given instance of Domain[T] is required. It is provided by default for integral and date-type types.
A custom domain can be defined (example) for specific types or constructed using a family of make functions (example).
Ordering
Intervals can be ordered as follows:
- If
a- < b+thena < b. - If
a- == b+, then:- If
a+ < b+, thena < b. - If
a+ == b+, thena == b. - Otherwise,
a > b.
- If
- Otherwise,
a > b.
val a = Interval.closed(0, 10) // [0, 10]
val b = Interval.closed(20, 30) // [20, 30]
List(b, a).sorted // List(a, b) // [0, 10], [20, 30]