Integer range generator

if 0 {Richard Suchenwirth 2004-01-25 - For counted loops, Tcl has inherited the for command from C with its lengthy syntax. Python on the other hand provides only a list iterator like Tcl's foreach, and offers an integer range constructor range() to iterate over counted loops:

    range(1,5) -> [1, 2, 3, 4]
    range(4)   -> [0, 1, 2, 3]  

A similar thing was the index vector generator, iota, in APL - Tcllib has struct::list iota too. In contrast to that, here you can also specify the step-width, which might also be negative. This construct comes handy in Tcl too, where we can then choose between

 for {set i 0} {$i < 5} {incr i} {...}
 foreach i [.. 0 5] {...}

I chose the fancy name ".." as suggestive for a range. Here's the code:}

 proc .. {a {b ""} {step 1}} {
    if {$b eq ""} {set b $a; set a 0} ;# argument shift
    if {![string is int $a] || ![string is int $b]} {
        scan $a %c a; scan $b %c b
        incr b $step ;# let character ranges include the last
        set mode %c
    } else {set mode %d}
    set ss [sgn $step]
    if {[sgn [expr {$b - $a}]] == $ss} {
        set res [format $mode $a]
        while {[sgn [expr {$b-$step-$a}]] == $ss} {
            lappend res [format $mode [incr a $step]]
        }
        set res
    } ;# one-armed if: else return empty list
 }

 proc sgn x {expr {($x>0) - ($x<0)}}

if 0 {For testing this, I came up with a cute and tiny asserter/tester routine:}

 proc must {cmd result} {
    if {[set r [uplevel 1 $cmd]] != $result} {
        error "$cmd -> $r, expected $result"
    }
 }

#-- Tests pass silently, but raise an error if expectations are not met:

 must {.. 5}            {0 1 2 3 4}
 must {.. 0 10 3}       {0 3 6 9}
 must {.. -10 -100 -30} {-10 -40 -70}
 must {.. 2 -2 -1}      {2 1 0 -1}
 must {.. 0 0}          {}
 must {.. A D}          {A B C D}
 must {.. z a -1}       {z y x w v u t s r q p o n m l k j i h g f e d c b a}

tombert The results/implementation are not consistent in the following way: .. A D returns a range including "D", .. 1 5 returns a range excluding "5";


RWT That's nice, but don't forget that all the power of tcltest is always at your fingertips. And test suites are quite easy. Just like you wrote, but add a test name and description to each one.

 package require tcltest
 namespace import ::tcltest::*
 test range-1.0 {zero to int}          {.. 5}            {0 1 2 3 4}
 test range-1.1 {specify increment}    {.. 0 10 3}       {0 3 6 9}
 test range-1.2 {negatives}            {.. -10 -100 -30} {-10 -40 -70}
 test range-1.3 {negative increment}   {.. 2 -2 -1}      {2 1 0 -1}
 test range-1.4 {zeros}                {.. 0 0}          {}
 test range-2.0 {cap letters}          {.. A D}          {A B C D}
 test range-2.1 {lowercase, backwards} {.. z a -1}       {z y x w v u t s r q p o n m l k j i h g f e d c b a}

RS Hm... On the iPaq, where I test much, I don't have tcltest. And in place of a large thing where the synopsis in the 630-line man page lists just 40 ways of calling, I prefer a 5-liner or less (

 proc must {c R} {if {[set r [uplevel 1 $c]] ne $R} {error "$c -> $r, not $R"}}

:-)which I can configure as I wish (e.g. add timing, or stack leak checks as in RPN again) - and have one dependency less... Nothing against Tcltest, but I usually prefer the simplest thing that works, and being (sort of) an engineer, I hate overengineering ;)


KPV It would be useful if this function also had the ability to generate alphabet ranges, e.g [.. A D] => {A B C D}. Perl has this capability and it's surprisingly useful. - RS: Your wish is my command :) It didn't take much to change, considering that characters are integers. For convenience I allowed (as your example suggests) that character ranges run up to the last specified character (while for integers, they run until just below the end limit...)

SS A direct extension is to consider ranges between two words, for example [.. z ac 1] => z aa ab. This makes it much more useful, for example you can use it to search a password with brute-force. I don't like the command name .., since there is one very good, descriptive and short: [range]. Also with the extension I proposed, the command needs an option or to be split in two commands, since e.g. 1 99 is also a valid alphabetical range. - RS: Regarding .., of course you can name it as you wish - I just made that up in fleeting fancy (and to show off that we can have more names than Python ;-) But while characters are integers, words are not (at least in format terms); for this purpose, different code will have to be written - see Mapping words to integers.


KPV I believe that in Perl and most likely in Python, when you use the range operator in the equivalent of a foreach loop, the process is optimized so that the actual full list of numbers is not created but each element is created individually as needed. Thus, if you do something like for (1 .. 1_000_000) { #code } you won't burn up a lot of memory.

Sarnold There is now foriter - a loop command optimized for speed. As you expect it, it does not create the full list of numbers.


D. McC Here's another integer range generator. The code isn't as concise as Richard Suchenwirth's, and it doesn't do character ranges or save memory when generating millions of integers--but it's intended to be a bit more comprehensible to a non-expert like me, and a good way to let "foreach" do the ordinary work of "for." Thanks to Glenn Jackman and Bryan Oakley for numerous suggestions, most (though not all) of which I accepted.

The usage is "range start cutoff finish ?step?": (1) the word "range," (2) the beginning of the range; (3) "to" if the range is inclusive or "no" if it excludes the last number; (4) the end of the range; and (5) a number for the increment if it isn't 1 (for an ascending range) or -1 (for a descending range). Thus:

 % range 1 to 5
 1 2 3 4 5

 % range 2 to 10 2
 2 4 6 8 10

This procedure will correctly generate a list of values in a specified range even if you don't know in advance whether the range will be ascending or descending; it will automatically change an unsigned increment to a negative one if the range is descending, so you can simply use the absolute value of the desired increment. (If the programmer insists on putting in a sign, it will use the sign.) Thus:

 % range 10 no 0 2
 10 8 6 4 2

If you want to create a row containing a specified number of entry widgets, with the name of each widget reflecting its position in the row (starting from 1), here's how:

 % set colnum 5
 5
 % foreach i [range 1 to $colnum] {
        grid [entry .ent($i) -bg white] -row 0 -column [expr $i-1] \
                -sticky news
 }

Here's the code:

 proc range {start cutoff finish {step 1}} {
        # If "start" and "finish" aren't integers, do nothing:
        if {[string is integer -strict $start] == 0 || [string is\
                integer -strict $finish] == 0} {
                error "range: Range must contain two integers"
        }

        # "Step" has to be an integer too, and
        # no infinite loops that go nowhere are allowed:
        if {$step == 0 || [string is integer -strict $step] == 0} {
                error "range: Step must be an integer other than zero"
        }

        # Does the range include the last number?
        switch $cutoff {
                "to" {set inclu 1}
                "no" {set inclu 0}
                default {
                        error "range: Use \"to\" for an inclusive\
                        range, or \"no\" for a noninclusive range"
                }
        }

        # Is the range ascending or descending (or neither)?
        set up [expr {$start <= $finish}]

        # If range is descending and step is positive but
        # doesn't have a "+" sign, change step to negative:
        if {$up == 0 && $step > 0 && [string first "+" $start] != 0} {
                set step [expr $step * -1]
        }

        # Initialize list variable and identify
        # class of integer range:
        set ranger [list]
        switch "$up $inclu" {
                "1 1" {set op "<=" ; # Ascending, inclusive range}
                "1 0" {set op "<" ;  # Ascending, noninclusive range}
                "0 1" {set op ">=" ;  # Descending, inclusive range}
                "0 0" {set op ">" ;  # Descending, noninclusive range}
        }

        # Generate a list containing the
        # specified range of integers:
        for {set i $start} "\$i $op $finish" {incr i $step} {
                lappend ranger $i
        }
        return $ranger
 }

RS: Your code is well documented :) - except for the switch "$up $inclu" body. What looks like comments there, is in fact data, testing for the cases "#", "inclusive", "noninclusive" repeatedly. The real danger lies in when you add an innocent word to such a seeming "comment": it breaks the switch syntax and leads to runtime errors.


D. McC Ugh! You're right--as I could easily have seen from a quick review of the relevant section in *Practical Programming with Tcl and Tk*, which I had actually read before! OK, I've changed the bad parts to good parts (I think) in the code above. In case anyone wants to see what the bad parts looked like, here's one of them:

 switch "$up $inclu" {
        # Ascending, inclusive range:
        "1 1" {set op "<="

(etc.)

Thanks--I hope I'll remember *that*, at least, from now on!


RS: Yes, your code is good now (I added a missing semicolon). In Tcl, code is just data - but the comment parsing works only on code before it is executed...


glennj You only use the variable $ascendo once, so wouldn't it be clearer to write:

    if {$start <= $finish} then {set up 1} else {set up 0}

or

    set up [expr {$start <= finish}]

? RS fully agrees - simple is best, KISS :)


D. McC Clearer, and more concise too. I may even be able to write clear, concise code myself someday, if I hang around this Wiki long enough. :o) I've put the "expr {$start <= finish}" expression in the code above. The un-concise code it replaced has been banished below, in case anyone wants to see it. Thanks!

 set ascendo [expr $finish - $start]
 if {$ascendo > -1} {
        set up 1
 } else {
        set up 0
 }

PWQ 20 Feb 04, I am not sure why a case statement is used to generate the operation. The following:

    lindex {> >= < <=}  [expr {($up << 1) + $inclu}]

Is three times faster than a case statement.

RS: Hm... yes, admirable construct. Never thought of that. But consider the poor maintainers deep-thinking about that command... One needs the Zen of Tcl to fully enjoy that.


See also range for a version with Python semantic.


Iterator using Closures also does integer ranges, but with a more general C++/STL Iterator feel...


HES I tried it with the unknown command. The syntax is fromNumber..toNumber (as in sh or bash). You can start with minus.

Save the body of the previeous unknown command:

  set unknown [info body unknown]

The unknown command for integer range from minus to plus:

 proc unknown {args} {
   set name [lindex $args 0]
   if {[regexp {[0-9]|-[0-9]{1,}\.\[0-9]|-[0-9]{1,}} $name]} {
      lassign [string map {.. " "} $name] n1 n2
      for {set i $n1} {$i <= [expr abs($n2)]} {incr i} {
         lappend range $i
      }
      if {[info exist range]} { return $range }
   } else {
      eval $::unknown
   }
 }

Try this:

 puts [-9..10]
 foreach n [0..19] { puts $n }