# Ranges

A range is a span of values between a start and an end, such as a
booking's hours or a band of cake sizes. `Range<T>` holds a `start` and
an `end` of one ordered type, and either end may be `none` for an open
end. Two standard ranges say once, in their type, whether they hold
their end: a `DateRange` includes its end date, and an `InstantRange`
excludes its end instant.

A range's start is never after its end, a rule named
`start_not_after_end`. So a range built from values is proved by a
condition or built under `attempt`, as an object's rule across its
fields is ([Rules across a type's parts](every-standard-type-meets-one-contract.md#rules-across-a-types-parts)).

Ranges are stored as PostgreSQL's range types (`daterange`,
`tstzrange`, `numrange`, and `int8range` for whole numbers and
durations), so their SQL forms are the range operators. A range that
holds nothing, because its start is its end, isn't stored: PostgreSQL
keeps every such range as `empty`, without its ends, so storing one is a
fault rather than losing where it was.

`T` must be a type `<` orders: one built on `Int`, `Decimal`, `Instant`,
`CalendarDate` or `Duration`. `Text` isn't one, because `<` doesn't take
it. Any other type is `type.ordering_operand`, reported where the type is
written or where a literal's ends give it. `Range` with no type argument
is `type.type_argument_count`.

A `Range<T>` holds its start and not its end, as `InstantRange` does and
as PostgreSQL's canonical ranges do. An open end holds everything on its
side, so a range with two open ends holds every value. Two ranges that
only meet, one's end being the other's start, don't overlap. A range
whose start is its end holds nothing, so `range.overlaps` is `false` and
`range.intersection` is `none` for it, as PostgreSQL's `&&` and
`NULLIF(a * b, 'empty')` give.

| Operation | Gives | Fails | SQL form |
|---|---|---|---|
| `range.contains(range, value)` | `Bool`: `value` is inside, by the range's own end rule | | `x @> a` |
| `range.overlaps(a, b)` | `Bool`: the two share at least one value | | `a && b` |
| `range.intersection(a, b)` | `Range<T>?`, the shared part; `none` when they don't overlap | | `NULLIF(a * b, 'empty')` |

The three operations take a `Range<T>`, an `InstantRange` or a
`DateRange`, which holds its end date. The ranges in one call are all of
one range type, since each type keeps its own rule for its end: an
`InstantRange` and a `Range<Instant>` don't mix (`type.mismatch`). A
range an operation gives is of that same type, so `range.intersection`
of two `InstantRange`s is an `InstantRange?`. `range.contains` takes a
value built on the range's item type.

An `InstantRange` written as an object literal has its two fields
checked as an object's fields declared `Instant` are, so a value of a
named instant type, such as `Moment`, is a mismatch there. A value of the
type, or a field declared `InstantRange`, has its `start` and `end` read
through it (`booking.slot.start`). An `InstantRange` holds every instant
from its start up to, but not including, its end. So two bookings that
meet at the hour don't overlap, and one that starts and ends at the same
instant holds nothing:

```jadpo
type Booking = Object {
    bench: BenchName
    slot: InstantRange
}

type BenchName = Text {
    min_length: 1
}

type BookingTime = Instant {}

type Booked = Bool {}

type DoubleBooked = Bool {}

/// Whether the bench is booked at `at`.
function booked_at(booking: Booking, at: BookingTime) -> Booked {
    return Booked(range.contains(booking.slot, at))
}

/// Whether two bookings of one bench double_booked.
function double_booked(first: Booking, second: Booking) -> DoubleBooked {
    return DoubleBooked(first.bench == second.bench and range.overlaps(first.slot, second.slot))
}

/// The time two bookings share, if any.
function common_time(first: Booking, second: Booking) -> InstantRange? {
    return range.intersection(first.slot, second.slot)
}
```

```jadpo
test "ranges include their start and exclude their end" {
    var morning: Booking = Booking {
        bench: "Oven bench"
        slot: InstantRange {
            start: "2026-10-26T09:00:00Z"
            end: "2026-10-26T12:00:00Z"
        }
    }
    var lunch: Booking = Booking {
        bench: "Oven bench"
        slot: InstantRange {
            start: "2026-10-26T11:00:00Z"
            end: "2026-10-26T13:00:00Z"
        }
    }
    var afternoon: Booking = Booking {
        bench: "Oven bench"
        slot: InstantRange {
            start: "2026-10-26T12:00:00Z"
            end: "2026-10-26T17:00:00Z"
        }
    }
    var nothing: Booking = Booking {
        bench: "Oven bench"
        slot: InstantRange {
            start: "2026-10-26T10:00:00Z"
            end: "2026-10-26T10:00:00Z"
        }
    }
    var lunch_overlap: InstantRange = InstantRange {
        start: "2026-10-26T11:00:00Z"
        end: "2026-10-26T12:00:00Z"
    }
    assert booked_at(morning, "2026-10-26T09:00:00Z")
    assert not booked_at(morning, "2026-10-26T12:00:00Z")
    assert double_booked(morning, lunch)
    assert not double_booked(morning, afternoon)  // they meet at noon
    assert not double_booked(morning, nothing)
    assert common_time(morning, lunch) == lunch_overlap
    assert common_time(morning, afternoon) == none
}
```

Write a `Range<T>` as `Range { start: ..., end: ... }`, giving both
ends, with `none` for an open one. Where a `Range<T>` is expected, its
ends are checked as `T?`. Elsewhere, its first end that isn't `none`
gives `T`. Read its ends as `range.start` and `range.end`, each a `T?`.

- **Text.** A range's canonical text is each end's canonical text joined
  by `/`, with an open end written `..`, as ISO 8601-2 writes one
  (`2026-10-01/..`). A start after its end breaks
  `start_not_after_end`.
- **JSON** is an object of `start` and `end`, with `null` for an open
  end.
- **For people,** `text.format` shows each end as its item type is shown,
  with the item type's options, joined by ` – `, and an open end as `…`.
  A range of numbers shows each end as a number is shown
  ([Numbers](numbers.md)) and takes no options.
- **Reading text.** `text.parse(raw, as: Range<T>)` reads a range of a
  standard type, or of a representation written as itself (`Range<Int>`,
  `Range<CalendarDate>`). A range of a type with rules of its own, such
  as `Range<LoafWeight>`, is `type.mismatch`, since nothing would check
  those rules on its ends.
- **No order, no shape.** A `Range<T>` isn't ordered.
  `type.standard_exists`, the error for an app type with a standard
  type's shape, matches no shape for it, since `InstantRange` already
  claims a start and an end.

Bands of cake sizes in slices, one with no upper limit:

```jadpo
type SliceCount = Int {
    min: 0
}

type CakeName = Text {
    min_length: 1
}

type Cake = Object {
    name: CakeName
    slices: SliceCount
}

type InBand = Bool {}

/// The cakes sold as large: 16 slices and up, with no upper limit.
function large() -> Range<SliceCount> {
    return Range {
        start: 16
        end: none
    }
}

/// Whether a cake's slice count is in `band`.
function in_band(cake: Cake, band: Range<SliceCount>) -> InBand {
    return InBand(range.contains(band, cake.slices))
}

/// The slice counts two bands share, if any.
function common_weights(first: Range<SliceCount>, second: Range<SliceCount>) -> Range<SliceCount>? {
    return range.intersection(first, second)
}
```

```jadpo
test "a range of any ordered type, either end open" {
    var medium: Range<SliceCount> = Range {
        start: 8
        end: 16
    }
    var small: Range<SliceCount> = Range {
        start: none
        end: 10
    }
    var both: Range<SliceCount> = Range {
        start: 8
        end: 10
    }
    var rye: Cake = Cake {
        name: "Rye"
        slices: 16
    }
    assert in_band(rye, large())
    assert not in_band(rye, medium)  // a range doesn't hold its end
    assert common_weights(medium, large()) == none  // they meet at 16
    assert common_weights(medium, small) == both
    assert range.overlaps(small, medium)
    assert text.from(large()) == "16/.."
}
```
