v2.1: Added Range()
Introduction
Currently AHK is missing a simple way to start iterating from a number other than 1, or to iterate backwards. This pull request adds a function similar to Python's range function.
Range returns an enumerator that enumerates from start to stop with optional step, and also supports (semi-)infinite enumerating.
Syntax
Range(stop) => enumerates from 1...stop with stop included.
Range([start := 1 , stop?, step := 1] => enumerates from start to stop with step, stop is included.
Arguments must be of numeric type or omitted, otherwise an error is thrown.
Omitting stop but providing step (or omitting all parameters) will create a semi-infinite enumerator by setting stop to MAXLONGLONG when step > 0, or MINLONGLONG when step < 0.
RangeEnumerator can be converted into an array with pre-existing syntax: Array(Range(5)*) => [1, 2, 3, 4, 5]
Design choices
Closed interval
Unlike in Python, this implementation of Range uses closed interval for stop. This is because AHK is 1-based, which means Array(Range(n)*).Length == n and this makes more intuitive sense. In Python range(0, n) is more commonly used and also results in length n.
INT64 instead of int
This was chosen to be consistent with AHK Integer type sizes and to provide maximal range for Range (because the enumerating can start at an arbitrary point). For RangeEnumerator::mStep type int would probably suffice, but since it would cause type-casting to INT64 anyway I decided to keep it INT64 also.
Tests
Unit tests
#Requires AutoHotkey v2.0
tests(script_object_bif)
class script_object_bif {
Range() {
throws(Range.Bind(0)) ; stop=0 not allowed
throws(Range.Bind(2,1)) ; start>stop when step>0 not allowed
throws(Range.Bind(1,2,-1)) ; stop<start when step<0 not allowed
throws(Range.Bind("a")) ; non-numeric parameters not allowed for now
throws(Range.Bind(, "b", "c")) ; non-numeric parameters not allowed for now
res := ""
for i in Range() { ; Range() defaults to Range([start:=1, stop?, step:=1]) creating an infinite enumerator
res .= i " "
if i > 4
break
}
equals(res, "1 2 3 4 5 ")
res := ""
for i in Range(,,2) { ; Omitting "stop" creates an infinite enumerator
res .= i " "
if i > 4
break
}
equals(res, "1 3 5 ")
res := ""
for i in Range(4)
res .= i " "
equals(res, "1 2 3 4 ") ; Range(stop) enumerates to n (not including)
res := ""
for i in Range(2, 4)
res .= i " "
equals(res, "2 3 4 ") ; Range(start, stop)
res := ""
for i in Range(3, 10, 2)
res .= i " "
equals(res, "3 5 7 9 ") ; Range(start, stop, step)
res := ""
for i, j in Range(4) ; Range(stop) with A_index
res .= i " " j " "
equals(res, "1 1 2 2 3 3 4 4 ")
res := ""
for i in Range(5,1,-1)
res .= i " "
equals(res, "5 4 3 2 1 ") ; negative step
res := ""
for i, j in Range(5,1,-1) ; negative step with A_index
res .= i " " j " "
equals(res, "1 5 2 4 3 3 4 2 5 1 ")
res := ""
for i in Range(,4) ; unset start defaults to 1
res .= i " "
equals(res, "1 2 3 4 ")
res := ""
for i in Range(2,,1) { ; unset stop defaults to INT_MAX with step>0
res .= i " "
if i > 5 ; consider 6 to be infinity...
break
}
equals(res, "2 3 4 5 6 ")
res := ""
for i in Range(2,,-2) { ; unset stop defaults to INT_MIN with step<0
res .= i " "
if i < -5 ; consider -6 to be infinity...
break
}
equals(res, "2 0 -2 -4 -6 ")
res := ""
for i in Range(2147483647, 2147483652) ; INT_MAX test
res .= i " "
equals(res, "2147483647 2147483648 2147483649 2147483650 2147483651 2147483652 ")
equals(Array(Range(5)*).Length, 5) ; Range to Array
res := ""
for i in Range(3) { ; Nested Ranges
res .= i " "
for j in Range(3)
res .= j " "
}
equals(res, "1 1 2 3 2 1 2 3 3 1 2 3 ")
res := ""
for i in Range(0.0, 5.2, 1.1) ; Floats/doubles are converted to integers
res .= i " "
equals(res, "0 1 2 3 4 5 ")
}
}
tests(classes*) {
for testclass in classes {
env := testclass()
for name in ObjOwnProps(testclass.Prototype) {
if SubStr(name, 1, 2) != '__' {
try
env.%name%()
catch as e
print "FAIL: " type(env) "." name errline(e)
else
print "PASS: " type(env) "." name
}
}
}
errline(e) => "`n" StrReplace(e.File, A_InitialWorkingDir "\") " (" e.Line ") : " e.Message
print(s) => OutputDebug(s "`n")
}
assert(condition, message:="FAIL", n:=-1) {
if !condition
throw Error(message, n)
}
equals(a, b) => assert(a == b, (a is Number ? a : '"' a '"') ' != ' (b is Number ? b : '"' b '"'), -2)
throws(f, m:="FAIL (didn't throw)") {
try f()
catch
return
assert(false, m, -2)
}
Performance tests
buf:= 0, res := "", loopCount := 1000
start := QPC()
for i in Range(loopCount)
for j in Range(loopCount)
buf := j
res .= "Range: " Round(QPC()-start) "`n"
start := QPC()
Loop loopCount {
Loop loopCount
buf := A_Index
}
res .= "Loop: " Round(QPC()-start) "`n"
start := QPC()
for i in Range(2,loopCount)
for j in Range(2,loopCount)
buf := j
res .= "Range offset: " Round(QPC()-start) "`n"
start := QPC()
Loop loopCount-1
Loop loopCount-1
buf := A_Index+1
res .= "Loop offset: " Round(QPC()-start) "`n"
start := QPC()
for i in _Range(1, loopCount)
for j in _Range(1, loopCount)
buf := j
res .= "Custom range: " Round(QPC()-start) "`n"
OutputDebug(res)
QPC() {
static c := 0, f := (DllCall("QueryPerformanceFrequency", "int64*", &c), c /= 1000)
return (DllCall("QueryPerformanceCounter", "int64*", &c), c / f)
}
_Range(Start, Stop, Step:=1) => (&n) => (n := Start, Start += Step, Step > 0 ? n <= Stop : n >= Stop)
Outputs the following in my setup:
Range: 308
Loop: 318
Range offset: 307
Loop offset: 427
Custom range: 1794
The user-defined enumerator that has a more limited functionality is already ~6x slower. Otherwise it's comparable to a simple Loop, with a small win over it when starting with an offset.
Memory tests
for i in Range()
continue
Running this for a while doesn't cause increasing memory use in Visual Studio debugger.
Final notes
This implementation chose not to support character enumeration such as Range('A', 'Z'), but such functionality may be added later on.
Feel free to make any kinds of modifications to this code.
thanks for your efforts :)
Cheers.