Int iterator

I was getting so tired of some loops thatt I wrote this… Not perfectly crashproof but workable.

pub fn IntIterator(comptime T: type) type {

    return struct {
        const Self = @This();

        from: T,
        to: T, // exclusive
        current: T,

        pub fn init(from: T, to: T) Self {
            return .{
                .from = from,
                .to = to,
                .current = from,
            };
        }

        pub fn next(self: *Self) ?T {
            if (self.current >= self.to) {
                return null;
            }
            defer self.current += 1;
            return self.current;
        }
    };
}
    var iter = IntIterator(i32).init(-10, 12);

    while (iter.next()) |v| {
        std.debug.print("{}, ",. { v });
    }
4 Likes

It’s a trivial construct, but I bet such APIs can be useful for some less trivial flows where a simple for is insufficient. By the way, your .from is unused and can be removed :slight_smile:

1 Like

Yep it is trivial. But it already saved me from “off by one” bugs.

1 Like
test "int iterator" {
    loop: switch (@as(i32, -10)) {
        12 => {},
        else => |i| {
            std.debug.print("{}, ",. { i });
            continue :loop i + 1;
        },
    }
}
6 Likes

:slight_smile: I did not run it, but doesnt 12 need a break :loop?

It’s a switch, so it ends implicitly.

2 Likes

Crazy… :slight_smile:
I wonder how the 3 versions compile (raw for, iter, loop switch)

Not raw integers but i used iterator for producing 2d coordinates. https://codeberg.org/andrewkraevskii/steam_train/src/commit/e0248bee3cbfce75d25da73f28c1228acf160195/src/main.zig#L475

i lke that one.

wait, this code runs, how is this possible? i thought continue only works inside for and while blocks?

zig lang reference states

continue:  
`continue` can be used in a loop to jump back to the beginning of the loop.
3 Likes

I think this is interesting, kinda reminds me of the range() in python. It would be cool if this could perform decrement or with steps:

IntIterator(i32).init(12, -10); // from 12 to -10
IntIterator(i32).initStep(-10, 12, 2); // from 12 to -10, with increment of 2

The continue jump is really fun especially with switch, and this is some of my favorite features since 0.16.0; though, isn’t this solution can only support iteration in a fixed range, which in this case, it can only perform -10 → 12 iteration? Besides, if I have some function calls, doesn’t it suggest that we need to put the function calls under the else block?

while is easier at this point:

test "int iterator" {
    const from: i32 = -11;
    var to: i32 = 13;
    to += 0;
    const step: i32 = 2;

    loop: switch (from) {
        else => |i| {
            if (i == to) break :loop;
            std.debug.print("{}, ",. { i });
            continue :loop i + step;
        },
    }

    std.debug.print("\n",. {});

    {
        var i = from;
        while (i != to) : (i += step) {
            std.debug.print("{}, ",. { i });
        }
    }
}

std.debug.print() is a function call. You can call as many functions as you like before continue.

Yeah, I think simplicity wins here since the labelled switch ends up becoming another style of iteration rather than an iterator which is similar yet with subtle difference despite a cool behavior.

For me, iterator provides an abstraction to how index or pointer are moved in loops so that we can iterate a collection without manually update the index, something like this:

var iter = Iter.init(...); // Depends on the type: it can be a range or a collection.

// yay! we don't need to handle the indexing since the iterator handles it
while (iter.next()) |iter_data|{
    ...
}

The pattern from the Op was extremely useful for one of my problems in a gui application where I need to handle a waveform canvas where the mouse may draw forward and backward. However, I didn’t thought of the iterator approach, I ended up swapping the numbers just to comply the for loop:

var sample_idx_start: usize = @intFromFloat(@round(std.math.clamp((idx_start / waveform_canvas_rect.w) * sample_frame_len, 0, sample_frame_len - 1)));
var sample_idx_end: usize = @intFromFloat(@round(std.math.clamp(idx_end / waveform_canvas_rect.w * sample_frame_len, 0, sample_frame_len)));

// we shall retain the orientation of the starting and ending index;
// otherwise, zig will crash if the ending index is smaller.
if (idx_end < idx_start) {
    std.mem.swap(usize, &sample_idx_start, &sample_idx_end);
}

const intepret_delta = (mag_end - mag_start) / @as(f32, @floatFromInt((sample_idx_end - sample_idx_start)));

for (sample_idx_start..sample_idx_end, 0..) |idx, i| {
    const interpret_y = mag_start + @as(f32, @floatFromInt(i)) * intepret_delta;
    frame_group.getSelectTimeFrame().?[idx].re = std.math.clamp(interpret_y, -1, 1);
}

With Op’s pattern, I could simply create an iterator with return an iteration struct containing both required indices:

var sample_idx_start: usize = @intFromFloat(@round(std.math.clamp((idx_start / waveform_canvas_rect.w) * sample_frame_len, 0, sample_frame_len - 1)));
var sample_idx_end: usize = @intFromFloat(@round(std.math.clamp(idx_end / waveform_canvas_rect.w * sample_frame_len, 0, sample_frame_len)));

const intepret_delta = (mag_end - mag_start) / @as(f32, @floatFromInt((sample_idx_end - sample_idx_start)));

// it could return a struct for both array index and the interpreted sample amount
var iter = IntIter.init(sample_idx_start, sample_idx_end, intepret_delta);
while (iter.next()) |item| {
    frame_group.getSelectTimeFrame().?[item.idx].re = std.math.clamp(item.interpret_y, -1, 1);
}

This also suggests I could generalize the solution with a single iterator type that provide the start, end indices and the delta of each steps, instead of writing multiple iteration of the same kind with slight variant (which was a problem I faced in that project). I also no longer need to use comment to explain why I suddenly do a memory swap just to reminds not to accidentally remove the swap in the future commits.

1 Like

Yep. My example was of course a simple one. It can be extended doing more like reverse, steps, returning converted data etc.
Too bad we cannot have 2 or more whiles int the same way as the Zig for loop.

Yeah, that’s a bit unfortunate, but that’s okay since we can still call the next iterator right after the while loop, so for me, this workaround is good enough despite not perfect:

const a = IntIterator(i32).init(-8, 13);
const b = IntIterator(i32).init(0, 21);

while (a.next()) |i| {
    const j = b.next() orelse break;
    ...
}

True. That was the best thing I could think of as well.

I wrote this many moons ago, when I needed to transverse arrays both directions, in a single loop, and sometimes needed to cycle (go to the first after reaching the last and vice-versa).
I know, too verbose …

// counts from start to end, ascending or descending, inclusive or exclusive
// values of start and end determines if Counter is ascending or descending
// the increment/decrement of each iteration is step, which must be a positive number
// next() function starts the Counter and also freezes (if not cyclical) it when it gets to the last possible number (<= end)
// when value receives the last possible number, the next call to next() returns false and ended becomes true and value remains frozen
// cycling means that if is at the last possible value, a call to next() gets to first possible number and ended never gets true
// reset starts over the Counter, with value receiving the first possible number and ended becomes false
// T is the type for: start, end, step and value. should be any int or float
// P is the type for: i and cycles. should be an unsigned integer
pub fn Counter(comptime T: type, comptime P: type) type {
    // TODO: check for types of T (int or float) and P (unsigned int)
    return struct {
        // start indicates the reference for the initial value (will also depend if it is inclusive or not)
        start: T,
        // end indicates the reference for the last velue (will also depend if it is inclusive or not)
        end: T,
        // step (always positive) indicates the increment/decrement (infered from start and end) for each iteration
        step: T,
        // value is the current iteration value, according to start/end/step set
        value: T,
        // i counts (starts at 0) the iterations. is always ascending, not affected by value or cycling
        i: P,
        // ended indicates with true if Counter got to the last possible value. it is always false when cycling
        ended: bool,
        // cycling indicates if Counter should cycle, i.e., calling next() after last possible value, it starts over
        cycling: bool,
        // cycles is the number of COMPLETED cycles
        cycles: P,
        // first call to next() starts the Counter and makes on == true
        on: bool,

        // constructs a default Counter: from start (inclusive) to end (exclusive), step == 1, cycling == false
        pub fn init(start: T, end: T) !Counter(T, P) {
            return initFull(start, true, end, false, 1, false);
        }

        // constructs a Counter, according to the passed parameters
        // if starts_inclusive is true, Counter starts from start, else starts from next possible number towards end
        // end, if reachable, depending on step, is where Counter stops counting
        // if end_inclusive is true it means that the end is a possible number (will be reacheable, depending on step)
        // step is the positive increment/decrement per iteration
        // cycling means that after Counter reachs the last value (<= end), it will start over
        // on is initialized to false, being true only after the first call to next()
        pub fn initFull(start: T, start_inclusive: bool, end: T, end_inclusive: bool, step: T, cycling: bool) !Counter(T, P) {
            if (step < 0) return error.StepMustBePositive;
            if ((start == end) and (!start_inclusive or !end_inclusive)) return error.InconsistentArgs;
            const start_aux = if (start_inclusive) start else math.nextAfter(T, start, end);
            const end_aux = if (end_inclusive) end else math.nextAfter(T, end, start);
            if (((step == 0) and (start_aux != end_aux))) return error.InconsistentArgs;
            return .{
                .start = start_aux,
                .end = end_aux,
                .step = step,
                .value = start_aux,
                .i = 0,
                .ended = false,
                .cycling = cycling,
                .cycles = 0,
                .on = false,
            };
        }

        // next() is the main function of Counter, is what makes it start, run and stop
        // first call to next() makes value = first possible numnber, i = 0, cycles = 0 and on = true
        // returns true if Counter got a next value and false otherwise (always true if cycling)
        // for a cycling Counter, after reaching the last possible number, it starts over
        pub fn next(self: *Counter(T, P)) bool {
            if (self.ended) return false;
            if (!(self.on)) {
                self.on = true;
                if (self.step == 0) self.ended = true;
                return true;
            }
            if (self.start < self.end) {
                if ((self.value + self.step) <= self.end) {
                    self.value += self.step;
                    self.i += 1;
                    return true;
                }
            } else {
                if (self.value >= (self.end + self.step)) {
                    self.value -= self.step;
                    self.i += 1;
                    return true;
                }
            }
            if (self.cycling) {
                self.value = self.start;
                self.i += 1;
                self.cycles += 1;
                return true;
            }
            self.ended = true;
            self.cycles = 1;
            return false;
        }

        // resets the Counter to its initial state
        pub fn reset(self: *Counter(T, P)) void {
            self.value = self.start;
            self.i = 0;
            self.ended = false;
            self.cycles = 0;
            self.on = false;
        }
    };
}

pub fn main() !void {
    // 1 to 11 (ascending), end exclusive, step 1, not cycling
    var counter1 = try Counter(i64, u64).init(1, 11);
    try expect(!counter1.on);
    while (counter1.next()) {}
    try expect(counter1.i == 9);
    try expect(counter1.value == 10);

    // 1 to 70 (ascending), end inclusive, step 3, not cycling
    var counter2 = try Counter(u128, u64).initFull(0, true, 69, false, 3, true);
    for (0..1000) |i| { // just to test it is counting OK
        _ = counter2.next();
        try expect(counter2.i == i);
    }
    try expect(counter2.i == 999);
    try expect(counter2.cycles == 43); // 999/69

    // 37 to -10 (descending), start inclusive, step 0.25, cycling
    var counter3 = try Counter(f128, u32).initFull(37.0, true, -10, true, 0.25, true);
    while (counter3.next()) {
        if (counter3.i == 350) break;
    }
    try expect(counter3.i == 350);
    try expect(counter3.cycles == 1);
    counter3.reset();
    try expect(!counter3.on);

    // 100 to -37 (descending), start inclusive, step 0.5, not cycling
    var counter4 = try Counter(f64, u64).initFull(100.0, true, -37, true, 0.5, false);
    while (counter4.next()) {}
    try expect(counter4.i == 274); // 137 * 2
    try expect(counter4.cycles == 1);
    try expect(counter4.value == -37);
}
2 Likes

Wrote this when needed to iterate over a multidimensional array, which gets ugly with all those nested for loops.
It only creates indices that you yourself have to apply to some data array
So there’s no automatic range checking or something like that.
My data array (C row major ordering) has a function .li that converts nD indices into a linear index into the data (that could do bound checks but doesn’t at the moment).
I needed to have some stencil loop iterating only over the interior 1..n-1 index in each dimension
and then seperately over each face, edge and corner (i.e. holding one,two and three index at 0 or n-1) and this is what i came up with.

I cut and pasted this from a larger code so there’s some types undefined in here but i hope the gist is clear. First part is some usage example, second is the Type. Look at LoopArray.next()

// vecdim holds nr of dimensions of the array e.g. 3 for an 3D NxMxO array,
// note it does not hold the size of each dimension 
// ui_ndarr is the type of the indices (should be usize)
const Iter = perm.LoopArray(vecdim, ui_ndarr);

// Create some loop
// .start, .stop and .step are @Vector(vecdim, ui_ndarr) values
// indicating start/ stop and step values for each dimension
var l3loop: Iter = .init(.{
   .start = ones,
   .stop = SRC.shape - ones,
   .step = l3array - twos, 
});

// finaly use the indices to get the wanted data
while (!l3loop.finished) : (l3loop.next()) {
    var fetch_indices: I_large = @splat(A3.li(l3loop.index));
    const vm: Fb = A3.data[fetch_indices[v]..][0..bivdim].*;

    do stuff
}
pub fn LoopArray(comptime dims: comptime_int, comptime index_type: type) type {
    // nested loops using a switch:
    // the arrays give start, stop and step values for each consequative loop
    const I = @Vector(dims, index_type);

    const LoopArrayOptions = struct {
        start: I = @splat(0),
        stop: I = undefined,
        step: I = @splat(1),
        finished: bool = false,
        index: I = @splat(0),
    };

    return struct {
        const Loop = @This();
        start: I,
        stop: I,
        step: I,
        finished: bool,
        index: I,

        pub fn init(options: LoopArrayOptions) Loop {
            //TODO: need check for setting current if start<current<stop
            //(but we might also support countdown loops so start may not be < stop)
            return Loop{
                .start = options.start,
                .stop = options.stop,
                .step = options.step,
                .finished = options.finished,
                .index = options.start,
            };
        }

        pub fn reset(self: *Loop) void {
            self.index = self.start;
            self.finished = false;
        }

        // nested loops using a switch:
        pub fn next(self: *Loop) void {
            const max_dim = dims - 1;
            const start_dim: index_type = max_dim;
            _ = sw: switch (start_dim) {
                max_dim => |dim| { // this is the most inner loop
                    if (self.index[dim] + self.step[dim] < self.stop[dim]) {
                        self.index[dim] += self.step[dim];
                        break :sw dim; // continue this level till its done
                    }
                    self.index[dim] = self.start[dim];
                    continue :sw dim - 1; // continue in the enclosing loop
                },

                0 => |dim| { // this is the most outer loop
                    if (self.index[dim] + self.step[dim] < self.stop[dim]) {
                        self.index[dim] += self.step[dim];
                        break :sw max_dim; // continue in the most inner loop
                    }
                    self.finished = true;
                    break :sw 0; // (else) we're done if the outer loop reaches its stop value
                },

                inline 1...max_dim - 1 => |dim| { // these are intermediate loops
                    if (self.index[dim] + self.step[dim] < self.stop[dim]) {
                        self.index[dim] += self.step[dim];
                        break :sw max_dim; // continue in the most inner loop
                    }
                    self.index[dim] = self.start[dim];
                    continue :sw dim - 1; // continue in the enclosing loop
                },

                else => unreachable,
            };
        }
    };
}
1 Like