Version 12 of Integer range generator

Updated 2004-02-15 13:44:44

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

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.


Arts and crafts of Tcl-Tk programming