update: date filters

This commit is contained in:
Lucas F. 2026-01-17 16:04:11 -03:00
parent 002d2b949e
commit e11d3fb034
2 changed files with 118 additions and 97 deletions

View file

@ -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_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 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 };
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 {
const d = time.Time.parse(value.string) catch return value;
const now = time.Time.parse(arg.?.string) catch return value;
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 },

View file

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