Ranges
Lazy, typed, iterable intervals with exclusive boundaries and custom stepping
BoxLang Ranges are first-class interval objects supporting lazy iteration, multiple element types, exclusive boundaries, custom stepping, and the IRangeable interface for custom types. Ranges are not arrays — they're lightweight objects that generate values on demand.
🚀 The Basics
Range as a First-Class Object
Ranges are created with the .. operator and produce a Range object:
myRange = 1..5
// Range object — NOT immediately an arrayRanges are iterable and seamlessly coerce to arrays when needed:
result = arrayToList( 1..5, "," ) // "1,2,3,4,5"
result = arrayLen( 1..10 ) // 10But they're not arrays — they're lightweight objects that generate values on demand. This means huge and even infinite ranges are cheap to create and use.
🔢 Supported Types
Integers and Decimals
Integer and decimal ranges work naturally with automatic direction detection:
1..5 // 1, 2, 3, 4, 5
3.5..1.5 // 3.5, 2.5, 1.5 (descending auto-detected)
(0..1).step(0.25) // 0, 0.25, 0.50, 0.75, 1.00Characters
Single-character strings produce character ranges:
DateTime
DateTime ranges iterate by day by default and support unit-based stepping:
Any Comparable Type (Contains-Only)
Any two values of the same type that are naturally comparable can form a range — even if there's no way to iterate between them. Multi-character strings are a good example: they have a well-defined lexicographic ordering, so contains() works, but there's no natural "next" value after "foo":
This works with any Java class that implements Comparable — including JDK types:
Attempting to iterate a non-iterable range throws an error:
This pattern works for any type with a natural ordering — use it as a bounds check without needing to enumerate values.
Custom Types via IRangeable
Any BoxLang or Java class can participate in ranges by implementing the ortus.boxlang.runtime.types.IRangeable interface. See Custom Rangeable Types below for complete details.
🌐 Unbounded and Half-Bounded Ranges
Ranges can be open on one or both ends:
Half-bounded ranges are lazy — you can iterate them with a break condition:
Open-start and fully-open ranges cannot be iterated (no starting point):
But they still support contains():
Typed Unbounded Ranges
A fully unbounded range (..) contains everything non-null by default. Use .type() to constrain it to a specific type — leveraging BoxLang's loose casting system:
Any BoxLang type name works — string, numeric, integer, boolean, date, array, struct, etc. For custom classes, the instanceof operator is used:
For exact Java class matching with no coercion, pass a Class reference directly:
🔀 Exclusive Boundaries
Four boundary modes via operators:
These affect both iteration and contains():
🪜 Custom Stepping
The step() method returns a new range (copy-on-write) with a custom step:
Unit-based stepping for DateTime and custom IRangeable types:
🌊 Lazy Iteration and Streaming
Ranges are never materialized into memory unless you ask. This means huge and even infinite ranges are cheap:
Full Java Stream API integration:
Use map, filter, takeWhile, anyMatch, limit, and all other stream operations.
🔍 Contains Semantics
Simple ranges (step = 1, no unit): Bounds Check
Stepped ranges (step > 1 or unit): Step-Reachability Check
When a range has a custom step, contains() verifies the value is actually reachable by the stepper — not just within bounds:
This follows the Python/Kotlin convention where a stepped range represents a discrete set.
For half-bounded stepped ranges, the iteration stops once the target is exceeded — so this is safe and terminates:
Non-iterable ranges: Always Bounds Check
Ranges that cannot be iterated (no start value, or no stepper) always fall back to a simple bounds check:
Range-in-Range Contains
You can check if an entire range fits within another:
🗜️ Clamping Values
The clamp() method snaps a value to the closest boundary if it falls outside the range:
Half-bounded ranges snap to the one bound that exists:
Fully unbounded ranges just type-check and return the value:
📍 Position Checks
Check whether a value falls before or after a range without getting a full contains() answer:
Exclusive boundaries are respected:
Incompatible types return false (same as contains()):
📋 Member Methods
Query Methods
contains(value)
Check if value (or inner range) is within this range
isValueBefore(value)
True if value is before the low boundary
isValueAfter(value)
True if value is after the high boundary
isEmpty()
True if iteration would produce zero elements
isAscending()
True if step is positive
isIterable()
True if the range can be iterated
isBounded()
True if both start and end are present
isUnbounded()
True if both start and end are absent
isHalfBounded()
True if exactly one bound is present
hasFrom() / hasTo()
Check individual bounds
Type Checking
isRange(value)
BIF to test if a value is a Range instance
Accessors
getFrom() / getTo()
Get bound values
getStep()
Get current step value
Transformation Methods
clamp(value)
Snap value to the closest boundary if out of bounds
type(typeName)
New range constrained to a BoxLang type (uses casters for coercion)
type(class)
New range constrained to an exact Java class (strict instanceof)
step(n)
New range with numeric step
step(n, unit)
New range with unit-based step
asc()
Force ascending (empty if already descending)
desc()
Force descending (empty if already ascending)
Conversion Methods
toArray()
Materialize to array (requires bounded + iterable)
stream()
Get a Java Stream for functional pipelines
toString()
Human-readable representation
equals(other)
Structural equality (bounds, step, exclusivity)
✅ Empty Ranges and Truthiness
Ranges are truthy if non-empty, falsy if empty:
🔢 Operator Precedence
Arithmetic binds tighter than the range operator:
You can use any expression as operands:
📝 Copy-on-Write Semantics
All modifier methods return new Range instances — the original is never mutated:
⚠️ Error Cases
These throw runtime errors:
🧩 Custom Rangeable Types: IRangeable
Any Java or BoxLang class can become rangeable by implementing ortus.boxlang.runtime.types.IRangeable. You need to provide:
rangeAdvance( step )— Return a new instance advanced bysteppositionsrangeCompare( other )— Compare to another instance (negative = less, 0 = equal, positive = greater)rangeCoerce( val )— Convert arbitrary values to your type forcontains()checksrangeStepFromUnit( amount, unit )— (Optional) Convert a unit-based step to a numeric steprangeUnitStepper( unit )— (Optional) Return a custom stepper closure for non-uniform stepping
Example 1: Fibonacci — Infinite Non-Linear Sequences
Not all sequences advance linearly. The Fibonacci sequence is a perfect example — each value depends on the previous two, so rangeAdvance(1) produces values that grow exponentially. BoxLang's range system handles this naturally.
Usage:
Example 2: Roman Numerals
A Roman numeral class that stores an integer internally and converts to/from Roman notation. This demonstrates iteration, stepping, and coercion-based contains.
Usage:
Example 3: Musical Notes with Unit Stepping
This is where it gets interesting. A Musical Note class that supports chromatic, whole-step, major-third, octave, major scale, and minor scale stepping — plus stream composition for infinite sequences.
Usage:
Implementation Checklist
rangeAdvance( step )— Return a new instance moved bysteppositions. This is your "next value" generator.rangeCompare( other )— Return negative/zero/positive like Java'scompareTo(). This defines ordering for contains checks and iteration bounds.rangeCoerce( val )— Convert foreign values to your type. Returnnullif the value can't be converted. This enablescontains()to accept multiple input types.rangeStepFromUnit( amount, unit )(optional) — Convert named units to numeric step values. Only needed if your type supports unit-based stepping with uniform intervals.rangeUnitStepper( unit )(optional) — Return a closure(current, amount) => nextValuefor non-uniform stepping patterns. Returnnullto fall through torangeStepFromUnit().
Key Insight: The key insight is that IRangeable types always use step-reachability for contains() — the range iterates through actual values produced by rangeAdvance() rather than doing a simple bounds check. This is essential for sequences like Fibonacci where the "distance" between values is non-uniform.
📚 Additional Resources
BoxLang Ranges: Supercharged — Community blog post covering all range features
BoxLang Ranges Part 2: Custom Types with IRangeable — Deep dive into
IRangeablewith complete examples
Last updated
Was this helpful?
