From e11d3fb03457b79b775817d26a847a3deca58548 Mon Sep 17 00:00:00 2001 From: "Lucas F." Date: Sat, 17 Jan 2026 16:04:11 -0300 Subject: [PATCH] update: date filters --- src/filters.zig | 110 +++++++++++++++++-------------------------- src/filters_test.zig | 105 +++++++++++++++++++++++++++++------------ 2 files changed, 118 insertions(+), 97 deletions(-) diff --git a/src/filters.zig b/src/filters.zig index b550c48..5f4fb3b 100644 --- a/src/filters.zig +++ b/src/filters.zig @@ -2,11 +2,15 @@ const std = @import("std"); const Value = @import("context.zig").Value; const std_time = std.time; +const time = @import("time.zig"); + pub const FilterError = error{ InvalidArgument, + InvalidCharacter, + Overflow, OutOfMemory, UnknownFilter, -}; +} || time.TimeError; const DictEntry = struct { key: []const u8, @@ -122,35 +126,9 @@ fn filter_cut(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!V return Value{ .string = try result.toOwnedSlice(alloc) }; } -// fn filter_date(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { -// // Por enquanto, simples: aceita string ou int (timestamp) e formata com strftime-like -// // Futuro: suporte completo a std.time -// _ = alloc; -// const format = switch (arg orelse Value{ .string = "d/m/Y" }) { -// .string => |f| f, -// else => "d/m/Y", -// }; -// -// const timestamp = switch (value) { -// .int => |i| @as(i64, i), -// .string => |s| std.fmt.parseInt(i64, s, 10) catch 0, -// else => 0, -// }; -// -// // Simulação simples (em produção usar std.time) -// const day = @rem(timestamp, 30) + 1; -// const month = @rem(timestamp / 30, 12) + 1; -// const year = 2026 + @divFloor(timestamp, 360); -// -// var buf: [64]u8 = undefined; -// const formatted = switch (format) { -// "d/m/Y" => std.fmt.bufPrint(&buf, "{d:0>2}/{d:0>2}/{d}", .{ day, month, year }) catch "??/??/????", -// "Y-m-d" => std.fmt.bufPrint(&buf, "{d}-{d:0>2}-{d:0>2}", .{ year, month, day }) catch "????-??-??", -// else => "formato não suportado", -// }; -// -// return Value{ .string = try alloc.dupe(u8, formatted) }; -// } +fn filter_date(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { + return dateTimeToString(alloc, value, arg); +} fn filter_default(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { _ = alloc; @@ -656,6 +634,12 @@ fn filter_make_list(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterE return Value{ .list = try list.toOwnedSlice(alloc) }; } +fn filter_now(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { + _ = value; + if (arg.?.string.len == 0) return Value{ .string = try time.Time.now().toStringAlloc(alloc, "F d, Y") }; + return Value{ .string = try time.Time.now().toStringAlloc(alloc, arg.?.string) }; +} + fn filter_phone2numeric(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { _ = arg; const s = switch (value) { @@ -959,45 +943,22 @@ fn filter_striptags(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterE return Value{ .string = try result.toOwnedSlice(alloc) }; } +fn filter_time(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { + return dateTimeToString(alloc, value, arg); +} + fn filter_timesince(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { - _ = arg; - const then = switch (value) { - .int => |i| @as(i64, i), - else => std_time.timestamp(), - }; + const d = time.Time.parse(value.string) catch return value; + const now = time.Time.parse(arg.?.string) catch return value; - const now = std_time.timestamp(); - var diff = now - then; - if (diff < 0) diff = -diff; - - if (diff < 60) { - return Value{ .string = try alloc.dupe(u8, "menos de um minuto") }; - } else if (diff < 3600) { - const mins = diff / 60; - const str = if (mins == 1) "1 minuto" else try std.fmt.allocPrint(alloc, "{d} minutos", .{mins}); - return Value{ .string = str }; - } else if (diff < 86400) { - const hours = diff / 3600; - const str = if (hours == 1) "1 hora" else try std.fmt.allocPrint(alloc, "{d} horas", .{hours}); - return Value{ .string = str }; - } else { - const days = diff / 86400; - const str = if (days == 1) "1 dia" else try std.fmt.allocPrint(alloc, "{d} dias", .{days}); - return Value{ .string = str }; - } + return Value{ .string = try d.timeSince(alloc, now) }; } fn filter_timeuntil(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { - _ = arg; - // Reutiliza timesince, mas com sinal invertido - const future = switch (value) { - .int => |i| @as(i64, i), - else => std_time.timestamp(), - }; + const d = time.Time.parse(value.string) catch return value; + const now = time.Time.parse(arg.?.string) catch return value; - const fake_past = Value{ .int = std_time.timestamp() }; - const since = try filter_timesince(alloc, fake_past, Value{ .int = future }); - return since; + return Value{ .string = try now.timeSince(alloc, d) }; } fn filter_title(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { @@ -1318,6 +1279,20 @@ fn filter_yesno(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError } // ==================== AUX FUNCTIONS ==================== +pub fn dateTimeToString(allocator: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { + if (value.string.len > 0) { + const t: time.Time = try time.Time.parse(value.string); + const arg_str: Value = arg orelse Value{ .string = "F d, Y" }; + const format_str: []const u8 = arg_str.string; + + const result: []const u8 = try t.toStringAlloc(allocator, format_str); + if (result.len > 0) { + return Value{ .string = result }; + } + } + return value; +} + pub fn capFirst(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { if (input.len == 0) return ""; @@ -1462,7 +1437,7 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{ .{ "capfirst", &filter_capfirst }, .{ "center", &filter_center }, .{ "cut", &filter_cut }, - // .{ "date", &filter_date }, + .{ "date", &filter_date }, .{ "default", &filter_default }, .{ "default_if_none", &filter_default_if_none }, .{ "dictsort", &filter_dictsort }, @@ -1487,6 +1462,7 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{ .{ "ljust", &filter_ljust }, .{ "lower", &filter_lower }, .{ "make_list", &filter_make_list }, + .{ "now", &filter_now }, .{ "phone2numeric", &filter_phone2numeric }, .{ "pluralize", &filter_pluralize }, .{ "pprint", &filter_pprint }, @@ -1498,9 +1474,9 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{ .{ "slugify", &filter_slugify }, .{ "stringformat", &filter_stringformat }, .{ "striptags", &filter_striptags }, - // .{ "time", &filter_time }, - // .{ "timesince", &filter_timesince }, - // .{ "timeuntil", &filter_timeuntil }, + .{ "time", &filter_time }, + .{ "timesince", &filter_timesince }, + .{ "timeuntil", &filter_timeuntil }, .{ "title", &filter_title }, .{ "truncatechars", &filter_truncatechars }, .{ "truncatechars_html", &filter_truncatechars_html }, diff --git a/src/filters_test.zig b/src/filters_test.zig index 205c72b..d31cb1b 100644 --- a/src/filters_test.zig +++ b/src/filters_test.zig @@ -3,8 +3,11 @@ const testing = std.testing; const Value = @import("context.zig").Value; const Context = @import("context.zig").Context; const builtin_filters = @import("filters.zig").builtin_filters; -const FilterError = @import("filters.zig").FilterError; +const filter = @import("filters.zig"); +const FilterError = filter.FilterError; +const time = @import("time.zig"); const std_time = std.time; +const RelativeDelta = @import("delta.zig").RelativeDelta; test "filters upper/lower, capfirst" { std.debug.print("____________________________________________________\n", .{}); @@ -180,7 +183,6 @@ test "builtin filters - join" { try ctx.set("mixed", mixed); const mixed_val = ctx.get("mixed").?; - const default_join = try join(ctx.allocator(), list_val, null); const custom_join = try join(ctx.allocator(), list_val, sep_dash); const mixed_join = try join(ctx.allocator(), mixed_val, null); @@ -558,9 +560,9 @@ test "builtin filters - pluralize" { // try testing.expectEqualStrings("", zero.string); } -test "builtin filters - addslashes, center, date" { +test "builtin filters - addslashes, center" { std.debug.print("____________________________________________________\n", .{}); - std.debug.print("24 - addslashes, center, date\n", .{}); + std.debug.print("24 - addslashes, center\n", .{}); const alloc = testing.allocator; var ctx = Context.init(alloc); @@ -568,21 +570,19 @@ test "builtin filters - addslashes, center, date" { try ctx.set("quote", "He's a good boy"); try ctx.set("texto", "zig"); + try ctx.set("time", time.Time.new(2026, 1, 1, 0, 0, 0)); const v_quote = ctx.get("quote").?; const v_texto = ctx.get("texto").?; const addslashes = builtin_filters.get("addslashes").?; const center = builtin_filters.get("center").?; - // const date = builtin_filters.get("date").?; const slashed = try addslashes(ctx.allocator(), v_quote, null); const centered = try center(ctx.allocator(), v_texto, Value{ .int = 10 }); - // const formatted = try date(ctx.allocator(), Value{ .int = 0 }, Value{ .string = "Y-m-d" }); try testing.expectEqualStrings("He\\'s a good boy", slashed.string); try testing.expectEqualStrings(" zig ", centered.string); - // try testing.expect(std.mem.startsWith(u8, formatted.string, "2026")); } test "builtin filters - dictsort and dictsortreversed" { @@ -760,27 +760,64 @@ test "builtin filters - random, safeseq" { try testing.expect(safe == .list); } -// test "builtin filters - date, time, timesince, timeuntil" { -// const alloc = testing.allocator; -// var ctx = Context.init(alloc); -// defer ctx.deinit(); -// -// const now = std_time.timestamp(); -// -// // const date = builtin_filters.get("date").?; -// const time = builtin_filters.get("time").?; -// const timesince = builtin_filters.get("timesince").?; -// // const timeuntil = builtin_filters.get("timeuntil").?; -// -// // const d = try date(ctx.allocator(), Value{ .int = now }, Value{ .string = "d/m/Y" }); -// const t = try time(ctx.allocator(), Value{ .int = now }, Value{ .string = "H:i" }); -// -// // try testing.expect(d.string.len > 0); -// try testing.expect(t.string.len > 0); -// -// const since = try timesince(ctx.allocator(), Value{ .int = now - 3600 }, null); -// try testing.expect(std.mem.indexOf(u8, since.string, "hora") != null); -// } +test "builtin filters - date, now, time, timesince, timeuntil" { + std.debug.print("____________________________________________________\n", .{}); + std.debug.print("31 - date, now, time, timesince, timeuntil\n", .{}); + + const alloc = testing.allocator; + var ctx = Context.init(alloc); + defer ctx.deinit(); + + const times = [_]time.Time{ + time.Time.new(2026, 1, 2, 13, 15, 10), + time.Time.new(2026, 1, 6, 19, 25, 0), + time.Time.new(2026, 1, 2, 13, 35, 0), + time.Time.new(2026, 1, 2, 13, 15, 19), + time.Time.new(2025, 1, 2, 13, 15,19), + time.Time.new(2024, 7, 5, 19, 4, 2), + }; + + const date_filter = builtin_filters.get("date").?; + const now_filter = builtin_filters.get("now").?; + const time_filter = builtin_filters.get("time").?; + const timesince_filter = builtin_filters.get("timesince").?; + const timeuntil_filer = builtin_filters.get("timeuntil").?; + + try ctx.set("dates", times); + const dates = ctx.get("dates").?; + + const date_formated = try date_filter(ctx.allocator(), dates.list[0], Value{ .string = "Y-m-d" }); + const now_formated = try now_filter(ctx.allocator(), dates.list[0], Value{ .string = "Y-m-d" }); + const time_formated = try time_filter(ctx.allocator(), dates.list[0], Value{ .string = "H:i:s" }); + const timesince_formated_1 = try timesince_filter(ctx.allocator(), dates.list[0], dates.list[1]); + const timesince_formated_2 = try timesince_filter(ctx.allocator(), dates.list[0], dates.list[2]); + const timesince_formated_3 = try timesince_filter(ctx.allocator(), dates.list[0], dates.list[3]); + const timesince_formated_4 = try timesince_filter(ctx.allocator(), dates.list[4], dates.list[0]); + const timesince_formated_5 = try timesince_filter(ctx.allocator(), dates.list[5], dates.list[0]); + + const timeuntil_formated_1 = try timeuntil_filer(ctx.allocator(), dates.list[1], dates.list[0]); + const timeuntil_formated_2 = try timeuntil_filer(ctx.allocator(), dates.list[2], dates.list[0]); + const timeuntil_formated_3 = try timeuntil_filer(ctx.allocator(), dates.list[3], dates.list[0]); + const timeuntil_formated_4 = try timeuntil_filer(ctx.allocator(), dates.list[0], dates.list[4]); + const timeuntil_formated_5 = try timeuntil_filer(ctx.allocator(), dates.list[0], dates.list[5]); + + try testing.expectEqualStrings("2026-01-02", date_formated.string); + try testing.expect(isDateFormat(now_formated.string)); + try testing.expectEqualStrings("13:15:10", time_formated.string); + + try testing.expectEqualStrings("4 days, 6 hours",timesince_formated_1.string); + try testing.expectEqualStrings("19 minutes",timesince_formated_2.string); + try testing.expectEqualStrings("0 minutes",timesince_formated_3.string); + try testing.expectEqualStrings("11 months, 4 weeks",timesince_formated_4.string); + try testing.expectEqualStrings("1 year, 5 months",timesince_formated_5.string); + + try testing.expectEqualStrings("4 days, 6 hours",timeuntil_formated_1.string); + try testing.expectEqualStrings("19 minutes",timeuntil_formated_2.string); + try testing.expectEqualStrings("0 minutes",timeuntil_formated_3.string); + try testing.expectEqualStrings("11 months, 4 weeks",timeuntil_formated_4.string); + try testing.expectEqualStrings("1 year, 5 months",timeuntil_formated_5.string); + +} test "builtin filters - urlize, urlizetrunc, wordwrap, unordered_list" { std.debug.print("____________________________________________________\n", .{}); @@ -807,7 +844,6 @@ test "builtin filters - urlize, urlizetrunc, wordwrap, unordered_list" { const long_text = "Este é um texto muito longo que precisa ser quebrado em várias linhas para caber na largura especificada"; const wrapped = try wordwrap(ctx.allocator(), Value{ .string = long_text }, Value{ .int = 20 }); try testing.expect(std.mem.indexOf(u8, wrapped.string, "\n") != null); - } test "builtin filters - unordered_list" { @@ -818,7 +854,6 @@ test "builtin filters - unordered_list" { var ctx = Context.init(alloc); defer ctx.deinit(); - const list = [_]Value{ Value{ .string = "item1" }, Value{ .string = "item2" } }; try ctx.set("lista", list); @@ -835,3 +870,13 @@ test "builtin filters - unordered_list" { ; try testing.expectEqualStrings(expected, ul.string); } + +fn isDateFormat(txt: []const u8) bool { + if (txt.len != 10) return false; + if (txt[4] != '-' or txt[7] != '-') return false; + for (txt, 0..) |c, i| { + if (i == 4 or i == 7) continue; + if (!std.ascii.isDigit(c)) return false; + } + return true; +}