How do I write this itoa better?

My main question is how do I get rid of the rem variable in my code below? I want to keep the value parameter as a signed 64 so I can handle negative numbers. I’m still learning zig but it’s very weird that I am not allowed to write i-- but I can write buf[0 .. itoa(123, &buf)]

const std = @import("std");

//out of lazyness assume 3 digits and positive
fn itoa(value: i64, buf: []u8) usize {
    var v = value;
    var i : usize = 2;
    //buf[3] = 0;
    while (true) {
        const rem : u8 = @intCast(@rem(v, 10)); //How do I remove this line?
        buf[i] = '0' + rem; //and get the mod here?
        v = @divFloor(v, 10);
        if (i == 0)
            break;
        i -= 1; //why can't I use -- here?
    }
    return 3;
}

pub fn main() void {
    var buf: [64]u8 = undefined;
    const output = buf[0 .. itoa(123, &buf)]; //why can I can use buf twice
    std.debug.print("Result: {s}\n", .{output});
}

Welcome to Ziggit m8, not sure if this is what you had in mind but here’s how I would do an itoa.

const std = @import("std");

fn itoa(value: i64, buf: []u8) []u8 {
    const digits = "0123456789";
    const signed: bool = if (value < 0) true else false;

    if (value == 0) {
        buf[0] = '0';
        return buf[0..1];
    }

    var abs_value = @abs(value);
    const digit_count = std.math.log10_int(abs_value) + 1;

    if (signed) {
        buf[0] = '-';
    }

    for (0..digit_count) |i| {
        buf[(digit_count + @intFromBool(signed)) - i - 1] = digits[@rem(abs_value, 10)];
        abs_value = @divTrunc(abs_value, 10);
    }

    return (buf[0 .. digit_count + @intFromBool(signed)]);
}

test "0" {
    var buf: [64]u8 = undefined;
    const output = itoa(0, buf[0..]);
    try std.testing.expect(std.mem.eql(u8, output, "0"));
}

test "maxInt" {
    var buf: [64]u8 = undefined;
    const output = itoa(std.math.maxInt(i64), buf[0..]);
    try std.testing.expect(std.mem.eql(u8, output, "9223372036854775807"));
}

test "minInt" {
    var buf: [64]u8 = undefined;
    const output = itoa(std.math.minInt(i64), buf[0..]);
    try std.testing.expect(std.mem.eql(u8, output, "-9223372036854775808"));
}

pub fn main() void {
    var buf: [64]u8 = undefined;
    const output = itoa(std.math.minInt(i64), buf[0..]);
    std.debug.print("Result: {s}\n", .{output});
}

That’s better than my code but I specifically wanted to know how to write '0' + (v%10). I tried int cast and truncate and it complained it didnt know the type to change it to despite it complaining '0' + wanted a u8

I don’t think it’s possible due to Zig’s safety rules, around signed integers. In Zig if I remember correctly, there is a distinction between runtime known signed integers and comptime known signed integers. So for example

const div = 10 / 5;
const rem = 10 % 7; 

are legal, because they are resolved at comptime to be safe to divide/mod. But for context, where you have a runtime known component, you are forced to use a builtin like @rem, @divTrunc etc. I belive there is also a version that returns an error in the std.math is you prefer errors.

This might be annoying at first, but this friction, is a mean to force you to think twice about what you are doing, and it makes it very clear that something is happening here.

As for the cast, you might need to use something like @as to specify the type you are trying to get.

//out of lazyness assume 3 digits and positive
fn itoa(value: i64, buf: []u8) usize {
    var v = value;
    var i : usize = 2;
    //buf[3] = 0;
    while (true) {
        buf[i] = '0' + @as(u8, @intCast(@rem(v, 10))); //and get the mod here?
        v = @divFloor(v, 10);
        if (i == 0)
            break;
        i -= 1; //why can't I use -- here?
    }
    return 3;
}
1 Like

hello and welcome. this sounded like a fun exercise. here’s what i came up with. this shows how i avoided the @as + @intCast. it also seemed easier to write to the end of the buffer first going backwards.

i added some tests. i would suggest to use a std.testing.expectEqual() method such as std.testing.expectEqualStrings() over std.testing.expect() as they will show the values when the test fails. i find that makes them easier to work with.

const std = @import("std");

fn itoa(value: i64, buf: []u8) []u8 {
    var pos = buf.len;
    var v: i128 = value;
    const neg = v < 0;
    if (neg) v = -v;
    while (true) {
        const c: u8 = @intCast(@mod(v, 10));
        v = @divTrunc(v, 10);
        pos -= 1;
        buf[pos] = c + '0';
        if (v == 0) break;
    }
    if (neg) {
        pos -= 1;
        buf[pos] = '-';
    }
    return buf[pos..];
}

fn testNum(i: i64) !void {
    var buf: [64]u8 = undefined;
    const actual = itoa(i, &buf);
    var buf2: [64]u8 = undefined;
    const expected = try std.fmt.bufPrint(&buf2, "{}", .{i});
    try std.testing.expectEqualStrings(expected, actual);
}

test {
    try testNum(0);
    try testNum(10);
    try testNum(-10);
    try testNum(100);
    try testNum(-100);
    try testNum(std.math.maxInt(i64));
    try testNum(std.math.minInt(i64));
}

test "fuzz" {
    var prng = std.Random.DefaultPrng.init(0);
    const r = prng.random();
    for (0..1_000) |_| {
        try testNum(r.int(i64));
    }
}
2 Likes

Your version is lovely @Travis – it showcases Zig’s ability to know how long an array / slice is, so that you can write into it in reverse order, and also how to create a “first-class citizen” slice from any point in the array / slice, to return that value. No need to know the number of digits ahead of time either. Full marks!

1 Like

Thanks @gonzo. After reading the initial post again i think i didn’t really answer well.

My main question is how do I get rid of the rem variable in my code below?

You either must use @as + @intCast or write a const decl.

it’s very weird that I am not allowed to write i-- but I can write buf[0 .. itoa(123, &buf)]

zig doesn’t have pre/postfix – or ++ operators. not sure what you mean by buf[0 .. itoa(123, &buf)]. that doesn’t look like it would compile since itoa() returns a []u8 which can’t be used as an index.

I think it would be better to use @abs here which automatically turns the result into a u64, this would make it more efficient and also allow you to use % and / instead of @mod and @divTrunc:

    const neg = value < 0;
    var v: u64 = @abs(value);
    while (true) {
        const c: u8 = @intCast(v%10);
        v /= 10;
2 Likes

If itoa() returned a size in that code, then that would be fine. i’m not sure why you thought you couldn’t write that except that it returns a []u8.

1 Like