HardwareEng.
HardwareEng.

Reputation: 99

How to generate sequence of numbers in Tcl

I'm looking for a way to generate a list of numbers, according to input from, to and step parameters.

Using incr is no good as I would like to support also float and double numbers.

For example, in case from=-0.3, to=0.25 and step=0.1, I would like to generate the list -0.3 -0.2 -0.1 0 0.1 0.2. I'm having troubles with the formatting and rounding.

Upvotes: 1

Views: 8341

Answers (4)

HardwareEng.
HardwareEng.

Reputation: 99

What do you think about the following solution:

proc ::General::Range {Start Stop {Step 1}} {
    if {$Step eq 0} {return Error-'ZeroStep'}
    if {$Start eq $Stop} {return $Start}
    set Range {}
    if {![string in integer $Step]} {
        # Double
        regexp {^\d+\.(\d+)$} $Step FullMatch ToFormat
        while {$Start <= $Stop} {
            lappend Range [string trimright $Start 0]
            set Start [format "%.${ToFormat}f" [expr {$Start + $Step}]]
        }
    } else {
        # Integer
        while {[expr {$Stop > 0 ? [expr {$Start <= $Stop}] : [expr {$Start >= $Stop}]}]} {lappend Range $Start; incr Start $Step}
    }
    return $Range
}

Upvotes: 0

glenn jackman
glenn jackman

Reputation: 246942

Here's an example using Tcl's coroutines to generate the next value in the range on-demand

% proc range {from to step} {
    yield
    set value $from
    while {$value <= $to} {
        yield $value
        set value [expr {$value + $step}]
    }
}
% coroutine generator range -0.35 0.25 0.1
% puts [generator]
-0.35
% puts [generator]
-0.24999999999999997
% puts [generator]
-0.14999999999999997
% puts [generator]
-0.04999999999999996
% puts [generator]
0.050000000000000044
% puts [generator]
0.15000000000000005
% puts [generator]

% puts [generator]
invalid command name "generator"

As Donal was saying, here we see the accumulating floating point errors. Applying his method:

proc range {from to step} {
    yield
    set i 0
    while 1 {
        set value [expr {$from + $i * $step}]
        yield $value
        if {$value > $to} break
        incr i
    }
}

we get the sequence

-0.35
-0.24999999999999997
-0.14999999999999997
-0.04999999999999993
0.050000000000000044
0.15000000000000002
0.2500000000000001

Upvotes: 0

Peter Lewerin
Peter Lewerin

Reputation: 13252

If you can remove the string prefixes yourself:

proc genNums {{from 0} {to 1} {step .1} {prec 1}} {
    if {$step < 0} {
        set op ::tcl::mathop::>
    } else {
        set op ::tcl::mathop::<
    }
    for {set n $from} {[$op $n $to]} {set n [expr {$n + $step}]} {
        lappend res [format %.*f $prec $n]
    }
    return $res 
}

% genNums -0.3 0.25 0.1
# => -0.3 -0.2 -0.1 0.0 0.1 0.2
% genNums -0.3 0.25 0.1 2
# => -0.30 -0.20 -0.10 0.00 0.10 0.20

But if you want, you can set it up so that you can pass the string to the command:

proc genNums args {
    array set params {from 0 to 1 step .1 prec 1}
    array set params [split [string map {= { }} $args]]
    if {$params(step) < 0} {
        set op ::tcl::mathop::>
    } else {
        set op ::tcl::mathop::<
    }
    for {set n $params(from)} {[$op $n $params(to)]} {set n [expr {$n + $params(step)}]} {
        lappend res [format %.*f $params(prec) $n]
    }
    return $res 
}

genNums from=-0.3 to=0.25 step=0.1
# => -0.3 -0.2 -0.1 0.0 0.1 0.2
% genNums from=-0.3 to=0.25 step=0.1 prec=2
# => -0.30 -0.20 -0.10 0.00 0.10 0.20

Documentation: + (operator), < (operator), array, expr, for, format, if, lappend, proc, return, set, split, string, Mathematical operators as Tcl commands

Upvotes: 1

Donal Fellows
Donal Fellows

Reputation: 137637

This is a classic problem in computing. Seriously.

What you need to do is to use integer iteration anyway and then scale by the step size. That minimises the errors. You also need to use format carefully.

set from -0.3
set to 0.25
set step 0.1
for {set i 0} true {incr i} {
    set x [expr {$i*$step + $from}]
    if {$x > $to} break
    set printable [format "%.1f" $x]
    puts "$i => $printable"
}

Upvotes: 2

Related Questions