From 4dbb7b84a80a165e57f26a18705e5f0671edb645 Mon Sep 17 00:00:00 2001 From: copygirl Date: Mon, 4 Sep 2023 15:41:49 +0200 Subject: [PATCH] Allow Path to handle numeric entity ids --- src/path.zig | 107 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 25 deletions(-) diff --git a/src/path.zig b/src/path.zig index 48f7047..0787efd 100644 --- a/src/path.zig +++ b/src/path.zig @@ -11,7 +11,7 @@ pub const Path = struct { /// Note that a relative path can still be interpreted as absolute. absolute: bool, /// The string parts that make up the path. - parts: []const []const u8, + parts: []const EntityPart, /// The allocator that was used to allocate `parts`. alloc: ?Allocator = null, /// Whether the parts outer array itself is owned by this `Path`. @@ -19,6 +19,12 @@ pub const Path = struct { /// Whether the parts inner strings are owned by this `Path`. owns_parts: bool = false, + /// Represents an `Entity` in a `Path`, either by name or its numeric id. + pub const EntityPart = union(enum) { + name: []const u8, + id: u32, + }; + /// Format used to parse and stringify `Path`s. /// /// When this type is formatted, you may use any of the `FormatOption` @@ -49,7 +55,7 @@ pub const Path = struct { /// Creates a new `Path` from the specified string parts. /// /// The resulting path does not own any of the given slices. - pub fn fromParts(absolute: bool, parts: []const []const u8) !Path { + pub fn fromParts(absolute: bool, parts: []const EntityPart) !Path { std.debug.assert(parts.len > 0); return .{ .absolute = absolute, .parts = parts }; } @@ -79,11 +85,15 @@ pub const Path = struct { } const parts_len = std.mem.count(u8, remaining, opt.sep) + 1; - const parts = try alloc.alloc([]const u8, parts_len); + const parts = try alloc.alloc(EntityPart, parts_len); var i: usize = 0; var it = std.mem.splitSequence(u8, remaining, opt.sep); - while (it.next()) |part| : (i += 1) parts[i] = part; + while (it.next()) |str| : (i += 1) + parts[i] = if (parseNumericId(str)) |id| + .{ .id = id } + else + .{ .name = str }; return .{ .absolute = absolute, @@ -95,15 +105,16 @@ pub const Path = struct { /// Creates a deep clone of this `Path` using the specified `Allocator`. pub fn clone(orig: Path, alloc: Allocator) !Path { - var parts = try alloc.dupe([]const u8, orig.parts); + var parts = try alloc.dupe(EntityPart, orig.parts); errdefer alloc.free(parts); var num_allocated: usize = 0; errdefer for (parts[0..num_allocated]) |part| - alloc.free(part); + if (part == .name) alloc.free(part.name); for (parts) |*part| { - part.* = try alloc.dupe(u8, part.*); + if (part.* == .name) + part.* = .{ .name = try alloc.dupe(u8, part.name) }; num_allocated += 1; } @@ -120,7 +131,8 @@ pub const Path = struct { pub fn deinit(self: Path) void { if (self.owns_parts) for (self.parts) |part| - self.alloc.?.free(part); + if (part == .name) + self.alloc.?.free(part.name); if (self.owns_array) self.alloc.?.free(self.parts); } @@ -166,23 +178,64 @@ pub const Path = struct { } // Parts. for (self.parts) |part| - result += part.len; + result += switch (part) { + .name => |name| name.len, + .id => |id| numDigits(id), + }; return result; } fn write(self: Path, opt: FormatOptions, writer: anytype) !void { - // Write prefix (if applicable), + // Write prefix (if applicable). if (self.absolute) { if (opt.prefix) |p| try writer.writeAll(p); } - // Write first part. - try writer.writeAll(self.parts[0]); - // Write other parts, each preceeded by separator. + // Write the first part. + switch (self.parts[0]) { + .name => |name| try writer.writeAll(name), + .id => |id| try writer.writeIntNative(u32, id), + } + // Write the remaining parts, each preceeded bu separator. for (self.parts[1..]) |part| { try writer.writeAll(opt.sep); - try writer.writeAll(part); + switch (part) { + .name => |name| try writer.writeAll(name), + .id => |id| try writer.writeIntNative(u32, id), + } + } + } + + /// Attempts to parse the specified string as a numeric entity id. + fn parseNumericId(str: []const u8) ?u32 { + if (str.len == 0 or str.len > 10) return null; + var result: u32 = 0; + var place: u32 = 1; + var it = std.mem.reverseIterator(str); + while (it.next()) |chr| { + const d = std.fmt.charToDigit(chr, 10) catch return null; + result = std.math.add(u32, result, d * place) catch return null; + place *%= 10; // Wrapping to avoid overflow error. } + return result; + } + + /// Calculates the numbers of digits of an entity id when formatted as a + /// string. Since entity ids tend to be small this is technically optimized + /// for smaller numbers but performance likely doesn't matter much. + fn numDigits(n: u32) usize { + // zig fmt: off + return if (n < 10) 1 + else if (n < 100) 2 + else if (n < 1000) 3 + else if (n < 10000) 4 + else if (n < 100000) 5 + else if (n < 1000000) 6 + else if (n < 10000000) 7 + else if (n < 100000000) 8 + else if (n < 1000000000) 9 + else 10; + // zig fmt: on } }; @@ -190,7 +243,6 @@ test Path { const alloc = std.testing.allocator; const expectFmt = std.testing.expectFmt; const expectEqual = std.testing.expectEqual; - const expectEqualDeep = std.testing.expectEqualDeep; const expectEqualStrings = std.testing.expectEqualStrings; // Paths may be constructed by parsing strings. @@ -200,7 +252,7 @@ test Path { // Alternatively they can be made by specifying the individual component // parts they're made of, as well as an argument specifying whether it's // an absolute path. - const absolute1 = try Path.fromParts(true, &.{ "I'm", "absolute!" }); + const absolute1 = try Path.fromParts(true, &.{ .{ .name = "I'm" }, .{ .name = "absolute!" } }); // No need for `deinit()`, does not own outer array nor inner string parts. // With `options` unspecified, it's not possible to represent an absolute @@ -217,15 +269,20 @@ test Path { // The internal component parts of the path can be accessed with `.parts`. try expectEqual(@as(usize, 3), relative.parts.len); - try expectEqualStrings("some", relative.parts[0]); - try expectEqualStrings("relative", relative.parts[1]); - try expectEqualStrings("path", relative.parts[2]); - try expectEqualDeep(@as([]const []const u8, &.{ "I'm", "absolute!" }), absolute1.parts); - try expectEqualDeep(@as([]const []const u8, &.{ "home", "copygirl" }), absolute2.parts); - - // Paths are formattable, and as the `fmt` specifier you can either use one - // of the pre-defined ones defined in the `FormatOptions` type, or just an - // empty string to use the default. + try expectEqualStrings("some", relative.parts[0].name); + try expectEqualStrings("relative", relative.parts[1].name); + try expectEqualStrings("path", relative.parts[2].name); + + // Parts can also be numeric ids, used for entities that don't have a name. + const numeric = try Path.fromString("100.101.bar", null, alloc); + defer numeric.deinit(); + try expectEqual(@as(usize, 3), numeric.parts.len); + try expectEqual(@as(u32, 100), numeric.parts[0].id); + try expectEqual(@as(u32, 101), numeric.parts[1].id); + try expectEqualStrings("bar", numeric.parts[2].name); + + // Paths are formattable. As format specifier you can use options defined + // on the `FormatOptions` type, or an empty string to use the default. try expectFmt("some.relative.path", "{}", .{relative}); try expectFmt("::I'm::absolute!", "{flecs_cpp}", .{absolute1}); try expectFmt("/home/copygirl", "{unix}", .{absolute2});