range

SS: Python's alike range. The semantic should be exactly the same (and seems the more sensible for Tcl also).

Code:

 # RangeLen(start, end, step)
 # 1. if step = 0
 # 2.     then ERROR
 # 3. if start = end
 # 4.     then return 0
 # 5. if step > 0 AND start > end
 # 6.     then ERROR
 # 7. if setp < 0 AND end > start
 # 8.     then ERROR
 # 9. return 1+((ABS(end-start)-1)/ABS(step))
 proc rangeLen {start end step} {
    if {$step == 0} {return -1}
    if {$start == $end} {return 0}
    if {$step > 0 && $start > $end} {return -1}
    if {$step < 0 && $end > $start} {return -1}
    expr {1+((abs($end-$start)-1)/abs($step))}
 }

 # Range(start, end, step)
 # 1. result <- EMPTY LIST
 # 2. len <- RangeLen(start, end, step)
 # 3. for i <- 0 to len - 1
 # 4.     result.append(start+(i*step))
 # 6. return result
 proc range args {
    # Check arity
    set l [llength $args]
    if {$l == 1} {
        set start 0
        set step 1
        set end [lindex $args 0]
    } elseif {$l == 2} {
        set step 1
        foreach {start end} $args break
    } elseif {$l == 3} {
        foreach {start end step} $args break
    } else {
        error {wrong # of args: should be "range ?start? end ?step?"}
    }

    # Generate the range
    set rlen [rangeLen $start $end $step]
    if {$rlen == -1} {
        error {invalid (infinite?) range specified}
    }
    set result {}
    for {set i 0} {$i < $rlen} {incr i} {
        lappend result [expr {$start+($i*$step)}]
    }
    return $result
 }

# Test + Examples:
 catch {console show}

 puts [range 10]                    ;# => 0 1 2 3 4 5 6 7 8 9
 puts [range 10 20]                 ;# => 10 11 12 13 14 15 16 17 18 19
 puts [range 10 20 2]               ;# => 10 12 14 16 18
 puts [range 20 10 -2]              ;# => 20 18 16 14 12

I think the Tcl core should include the range command as proposed in my reference implementation (that's semantically equivalent to Python's) for the following reasons:

  • Sequences of natural numbers are often useful in programming, so the command is of very general use.
  • It may become idiomatic to write: foreach x [range 0 10] instead of for {set x 0} {$x < 10} {incr x} for small loops. It's much clear.
  • The Python semantic appears to be the best for languages where indexes are zero-based (Pascal noted it, and I think it's a smart observation).
  • If implemented in a smart way, range may speed-up Tcl loops compared to for. A smart implementation should cache small ranges and return the shared object for the next call.
  • At some point Tcl may include TIP 192 in order to make ranges used for loops to have a space complexity of O(1).

I'm ready to implement and TIP-fy it, but it's probably best to "sense" the Tclers ideas before to go forward. So, comments are very welcomed.

SS 23Oct2004: An implementation of this can be found at [L1 ]. Apply against HEAD.


RS: Here's a minimal variant that only does step-width 1:

 proc range {from to} {
    if {$to>$from} {concat [range $from [incr to -1]] $to}
 }
 % range 0 5
 0 1 2 3 4
 % range 1 6
 1 2 3 4 5

DKF: Extending is easy.

  proc range {from to {step 1}} {
     if {$to>$from} {concat $from [range [incr from $step] $to $step]}
  }

however this is inefficient. Better to do this:

  proc range {from to {step 1}} {
     set res $from; while {$to>$from} {lappend res [incr from $step]}; return $res
  }

or even this, adapting to work with negative steps...

  proc range {from to {step 1}} {
     set res $from
     while {$step>0?$to>$from:$to<$from} {lappend res [incr from $step]}
     return $res
  }

glennj This implementation matches the Python 2.6.2 version [L2 ]

    proc range args {
        foreach {start stop step} [switch -exact -- [llength $args] {
            1 {concat 0 $args 1}
            2 {concat   $args 1}
            3 {concat   $args  }
            default {error {wrong # of args: should be "range ?start? stop ?step?"}}
        }] break
        if {$step == 0} {error "cannot create a range when step == 0"}
        set range [list]
        while {$step > 0 ? $start < $stop : $stop < $start} {
            lappend range $start
            incr start $step
        }
        return $range
    }
    
    range 10        ;# ==> 0 1 2 3 4 5 6 7 8 9
    range 1 11      ;# ==> 1 2 3 4 5 6 7 8 9 10
    range 0 30 5    ;# ==> 0 5 10 15 20 25
    range 0 10 3    ;# ==> 0 3 6 9
    range 0 -10 -1  ;# ==> 0 -1 -2 -3 -4 -5 -6 -7 -8 -9
    range 0         ;# ==> empty
    range 1 0       ;# ==> empty

caroline I would like a range-generator in a new Tcl version, too. Meanwhile:

1) Make a massive integer-collection that suits your usage:

   set number_list ""
   for {set N 0} {$N <= 10000} {incr N} {
        lappend number_list $N
   }

2) ..then just query that list whenever you need a range:

   set range_2_to_14 [lrange $number_list 2 14]

LV When this topic first arose some time ago, the Python function was described to me as one in which the numbers were only generated as needed. That way, if one specified a range of a million, one got them, but didn't wait around for them or carry the penalty of the long list. That delayed generation of value functionality, generalized beyond integers, seems like something more useful for Tcl. It might be used for floats, alphas, etc. CL notes that Python has both range() and xrange(); only the latter is lazy.

SS: TIP 192 (lazy lists) covers exactly this aspect.

Sarnold: foriter - a loop command is also lazy, and saves 20% against for. Is someone interested having it into the core ?

 foriter i $start $end $step {...}

is the lazy version of:

 foreach i [range $start $end $step] {...}

See also Integer range generator, makeranges


BSW - 2012-07-02 21:04:54

I prefer that step be an absolute value for the step size and the direction of the steps be dependent upon the parameters passed to range. Thus, a range from 5 to 1 would be steps of -1 and a range from 1 to 5 would be steps of 1 if left with the default step size. This way if range is being used without knowing whether or not the first parameter will be smaller than the second it still produces a range.

 proc range {from to {step 1}} {
     set res $from
     if {$from > $to } { 
          set step [expr -1 * (abs($step))]
          set mode "decreasing"
     } else {
          set mode "increasing"
     }
     while {$step>0?$to>$from:$to<$from} {
          set incrCandidate [incr from $step]
          if {$mode == "decreasing" && $incrCandidate >= $to} {
               lappend res $incrCandidate
          } elseif { $mode == "increasing" && $incrCandidate <= $to} {
               lappend res $incrCandidate
          }
     }
     return $res
 }

 %range 1 5
 1 2 3 4 5

 
 %range 5 1
 5 4 3 2 1

ET: Here's a version of range, as described in a recent [email protected] discussion (jul 2022). This is being proposed for inclusion as a standard Tcl command. It is now TIP 629. https://core.tcl-lang.org/tips/doc/trunk/tip/629.md

This prototype supports 2 interfaces

  • Python style, with 1 to 3 integer only arguments
1-arg
range a ;# returns a list beginning with 0, up to but not including the integer a.
2-arg
range a b ;# returns a list beginning with a, up to but not including the integer b.
3-arg
range a b c ;# returns a list beginning with a, up to but not including the integer b stepping by c.
  • An operator style allows for several additional range operations

range start opcode end/num ?by? ?step-size?

Where opcode can be,

..includes the final one
toincludes the final one
..=includes the final one
..<does not include the final one
-countproduces a range with num items

The range values can also be floating point and if the start is greater than the ending value, it will create a descending range. Both values can accept simple expressions, similar to indices in commands such as [string index $string $index-1]

update 7/5/2022 - Due to problems with computing the number of elements beforehand, which failed with floating point step sizes, here's one that just uses lappend to build up the result. However, it can still get drift, and so the last element is not always correct. To get the result one wants, it is likely necessary to use the -count format with floats. The problem was that modular integer arithmetic doesn't work with floats. Perhaps this is why Python only permits integer arguments.

proc range {args} {
    lassign $args a op b by step
    if { $by eq "" } {
        set by by
    }
    if { $step eq "" } {
        set step 1
    }
    set l [llength $args]
    if       { $l < 1 } {
        error "missing args for range"
    } elseif { $l == 1 } { ;#python compatible
        if { $a <= 0 } {
            return {}
        }
        set b $a
        set a 0
        set op ..<
    } elseif { $l == 2 } {;#python compatible just 2 integers, from ..< to
        set b $op
        set op ..<
    } elseif { $l == 3 } {
        if { [string is integer $op] } {;#python compatible, all 3 must be integers
            set step $b
            set b $op
            set op ..<
            set by by
        }
    }
    set a [expr $a]
    set b [expr $b]
    set include 0
    if { $op eq "..<" } {
        incr include -1
        set op ".."
    }
    if       { $op eq ".." || $op eq "to" || $op eq "..="} {
        if { $a > $b && $step > 0 } {
            set step [expr {   0 - $step   }]
        }
        if { $step == 0 || ($step < 0 && $a <= $b) || ($step > 0 && $b < $a)} {
            return {}
        }
        if { $by ne "by" } {
            error "range: unknown term for by : $by"
        }
        set step [expr {   abs($step)   }]
        set result {}
        if { $a <=  $b } {
            if { $include != 0 } {
                for {set n $a} {$n < $b } {set n [expr {   $n + $step   }]} {
                    lappend result $n
                }
            } else {
                for {set n $a} {$n <= $b } {set n [expr {   $n + $step   }]} {
                    lappend result $n
                }
            }
        } else {
            if { $include != 0 } {
                for {set n $a} {$n > $b } {set n [expr {   $n - $step   }]} {
                    lappend result $n
                }
            } else {
                for {set n $a} {$n >= $b } {set n [expr {   $n - $step   }]} {
                    lappend result $n
                }
            }
        }
        return $result
    } elseif { $op eq "-count" } {
        set a [expr {   $a - $step   }]
        lmap b [lrepeat [expr {   int($b)   }] 0] {set a [expr {   $a + $step   }]}
    } else {
        error "unknown range op $op"
    }
    
}

Here are some example uses:

% set last 10
10
% range 0 .. $last
0 1 2 3 4 5 6 7 8 9 10
% range 0 ..< $last
0 1 2 3 4 5 6 7 8 9
% range 0 .. $last-1
0 1 2 3 4 5 6 7 8 9
% range $last .. 0
10 9 8 7 6 5 4 3 2 1 0
% range $last ..< 0
10 9 8 7 6 5 4 3 2 1
% range 0 -count 5
0 1 2 3 4
% range 0.0 ..< $last
0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0
% range $last .. 0
10 9 8 7 6 5 4 3 2 1 0
% range $last ..< 0
10 9 8 7 6 5 4 3 2 1
% range 0 .. 20 by 2
0 2 4 6 8 10 12 14 16 18 20
% range 0 ..< 20 by 2
0 2 4 6 8 10 12 14 16 18
% range 5
0 1 2 3 4
% range 5 10
5 6 7 8 9
% range 5 10 2
5 7 9