From c7d52a0f3238b04cf3b88195ca638290c221ac73 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Fri, 16 Jan 2026 19:36:55 -0300 Subject: [PATCH] update: add delta, util, meta --- src/delta.zig | 68 +++++++++++++++ src/delta_test.zig | 182 +++++++++++++++++++++++++++++++++++++++ src/meta.zig | 208 +++++++++++++++++++++++++++++++++++++++++++++ src/util.zig | 149 ++++++++++++++++++++++++++++++++ 4 files changed, 607 insertions(+) create mode 100644 src/delta.zig create mode 100644 src/delta_test.zig create mode 100644 src/meta.zig create mode 100644 src/util.zig diff --git a/src/delta.zig b/src/delta.zig new file mode 100644 index 0000000..67ed0d7 --- /dev/null +++ b/src/delta.zig @@ -0,0 +1,68 @@ +// Em time.zig (ou crie um novo arquivo relativedelta.zig e importe) + +pub const RelativeDelta = struct { + years: i32 = 0, + months: i32 = 0, + days: i32 = 0, + hours: i32 = 0, + minutes: i32 = 0, + seconds: i32 = 0, + + pub fn init(fields: struct { + years: i32 = 0, + months: i32 = 0, + days: i32 = 0, + hours: i32 = 0, + minutes: i32 = 0, + seconds: i32 = 0, + }) RelativeDelta { + return .{ + .years = fields.years, + .months = fields.months, + .days = fields.days, + .hours = fields.hours, + .minutes = fields.minutes, + .seconds = fields.seconds, + }; + } + + // Helpers úteis (muito usados depois) + pub fn isZero(self: RelativeDelta) bool { + return self.years == 0 and + self.months == 0 and + self.days == 0 and + self.hours == 0 and + self.minutes == 0 and + self.seconds == 0; + } + + pub fn normalize(self: *RelativeDelta) void { + // Normaliza meses → anos + meses + if (self.months >= 12 or self.months <= -12) { + const carry = @divTrunc(self.months, 12); + self.years += carry; + self.months -= carry * 12; + } + + // Normaliza segundos → minutos + segundos + if (self.seconds >= 60 or self.seconds <= -60) { + const carry = @divTrunc(self.seconds, 60); + self.minutes += carry; + self.seconds -= carry * 60; + } + + // Normaliza minutos → horas + minutos + if (self.minutes >= 60 or self.minutes <= -60) { + const carry = @divTrunc(self.minutes, 60); + self.hours += carry; + self.minutes -= carry * 60; + } + + // Normaliza horas → dias + horas + if (self.hours >= 24 or self.hours <= -24) { + const carry = @divTrunc(self.hours, 24); + self.days += carry; + self.hours -= carry * 24; + } + } +}; diff --git a/src/delta_test.zig b/src/delta_test.zig new file mode 100644 index 0000000..4f3a08e --- /dev/null +++ b/src/delta_test.zig @@ -0,0 +1,182 @@ +const std = @import("std"); +const testing = std.testing; +const Time = @import("time.zig").Time; +const Context = @import("context.zig").Context; +const RelativeDelta = @import("delta.zig").RelativeDelta; + +test "relativedelta rigoroso - meses" { + const a = try Time.parse("2025-03-31"); + const b = try Time.parse("2025-01-31"); + const delta = a.subRelative(b); + try testing.expectEqual(delta.months, 2); + try testing.expectEqual(delta.years, 0); + try testing.expectEqual(delta.days, 0); + + const rev = b.subRelative(a); + try testing.expectEqual(rev.months, -2); +} + +test "relativedelta - overflow de dia" { + const jan31 = try Time.parse("2023-01-31"); + const mar01 = try Time.parse("2023-03-01"); + + const delta = mar01.subRelative(jan31); + // Esperado algo como: +1 mês +1 dia (ou +2 meses -30 dias, mas dateutil prefere o primeiro) + try testing.expect(delta.months == 1); + try testing.expect(delta.days == 1); +} + +test "bissexto: 2021-02-28 - 2020-02-29" { + const a = try Time.parse("2021-02-28"); + const b = try Time.parse("2020-02-29"); + const delta = a.subRelative(b); + try testing.expectEqual(delta.years, 1); + try testing.expectEqual(delta.months, 0); + try testing.expectEqual(delta.days, 0); +} + +test "bissexto: 2021-03-01 - 2020-02-29" { + const a = try Time.parse("2021-03-01"); + const b = try Time.parse("2020-02-29"); + const delta = a.subRelative(b); + try testing.expectEqual(delta.years, 1); + try testing.expectEqual(delta.months, 0); + try testing.expectEqual(delta.days, 1); +} + +test "bissexto: 2021-02-27 - 2020-02-29" { + const a = try Time.parse("2021-02-27"); + const b = try Time.parse("2020-02-29"); + const delta = a.subRelative(b); + try testing.expectEqual(delta.years, 0); + try testing.expectEqual(delta.months, 11); + try testing.expectEqual(delta.days, 29); +} + +test "bissexto reverso: 2020-02-29 - 2021-02-28" { + const a = try Time.parse("2020-02-29"); + const b = try Time.parse("2021-02-28"); + const delta = a.subRelative(b); + try testing.expectEqual(delta.years, -1); + try testing.expectEqual(delta.months, 0); + try testing.expectEqual(delta.days, 0); +} + +test "addRelative: anos normais (não bissexto)" { + // 2023 não é bissexto + const base = try Time.parse("2023-06-15"); + const expected = try Time.parse("2026-06-15"); + + const delta = RelativeDelta.init(.{ .years = 3 }); + const result = base.addRelative(delta); + + try testing.expectEqualStrings( + try expected.toString( null), + try result.toString( null), + ); +} + +test "addRelative: anos normais com overflow de dia" { + // Janeiro 31 + 2 anos → deve ir para 31/jan (2025 não bissexto) + const base = try Time.parse("2023-01-31"); + const expected = try Time.parse("2025-01-31"); + + const delta = RelativeDelta.init(.{ .years = 2 }); + const result = base.addRelative(delta); + + try testing.expectEqualStrings( + try expected.toString( null), + try result.toString( null), + ); +} + +test "addRelative: de 29/fev bissexto + 1 ano (vai para 28/fev)" { + // 2020 foi bissexto → +1 ano deve ir para 2021-02-28 (não bissexto) + const base = try Time.parse("2020-02-29"); + const expected = try Time.parse("2021-02-28"); + + const delta = RelativeDelta.init(.{ .years = 1 }); + const result = base.addRelative(delta); + + try testing.expectEqualStrings( + try expected.toString( null), + try result.toString( null), + ); +} + +test "addRelative: de 29/fev bissexto + 4 anos (permanece 29/fev)" { + // 2020 → 2024 (ambos bissextos) + const base = try Time.parse("2020-02-29"); + const expected = try Time.parse("2024-02-29"); + + const delta = RelativeDelta.init(.{ .years = 4 }); + const result = base.addRelative(delta); + + try testing.expectEqualStrings( + try expected.toString( null), + try result.toString( null), + ); +} + +test "addRelative: de 29/fev + 1 ano + 1 mês (vai para março)" { + // 2020-02-29 + 1 ano → 2021-02-28 + 1 mês → 2021-03-28 + const base = try Time.parse("2020-02-29"); + const expected = try Time.parse("2021-03-28"); + + const delta = RelativeDelta.init(.{ .years = 1, .months = 1 }); + const result = base.addRelative(delta); + + try testing.expectEqualStrings( + try expected.toString( null), + try result.toString( null), + ); +} + +test "addRelative: meses com overflow (31 → 28/30)" { + const cases = [_]struct { base: []const u8, months: i32, expected: []const u8 }{ + .{ .base = "2023-01-31", .months = 1, .expected = "2023-02-28" }, // não bissexto + .{ .base = "2024-01-31", .months = 1, .expected = "2024-02-29" }, // bissexto + .{ .base = "2023-03-31", .months = -1, .expected = "2023-02-28" }, + .{ .base = "2023-01-31", .months = 2, .expected = "2023-03-31" }, + .{ .base = "2023-08-31", .months = 1, .expected = "2023-09-30" }, // setembro tem 30 + }; + + for (cases) |c| { + const base_t = try Time.parse(c.base); + const exp_t = try Time.parse(c.expected); + + const delta = RelativeDelta.init(.{ .months = c.months }); + const result = base_t.addRelative(delta); + + try testing.expectEqualStrings( + try exp_t.toString( null), + try result.toString( null), + ); + } +} + +test "addRelative: combinação anos + meses + dias (bissexto envolvido)" { + // 2024-02-29 + 1 ano + 2 meses + 3 dias + // → 2025-02-28 + 2 meses → 2025-04-28 + 3 dias → 2025-05-01 + const base = try Time.parse("2024-02-29"); + const expected = try Time.parse("2025-05-01"); + + const delta = RelativeDelta.init(.{ .years = 1, .months = 2, .days = 3 }); + const result = base.addRelative(delta); + + try testing.expectEqualStrings( + try expected.toString( null), + try result.toString( null), + ); +} + +test "addRelative: delta zero não altera data" { + const base = try Time.parse("2025-07-20"); + const delta = RelativeDelta.init(.{}); + const result = base.addRelative(delta); + + try testing.expectEqualStrings( + try base.toString( null), + try result.toString( null), + ); +} diff --git a/src/meta.zig b/src/meta.zig new file mode 100644 index 0000000..723a54d --- /dev/null +++ b/src/meta.zig @@ -0,0 +1,208 @@ +// https://github.com/cztomsik/tokamak +const std = @import("std"); +const util = @import("util.zig"); + +// https://github.com/ziglang/zig/issues/19858#issuecomment-2370673253 +// NOTE: I've tried to make it work with enum / packed struct but I was still +// getting weird "operation is runtime due to this operand" here and there +// but it should be possible because we do something similar in util.Smol +pub const TypeId = *const struct { + name: [*:0]const u8, + + pub fn sname(self: *const @This()) []const u8 { + // NOTE: we can't switch (invalid record Zig 0.14.1) + if (self == tid([]const u8)) return "str"; + if (self == tid(?[]const u8)) return "?str"; + return shortName(std.mem.span(self.name), '.'); + } +}; + +pub inline fn tid(comptime T: type) TypeId { + const H = struct { + const id: Deref(TypeId) = .{ .name = @typeName(T) }; + }; + return &H.id; +} + +pub fn tids(comptime types: []const type) []const TypeId { + var buf = util.Buf(TypeId).initComptime(types.len); + for (types) |T| buf.push(tid(T)); + return buf.finish(); +} + +/// Ptr to a comptime value, wrapped together with its type. We use this to +/// pass around values (including a concrete fun types!) during the Bundle +/// compilation. +pub const ComptimeVal = struct { + type: type, + ptr: *const anyopaque, + + pub fn wrap(comptime val: anytype) ComptimeVal { + return .{ .type = @TypeOf(val), .ptr = @ptrCast(&val) }; + } + + pub fn unwrap(self: ComptimeVal) self.type { + return @as(*const self.type, @ptrCast(@alignCast(self.ptr))).*; + } +}; + +pub fn dupe(allocator: std.mem.Allocator, value: anytype) !@TypeOf(value) { + return switch (@typeInfo(@TypeOf(value))) { + .optional => try dupe(allocator, value orelse return null), + .@"struct" => |s| { + var res: @TypeOf(value) = undefined; + inline for (s.fields) |f| @field(res, f.name) = try dupe(allocator, @field(value, f.name)); + return res; + }, + .pointer => |p| switch (p.size) { + .slice => if (p.child == u8) allocator.dupe(p.child, value) else error.NotSupported, + else => value, + }, + else => value, + }; +} + +pub fn free(allocator: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .optional => if (value) |v| free(allocator, v), + .@"struct" => |s| { + inline for (s.fields) |f| free(allocator, @field(value, f.name)); + }, + .pointer => |p| switch (p.size) { + .slice => if (p.child == u8) allocator.free(value), + else => {}, + }, + else => {}, + } +} + +pub fn upcast(context: anytype, comptime T: type) T { + return .{ + .context = context, + .vtable = comptime brk: { + const Impl = Deref(@TypeOf(context)); + var vtable: T.VTable = undefined; + for (std.meta.fields(T.VTable)) |f| { + @field(vtable, f.name) = @ptrCast(&@field(Impl, f.name)); + } + + const copy = vtable; + break :brk © + }, + }; +} + +pub fn Return(comptime fun: anytype) type { + return switch (@typeInfo(@TypeOf(fun))) { + .@"fn" => |f| f.return_type.?, + else => @compileError("Expected a function, got " ++ @typeName(@TypeOf(fun))), + }; +} + +pub fn Result(comptime fun: anytype) type { + const R = Return(fun); + + return switch (@typeInfo(R)) { + .error_union => |r| r.payload, + else => R, + }; +} + +pub fn LastArg(comptime fun: anytype) type { + const params = @typeInfo(@TypeOf(fun)).@"fn".params; + return params[params.len - 1].type.?; +} + +pub inline fn isStruct(comptime T: type) bool { + return @typeInfo(T) == .@"struct"; +} + +pub inline fn isTuple(comptime T: type) bool { + return switch (@typeInfo(T)) { + .@"struct" => |s| s.is_tuple, + else => false, + }; +} + +pub inline fn isGeneric(comptime fun: anytype) bool { + return @typeInfo(@TypeOf(fun)).@"fn".is_generic; +} + +pub inline fn isOptional(comptime T: type) bool { + return @typeInfo(T) == .optional; +} + +pub inline fn isOnePtr(comptime T: type) bool { + return switch (@typeInfo(T)) { + .pointer => |p| p.size == .one, + else => false, + }; +} + +pub inline fn isSlice(comptime T: type) bool { + return switch (@typeInfo(T)) { + .pointer => |p| p.size == .slice, + else => false, + }; +} + +pub inline fn isString(comptime T: type) bool { + return switch (@typeInfo(T)) { + .pointer => |ptr| ptr.child == u8 or switch (@typeInfo(ptr.child)) { + .array => |arr| arr.child == u8, + else => false, + }, + else => false, + }; +} + +pub fn Deref(comptime T: type) type { + return if (isOnePtr(T)) std.meta.Child(T) else T; +} + +pub fn Unwrap(comptime T: type) type { + return switch (@typeInfo(T)) { + .optional => |o| o.child, + else => T, + }; +} + +pub fn Const(comptime T: type) type { + return switch (@typeInfo(T)) { + .pointer => |p| { + var info = p; + info.is_const = true; + return @Type(.{ .pointer = info }); + }, + else => T, + }; +} + +pub inline fn hasDecl(comptime T: type, comptime name: []const u8) bool { + return switch (@typeInfo(T)) { + .@"struct", .@"union", .@"enum", .@"opaque" => @hasDecl(T, name), + else => false, + }; +} + +pub fn fieldTypes(comptime T: type) []const type { + const fields = std.meta.fields(T); + var buf = util.Buf(type).initComptime(fields.len); + for (fields) |f| buf.push(f.type); + return buf.finish(); +} + +pub fn fnParams(comptime fun: anytype) []const type { + const info = @typeInfo(@TypeOf(fun)); + if (info != .@"fn") @compileError("Expected a function, got " ++ @typeName(@TypeOf(fun))); + + const params = info.@"fn".params; + var buf = util.Buf(type).initComptime(params.len); + for (params) |param| buf.push(param.type.?); + return buf.finish(); +} + +// TODO: move somewhere else? +fn shortName(name: []const u8, delim: u8) []const u8 { + return if (std.mem.lastIndexOfScalar(u8, name, delim)) |i| name[i + 1 ..] else name; +} diff --git a/src/util.zig b/src/util.zig new file mode 100644 index 0000000..a36f8ff --- /dev/null +++ b/src/util.zig @@ -0,0 +1,149 @@ +// https://github.com/cztomsik/tokamak +const std = @import("std"); + +pub const whitespace = std.ascii.whitespace; + +pub fn plural(n: i64, singular: []const u8, plural_str: []const u8) []const u8 { + return if (n == 1) singular else plural_str; +} + +pub fn trim(slice: []const u8) []const u8 { + return std.mem.trim(u8, slice, &whitespace); +} + +pub fn truncateEnd(text: []const u8, width: usize) []const u8 { + return if (text.len <= width) text else text[text.len - width ..]; +} + +pub fn truncateStart(text: []const u8, width: usize) []const u8 { + return if (text.len <= width) text else text[0..width]; +} + +pub fn countScalar(comptime T: type, slice: []const T, value: T) usize { + var n: usize = 0; + for (slice) |c| { + if (c == value) n += 1; + } + return n; +} + +pub fn Cmp(comptime T: type) type { + return struct { + // TODO: can we somehow flatten the anytype? + // pub const cmp = if (std.meta.hasMethod(T, "cmp")) T.cmp else std.math.order; + + pub fn cmp(a: T, b: T) std.math.Order { + if (std.meta.hasMethod(T, "cmp")) { + return a.cmp(b); + } + + return std.math.order(a, b); + } + + pub fn lt(a: T, b: T) bool { + return @This().cmp(a, b) == .lt; + } + + pub fn eq(a: T, b: T) bool { + return @This().cmp(a, b) == .eq; + } + + pub fn gt(a: T, b: T) bool { + return @This().cmp(a, b) == .gt; + } + }; +} + +pub fn cmp(a: anytype, b: @TypeOf(a)) std.math.Order { + return Cmp(@TypeOf(a)).cmp(a, b); +} + +pub fn lt(a: anytype, b: @TypeOf(a)) bool { + return Cmp(@TypeOf(a)).lt(a, b); +} + +pub fn eq(a: anytype, b: @TypeOf(a)) bool { + return Cmp(@TypeOf(a)).eq(a, b); +} + +pub fn gt(a: anytype, b: @TypeOf(a)) bool { + return Cmp(@TypeOf(a)).gt(a, b); +} + +// /// --------------------------------------------------------------- +// /// 1️⃣ Detecta se um tipo é std.ArrayList(T) +// /// --------------------------------------------------------------- +// fn isArrayList(comptime T: type) bool { +// // @typeName devolve a string completa, por exemplo: +// // "std.ArrayList(i32)" ou "std.ArrayList([]const u8)" +// const name = @typeName(T); +// // Queremos garantir que o prefixo seja exatamente "std.ArrayList(" +// // (inclui o parêntese de abertura para evitar colisões com nomes +// // semelhantes, como "my_std.ArrayListHelper"). +// // return std.mem.startsWith(u8, name, "std.ArrayList("); +// return std.mem.startsWith(u8, name, "array_list"); +// } +// +// /// --------------------------------------------------------------- +// /// 2️⃣ Obtém o tipo dos elementos armazenados em std.ArrayList(T) +// /// --------------------------------------------------------------- +// fn arrayListElemType(comptime ListT: type) type { +// // Sabemos que ListT tem a forma std.ArrayList(T). O primeiro campo +// // interno do struct é `items: []T`. Vamos ler esse campo. +// const ti = @typeInfo(ListT); +// if (ti != .@"struct") @compileError("Esperado um struct"); +// +// // O campo `items` está na posição 0 da lista de fields: +// const items_field = ti.@"struct".fields[0]; +// // Seu tipo é []T (slice). Em Zig, slices são representados como +// // ponteiros (`*T`) com comprimento separado, mas o tipo declarado +// // aqui é exatamente `[]T`, que corresponde a .pointer com +// // .is_slice = true. +// const slice_type = items_field.type; +// const slice_info = @typeInfo(slice_type); +// if (slice_info != .pointer) @compileError("Campo `items` não é slice"); +// +// // O tipo filho da slice é o T que procuramos. +// return slice_info.pointer.child; +// } +// +// /// --------------------------------------------------------------- +// /// 3️⃣ Função genérica que aceita *qualquer* tipo e age +// /// de acordo se o argumento for um ArrayList ou não. +// /// --------------------------------------------------------------- +// pub fn handle(comptime ArgT: type, arg: ArgT) void { +// if (isArrayList(ArgT)) { +// // É um ArrayList – descobrimos o tipo dos elementos. +// const Elem = arrayListElemType(ArgT); +// +// // Exemplo de uso genérico: imprimir tamanho e tipo dos itens. +// std.debug.print( +// "Recebi um std.ArrayList<{s}> contendo {d} itens.\n", +// .{ @typeName(Elem), arg.items.len }, +// ); +// +// // // Iterar de forma genérica (não sabemos o tipo exato de Elem, +// // // então só fazemos operações que são válidas para *qualquer* tipo). +// // var it = arg.iterator(); +// // while (it.next()) |item| { +// // // `item` tem tipo `Elem`. Se precisar de lógica específica, +// // // pode fazer outra inspeção de tipo aqui. +// // _ = item; // evita warning de variável não usada +// // } +// for(arg.items) |item| { +// _=item; +// } +// } else { +// // Não é um ArrayList – apenas informamos o tipo real. +// std.debug.print( +// "O argumento NÃO é um std.ArrayList (é {s}).\n", +// .{@typeName(ArgT)}, +// ); +// } +// } + +test { + try std.testing.expect(lt(1, 2)); + try std.testing.expect(eq(2, 2)); + try std.testing.expect(gt(2, 1)); +}