In Part 1, we covered BoxLang's first-class range system: lazy evaluation, exclusive boundaries, built-in types (integers, decimals, characters, dates), custom stepping, Java Stream integration, and contains() semantics. If you haven't read it yet, start there.
Part 2 answers a different question: what about your types?
What if you have a domain object that has a logical progression, and you want it to participate fully in BoxLang's range ecosystem? You want to iterate it with for..in, test membership with contains(), compose it with .stream().limit().map(), and use named units like "major" or "octave" as step arguments. BoxLang makes this possible through a single interface: ortus.boxlang.runtime.types.IRangeable.
The IRangeable Interface
Any BoxLang or Java class can become a first-class range type by implementing IRangeable. The surface area is intentionally small: three required methods and two optional ones.
| Method | Required | Purpose |
|---|---|---|
rangeAdvance( step ) | Yes | Return a new instance advanced by step positions |
rangeCompare( other ) | Yes | Compare to another instance (negative / zero / positive) |
rangeCoerce( val ) | Yes | Convert arbitrary values to your type for contains() |
rangeStepFromUnit( amount, unit ) | No | Map named units to a numeric step value (uniform stepping) |
rangeUnitStepper( unit ) | No | Return a closure for non-uniform stepping patterns |
The distinction between the two optional methods is important and we will come back to it. For now, let us build three complete examples that show the full range of what IRangeable can express.
Example 1: Fibonacci - Infinite Non-Linear Sequences
The Fibonacci sequence is deliberately chosen as the first example because it breaks the assumption that rangeAdvance(1) is a simple increment. Each Fibonacci value depends on the previous two, so the step produces values that grow exponentially. BoxLang's range system handles this naturally because it never assumes linearity.
class Fib implements="java:ortus.boxlang.runtime.types.IRangeable" {
property name="prev" type="integer" default=0;
property name="current" type="integer" default=1;
function rangeAdvance( step ) {
var result = this
for( var i = 1; i <= step; i++ ) {
result = new Fib(
prev : result.getCurrent(),
current : result.getPrev() + result.getCurrent()
)
}
return result
}
function rangeCompare( other ) {
return variables.current - other.getCurrent()
}
function rangeCoerce( val ) {
if( val instanceof "Fib" ) return val
if( isNumeric( val ) ) return new Fib( current : int( val ) )
return null
}
}
No rangeStepFromUnit(), no rangeUnitStepper(). Fibonacci only needs the three core methods. The class carries two fields so each instance knows how to produce the next value.
Lazy Infinite Sequence
Because Fibonacci is unbounded, you use a half-bounded range and stream operations:
// First 10 Fibonacci numbers
( new Fib().. ).stream().limit( 10 ).map( .getCurrent() ).toList()
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
// Every other Fibonacci number
( new Fib().. ).step( 2 ).stream().limit( 5 ).map( .getCurrent() ).toList()
// [1, 2, 5, 13, 34]
contains() as Sequence Membership
Because Fib implements IRangeable, the range knows that rangeAdvance(1) is not a simple increment. It walks the actual sequence to check reachability:
( new Fib().. ).contains( 13 ) // true - 13 is a Fibonacci number
( new Fib().. ).contains( 14 ) // false - 14 is not
( new Fib().. ).contains( 144 ) // true
( new Fib().. ).contains( 150 ) // false
rangeCoerce() converts the plain integer 13 into new Fib( current: 13 ), then the range walks the sequence 1, 1, 2, 3, 5, 8, 13 until a match is found or the target is exceeded.
for..in with Break
under100 = []
for( f in new Fib().. ) {
if( f.getCurrent() > 100 ) break
under100.append( f.getCurrent() )
}
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
The key insight: IRangeable types always use step-reachability for contains(). The range iterates through actual values produced by rangeAdvance() rather than doing a bounds check. This is essential for non-linear sequences where the distance between values is non-uniform.
Example 2: Roman Numerals
A Roman numeral class that stores an integer internally and converts to and from Roman notation. This example demonstrates bounded iteration, uniform stepping, and rangeCoerce() accepting multiple input types simultaneously.
class Roman implements="java:ortus.boxlang.runtime.types.IRangeable" {
property name="value" type="integer" default=0;
static {
VALUES = [ 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 ]
SYMBOLS = [ "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I" ]
}
function init( input ) {
variables.value = isNumeric( input ) ? int( input ) : static.fromRoman( input )
return this
}
static function fromRoman( s ) {
s = uCase( s )
return static.VALUES.reduce( ( acc, val, idx ) => {
var sym = static.SYMBOLS[ idx ]
while( mid( s, acc.pos, sym.len() ) == sym ) {
acc.result += val
acc.pos += sym.len()
}
return acc
}, { result : 0, pos : 1 } ).result
}
static function toRoman( num ) {
return static.VALUES.reduce( ( result, val, idx ) => {
var count = int( num / val )
num -= count * val
return result & repeatString( static.SYMBOLS[ idx ], count )
}, "" )
}
function toString() { return static.toRoman( variables.value ) }
function rangeAdvance( step ){ return new Roman( variables.value + step ) }
function rangeCompare( other ){ return variables.value - other.getValue() }
function rangeCoerce( val ) {
if( val instanceof "Roman" ) return val
if( isNumeric( val ) ) return new Roman( val )
if( isSimpleValue( val ) ) return new Roman( val )
return null
}
}
Iterating I through X
result = []
for( r in new Roman( "I" )..new Roman( "X" ) ) {
result.append( r.toString() )
}
// ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"]
Stepping by 2
result = []
for( r in ( new Roman( "I" )..new Roman( "X" ) ).step( 2 ) ) {
result.append( r.toString() )
}
// ["I", "III", "V", "VII", "IX"]
contains() Accepts Multiple Input Types
The rangeCoerce() method is what makes this work. The range can accept Roman instances, integers, or Roman numeral strings and coerce them all correctly:
range = new Roman( "I" )..new Roman( "C" ) // 1 to 100
// Roman instances
range.contains( new Roman( "V" ) ) // true
range.contains( new Roman( "D" ) ) // false - 500 > 100
// Raw integers
range.contains( 5 ) // true
range.contains( 500 ) // false
// Roman numeral strings
range.contains( "V" ) // true
range.contains( "D" ) // false
One rangeCoerce() implementation, three accepted input types. The range machinery handles the rest.
Example 3: Musical Notes with Unit Stepping
This is where IRangeable shows its full power. A Note class backed by a MIDI integer, supporting chromatic, whole-step, major-third, and octave stepping via rangeStepFromUnit(), plus non-uniform major and minor scale stepping via rangeUnitStepper().
class Note implements="java:ortus.boxlang.runtime.types.IRangeable" {
property name="midi" type="integer" default=60;
static {
NOTES = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ]
MAJOR = [ 2, 2, 1, 2, 2, 2, 1 ]
MINOR = [ 2, 1, 2, 2, 1, 2, 2 ]
}
function init( input ) {
variables.midi = isNumeric( input ) ? int( input ) : static.parseName( input )
return this
}
static function parseName( name ) {
name = uCase( name )
var hasSharp = name.len() > 2 && mid( name, 2, 1 ) == "#"
var notePart = hasSharp ? left( name, 2 ) : left( name, 1 )
var octave = int( right( name, name.len() - notePart.len() ) )
var idx = arrayFind( static.NOTES, notePart )
return ( octave + 1 ) * 12 + idx - 1
}
function toString() {
return static.NOTES[ variables.midi mod 12 + 1 ] & ( int( variables.midi / 12 ) - 1 )
}
function rangeAdvance( step ) { return new Note( variables.midi + step ) }
function rangeCompare( other ) { return variables.midi - other.getMidi() }
function rangeCoerce( val ) {
if( val instanceof "Note" ) return val
if( isNumeric( val ) ) return new Note( val )
if( isSimpleValue( val ) ) return new Note( val )
return null
}
// Uniform unit stepping: each step is the same fixed number of semitones
function rangeStepFromUnit( amount, unit ) {
switch( unit ) {
case "chromatic" : return amount
case "whole" : return amount * 2
case "third" : return amount * 4
case "octave" : return amount * 12
}
throw( message : "Unsupported unit: " & unit )
}
// Non-uniform stepping: step size varies by position in the scale
function rangeUnitStepper( unit ) {
if( unit != "major" && unit != "minor" ) return null
var root = variables.midi
var intervals = ( unit == "major" ) ? static.MAJOR : static.MINOR
var cumulative = [ 0 ]
for( var i = 1; i <= 7; i++ ) {
cumulative.append( cumulative[ i ] + intervals[ i ] )
}
return ( current, amount ) => {
var offset = current.getMidi() - root
var octaves = int( offset / 12 )
var remainder = offset mod 12
var degree = 0
for( var i = 2; i <= cumulative.len(); i++ ) {
if( cumulative[ i ] <= remainder ) degree = i - 1
}
var newDegree = octaves * 7 + degree + amount
var newOctaves = int( newDegree / 7 )
var newDegreeInOctave = newDegree mod 7
return new Note( root + ( newOctaves * 12 ) + cumulative[ newDegreeInOctave + 1 ] )
}
}
}
Understanding the Two Stepper Mechanisms
This is the architectural distinction that makes IRangeable expressive for music theory, but the principle applies to any domain:
rangeStepFromUnit() handles uniform steps where each advance is the same fixed size. "chromatic" is always 1 semitone. "octave" is always 12. The step count is constant regardless of where you are in the sequence.
rangeUnitStepper() handles non-uniform steps where the size varies by position. The major scale has the interval pattern [2, 2, 1, 2, 2, 2, 1] (whole, whole, half, whole, whole, whole, half). There is no single integer you can multiply by to get the next scale degree from any arbitrary position. So this method returns a closure that receives the current note and computes the correct next note based on scale degree within the root's interval pattern.
When you call .step( 1, "major" ), the range engine checks rangeUnitStepper() first. If it returns a closure, that closure drives iteration. If it returns null, the engine falls through to rangeStepFromUnit().
Chromatic and Uniform Stepping
// Chromatic: every semitone
result = []
for( n in ( new Note( "C4" )..new Note( "D4" ) ).step( 1, "chromatic" ) ) {
result.append( n.toString() )
}
// ["C4", "C#4", "D4"]
// Whole steps
result = []
for( n in ( new Note( "C4" )..new Note( "C5" ) ).step( 1, "whole" ) ) {
result.append( n.toString() )
}
// ["C4", "D4", "E4", "F#4", "G#4", "A#4", "C5"]
// Major thirds
result = []
for( n in ( new Note( "C4" )..new Note( "C6" ) ).step( 1, "third" ) ) {
result.append( n.toString() )
}
// ["C4", "E4", "G#4", "C5", "E5", "G#5", "C6"]
// Octaves
result = []
for( n in ( new Note( "C4" )..new Note( "C7" ) ).step( 1, "octave" ) ) {
result.append( n.toString() )
}
// ["C4", "C5", "C6", "C7"]
Non-Uniform Scale Stepping
The major scale has irregular intervals. rangeUnitStepper() handles this without the caller needing to know anything about interval patterns:
// C Major scale
result = []
for( n in ( new Note( "C4" )..new Note( "C5" ) ).step( 1, "major" ) ) {
result.append( n.toString() )
}
// ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
// A Natural Minor scale
result = []
for( n in ( new Note( "A3" )..new Note( "A4" ) ).step( 1, "minor" ) ) {
result.append( n.toString() )
}
// ["A3", "B3", "C4", "D4", "E4", "F4", "G4", "A4"]
Half-Bounded Range + Stream
A half-bounded range with limit() lets you collect exactly one octave from any starting note without specifying an end:
chromaticScale = ( new Note( "C4" ).. ).step( 1, "chromatic" )
.stream()
.limit( 13 )
.map( n => n.toString() )
.toList()
// ["C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C5"]
No end note needed. Start here, give me 13 chromatic notes.
Step-Reachability in contains()
On a stepped range, contains() checks whether the value is produced by the stepper, not just within bounds:
// Major thirds from C4 to C5 produce: C4, E4, G#4, C5
thirdRange = ( new Note( "C4" )..new Note( "C5" ) ).step( 1, "third" )
thirdRange.contains( "C4" ) // true
thirdRange.contains( "E4" ) // true
thirdRange.contains( "G#4" ) // true
thirdRange.contains( "D4" ) // false - within bounds but not reachable by thirds
thirdRange.contains( "F4" ) // false
thirdRange.contains( 62 ) // false - D4 as MIDI; coercion still works, still unreachable
// Half-bounded octave reachability
( new Note( "C4" ).. ).step( 1, "octave" ).contains( "C6" ) // true
( new Note( "C4" ).. ).step( 1, "octave" ).contains( "G7" ) // false - octaves only hit C notes
// Scale membership
( new Note( "C4" ).. ).step( 1, "major" ).contains( "F#5" ) // false - not in C major
( new Note( "C4" ).. ).step( 1, "major" ).contains( "F5" ) // true
Implementation Checklist
When implementing IRangeable on your own class, work through these five methods in order:
-
rangeAdvance( step )- Return a new instance advanced bysteppositions. Never mutatethis. This is your "what comes next" generator. -
rangeCompare( other )- Return negative/zero/positive like Java'scompareTo(). This defines ordering: what "less than" and "greater than" mean for your type. Used for bounds checks and iteration termination. -
rangeCoerce( val )- Convert foreign values to your type. Returnnullif the value cannot be converted. This is what letscontains()accept integers, strings, or whatever else makes sense as input for your type. -
rangeStepFromUnit( amount, unit )(optional) - Convert named unit strings to a numeric step value. Implement this when your type supports unit-based stepping where the step size is constant (every advance moves the same distance). -
rangeUnitStepper( unit )(optional) - Return a closure( current, amount ) => nextValuefor stepping patterns where the interval varies by position. Returnnullto fall through torangeStepFromUnit(). Implement this for irregular sequences like scales, non-uniform grids, or anything where "the next step" depends on where you currently are.
What This Unlocks
IRangeable transforms ranges from a language feature into an extensible framework for modeling real-world sequences. The same five-method interface that describes Fibonacci numbers and musical scales works equally well for:
- Software versions -
1.0.0..2.0.0iterating over semver patches, then minors, then majors with"minor"and"major"step units - Fiscal periods - Quarter ranges with
"quarter"and"year"stepping across a custom fiscal calendar - Workflow states - Ordered state machine transitions where
contains()tells you whether a state is reachable from a given starting point - Geographic coordinates - Grid traversal with
rangeStepFromUnit()mapping degrees, minutes, and seconds - Inventory SKUs - Product catalog ranges where
rangeCoerce()accepts both internal IDs and human-readable codes
Any domain concept with a logical ordering and a progression rule can become a first-class BoxLang range type. Your domain objects participate in iteration, streaming, membership testing, and clamping exactly the same way integers and dates do.
Resources
- BoxLang Ranges Documentation
- BoxLang Ranges Part 1: Supercharged
- BoxLang 1.14.0 Release Notes
- ForgeBox - BoxLang modules and packages
BoxLang is a professional open-source product. The core is free under Apache 2.0. BoxLang+ and BoxLang++ unlock premium modules, priority support, and enterprise features. Explore plans at boxlang.io/plans.
Add Your Comment