Passing a list of strings as a function argument from a decltest

Hello,

I’m passing a list of strings to a function as an argument. I can run individual unit tests like:

test "'fl' is the longest common prefix among the strings" {
    var input = [_][]const u8{ "flower", "flow", "flight" };
    const got = try longestCommonPrefix(&input);
    const want: []const u8 = "fl";

    try testing.expectEqualStrings(want, got);
}

test "no common prefix among strings" {
    var input = [_][]const u8{ "dog", "racecar", "car" };
    const got = try longestCommonPrefix(&input);
    const want: []const u8 = "";

    try testing.expectEqualStrings(want, got);
}

But when I declare a decltest similar to the unit tests like:

test longestCommonPrefix {
    const test_cases = [_]struct {
        description: []const u8,
        input: [][]const u8,
        expected: []const u8,
    }{
        .{
            .description = "'fl' is the longest common prefix among the strings",
            .input = [_][]const u8{ "flower", "flow", "flight" },
            .expected = "fl",
        },
        .{
            .description = "no common prefix among strings",
            .input = [_][]const u8{ "dog", "racecar", "car" },
            .expected = "",
        },
    };

    for (test_cases) |tc| {
        const got = try longestCommonPrefix(tc.input);
        const want = tc.expected;

        std.debug.print("{s}\n", .{tc.description});
        try testing.expectEqualStrings(want, got);
    }
}

I get an error:

array literal requires address-of operator (&) to coerce to slice type '[][]const u8'
            .input = [_][]const u8{ "flower", "flow", "flight" },
            ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I added & to the .input, but it gives a different error:

error: expected type '[][]const u8', found '*const [3][]const u8'
            .input = &[_][]const u8{ "flower", "flow", "flight" },
            ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
longest_common_prefix.zig:54:14: note: cast discards const qualifier


The implementation is:

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

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

fn ascending(_: void, lhs: []const u8, rhs: []const u8) bool {
    return std.mem.order(u8, lhs, rhs) == .lt;
}

fn longestCommonPrefix(strs: [][]const u8) ![]const u8 {
    var result: std.ArrayList(u8) = .init(allocator);

    std.mem.sort([]const u8, strs, {}, ascending);

    const first = strs[0];
    const last = strs[strs.len - 1];

    for (0..@min(first.len, last.len)) |i| {
        if (first[i] == last[i]) {
            try result.append(first[i]);
        } else {
            break;
        }
    }

    return result.toOwnedSlice();
}

How can I correctly run the decltest against it?

*const [3][]const u8 (note: const appears twice) does not coerce to [][]const u8. You want to type input as []const []const u8. Intermediate values, like struct/array initialisers, are always const, therefore &.{ "a" } requires []const []const u8.

The function is calling std.mem.sort on that passed slice though, which wouldn’t work if the type was swapped to []const []const u8. The slice would have to be copied in order to sort it, which may be preferable since it’s not clear if the input should be modified.

The other option is to keep the input type as is and allocate a non-const slice for each input.

I’m seeing different errors when I change the input to []const []const u8.

As @permutationlock mentioned, its with std.mem.sort.

Ah, I didn’t even look at that part of the post, only the compile error.

However, this seems like a bit of an XY question and mutating the passed array by sorting it doesn’t seem necessary to implement the function. For example, this implementation does not require a sorted array and returns a substring of the first element in the input list, so there’s no allocation required:

fn longestCommonPrefix(strs: []const []const u8) []const u8 {
    if (strs.len == 0) return "";
    const first = strs[0];
    const rest = strs[1..];
    const prefix: []const u8 = prefix: for (first, 0..) |expected_char, i| {
        for (rest) |str| {
            if (i >= str.len or str[i] != expected_char) {
                break :prefix first[0..i];
            }
        }
    } else first;
    return prefix;
}

Regardless, if you really need to sort the strings, they need to be stored in a mutable array. The easiest way to do so with your current setup would probably be to dupe the constant arrays like this inside the for loop iterating over test cases:

const copy = try std.testing.allocator.dupe([]const u8, tc.input);
defer std.testing.allocator.free(copy);

const got = try longestCommonPrefix(copy);
3 Likes