dwyl / dwyl/learn-zig

Ziggy "while", "continue", "break", "arrayList" and tests

Open
#6 4 comments 3 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
4
Forks
1
PR merge metrics
No merged PRs in 30d

Description

The code below can be copy/pasted in this playground: (except the "test" part which needs the Zig compiler).

## What?

A continuation of the `Zig` journey. A short write-up on `while` loops with `continue`, `break` but also the builtin `test` and about building dynamic arrays.

## `while` loop with continue, break 🤔

We have a function `whileWithContinueBreak` below.
The function takes nothing and returns nothing. It uses a `while` loop to print to stdout.

The `while` loop has:
- an exit condition first, `(value<10)`,
- followed by how the iterator is increased `: (value += 1)`. Note that we could have put `value += 1` in the block instead

It basically showcases the usage of:
- `continue`: this skips this iteration
- `break`: this returns the whole loop

> See the next comment for the "while expression".

This function is called in the **mandatory** "main" function.

> a function can still return with the keyword `return`. It is used later.

```zig
// my_file.zig

const std = @import("std");

fn whileWithContinueBreak() void {
var value: u8 = 0;
while (value < 10) : (value += 1) {
if (value == 3) continue; // skip this loop

if (value == 6) break; // return...!

// if all good, do this
// value += 1 is an alternative
std.debug.print("{}\t", .{value});
}
}

pub fun main() void {
whileWithContinueBreak();
}
```

When you run this with `zig run my_file.zig`, you get:

```
0 1 2 4 5
```

## What if I want to test this?

Surely enough I have to persist the output in some way.

The ingredients are:

- since we are building something dynamic, we will use the `ArrayList` struct to append the result. We cannot append things to a slice which is just a pointer+length to an Array.
- we need to allocate memory to this thing,
- we will also use the builtin "test" directly in your code. It won't be in the compiled code.

Some notes:
- every argument in a function needs a type. The type of the "allocator" is `std.mem.Allocator`.
- It returns an array of bytes, `[]u8` (the type of a string).
- Furthermore, it **maybe** return an error - because of memory allocation - thus the `!`.
- Since "main" calls a possibly erroring function, it also needs a `!`, so the return type of "main" is `!void`.
- the arguments of a function are immutable. If you want/need to mutate it, you must create a new variable. This is not the case here.

Below, we use `return` as normal.

```zig
const std = @import("std");

fn appendRawNumberWithContinueBreak(allocator: std.mem.Allocator) ![]u8 {
var result = std.ArrayList(u8).init(allocator);

var value: u8 = 0;
while (value < 10) : (value += 1) {
if (value == 3) continue;
if (value == 6) break;
// if all good, do this
try result.append(value);
}

return result.toOwnedSlice();
}

pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

const result = try appendRawNumberWithContinueBreak(allocator);
defer allocator.free(result);
std.debug.print("\nString result: {any}\n", .{result});
}
```

Above,
- we instantiate the memory allocator in the "main",
- and pass it to the function "appendRawNumberWithContinueBreak" such that we can allocate memory at the `ArrayList` struct.
- we `append` a number to this ArrayList
- we return a **slice** with `toOwnedSlice`. It is a copy of the ArrayList data but cleaned from all the stuff that comes along with an ArrayList.

When we run this, we get the same result as above:

```
0 1 2 4 5
```

We now add a test block, simply declared by `test "nameOfTheTest"`.
It uses its own memory allocator

> we pass an array to the function `std.testing.expectEqualSlice` as the "expected" value. We need to pass a reference, not the array itself, thus we get a pointer by using `&`. To dereference it, you may use `xxx.*`. But in practice, the compiler is smart enough not to need it, just like `Go`.

```zig
test "myTests" {
// using std.testing.allocator
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();

const expected = [_]u8{ 0, 1, 2, 4, 5 }; // we let the compiler infer the length with "_"
const result = try appendRawNumberWithContinueBreak(allocator);
defer allocator.free(result);

try std.testing.expectEqualSlices(u8, &expected, result);
}
```

Surely enough we get 🎉

```
All 1 tests passed.
```

## Comparing strings

Instead of a test, you maybe need to compare strings directly.
‼️ You can't do "string1 == string2", but you can use `std.mem.eql`.

The new function "testAppendRawNumberWithContinueBreak" will use `std.mem.eql` to compare the result to the "expected" input.
Note that we pass the pointer to the array, and the allocator.
It will return a "maybe" boolean.

In the test, use `std.testing.expect`, a function that expects a boolean to be true.

> if you expect the boolean false, just the return the "not" version of the result: `!result`.

```zig
fn testAppendRawNumberWithContinueBreak(expected: []const u8, allocator: std.mem.Allocator) !bool {
const result = try appendRawNumberWithContinueBreak(allocator);
defer allocator.free(result);

return std.mem.eql(u8, expected, result);
}

test "myTests" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();

const expected = [_]u8{ 0, 1, 2, 4, 5 };
const result = try appendRawNumberWithContinueBreak(&expected, allocator);
defer allocator.free(result);

try std.testing.expectEqualSlices(u8, &expected, result);

// new test
const result2 = try testAppendRawNumberWithContinueBreak(&expected, allocator);
try std.testing.expect(result2);
}
```

## What if we want to return a string instead of an array?

In this case, we need to convert a digit into an ascii. We use `std.fmt.bufPrint`, and add it to the ArraList with `appendSlice` (because we are adding a `[]u8`).

```zig
fn appendNumberAsStringtWithContinueBreak(allocator: std.mem.Allocator) ![]u8 {
var value: u8 = 0;
var result = std.ArrayList(u8).init(allocator);
while (value <10) : (value += 1) {
if (value == 3) continue; // skip this loop
if (value == 6) break; // return...!

// if all good, do this:

// Convert number to string and append it
const MAX_DIGIT_STRING_LENGTH = 2;
var buf: [MAX_DIGIT_STRING_LENGTH]u8 = undefined;
const str = try std.fmt.bufPrint(&buf, "{}", .{value});
try result.appendSlice(str);
}
return result.toOwnedSlice();
}

pub fn main() !void {
[...]
const result2 = try appendNumberAsStringtWithContinueBreak(allocator);
defer allocator.free(result2);
std.debug.print("\nString result: {any}\n", .{result2});
}

test "myTests" {
[...]
const expected3 = "01245"; // &[_]u8{ '0', '1', '2', '4', '5' };// this is already a pointer

const result3 = try appendNumberAsStringtWithContinueBreak(allocator);
defer allocator.free(result3);
try std.testing.expectEqualStrings(expected3, result3);
}
```

When we run: `zig build my_file.zig`, we get:

```
# this first function
0 1 2 4 5
# the new function
01245
```

and the tests:

```
All 1 tests passed.
```

## Structured arguments

Zig does not use key/values as arguments, but you can use a `struct`.

Consider:

```zig
const Args = struct {
expected: []const u8,
allocator: std.mem.Allocator,
};
```

then you can assign values in the arguments of a function with `.{ .expected = ...,}`, and reference its fields when using it: `args.allocator`.

```zig
fn testAppendNumberAsStringtWithContinueBreak(args: Args) !bool {
const result = try appendNumberAsStringtWithContinueBreak(args.allocator);
defer args.allocator.free(result);

return std.mem.eql(u8, args.expected, result);
}

pub fn main() !void {
[...]
const allocator = arena.allocator();

const result4 = try testAppendNumberAsStringtWithContinueBreak(.{
.expected = "01245",
.allocator = allocator,
});
}
```

## All in one

all in one

```zig
const std = @import("std");
const testing = std.testing;

const Args = struct {
expected: []const u8,
allocator: std.mem.Allocator,
};

/// Prints numbers from 0 to 9, skipping 3 and stopping at 6
/// Does not return any value
fn whileWithContinueBreak() void {
var value: u8 = 0;
while (value < 10) : (value += 1) {
if (value == 3) continue; // skip this loop

if (value == 6) break; // return...!

// if all good, do this
std.debug.print("{}\t", .{value});
}
}

/// Creates a string by concatenating numbers from 0 to 9,
/// skipping 3 and stopping at 6
/// Arguments:
/// - allocator: Memory allocator used for string creation
/// Returns: A slice containing the concatenated string
/// Error: Returns error if memory allocation fails
fn appendNumberAsStringtWithContinueBreak(allocator: std.mem.Allocator) ![]u8 {
var value: u8 = 0;
var result = std.ArrayList(u8).init(allocator);
while (value < 10) : (value += 1) {
if (value == 3) continue; // skip this loop
if (value == 6) break; // return...!

// if all good, do this

// Convert number to string and append it
const MAX_DIGIT_STRING_LENGTH = 2;
var buf: [MAX_DIGIT_STRING_LENGTH]u8 = undefined;
const str = try std.fmt.bufPrint(&buf, "{}", .{value});
try result.appendSlice(str);
}

return result.toOwnedSlice();
}

fn appendRawNumberWithContinueBreak(allocator: std.mem.Allocator) ![]u8 {
var result = std.ArrayList(u8).init(allocator);

var value: u8 = 0;
while (value < 10) : (value += 1) {
if (value == 3) continue;
if (value == 6) break;
// if all good, do this
try result.append(value); // Append raw number
}

return result.toOwnedSlice();
}

fn testAppendNumberAsStringtWithContinueBreak(args: Args) !bool {
const result = try appendNumberAsStringtWithContinueBreak(args.allocator);
defer args.allocator.free(result);

return std.mem.eql(u8, args.expected, result);
}

fn testAppendRawNumberWithContinueBreak(expected: []const u8, allocator: std.mem.Allocator) !bool {
const result = try appendRawNumberWithContinueBreak(allocator);
defer allocator.free(result);

return std.mem.eql(u8, expected, result);
}

pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

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

const result1 = try appendNumberAsStringtWithContinueBreak(allocator);
defer allocator.free(result1);

const result2 = try appendRawNumberWithContinueBreak(allocator);
defer allocator.free(result2);

const expected3 = "01245";
const result3 = try testAppendNumberAsStringtWithContinueBreak(.{
.expected = expected3,
.allocator = allocator,
});

const expected4 = [_]u8{ 0, 1, 2, 4, 5 };
const result4: bool = try testAppendRawNumberWithContinueBreak(&expected4, allocator);

std.debug.print("\nString result: {s}\n", .{result1});
std.debug.print("\nString result: {any}\n", .{result2});
std.debug.print("\nString result: {any}\n", .{result3});
std.debug.print("\nString result: {any}\n", .{result4});
}

test "whileWithContinueBreak" {
// using std.testing.allocator
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();

const expected1 = "01245"; // &[_]u8{ '0', '1', '2', '4', '5' };
const result1 = try appendNumberAsStringtWithContinueBreak(allocator);
defer allocator.free(result1);

const expected2 = [_]u8{ 0, 1, 2, 4, 5 };
const result2 = try appendRawNumberWithContinueBreak(allocator);
defer allocator.free(result2);

const result3 = try testAppendRawNumberWithContinueBreak(&expected2, allocator);

try testing.expectEqualSlices(u8, expected1, result1);
try testing.expectEqualStrings(&expected2, result2);
try std.testing.expect(result3);
}

````

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.