Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Standard library

Standard library

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).

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 InstantRanges 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:

example
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)}
example
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) 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:

example
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)}
example
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/.."}

Markdown for agents: /docs/ranges.md.Built from Jadpo main at a707efb.