update: add delta, util, meta

This commit is contained in:
Lucas F. 2026-01-16 19:36:55 -03:00
parent c4319f5da3
commit c7d52a0f32
4 changed files with 607 additions and 0 deletions

68
src/delta.zig Normal file
View file

@ -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;
}
}
};

182
src/delta_test.zig Normal file
View file

@ -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),
);
}

208
src/meta.zig Normal file
View file

@ -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 &copy;
},
};
}
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;
}

149
src/util.zig Normal file
View file

@ -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 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));
}