Sort array by two parameters

Hi,

still dealing with my directory entry lists I have a general question: is it possible to sort a list by two different parameters of the list items in a single step? I just read throush the std.mem.sort... functions but feel a little bit lost understanding the sorting algorithm.

The case: I have an array of data structs. Each data struct represents an item from within a local directory. Thus, in general, every item has a name and can be either a file or a subdir (other types like pipes/sockets etc are left out for now). After collecting the items I want to sort them by two rules:

  1. Sort directories before files
  2. Sort directories by name and files by name

However, I’m not able to find out how to achieve this with one sorting operation on the whole list (or even find out if thats possible at all).

Here some example code:

Expand code
const std = @import("std");

const Item = struct {
    name: []const u8,
    is_dir: bool,
};

fn lessItem(_: void, lhs: Item, rhs: Item) bool {
    return std.ascii.orderIgnoreCase(lhs.name, rhs.name) == .lt;
}

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    _ = gpa;

    var list = [_]Item{
        .{ .name = "c_dir", .is_dir = true },
        .{ .name = "a_file", .is_dir = false },
        .{ .name = "c_file", .is_dir = false },
        .{ .name = "a_dir", .is_dir = true },
        .{ .name = "b_file", .is_dir = false },
        .{ .name = "b_dir", .is_dir = true },
    };

    std.log.info("Before sorting", .{});
    for (list) |i| {
        std.debug.print("{s}\n", .{i.name});
    }
    std.debug.print("\n", .{});

    std.mem.sort(Item, &list, {}, lessItem);

    std.log.info("After sorting", .{});
    for (list) |i| {
        std.debug.print("{s}\n", .{i.name});
    }
}

The example code produces (ofc because it only sorts by name):

a_dir
a_file
b_dir
b_file
c_dir
c_file

But I want:

a_dir
b_dir
c_dir
a_file
b_file
c_file

For now, to achieve the latter I first collect the code into two separate array lists, one for dirs, one for files, sort every list on its own and only then merge it into the main list. However, that introduces some extra loops. And I would like to reduce this overhead if thats possible (maybe through Context?).

1 Like

You just compare both in the lessThan function, with the kind (file or dir) a higher priority.

I think one way to do it, is a bit of the X/Y problem you could potentially discriminate the two in an other place, basically file goes in one array and and dir in another one, this can be an option maybe ?

Thats what I’m doing right now. But because in the end I need both in the same array hash map. That introduces extra loops where I have to assign each item from both array to the array hash map.

Since ArrayHashMap has its own sorting method on the values slice, I try to sort the values directly when they already are assigned to the slice to avoid double assignment (first to separate arrays, then to the array hash map).

I tried to get that going, but just have been to dumb to find a solution. E.g. changing the function to the following almost works except for the last entry…:

fn lessItem(_: void, lhs: Item, rhs: Item) bool {
    if (lhs.is_dir and rhs.is_dir) {
        return std.ascii.orderIgnoreCase(lhs.name, rhs.name) == .lt;
    } else if (!lhs.is_dir and !rhs.is_dir) {
        return std.ascii.orderIgnoreCase(lhs.name, rhs.name) == .lt;
    } else return lhs.is_dir == rhs.is_dir;
}
1 Like
fn lessItem(_: void, lhs: Item, rhs: Item) bool {
    if (lhs.is_dir != rhs.is_dir)
        return lhs.is_dir;

    return std.ascii.orderIgnoreCase(lhs.name, rhs.name) == .lt;
}

you can do that too :slight_smile:

1 Like

Aahh damn. Great. Thats it. Somehow I was thinking too complicated. Thanks!!

1 Like

Sorry for following up on this: I tried to use this with a ArrayHashMap.sortUnstable() method directly, as I need it for my real code:

Code
const std = @import("std");

const Item = struct {
    name: []const u8,
    is_dir: bool,
};

fn lessItem(_: void, lhs: Item, rhs: Item) bool {
    if (lhs.is_dir != rhs.is_dir)
        return lhs.is_dir;

    return std.ascii.orderIgnoreCase(lhs.name, rhs.name) == .lt;
}

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;

    var ahm: std.array_hash_map.Auto(u64, Item) = .empty;
    defer ahm.clearAndFree(gpa);

    const list = [_]Item{
        .{ .name = "c_dir", .is_dir = true },
        .{ .name = "a_file", .is_dir = false },
        .{ .name = "c_file", .is_dir = false },
        .{ .name = "a_dir", .is_dir = true },
        .{ .name = "b_file", .is_dir = false },
        .{ .name = "b_dir", .is_dir = true },
    };

    for (list, 1..) |it, id| {
        try ahm.put(gpa, id, it);
    }

    ahm.sortUnstable(lessItem);
}

But that throws an error:

$ zig run sort_items.zig -freference-trace=8
/home/lukeflo/.cache/zxc/versions/0.16.0/lib/std/multi_array_list.zig:606:36: error: cannot store runtime value in compile time variable
                .slice = self.slice(),
                         ~~~~~~~~~~^~
referenced by:
    sortUnstable__anon_35734: /home/lukeflo/.cache/zxc/versions/0.16.0/lib/std/multi_array_list.zig:640:30
    sortContextInternal__anon_32922: /home/lukeflo/.cache/zxc/versions/0.16.0/lib/std/array_hash_map.zig:960:55
    sortUnstable [inlined]: /home/lukeflo/.cache/zxc/versions/0.16.0/lib/std/array_hash_map.zig:938:44
    main: sort_items.zig:34:21
    callMain [inlined]: /home/lukeflo/.cache/zxc/versions/0.16.0/lib/std/start.zig:737:30
    callMainWithArgs [inlined]: /home/lukeflo/.cache/zxc/versions/0.16.0/lib/std/start.zig:638:20
    posixCallMainAndExit: /home/lukeflo/.cache/zxc/versions/0.16.0/lib/std/start.zig:590:38
    _start: /home/lukeflo/.cache/zxc/versions/0.16.0/lib/std/start.zig:469:40

I skimmed through the internal functions, but am unsure if that might be a bug or an error by myself (but I followed the instructions from the doc comments here).


PS: I could also split this up into a new topic if wanted

There is also the option to use a stable sorting algorithm (one that preserves the previous order of elements which are equal) and then apply two separate sorts in reverse sequence, so you would first sort by 2. and then by 1. and end up with the same result.

The combined sort lessThan compare-function is likely faster (because of less iterations), but if you deal with a combinatoric explosion of many different sorting orders that can be combined in arbitrary ways at runtime, it might be easier to implement than pre-generating the code for all the possibilities.

1 Like

Thats a good tipp, thanks for that. However, for my current use case the sorting fn posted by @pierrelgol is perfectly fine. And of course, its the solution to the initial question.

Now I’m still dealing with the issue that I run into the error named above when trying to apply this onto the sort() (stable as unstable) methods of std.array_hash_map.ArrayHashMap directly to avoid unneeded loops.

I’m not that experienced with code/compiler internals of a language like Zig, but it somehow feels to me there might be something quirky inside the std definitions of the underlying MultiArrayList or similar…

1 Like

I’m sorry that I can’t resolve this but I was able to get a better error message out of the compiler by changing the initialization a bit from the const thing: Thing = .{}; to the const thing = Thing{}; pattern:

        /// `ctx` has the following method:
        /// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
        fn sortInternal(self: Self, a: usize, b: usize, ctx: anytype, comptime mode: std.sort.Mode) void {
            const sort_context = struct {
                sub_ctx: @TypeOf(ctx),
                slice: Slice,

                pub fn swap(sc: @This(), a_index: usize, b_index: usize) void {
                    inline for (field_types, 0..) |field_type, i| {
                        if (@sizeOf(field_type) != 0) {
                            const field: Field = @fromBackingInt(@intCast(i));
                            const ptr = sc.slice.items(field);
                            mem.swap(field_type, &ptr[a_index], &ptr[b_index]);
                        }
                    }
                }

                pub fn lessThan(sc: @This(), a_index: usize, b_index: usize) bool {
                    return sc.sub_ctx.lessThan(a_index, b_index);
                }
            }{
                .sub_ctx = ctx,
                .slice = self.slice(),
            };

            switch (mode) {
                .stable => mem.sortContext(a, b, sort_context),
                .unstable => mem.sortUnstableContext(a, b, sort_context),
            }
        }

This then yields

zig run main.zig -freference-trace
/home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/multi_array_list.zig:609:30: error: unable to resolve comptime value
                .slice = self.slice(),
                ~~~~~~~~~~~~~^~~~~~~~
/home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/multi_array_list.zig:609:30: note: initializer of comptime-only struct 'multi_array_list.MultiArrayList(array_hash_map.Custom(u64,main.Item,array_hash_map.AutoContext(u64),false).Data).sortInternal__func_829__struct_830' must be comptime-known
/home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/multi_array_list.zig:591:26: note: struct requires comptime because of this field
                sub_ctx: @TypeOf(ctx),
                         ^~~~~~~~~~~~
/home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/multi_array_list.zig:591:26: note: use '*const fn (void, main.Item, main.Item) bool' for a function pointer type
referenced by:
    sortUnstable__func_815: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/multi_array_list.zig:643:30
    sortContextInternal__func_786: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/array_hash_map.zig:962:55
    sortUnstable [inlined]: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/array_hash_map.zig:940:44
    main: main.zig:35:25
    callMain [inlined]: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/start.zig:827:30
    callMainWithArgs [inlined]: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/start.zig:729:20
    posixCallMainAndExit: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/start.zig:681:38
    _start: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/start.zig:560:40
    comptime: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/start.zig:88:67
    start: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/std.zig:114:27
    comptime: /home/pzittlau/zig/zig-x86_64-linux-0.17.0-dev.2267+48abf1a34/lib/std/std.zig:240:9

Which is understandable. But I don’t quite know how to resolve it without rewriting parts of how this works. Maybe someone else has more experience with this.

1 Like

Ok, thats indeed more informative. But as you stated I’m also not quite sure how to solve this. Like you I already suggested it might be related to the @TypeOf(ctx) part. In my function this parameter is more or less skipped with _: void. I tested some other possibilities (using std.array_hash_map.AutoContext explicitly etc.), but wasn’t able to get it to compile. Thus, really would appreciate some help by someone who knows more about the internals, or, at least, could judge if this might be a bug or usage error.

The compile error does not make it clear at all, but the issue is that you should not directly pass a compare function to sortUnstable, but instead an instance of a struct that has a lessThan method. Another problem is that this function only receives indexes into the array, not the values. But we can store a pointer to the ArrayHashMap in the context struct, so we can use the index to retrieve the Item. The sort context struct then becomes:

const SortContext = struct {
    ahm: *std.array_hash_map.Auto(u64, Item),
    
    pub fn lessThan(ctx: SortContext, a_index: usize, b_index: usize) bool {
        const lhs = ctx.ahm.entries.get(a_index).value;
        const rhs = ctx.ahm.entries.get(b_index).value;
        if (lhs.is_dir != rhs.is_dir)
            return lhs.is_dir;

        return std.ascii.orderIgnoreCase(lhs.name, rhs.name) == .lt;
    }
};

And we can call sortUnstable like this:

ahm.sortUnstable(SortContext {
    .ahm = &ahm,
});
Complete code
const std = @import("std");

const Item = struct {
    name: []const u8,
    is_dir: bool,
};

const SortContext = struct {
    ahm: *std.array_hash_map.Auto(u64, Item),
    
    pub fn lessThan(ctx: SortContext, a_index: usize, b_index: usize) bool {
        const lhs = ctx.ahm.entries.get(a_index).value;
        const rhs = ctx.ahm.entries.get(b_index).value;
        if (lhs.is_dir != rhs.is_dir)
            return lhs.is_dir;

        return std.ascii.orderIgnoreCase(lhs.name, rhs.name) == .lt;
    }
};

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;

    var ahm: std.array_hash_map.Auto(u64, Item) = .empty;
    defer ahm.clearAndFree(gpa);

    const list = [_]Item{
        .{ .name = "c_dir", .is_dir = true },
        .{ .name = "a_file", .is_dir = false },
        .{ .name = "c_file", .is_dir = false },
        .{ .name = "a_dir", .is_dir = true },
        .{ .name = "b_file", .is_dir = false },
        .{ .name = "b_dir", .is_dir = true },
    };

    for (list, 1..) |it, id| {
        try ahm.put(gpa, id, it);
    }

    ahm.sortUnstable(SortContext {
        .ahm = &ahm,
    });
    
    var iterator = ahm.iterator();
    while (iterator.next()) |item| {
        std.debug.print("{s}\n", .{item.value_ptr.name});
    }
}
3 Likes
const Item = struct {
    name: []const u8,
    is_dir: bool,
};

pub const Context = struct {
    map: std.array_hash_map.Auto(u64, Item) = .empty,

    pub fn lessThan(ctx: Context, l: usize, r: usize) bool {
        const lhs = ctx.map.entries.get(l).value;
        const rhs = ctx.map.entries.get(r).value;

        if (lhs.is_dir != rhs.is_dir)
            return lhs.is_dir;

        return std.ascii.orderIgnoreCase(lhs.name, rhs.name) == .lt;
    }
};

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;

    var ahm: std.array_hash_map.Auto(u64, Item) = .empty;
    defer ahm.clearAndFree(gpa);

    const list = [_]Item{
        .{ .name = "c_dir", .is_dir = true },
        .{ .name = "a_file", .is_dir = false },
        .{ .name = "c_file", .is_dir = false },
        .{ .name = "a_dir", .is_dir = true },
        .{ .name = "b_file", .is_dir = false },
        .{ .name = "b_dir", .is_dir = true },
    };

    for (list, 1..) |it, id| {
        try ahm.put(gpa, id, it);
    }

    ahm.sortUnstable(Context{ .map = ahm });
}

EDIT : @TerenceTux found it first, I didn’t see his reponse

1 Like

@TerenceTux @pierrelgol thanks to both of you. It works now!

What I fell for is the fact that std.mem.sortUnstable explicitly takes a function as parameter, while std.array_hash_map.ArrayHashMap.sortUnstable takes a sort_ctx as parameter. Now, knowing what to look for, the difference is clear. But since the functions are named similar the chance to confuse something is apparent; particulary because the lessThan function is also mentioned in the description of the latter. A little bit more detailed description, especially what is meant with Context in the second case, might be helpful fore sure. However, for now it works good for me. Thanks again to everybody who replied here :slightly_smiling_face:

1 Like

yeah I agree, and honestly the error message are very lacking for this. they could definitely use some love to make it more obvious.

1 Like