Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.1: Added Range() #321

Open
wants to merge 3 commits into
base: alpha
Choose a base branch
from
Open

v2.1: Added Range() #321

wants to merge 3 commits into from

Conversation

Descolada
Copy link
Contributor

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.

@Descolada Descolada changed the title v2.1: Added Range v2.1: Added Range() Mar 12, 2023
@HelgeffegleH
Copy link
Contributor

thanks for your efforts :)

Cheers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants