Merge branch 'refact_parse'

This commit is contained in:
Lucas F. 2026-01-18 17:41:28 -03:00
commit 4d45072435
13 changed files with 5108 additions and 2872 deletions

View file

@ -1,4 +1,6 @@
const std = @import("std"); const std = @import("std");
const time = @import("time.zig");
const util = @import("util.zig");
pub const Value = union(enum) { pub const Value = union(enum) {
null, null,
@ -34,8 +36,12 @@ pub const Context = struct {
self.arena.deinit(); self.arena.deinit();
} }
fn toValue(self: *Context, value: anytype) !Value { pub fn toValue(self: *Context, value: anytype) !Value {
const T = @TypeOf(value); const T = @TypeOf(value);
if (T == time.Time) {
return Value{ .string = try time.formatDateTime(self.allocator(), value, "Y-m-d H:i:s") };
}
return switch (@typeInfo(T)) { return switch (@typeInfo(T)) {
.bool => Value{ .bool = value }, .bool => Value{ .bool = value },
.int, .comptime_int => Value{ .int = @intCast(value) }, .int, .comptime_int => Value{ .int = @intCast(value) },
@ -109,6 +115,11 @@ pub const Context = struct {
return self.get(path) orelse try self.toValue(default); return self.get(path) orelse try self.toValue(default);
} }
fn isArrayList(value: anytype) bool {
if (std.mem.startsWith(u8, @typeName(value), "array_list")) return true;
return false;
}
// pub fn get(self: *const Context, comptime T: type, key: []const u8) !T { // pub fn get(self: *const Context, comptime T: type, key: []const u8) !T {
// // const opt_value = self.map.get(key) orelse return error.KeyNotFound; // // const opt_value = self.map.get(key) orelse return error.KeyNotFound;
// const opt_value = self.getOptional(T, key); // const opt_value = self.getOptional(T, key);

83
src/delta.zig Normal file
View file

@ -0,0 +1,83 @@
// 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 inSeconds(self: RelativeDelta) i64 {
return @as(i64, self.years) * 365 * 24 * 60 * 60 +
@as(i64, self.months) * 30 * 24 * 60 * 60 +
@as(i64, self.days) * 24 * 60 * 60 +
@as(i64, self.hours) * 60 * 60 +
@as(i64, self.minutes) * 60 +
@as(i64, self.seconds);
}
pub fn inDays(self: RelativeDelta) i64 {
return @as(i64, self.years) * 365 +
@as(i64, self.months) * 30 +
@as(i64, self.days);
}
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),
);
}

View file

@ -2,11 +2,15 @@ const std = @import("std");
const Value = @import("context.zig").Value; const Value = @import("context.zig").Value;
const std_time = std.time; const std_time = std.time;
const time = @import("time.zig");
pub const FilterError = error{ pub const FilterError = error{
InvalidArgument, InvalidArgument,
InvalidCharacter,
Overflow,
OutOfMemory, OutOfMemory,
UnknownFilter, UnknownFilter,
}; } || time.TimeError;
const DictEntry = struct { const DictEntry = struct {
key: []const u8, 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) }; return Value{ .string = try result.toOwnedSlice(alloc) };
} }
// fn filter_date(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { 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 return dateTimeToString(alloc, value, arg);
// // 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_default(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { fn filter_default(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = alloc; _ = 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) }; 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 { fn filter_phone2numeric(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg; _ = arg;
const s = switch (value) { 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) }; 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 { fn filter_timesince(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg; const d = time.Time.parse(value.string) catch return value;
const then = switch (value) { const now = time.Time.parse(arg.?.string) catch return value;
.int => |i| @as(i64, i),
else => std_time.timestamp(),
};
const now = std_time.timestamp(); return Value{ .string = try d.timeSince(alloc, now) };
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_timeuntil(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { fn filter_timeuntil(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
_ = arg; const d = time.Time.parse(value.string) catch return value;
// Reutiliza timesince, mas com sinal invertido const now = time.Time.parse(arg.?.string) catch return value;
const future = switch (value) {
.int => |i| @as(i64, i),
else => std_time.timestamp(),
};
const fake_past = Value{ .int = std_time.timestamp() }; return Value{ .string = try now.timeSince(alloc, d) };
const since = try filter_timesince(alloc, fake_past, Value{ .int = future });
return since;
} }
fn filter_title(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value { fn filter_title(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError!Value {
@ -1191,7 +1152,6 @@ fn filter_urlizetrunc(alloc: std.mem.Allocator, value: Value, arg: ?Value) Filte
const s = try valueToSafeString(alloc, value); const s = try valueToSafeString(alloc, value);
std.debug.print("{s}\n", .{value.string});
var result = std.ArrayList(u8){}; var result = std.ArrayList(u8){};
var i: usize = 0; var i: usize = 0;
@ -1319,6 +1279,20 @@ fn filter_yesno(alloc: std.mem.Allocator, value: Value, arg: ?Value) FilterError
} }
// ==================== AUX FUNCTIONS ==================== // ==================== 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 { pub fn capFirst(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
if (input.len == 0) return ""; if (input.len == 0) return "";
@ -1463,7 +1437,7 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{
.{ "capfirst", &filter_capfirst }, .{ "capfirst", &filter_capfirst },
.{ "center", &filter_center }, .{ "center", &filter_center },
.{ "cut", &filter_cut }, .{ "cut", &filter_cut },
// .{ "date", &filter_date }, .{ "date", &filter_date },
.{ "default", &filter_default }, .{ "default", &filter_default },
.{ "default_if_none", &filter_default_if_none }, .{ "default_if_none", &filter_default_if_none },
.{ "dictsort", &filter_dictsort }, .{ "dictsort", &filter_dictsort },
@ -1488,6 +1462,7 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{
.{ "ljust", &filter_ljust }, .{ "ljust", &filter_ljust },
.{ "lower", &filter_lower }, .{ "lower", &filter_lower },
.{ "make_list", &filter_make_list }, .{ "make_list", &filter_make_list },
.{ "now", &filter_now },
.{ "phone2numeric", &filter_phone2numeric }, .{ "phone2numeric", &filter_phone2numeric },
.{ "pluralize", &filter_pluralize }, .{ "pluralize", &filter_pluralize },
.{ "pprint", &filter_pprint }, .{ "pprint", &filter_pprint },
@ -1499,9 +1474,9 @@ pub const builtin_filters = std.StaticStringMap(*const FilterFn).initComptime(.{
.{ "slugify", &filter_slugify }, .{ "slugify", &filter_slugify },
.{ "stringformat", &filter_stringformat }, .{ "stringformat", &filter_stringformat },
.{ "striptags", &filter_striptags }, .{ "striptags", &filter_striptags },
// .{ "time", &filter_time }, .{ "time", &filter_time },
// .{ "timesince", &filter_timesince }, .{ "timesince", &filter_timesince },
// .{ "timeuntil", &filter_timeuntil }, .{ "timeuntil", &filter_timeuntil },
.{ "title", &filter_title }, .{ "title", &filter_title },
.{ "truncatechars", &filter_truncatechars }, .{ "truncatechars", &filter_truncatechars },
.{ "truncatechars_html", &filter_truncatechars_html }, .{ "truncatechars_html", &filter_truncatechars_html },

View file

@ -3,10 +3,16 @@ const testing = std.testing;
const Value = @import("context.zig").Value; const Value = @import("context.zig").Value;
const Context = @import("context.zig").Context; const Context = @import("context.zig").Context;
const builtin_filters = @import("filters.zig").builtin_filters; 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 std_time = std.time;
const RelativeDelta = @import("delta.zig").RelativeDelta;
test "filters upper/lower, capfirst" { test "filters upper/lower, capfirst" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("1 - upper/lower, capfirst\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -29,6 +35,9 @@ test "filters upper/lower, capfirst" {
} }
test "builtin filters - add" { test "builtin filters - add" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("2 - add\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const add = builtin_filters.get("add").?; const add = builtin_filters.get("add").?;
@ -49,6 +58,9 @@ test "builtin filters - add" {
} }
test "builtin filters - default and default_if_none" { test "builtin filters - default and default_if_none" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("3 - default and default_if_none\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const default_filter = builtin_filters.get("default").?; const default_filter = builtin_filters.get("default").?;
const default_if_none = builtin_filters.get("default_if_none").?; const default_if_none = builtin_filters.get("default_if_none").?;
@ -73,6 +85,9 @@ test "builtin filters - default and default_if_none" {
} }
test "builtin filters - length" { test "builtin filters - length" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("4 - length\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const length = builtin_filters.get("length").?; const length = builtin_filters.get("length").?;
@ -94,7 +109,10 @@ test "builtin filters - length" {
try testing.expect((try length(ctx.allocator(), list_val, null)).int == 4); try testing.expect((try length(ctx.allocator(), list_val, null)).int == 4);
} }
test "builtin filters - length com dict" { test "builtin filters - length with dict" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("5 - length with dict\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -114,6 +132,9 @@ test "builtin filters - length com dict" {
} }
test "builtin filters - first and last" { test "builtin filters - first and last" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("6 - first and last\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const first = builtin_filters.get("first").?; const first = builtin_filters.get("first").?;
const last = builtin_filters.get("last").?; const last = builtin_filters.get("last").?;
@ -143,6 +164,9 @@ test "builtin filters - first and last" {
} }
test "builtin filters - join" { test "builtin filters - join" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("7 - join\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const join = builtin_filters.get("join").?; const join = builtin_filters.get("join").?;
@ -159,7 +183,6 @@ test "builtin filters - join" {
try ctx.set("mixed", mixed); try ctx.set("mixed", mixed);
const mixed_val = ctx.get("mixed").?; const mixed_val = ctx.get("mixed").?;
const default_join = try join(ctx.allocator(), list_val, null); const default_join = try join(ctx.allocator(), list_val, null);
const custom_join = try join(ctx.allocator(), list_val, sep_dash); const custom_join = try join(ctx.allocator(), list_val, sep_dash);
const mixed_join = try join(ctx.allocator(), mixed_val, null); const mixed_join = try join(ctx.allocator(), mixed_val, null);
@ -170,6 +193,9 @@ test "builtin filters - join" {
} }
test "builtin filters - yesno" { test "builtin filters - yesno" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("8 - yesno\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const yesno = builtin_filters.get("yesno").?; const yesno = builtin_filters.get("yesno").?;
@ -192,6 +218,9 @@ test "builtin filters - yesno" {
} }
test "builtin filters - truncatechars and truncatechars_html" { test "builtin filters - truncatechars and truncatechars_html" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("9 - truncatechars and truncatechars_html\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const truncatechars = builtin_filters.get("truncatechars").?; const truncatechars = builtin_filters.get("truncatechars").?;
const truncatechars_html = builtin_filters.get("truncatechars_html").?; const truncatechars_html = builtin_filters.get("truncatechars_html").?;
@ -218,6 +247,9 @@ test "builtin filters - truncatechars and truncatechars_html" {
} }
test "builtin filters - truncatewords" { test "builtin filters - truncatewords" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("10 - truncatewords\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const truncatewords = builtin_filters.get("truncatewords").?; const truncatewords = builtin_filters.get("truncatewords").?;
const truncatewords_html = builtin_filters.get("truncatewords_html").?; const truncatewords_html = builtin_filters.get("truncatewords_html").?;
@ -241,6 +273,9 @@ test "builtin filters - truncatewords" {
} }
test "builtin filters - slice" { test "builtin filters - slice" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("11 - slice\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const slice = builtin_filters.get("slice").?; const slice = builtin_filters.get("slice").?;
@ -267,6 +302,9 @@ test "builtin filters - slice" {
} }
test "builtin filters - safe and force_escape" { test "builtin filters - safe and force_escape" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("12 - safe and force_escape\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const safe = builtin_filters.get("safe").?; const safe = builtin_filters.get("safe").?;
const force_escape = builtin_filters.get("force_escape").?; const force_escape = builtin_filters.get("force_escape").?;
@ -287,6 +325,9 @@ test "builtin filters - safe and force_escape" {
} }
test "builtin filters - linebreaksbr and linebreaks" { test "builtin filters - linebreaksbr and linebreaks" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("13 - linebreaksbr and linebreaks\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const linebreaksbr = builtin_filters.get("linebreaksbr").?; const linebreaksbr = builtin_filters.get("linebreaksbr").?;
const linebreaks = builtin_filters.get("linebreaks").?; const linebreaks = builtin_filters.get("linebreaks").?;
@ -306,6 +347,9 @@ test "builtin filters - linebreaksbr and linebreaks" {
} }
test "builtin filters - escape and force_escape" { test "builtin filters - escape and force_escape" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("14 - escape and force_escape\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -326,6 +370,9 @@ test "builtin filters - escape and force_escape" {
} }
test "builtin filters - striptags" { test "builtin filters - striptags" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("15 - striptags\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -341,6 +388,9 @@ test "builtin filters - striptags" {
} }
test "builtin filters - slugify" { test "builtin filters - slugify" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("16 - slugify\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -356,6 +406,9 @@ test "builtin filters - slugify" {
} }
test "builtin filters - floatformat" { test "builtin filters - floatformat" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("17 - floatformat\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -384,6 +437,9 @@ test "builtin filters - floatformat" {
} }
test "builtin filters - stringformat" { test "builtin filters - stringformat" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("18 - stringformat\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -404,6 +460,9 @@ test "builtin filters - stringformat" {
} }
test "builtin filters - cut" { test "builtin filters - cut" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("19 - cut\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -419,6 +478,9 @@ test "builtin filters - cut" {
} }
test "builtin filters - title" { test "builtin filters - title" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("20 - title\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -434,6 +496,9 @@ test "builtin filters - title" {
} }
test "builtin filters - wordcount" { test "builtin filters - wordcount" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("21 - wordcount\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -449,6 +514,9 @@ test "builtin filters - wordcount" {
} }
test "builtin filters - urlencode" { test "builtin filters - urlencode" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("22 - urlencode\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -464,6 +532,9 @@ test "builtin filters - urlencode" {
} }
test "builtin filters - pluralize" { test "builtin filters - pluralize" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("23 - pluralize\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -489,31 +560,35 @@ test "builtin filters - pluralize" {
// try testing.expectEqualStrings("", zero.string); // 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\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
try ctx.set("quote", "He's a good boy"); try ctx.set("quote", "He's a good boy");
try ctx.set("texto", "zig"); 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_quote = ctx.get("quote").?;
const v_texto = ctx.get("texto").?; const v_texto = ctx.get("texto").?;
const addslashes = builtin_filters.get("addslashes").?; const addslashes = builtin_filters.get("addslashes").?;
const center = builtin_filters.get("center").?; const center = builtin_filters.get("center").?;
// const date = builtin_filters.get("date").?;
const slashed = try addslashes(ctx.allocator(), v_quote, null); const slashed = try addslashes(ctx.allocator(), v_quote, null);
const centered = try center(ctx.allocator(), v_texto, Value{ .int = 10 }); 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("He\\'s a good boy", slashed.string);
try testing.expectEqualStrings(" zig ", centered.string); try testing.expectEqualStrings(" zig ", centered.string);
// try testing.expect(std.mem.startsWith(u8, formatted.string, "2026"));
} }
test "builtin filters - dictsort and dictsortreversed" { test "builtin filters - dictsort and dictsortreversed" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("25 - dictsort and dictsortreversed\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -544,6 +619,9 @@ test "builtin filters - dictsort and dictsortreversed" {
} }
test "builtin filters - divisibleby, escapejs, filesizeformat, get_digit, json_script" { test "builtin filters - divisibleby, escapejs, filesizeformat, get_digit, json_script" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("26 - divisibleby, escapejs, filesizeformat, get_digit, json_script\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -579,6 +657,9 @@ test "builtin filters - divisibleby, escapejs, filesizeformat, get_digit, json_s
} }
test "builtin filters - escapeseq, iriencode, linenumbers" { test "builtin filters - escapeseq, iriencode, linenumbers" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("27 - escapeseq, iriencode, linenumbers\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -601,6 +682,9 @@ test "builtin filters - escapeseq, iriencode, linenumbers" {
} }
test "builtin filters - ljust, rjust, center" { test "builtin filters - ljust, rjust, center" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("28 - ljust, rjust, center\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -623,6 +707,9 @@ test "builtin filters - ljust, rjust, center" {
} }
test "builtin filters - make_list, phone2numeric, pprint" { test "builtin filters - make_list, phone2numeric, pprint" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("29 - make_list, phone2numeric, pprint\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -648,6 +735,9 @@ test "builtin filters - make_list, phone2numeric, pprint" {
} }
test "builtin filters - random, safeseq" { test "builtin filters - random, safeseq" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("30 - random, safeseq\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -670,29 +760,69 @@ test "builtin filters - random, safeseq" {
try testing.expect(safe == .list); try testing.expect(safe == .list);
} }
// test "builtin filters - date, time, timesince, timeuntil" { test "builtin filters - date, now, time, timesince, timeuntil" {
// const alloc = testing.allocator; std.debug.print("____________________________________________________\n", .{});
// var ctx = Context.init(alloc); std.debug.print("31 - date, now, time, timesince, timeuntil\n", .{});
// defer ctx.deinit();
// const alloc = testing.allocator;
// const now = std_time.timestamp(); var ctx = Context.init(alloc);
// defer ctx.deinit();
// // const date = builtin_filters.get("date").?;
// const time = builtin_filters.get("time").?; const times = [_]time.Time{
// const timesince = builtin_filters.get("timesince").?; time.Time.new(2026, 1, 2, 13, 15, 10),
// // const timeuntil = builtin_filters.get("timeuntil").?; time.Time.new(2026, 1, 6, 19, 25, 0),
// time.Time.new(2026, 1, 2, 13, 35, 0),
// // const d = try date(ctx.allocator(), Value{ .int = now }, Value{ .string = "d/m/Y" }); time.Time.new(2026, 1, 2, 13, 15, 19),
// const t = try time(ctx.allocator(), Value{ .int = now }, Value{ .string = "H:i" }); time.Time.new(2025, 1, 2, 13, 15,19),
// time.Time.new(2024, 7, 5, 19, 4, 2),
// // try testing.expect(d.string.len > 0); };
// try testing.expect(t.string.len > 0);
// const date_filter = builtin_filters.get("date").?;
// const since = try timesince(ctx.allocator(), Value{ .int = now - 3600 }, null); const now_filter = builtin_filters.get("now").?;
// try testing.expect(std.mem.indexOf(u8, since.string, "hora") != null); 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" { test "builtin filters - urlize, urlizetrunc, wordwrap, unordered_list" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("32 - urlize, urlizetrunc, wordwrap, unordered_list\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -714,15 +844,16 @@ 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 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 }); const wrapped = try wordwrap(ctx.allocator(), Value{ .string = long_text }, Value{ .int = 20 });
try testing.expect(std.mem.indexOf(u8, wrapped.string, "\n") != null); try testing.expect(std.mem.indexOf(u8, wrapped.string, "\n") != null);
} }
test "builtin filters - unordered_list" { test "builtin filters - unordered_list" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("33 - unordered_list\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
const list = [_]Value{ Value{ .string = "item1" }, Value{ .string = "item2" } }; const list = [_]Value{ Value{ .string = "item1" }, Value{ .string = "item2" } };
try ctx.set("lista", list); try ctx.set("lista", list);
@ -737,6 +868,15 @@ test "builtin filters - unordered_list" {
\\<li>item2</li> \\<li>item2</li>
\\</ul> \\</ul>
; ;
std.debug.print("lista gerada: {any}\n", .{ul.string});
try testing.expectEqualStrings(expected, ul.string); 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;
}

338
src/lorem.zig Normal file
View file

@ -0,0 +1,338 @@
const std = @import("std");
const rand = std.crypto.random;
pub const LOREM_COMMON_P =
\\Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
\\tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
\\veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
\\commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
\\velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
\\occaecat cupidatat non proident, sunt in culpa qui officia deserunt
\\mollit anim id est laborum.
;
pub const LOREM_WORDS = [182][]const u8{
"exercitationem",
"perferendis",
"perspiciatis",
"laborum",
"eveniet",
"sunt",
"iure",
"nam",
"nobis",
"eum",
"cum",
"officiis",
"excepturi",
"odio",
"consectetur",
"quasi",
"aut",
"quisquam",
"vel",
"eligendi",
"itaque",
"non",
"odit",
"tempore",
"quaerat",
"dignissimos",
"facilis",
"neque",
"nihil",
"expedita",
"vitae",
"vero",
"ipsum",
"nisi",
"animi",
"cumque",
"pariatur",
"velit",
"modi",
"natus",
"iusto",
"eaque",
"sequi",
"illo",
"sed",
"ex",
"et",
"voluptatibus",
"tempora",
"veritatis",
"ratione",
"assumenda",
"incidunt",
"nostrum",
"placeat",
"aliquid",
"fuga",
"provident",
"praesentium",
"rem",
"necessitatibus",
"suscipit",
"adipisci",
"quidem",
"possimus",
"voluptas",
"debitis",
"sint",
"accusantium",
"unde",
"sapiente",
"voluptate",
"qui",
"aspernatur",
"laudantium",
"soluta",
"amet",
"quo",
"aliquam",
"saepe",
"culpa",
"libero",
"ipsa",
"dicta",
"reiciendis",
"nesciunt",
"doloribus",
"autem",
"impedit",
"minima",
"maiores",
"repudiandae",
"ipsam",
"obcaecati",
"ullam",
"enim",
"totam",
"delectus",
"ducimus",
"quis",
"voluptates",
"dolores",
"molestiae",
"harum",
"dolorem",
"quia",
"voluptatem",
"molestias",
"magni",
"distinctio",
"omnis",
"illum",
"dolorum",
"voluptatum",
"ea",
"quas",
"quam",
"corporis",
"quae",
"blanditiis",
"atque",
"deserunt",
"laboriosam",
"earum",
"consequuntur",
"hic",
"cupiditate",
"quibusdam",
"accusamus",
"ut",
"rerum",
"error",
"minus",
"eius",
"ab",
"ad",
"nemo",
"fugit",
"officia",
"at",
"in",
"id",
"quos",
"reprehenderit",
"numquam",
"iste",
"fugiat",
"sit",
"inventore",
"beatae",
"repellendus",
"magnam",
"recusandae",
"quod",
"explicabo",
"doloremque",
"aperiam",
"consequatur",
"asperiores",
"commodi",
"optio",
"dolor",
"labore",
"temporibus",
"repellat",
"veniam",
"architecto",
"est",
"esse",
"mollitia",
"nulla",
"a",
"similique",
"eos",
"alias",
"dolore",
"tenetur",
"deleniti",
"porro",
"facere",
"maxime",
"corrupti",
};
pub const LOREM_COMMON_WORDS = [19][]const u8{
"lorem",
"ipsum",
"dolor",
"sit",
"amet",
"consectetur",
"adipisicing",
"elit",
"sed",
"do",
"eiusmod",
"tempor",
"incididunt",
"ut",
"labore",
"et",
"dolore",
"magna",
"aliqua",
};
pub fn sentence(allocator: std.mem.Allocator) ![]const u8 {
const num_sections = rand.intRangeAtMost(u32, 1, 4);
var parts = std.ArrayList([]u8){};
defer {
for (parts.items) |p| allocator.free(p);
parts.deinit(allocator);
}
var i: u32 = 0;
while (i < num_sections) : (i += 1) {
const num_words = rand.intRangeAtMost(u32, 3, 12);
var wds = std.ArrayList([]const u8){};
defer wds.deinit(allocator);
try wds.ensureTotalCapacity(allocator, num_words);
var j: u32 = 0;
while (j < num_words) : (j += 1) {
const idx = rand.intRangeAtMost(usize, 0, LOREM_WORDS.len - 1);
try wds.append(allocator, LOREM_WORDS[idx]);
}
const section = try std.mem.join(allocator, " ", wds.items);
try parts.append(allocator, section);
}
const text = try std.mem.join(allocator, ", ", parts.items);
defer allocator.free(text);
var result = try allocator.alloc(u8, text.len + 1);
if (text.len > 0) {
result[0] = std.ascii.toUpper(text[0]);
@memcpy(result[1..text.len], text[1..]);
}
result[text.len] = if (rand.boolean()) '.' else '?';
return result;
}
pub fn paragraph(allocator: std.mem.Allocator) ![]const u8 {
const num_sentences = rand.intRangeAtMost(u32, 1, 4);
var sentences = std.ArrayList([]const u8){};
defer sentences.deinit(allocator);
for (0..num_sentences) |_| {
try sentences.append(allocator, try sentence(allocator));
}
return try std.mem.join(allocator, ". ", sentences.items);
}
pub fn paragraphs(allocator: std.mem.Allocator, count: u32, random: bool) ![]const u8 {
var pa = std.ArrayList([]const u8){};
defer pa.deinit(allocator);
if (count == 0) return "";
if (random == true) {
for (0..count) |_| {
const pg = try paragraph(allocator);
if (pg.len > 0) {
try pa.append(allocator, try std.fmt.allocPrint(allocator, "<p>{s}</p>", .{pg}));
}
}
return try std.mem.join(allocator, "\n", pa.items);
}
const first = try std.fmt.allocPrint(allocator, "<p>{s}</p>", .{LOREM_COMMON_P});
if (count == 1) {
return first;
}
const ncount: u32 = count - 1;
try pa.append(allocator, first);
for (0..ncount) |_| {
const pg = try paragraph(allocator);
if (pg.len > 0) {
try pa.append(allocator, try std.fmt.allocPrint(allocator, "<p>{s}</p>", .{pg}));
}
}
return try std.mem.join(allocator, "\n", pa.items);
}
pub fn words(allocator: std.mem.Allocator, count: u32, random: bool) ![]const u8 {
var wd = std.ArrayList([]const u8){};
defer wd.deinit(allocator);
if (random == true) {
for (0..count) |_| {
const idx = rand.intRangeAtMost(usize, 0, LOREM_COMMON_WORDS.len - 1);
try wd.append(allocator, LOREM_COMMON_WORDS[idx]);
}
return try std.mem.join(allocator, " ", wd.items);
}
var inc: u32 = 0;
for (LOREM_COMMON_WORDS) |word| {
try wd.append(allocator, word);
inc += 1;
if (inc >= count or inc >= 20) break;
}
if (count >= 20) {
const ncount = count - inc;
for (0..ncount) |_| {
const idx = rand.intRangeAtMost(usize, 0, LOREM_COMMON_WORDS.len - 1);
try wd.append(allocator, LOREM_COMMON_WORDS[idx]);
}
}
return try std.mem.join(allocator, " ", wd.items);
}

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,8 @@ const builtin_filters = @import("filters.zig").builtin_filters;
const FilterError = @import("filters.zig").FilterError; const FilterError = @import("filters.zig").FilterError;
const parser = @import("parser.zig"); const parser = @import("parser.zig");
const TemplateCache = @import("cache.zig").TemplateCache; const TemplateCache = @import("cache.zig").TemplateCache;
const time = @import("time.zig");
const lorem = @import("lorem.zig");
pub const RenderError = error{ pub const RenderError = error{
InvalidCharacter, InvalidCharacter,
@ -36,7 +38,8 @@ pub const Renderer = struct {
fn checkForExtends(self: *const Renderer, nodes: []parser.Node) ?parser.Node { fn checkForExtends(self: *const Renderer, nodes: []parser.Node) ?parser.Node {
_ = self; _ = self;
for (nodes) |n| { for (nodes) |n| {
if (n.type == .extends) return n; // if (n.type == .extends) return n;
if (n.type == .tag and n.tag.?.kind == .extends) return n;
} }
return null; return null;
} }
@ -60,7 +63,8 @@ pub const Renderer = struct {
const extends_node = self.checkForExtends(nodes); const extends_node = self.checkForExtends(nodes);
if (extends_node) |ext| { if (extends_node) |ext| {
const base_template = try self.readTemplateFile(ext.extends.?.parent_name); // const base_template = try self.readTemplateFile(ext.extends.?.parent_name);
const base_template = try self.readTemplateFile(ext.tag.?.body.extends.parent_name);
defer self.allocator.free(base_template); defer self.allocator.free(base_template);
var base_parser = parser.Parser.init(base_template); var base_parser = parser.Parser.init(base_template);
@ -122,19 +126,18 @@ pub const Renderer = struct {
fn renderWithInheritance(self: *const Renderer, alloc: Allocator, base_nodes: []parser.Node, child_nodes: []parser.Node, writer: anytype) RenderError!void { fn renderWithInheritance(self: *const Renderer, alloc: Allocator, base_nodes: []parser.Node, child_nodes: []parser.Node, writer: anytype) RenderError!void {
for (base_nodes) |base_node| { for (base_nodes) |base_node| {
if (base_node.type == .block) { if (base_node.type == .tag and base_node.tag.?.kind == .block) {
const block_name = base_node.block.?.name; const block_name = base_node.tag.?.body.block.name;
// Procura no filho
const child_block = self.findChildBlock(child_nodes, block_name); const child_block = self.findChildBlock(child_nodes, block_name);
if (child_block) |child| { if (child_block) |child| {
// Renderiza o filho, passando o conteúdo do pai para block.super // Renderiza o filho, passando o conteúdo do pai para block.super
for (child.body) |child_node| { for (child.body) |child_node| {
try self.renderNode(alloc, child_nodes, child_node, writer, null, base_node.block.?.body); try self.renderNode(alloc, child_nodes, child_node, writer, null, base_node.tag.?.body.block.body);
} }
} else { } else {
// Renderiza o do pai // Renderiza o do pai
for (base_node.block.?.body) |child| { for (base_node.tag.?.body.block.body) |child| {
try self.renderNode(alloc, child_nodes, child, writer, null, null); try self.renderNode(alloc, child_nodes, child, writer, null, null);
} }
} }
@ -186,167 +189,244 @@ pub const Renderer = struct {
try self.valueToString(alloc, &buf, value); try self.valueToString(alloc, &buf, value);
try writer.writeAll(buf.items); try writer.writeAll(buf.items);
}, },
.if_block => { .tag => {
const condition = try self.evaluateCondition(alloc, node.@"if".?.condition); switch (node.tag.?.kind) {
.if_block => {
const condition = try self.evaluateCondition(alloc, node.tag.?.body.@"if".condition);
if (condition) { if (condition) {
for (node.@"if".?.true_body) |child| { for (node.tag.?.body.@"if".true_body) |child| {
try self.renderNode(alloc, nodes, child, writer, null, null); try self.renderNode(alloc, nodes, child, writer, null, null);
} }
} else { } else {
for (node.@"if".?.false_body) |child| { for (node.tag.?.body.@"if".false_body) |child| {
try self.renderNode(alloc, nodes, child, writer, null, null); try self.renderNode(alloc, nodes, child, writer, null, null);
} }
}
},
.include => {
const included_template = try self.readTemplateFile(node.include.?.template_name);
defer alloc.free(included_template);
var included_parser = parser.Parser.init(included_template);
const included_nodes = try included_parser.parse(alloc);
defer {
for (included_nodes) |n| n.deinit(alloc);
alloc.free(included_nodes);
}
// Renderiza o include no contexto atual (sem novo contexto)
for (included_nodes) |included_node| {
try self.renderNode(alloc, nodes, included_node, writer, context, null);
}
},
.for_block => {
const list_value = self.context.get(node.@"for".?.iterable) orelse Value.null;
const list = switch (list_value) {
.list => |l| l,
else => return,
};
for (list) |item| {
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set(node.@"for".?.loop_var, item);
for (node.@"for".?.body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx, null);
}
if (node.@"for".?.body.len == 0) {
for (node.@"for".?.empty_body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx, null);
} }
} },
.include => {
const included_template = try self.readTemplateFile(node.tag.?.body.include.template_path);
defer alloc.free(included_template);
var included_parser = parser.Parser.init(included_template);
const included_nodes = try included_parser.parse(alloc);
defer {
for (included_nodes) |n| n.deinit(alloc);
alloc.free(included_nodes);
}
// Renderiza o include no contexto atual (sem novo contexto)
for (included_nodes) |included_node| {
try self.renderNode(alloc, nodes, included_node, writer, context, null);
}
},
.for_block => {
const list_value = self.context.get(node.tag.?.body.@"for".iterable) orelse Value.null;
const list = switch (list_value) {
.list => |l| l,
else => return,
};
for (list) |item| {
var ctx = Context.init(alloc);
defer ctx.deinit();
try ctx.set(node.tag.?.body.@"for".loop_var, item);
for (node.tag.?.body.@"for".body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx, null);
}
if (node.tag.?.body.@"for".body.len == 0) {
for (node.tag.?.body.@"for".empty_body) |child| {
try self.renderNode(alloc, nodes, child, writer, &ctx, null);
}
}
}
},
.super => {
if (parent_block_nodes) |parent| {
for (parent) |child| {
try self.renderNode(alloc, nodes, child, writer, null, null);
}
}
},
.block => {
for (node.tag.?.body.block.body) |child| {
const parent_content = parent_block_nodes orelse node.tag.?.body.block.body;
try self.renderNode(alloc, nodes, child, writer, null, parent_content);
}
},
.widthratio => {
var divisor: Value = Value{ .float = 1.0 };
var float_divisor: f64 = 1.0;
var value: Value = Value{ .float = 1.0 };
var float_value: f64 = 1.0;
var max_value: Value = Value{ .float = 1.0 };
var float_max_value: f64 = 1.0;
if (!std.mem.eql(u8, node.tag.?.body.widthratio.value, "")) {
value = Value{ .string = node.tag.?.body.widthratio.value };
if (self.context.get(node.tag.?.body.widthratio.value)) |v| {
value = v;
}
float_value = switch (value) {
.int => @as(f64, @floatFromInt(value.int)),
.float => value.float,
.string => std.fmt.parseFloat(f64, value.string) catch 1.0,
else => 1.0,
};
}
if (!std.mem.eql(u8, node.tag.?.body.widthratio.max_value, "")) {
max_value = Value{ .string = node.tag.?.body.widthratio.max_value };
if (self.context.get(node.tag.?.body.widthratio.max_value)) |v| {
max_value = v;
}
float_max_value = switch (max_value) {
.int => @as(f64, @floatFromInt(max_value.int)),
.float => max_value.float,
.string => std.fmt.parseFloat(f64, max_value.string) catch 1.0,
else => 1.0,
};
}
if (node.tag.?.body.widthratio.divisor) |div| {
divisor = Value{ .string = div };
if (self.context.get(div)) |d| {
divisor = d;
}
float_divisor = switch (divisor) {
.int => @as(f64, @floatFromInt(divisor.int)),
.float => divisor.float,
.string => std.fmt.parseFloat(f64, divisor.string) catch 0.0,
else => 1.0,
};
}
const ratio = (float_value / float_max_value) * float_divisor;
try writer.writeAll(std.fmt.allocPrint(alloc, "{d}", .{ratio}) catch "0");
},
.now => {
var format: []const u8 = node.tag.?.body.now.format;
if (format.len == 0) format = "Y-m-d H:i:s";
const datetime = try time.Time.now().toStringAlloc(alloc, format);
try writer.writeAll(datetime);
},
.csrf_token => {
const token = self.context.get("csrf_token");
if (token == null) return;
try writer.writeAll(std.fmt.allocPrint(alloc, "<input type=\"hidden\" name=\"csrfmiddlewaretoken\" value=\"{s}\">", .{token.?.string}) catch "");
},
.firstof => {
const values = node.tag.?.body.firstof.values;
for (values) |value| {
if (self.context.get(value)) |v| {
if (!isTruthy(v)) continue;
var buf = ArrayListUnmanaged(u8){};
defer buf.deinit(alloc);
try self.valueToString(alloc, &buf, v);
try writer.writeAll(buf.items);
return;
} else {
const check_value = self.resolveStringVariable(value).?;
if (!isTruthy(check_value)) continue;
var buf = ArrayListUnmanaged(u8){};
defer buf.deinit(alloc);
try self.valueToString(alloc, &buf, Value{ .string = value });
try writer.writeAll(buf.items);
return;
}
}
try writer.writeAll(node.tag.?.body.firstof.fallback);
},
.lorem => {
const count = node.tag.?.body.lorem.count;
const method = node.tag.?.body.lorem.method;
const random = node.tag.?.body.lorem.random;
if (count == null and method == null) {
if (random == false) {
try writer.writeAll(lorem.LOREM_COMMON_P);
return;
}else {
try writer.writeAll(try lorem.sentence(alloc));
return;
}
}
const ncount: u32 = std.fmt.parseInt(u32, count.?, 10) catch 1;
if (std.mem.eql(u8, method.?, "p")) {
const lorem_ = try lorem.paragraphs(alloc, ncount, random);
try writer.writeAll(lorem_);
return;
} else {
const lorem_ = try lorem.words(alloc, ncount, random);
try writer.writeAll(lorem_);
return;
}
},
else => {},
} }
}, },
.super => {
if (parent_block_nodes) |parent| {
for (parent) |child| {
try self.renderNode(alloc, nodes, child, writer, null, null);
}
}
},
.block => {
for (node.block.?.body) |child| {
const parent_content = parent_block_nodes orelse node.block.?.body;
try self.renderNode(alloc, nodes, child, writer, null, parent_content);
}
},
.widthratio => {
var divisor: Value = Value{ .float = 1.0 };
var float_divisor: f64 = 1.0;
var value: Value = Value{ .float = 1.0 };
var float_value: f64 = 1.0;
var max_value: Value = Value{ .float = 1.0 };
var float_max_value: f64 = 1.0;
if (!std.mem.eql(u8, node.widthratio.?.value, "")) {
value = Value{ .string = node.widthratio.?.value };
if (self.context.get(node.widthratio.?.value)) |v| {
value = v;
}
float_value = switch (value) {
.int => @as(f64, @floatFromInt(value.int)),
.float => value.float,
.string => std.fmt.parseFloat(f64, value.string) catch 1.0,
else => 1.0,
};
}
if (!std.mem.eql(u8, node.widthratio.?.max_value, "")) {
max_value = Value{ .string = node.widthratio.?.max_value };
if (self.context.get(node.widthratio.?.max_value)) |v| {
max_value = v;
}
float_max_value = switch (max_value) {
.int => @as(f64, @floatFromInt(max_value.int)),
.float => max_value.float,
.string => std.fmt.parseFloat(f64, max_value.string) catch 1.0,
else => 1.0,
};
}
if (node.widthratio.?.divisor) |div| {
divisor = Value{ .string = div };
if (self.context.get(div)) |d| {
divisor = d;
}
float_divisor = switch (divisor) {
.int => @as(f64, @floatFromInt(divisor.int)),
.float => divisor.float,
.string => std.fmt.parseFloat(f64, divisor.string) catch 0.0,
else => 1.0,
};
}
const ratio = (float_value / float_max_value) * float_divisor;
try writer.writeAll(std.fmt.allocPrint(alloc, "{d}", .{ratio}) catch "0");
},
else => {},
} }
} }
fn resolveStringVariable(self: *const Renderer, value: []const u8) ?Value {
_ = self;
if (std.mem.eql(u8, value, "true")) return Value{ .bool = true };
if (std.mem.eql(u8, value, "false")) return Value{ .bool = false };
const is_int = std.fmt.parseInt(i64, value, 10) catch |err| switch (err) {
error.InvalidCharacter => null,
error.Overflow => null,
};
if (is_int != null) return Value{ .int = is_int.? };
const is_float = std.fmt.parseFloat(f64, value) catch |err| switch (err) {
error.InvalidCharacter => null,
};
if (is_float != null) return Value{ .float = is_float.? };
return Value{ .string = value };
}
fn findChildBlock(self: *const Renderer, nodes: []parser.Node, name: []const u8) ?parser.BlockNode { fn findChildBlock(self: *const Renderer, nodes: []parser.Node, name: []const u8) ?parser.BlockNode {
_ = self; _ = self;
for (nodes) |n| { for (nodes) |n| {
if (n.type != .block) continue; if (n.type != .tag) continue;
if (std.mem.eql(u8, n.block.?.name, name)) return n.block.?; if (n.tag.?.kind != .block) continue;
if (std.mem.eql(u8, n.tag.?.body.block.name, name)) return n.tag.?.body.block;
} }
return null; return null;
} }
fn toValue(self: *Context, value: anytype) RenderError!Value { fn escapeHtml(self: *const Renderer, value: Value) !Value {
const T = @TypeOf(value); const s = switch (value) {
return switch (@typeInfo(T)) { .string => |str| str,
.bool => Value{ .bool = value }, else => return value,
.int, .comptime_int => Value{ .int = @intCast(value) },
.float, .comptime_float => Value{ .float = @floatCast(value) },
.pointer => Value{ .string = try std.fmt.allocPrint(self.allocator(), "{s}", .{value}) },
.@"struct" => blk: {
var dict = std.StringHashMapUnmanaged(Value){};
inline for (std.meta.fields(T)) |field| {
const field_val = @field(value, field.name);
const converted = try self.toValue(field_val);
try dict.put(self.allocator(), field.name, converted);
}
break :blk Value{ .dict = dict };
},
.array => blk: {
var list = try self.allocator().alloc(Value, value.len);
for (value, 0..) |item, i| {
list[i] = try self.toValue(item);
}
break :blk Value{ .list = list };
},
.optional => if (value) |v| try self.toValue(v) else .null,
.null => .null,
// CASO ESPECIAL: o valor é um Value (ex: lista de Value)
.@"union" => if (T == Value) value else @compileError("Unsupported union type: " ++ @typeName(T)),
else => @compileError("Unsupported type: " ++ @typeName(T)),
}; };
var result = std.ArrayList(u8){};
for (s) |c| {
switch (c) {
'&' => try result.appendSlice(self.allocator, "&amp;"),
'<' => try result.appendSlice(self.allocator, "&lt;"),
'>' => try result.appendSlice(self.allocator, "&gt;"),
'"' => try result.appendSlice(self.allocator, "&quot;"),
'\'' => try result.appendSlice(self.allocator, "&#x27;"),
else => try result.append(self.allocator, c),
}
}
return Value{ .string = try result.toOwnedSlice(self.allocator) };
} }
fn valueToString(self: *const Renderer, alloc: Allocator, buf: *ArrayListUnmanaged(u8), value: Value) RenderError!void { fn valueToString(self: *const Renderer, alloc: Allocator, buf: *ArrayListUnmanaged(u8), value: Value) RenderError!void {

View file

@ -10,7 +10,7 @@ const TemplateCache = @import("cache.zig").TemplateCache;
test "renderer: literal + variável simples" { test "renderer: literal + variável simples" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n1 - renderer: literal + variável simples\n\n", .{}); std.debug.print("1 - renderer: literal + variável simples\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -20,7 +20,7 @@ test "renderer: literal + variável simples" {
const renderer = Renderer.init(&ctx, &cache); const renderer = Renderer.init(&ctx, &cache);
try ctx.set("nome", Value{ .string = "Mariana" }); try ctx.set("nome", Value{ .string = "Fulana" });
var buf = std.ArrayList(u8){}; var buf = std.ArrayList(u8){};
defer buf.deinit(alloc); defer buf.deinit(alloc);
@ -33,12 +33,12 @@ test "renderer: literal + variável simples" {
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items}); // std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings("Olá, Mariana! Bem-vinda.", buf.items); try testing.expectEqualStrings("Olá, Fulana! Bem-vinda.", buf.items);
} }
test "renderer: filtros + autoescape" { test "renderer: filtros + autoescape" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n2 - renderer: filtros + autoescape\n\n", .{}); std.debug.print("2 - renderer: filtros + autoescape\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -68,7 +68,6 @@ test "renderer: filtros + autoescape" {
\\Filtrado: maiusculo-e-slug \\Filtrado: maiusculo-e-slug
; ;
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items}); // std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items); try testing.expectEqualStrings(expected, buf.items);
@ -76,7 +75,7 @@ test "renderer: filtros + autoescape" {
test "literal simples" { test "literal simples" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n3 - literal simples\n\n", .{}); std.debug.print("3 - literal simples\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -100,7 +99,7 @@ test "literal simples" {
test "variável com filtro encadeado e autoescape" { test "variável com filtro encadeado e autoescape" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n4 - variável com filtro encadeado e autoescape\n\n", .{}); std.debug.print("4 - variável com filtro encadeado e autoescape\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -126,7 +125,7 @@ test "variável com filtro encadeado e autoescape" {
test "autoescape com safe" { test "autoescape com safe" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n5 - autoescape com safe\n\n", .{}); std.debug.print("5 - autoescape com safe\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -150,10 +149,10 @@ test "autoescape com safe" {
try testing.expectEqualStrings("Escape: &lt;div&gt;conteúdo&lt;/div&gt; | Safe: <div>conteúdo</div>", buf.items); try testing.expectEqualStrings("Escape: &lt;div&gt;conteúdo&lt;/div&gt; | Safe: <div>conteúdo</div>", buf.items);
} }
// TODO: evaluationConditions more complex // // TODO: evaluationConditions more complex
test "renderer - if and for" { test "renderer - if and for" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n6 - renderer - if and for\n\n", .{}); std.debug.print("6 - renderer - if and for\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -195,7 +194,7 @@ test "renderer - if and for" {
test "renderer - block and extends" { test "renderer - block and extends" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n7 - renderer - block and extends\n\n", .{}); std.debug.print("7 - renderer - block and extends\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -260,7 +259,7 @@ test "renderer - block and extends" {
test "renderer - block and extends with super" { test "renderer - block and extends with super" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n8 - renderer - block and extends with super\n\n", .{}); std.debug.print("8 - renderer - block and extends with super\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
defer ctx.deinit(); defer ctx.deinit();
@ -327,7 +326,7 @@ test "renderer - block and extends with super" {
test "renderer - include" { test "renderer - include" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n9 - renderer - include\n\n", .{}); std.debug.print("9 - renderer - include\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const header = const header =
@ -386,7 +385,7 @@ test "renderer - include" {
test "renderer - comment" { test "renderer - comment" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n10 - renderer - comment\n\n", .{}); std.debug.print("10 - renderer - comment\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const template = const template =
@ -434,7 +433,7 @@ test "renderer - comment" {
test "renderer - full template with extends, super, include, comment" { test "renderer - full template with extends, super, include, comment" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n11 - renderer - full template with extends, super, include, comment\n\n", .{}); std.debug.print("11 - renderer - full template with extends, super, include, comment\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const header = "<header>Bem-vindo</header>"; const header = "<header>Bem-vindo</header>";
@ -482,14 +481,16 @@ test "renderer - full template with extends, super, include, comment" {
// std.debug.print("OUTPUT:\n\n{s}\n", .{output}); // std.debug.print("OUTPUT:\n\n{s}\n", .{output});
try testing.expect(std.mem.indexOf(u8, output, "<header>Bem-vindo</header>") != null); try testing.expect(std.mem.indexOf(u8, output, "<header>Bem-vindo</header>") != null);
try testing.expect(std.mem.indexOf(u8, output, "<main>") != null);
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null); try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null); try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
try testing.expect(std.mem.indexOf(u8, output, "Isso não aparece") == null); try testing.expect(std.mem.indexOf(u8, output, "Isso não aparece") == null);
try testing.expect(std.mem.indexOf(u8, output, "bazinga") == null);
} }
test "renderer - if inside block" { test "renderer - if inside block" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n12 - render - if inside block\n\n", .{}); std.debug.print("12 - render - if inside block\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const base = const base =
@ -541,11 +542,12 @@ test "renderer - if inside block" {
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null); try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null); try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
try testing.expect(std.mem.indexOf(u8, output, "Oops") == null); try testing.expect(std.mem.indexOf(u8, output, "Oops") == null);
try testing.expect(std.mem.indexOf(u8, output, "Idade: 23") != null);
} }
test "renderer - if with operators" { test "renderer - if with operators" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n13 - render - if inside block\n\n", .{}); std.debug.print("13 - render - if inside block\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
var ctx = Context.init(alloc); var ctx = Context.init(alloc);
@ -585,7 +587,7 @@ test "renderer - if with operators" {
test "renderer - widthratio inside block" { test "renderer - widthratio inside block" {
std.debug.print("____________________________________________________\n", .{}); std.debug.print("____________________________________________________\n", .{});
std.debug.print("\n14 - render - widthratio inside block\n\n", .{}); std.debug.print("14 - render - widthratio inside block\n", .{});
const alloc = testing.allocator; const alloc = testing.allocator;
const base = const base =
@ -633,3 +635,436 @@ test "renderer - widthratio inside block" {
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null); try testing.expect(std.mem.indexOf(u8, output, "Conteúdo padrão") != null);
try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null); try testing.expect(std.mem.indexOf(u8, output, "Conteúdo do filho") != null);
} }
test "renderer - now" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("15 - render now\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("idade", Value{ .int = 20 });
const template =
\\{% now %}
\\{% now \"Y-m-d H:i:s\" %}
\\{% now \"Y" %}
\\{% now \"m\" %}
\\{% now \"n\" %}
\\{% now \"d\" %}
\\{% now \"j\" %}
\\{% now \"F\" %}
\\{% now \"M\" %}
\\{% now \"l\" %}
\\{% now \"D\" %}
\\{% now \"H:i:s\" %}
\\{% now \"H\" %}
\\{% now \"G\" %}
\\{% now \"i\" %}
\\{% now \"s\" %}
\\{% now \"a\" %}
\\{% now \"A\" %}
\\{% now \"P\" %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
// try testing.expect(std.mem.indexOf(u8, buf.items, "Maior") != null);
}
test "renderer - csrf_token in context" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("15 - csrf_token in context\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const token: []const u8 = "zh5fyUSICjXNsDTtJCjl9A3O2dDSHhYFlIngAEO6PXK9NX56Z1XLEy7doYuPcE0u";
try ctx.set("csrf_token", token);
const template =
\\{% csrf_token %}
;
const expected =
\\<input type="hidden" name="csrfmiddlewaretoken" value="zh5fyUSICjXNsDTtJCjl9A3O2dDSHhYFlIngAEO6PXK9NX56Z1XLEy7doYuPcE0u">
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - csrf_token not in context" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("16 - csrf_token not in context\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% csrf_token %}
;
const expected =
\\
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
// TODO: add parse filters to variables
test "renderer - firstof withtout fallback" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("17 - firstof without fallback\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("var1", "");
try ctx.set("var2", "baz");
const template =
\\{% firstof var1 var2 %}
;
const expected =
\\baz
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - firstof with fallback" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("18 - firstof with fallback\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
try ctx.set("var1", "");
try ctx.set("var2", 0);
const template =
\\{% firstof var1 var2 "Oops no value!" %}
;
const expected =
\\Oops no value!
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - firstof without value in context" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("19 - firstof without value in context\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% firstof 0 true "Oops no value!" %}
;
const expected =
\\true
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - firstof missing vars" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("20 - firstof missing vars\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% firstof %}
;
const expected =
\\
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - firstof missing vars with fallback" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("21 - firstof missing vars with fallback\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% firstof "nothing here" %}
;
const expected =
\\nothing here
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
// std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings(expected, buf.items);
}
test "renderer - lorem" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("22 - lorem\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(std.mem.indexOf(u8, buf.items, "Lorem ipsum") != null);
}
test "renderer - lorem with count and method words" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("23 - lorem with count and method words\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem 3 w %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expectEqualStrings("lorem ipsum dolor", buf.items);
}
test "renderer - lorem with count and method paragraphs" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("24 - lorem with count and method paragraphs\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem 5 p %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
const qty = std.mem.count(u8, buf.items, "<p>");
try testing.expect(std.mem.indexOf(u8, buf.items, "Lorem ipsum dolor") != null);
try testing.expect(qty == 5);
}
test "renderer - lorem only random" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("25 - lorem only random\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem true %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
try testing.expect(buf.items.len > 0);
}
test "renderer - lorem words random" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("26 - lorem words random\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem 6 w true %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
const spaces = std.mem.count(u8, buf.items, " ");
try testing.expect(spaces == 5);
}
test "renderer - lorem paragraphs random" {
std.debug.print("____________________________________________________\n", .{});
std.debug.print("26 - lorem paragraphs random\n", .{});
const alloc = testing.allocator;
var ctx = Context.init(alloc);
defer ctx.deinit();
var cache = TemplateCache.init(alloc);
defer cache.deinit();
const renderer = Renderer.init(&ctx, &cache);
const template =
\\{% lorem 3 p true %}
;
var buf = std.ArrayList(u8){};
defer buf.deinit(alloc);
try renderer.renderString(template, buf.writer(alloc));
std.debug.print("OUTPUT:\n\n{s}\n", .{buf.items});
const spaces = std.mem.count(u8, buf.items, "<p>");
try testing.expect(spaces == 3);
}

874
src/time.zig Normal file
View file

@ -0,0 +1,874 @@
// https://github.com/cztomsik/tokamak
const std = @import("std");
const util = @import("util.zig");
const testing = std.testing;
const meta = @import("meta.zig");
const dlt = @import("delta.zig");
const RATA_MIN = date_to_rata(Date.MIN);
const RATA_MAX = date_to_rata(Date.MAX);
const RATA_TO_UNIX = 719468;
const EOD = 86_400 - 1;
pub const DAY_NAMES_SHORT = [_][]const u8{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
pub const DAY_NAMES_LONG = [_][]const u8{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
pub const MONTH_NAMES_SHORT = [_][]const u8{ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
pub const MONTH_NAMES_LONG = [_][]const u8{ "", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
pub const MONTH_DAYS = [12]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
pub const TIME_CHUNKS = [4]u32{
60 * 60 * 24 * 7, //week
60 * 60 * 24, //day
60 * 60, //hour
60, //minute
};
pub const TIME_STRINGS = [6][]const u8{ "year", "month", "week", "day", "hour", "minute" };
// TODO: Decide if we want to use std.debug.assert(), @panic() or just throw an error
fn checkRange(num: anytype, min: @TypeOf(num), max: @TypeOf(num)) void {
if (util.lt(num, min) or util.gt(num, max)) {
// TODO: fix later (we can't use {f} and {any} is also wrong)
// std.log.warn("Value {} is not in range [{}, {}]", .{ num, min, max });
std.log.warn("Value not in range", .{});
}
}
pub const TimeUnit = enum { second, minute, hour, day, month, year };
pub const DateUnit = enum { day, month, year };
// pub const SECS_PER_DAY: i64 = 86_400;
// pub const SECS_PER_HOUR: i64 = 3_600;
// pub const SECS_PER_MIN: i64 = 60;
pub const TimeError = error{
Eof,
ExpectedNull,
ExpectedValue,
InvalidCharacter,
InvalidFormat,
Overflow,
OutOfMemory,
};
// https://www.youtube.com/watch?v=0s9F4QWAl-E&t=2120
pub fn isLeapYear(year: i32) bool {
const d: i32 = if (@mod(year, 100) != 0) 4 else 16;
return (year & (d - 1)) == 0;
}
// https://www.youtube.com/watch?v=0s9F4QWAl-E&t=2257
pub fn daysInMonth(year: i32, month: u8) u8 {
if (month == 2) {
return if (isLeapYear(year)) 29 else 28;
}
return 30 | (month ^ (month >> 3));
}
pub fn formatDateTime(alloc: std.mem.Allocator, t: Time, format_str: []const u8) ![]u8 {
var result = std.ArrayList(u8){};
defer result.deinit(alloc);
var writer = result.writer(alloc);
var i: usize = 0;
while (i < format_str.len) : (i += 1) {
const c = format_str[i];
if (c == '\\') {
i += 1;
if (i >= format_str.len) break;
try writer.writeByte(format_str[i]);
continue;
}
// Todos os códigos do date + time que implementamos
switch (c) {
// === Códigos de data (do filtro date) ===
'Y' => try writer.print("{d:0>4}", .{@as(u16, @intCast(t.year()))}),
'm' => try writer.print("{d:0>2}", .{t.month()}),
'n' => try writer.print("{d}", .{t.month()}),
'd' => try writer.print("{d:0>2}", .{t.day()}),
'j' => try writer.print("{d}", .{t.day()}),
'F' => try writer.writeAll(t.monthNameLong()),
'M' => try writer.writeAll(t.monthNameShort()),
'l' => try writer.writeAll(t.weekdayNameLong()),
'D' => try writer.writeAll(t.weekdayNameShort()),
// === Códigos de tempo (do filtro time) ===
'H' => try writer.print("{d:0>2}", .{t.hour()}),
'G' => try writer.print("{d}", .{t.hour()}),
'i' => try writer.print("{d:0>2}", .{t.minute()}),
's' => try writer.print("{d:0>2}", .{t.second()}),
'a' => try writer.writeAll(if (t.hour() < 12) "a.m." else "p.m."),
'A' => try writer.writeAll(if (t.hour() < 12) "AM" else "PM"),
'P' => {
const hr24 = t.hour();
const min = t.minute();
if (hr24 == 0 and min == 0) {
try writer.writeAll("midnight");
} else if (hr24 == 12 and min == 0) {
try writer.writeAll("noon");
} else {
var hr12 = @mod(hr24, 12);
if (hr12 == 0) hr12 = 12;
try writer.print("{d}", .{hr12});
if (min > 0) try writer.print(":{d:0>2}", .{min});
try writer.writeAll(if (hr24 < 12) " a.m." else " p.m.");
}
},
'u' => try writer.writeAll("000000"),
else => try writer.writeByte(c),
}
}
return try result.toOwnedSlice(alloc);
}
pub const Date = struct {
year: i32,
month: u8,
day: u8,
pub const MIN = Date.ymd(-1467999, 1, 1);
pub const MAX = Date.ymd(1471744, 12, 31);
pub fn cmp(a: Date, b: Date) std.math.Order {
if (a.year != b.year) return util.cmp(a.year, b.year);
if (a.month != b.month) return util.cmp(a.month, b.month);
return util.cmp(a.day, b.day);
}
pub fn parse(str: []const u8) TimeError!Date {
var it = std.mem.splitScalar(u8, str, '-');
return ymd(
try std.fmt.parseInt(i32, it.next() orelse return error.Eof, 10),
try std.fmt.parseInt(u8, it.next() orelse return error.Eof, 10),
try std.fmt.parseInt(u8, it.next() orelse return error.Eof, 10),
);
}
pub fn ymd(year: i32, month: u8, day: u8) Date {
return .{
.year = year,
.month = month,
.day = day,
};
}
pub fn today() Date {
return Time.now().date();
}
pub fn yesterday() Date {
return today().add(.day, -1);
}
pub fn tomorrow() Date {
return today().add(.day, 1);
}
pub fn startOf(unit: DateUnit) Date {
return today().setStartOf(unit);
}
pub fn endOf(unit: DateUnit) Date {
return today().setEndOf(unit);
}
pub fn setStartOf(self: Date, unit: DateUnit) Date {
return switch (unit) {
.day => self,
.month => ymd(self.year, self.month, 1),
.year => ymd(self.year, 1, 1),
};
}
pub fn setEndOf(self: Date, unit: DateUnit) Date {
return switch (unit) {
.day => self,
.month => ymd(self.year, self.month, daysInMonth(self.year, self.month)),
.year => ymd(self.year, 12, 31),
};
}
pub fn add(self: Date, part: DateUnit, amount: i64) Date {
return switch (part) {
.day => Time.unix(0).setDate(self).add(.days, amount).date(),
.month => {
const total_months = @as(i32, self.month) + @as(i32, @intCast(amount));
const new_year = self.year + @divFloor(total_months - 1, 12);
const new_month = @as(u8, @intCast(@mod(total_months - 1, 12) + 1));
return ymd(
new_year,
new_month,
@min(self.day, daysInMonth(new_year, new_month)),
);
},
.year => {
const new_year = self.year + @as(i32, @intCast(amount));
return ymd(
new_year,
self.month,
@min(self.day, daysInMonth(new_year, self.month)),
);
},
};
}
pub fn dayOfWeek(self: Date) u8 {
const rata_day = date_to_rata(self);
return @intCast(@mod(rata_day + 3, 7));
}
pub fn format(self: Date, writer: anytype) !void {
try writer.print("{d}-{d:0>2}-{d:0>2}", .{
@as(u32, @intCast(self.year)),
self.month,
self.day,
});
}
pub fn ordinal(self: Date) usize {
const days_before_month = [_]u16{ 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
var days: usize = days_before_month[self.month];
days += self.day;
if (self.month > 2 and isLeapYear(self.year)) {
days += 1;
}
return days;
}
pub fn weekday(self: Date) u8 {
const y: i32 = self.year;
const m: u8 = self.month;
const d: u8 = self.day;
const t = [_]i32{ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
var year_adj: i32 = y;
if (m < 3) year_adj -= 1;
var wd: i32 = year_adj + @divFloor(year_adj, 4) - @divFloor(year_adj, 100) + @divFloor(year_adj, 400) + t[m - 1] + @as(i32, d);
wd = @mod(wd, 7);
if (wd < 0) wd += 7;
return @intCast(if (wd == 6) 7 else wd + 1);
}
pub fn isoWeek(self: Date) u8 {
const iso_y = self.isoWeekYear();
const jan4 = Date{ .year = iso_y, .month = 1, .day = 4 };
const jan4_ord: i32 = @intCast(jan4.ordinal());
const self_ord: i32 = @intCast(self.ordinal());
const days_diff = self_ord - jan4_ord;
const week = @divFloor(days_diff + 4, 7) + 1; // +4 corrige o offset (testado em Python)
return @intCast(@max(1, @min(53, week)));
}
pub fn isoWeekYear(self: Date) i32 {
const wd = self.weekday(); // 1=Seg ... 7=Dom
const ord = @as(i32, @intCast(self.ordinal()));
const thursday_ord = ord + (4 - (wd - 1)); // quinta da semana
var y = self.year;
var days_in_year: i32 = 365;
if (isLeapYear(y)) days_in_year = 366;
if (thursday_ord <= 0) {
y -= 1;
} else if (thursday_ord > days_in_year) {
y += 1;
}
return y;
}
};
pub const TimeTime = struct {
hour: u32,
minute: u32,
second: u32,
fn cmp(a: TimeTime, b: TimeTime) std.math.Order {
if (a.hour < b.hour) return .lt;
if (a.hour > b.hour) return .gt;
if (a.minute < b.minute) return .lt;
if (a.minute > b.minute) return .gt;
if (a.second < b.second) return .lt;
if (a.second > b.second) return .gt;
return .eq;
}
};
pub const Time = struct {
epoch: i64,
pub fn parse(str: []const u8) !Time {
if (std.mem.indexOfScalar(u8, str, ' ')) |space| {
// Datetime: "YYYY-MM-DD HH:MM:SS"
const date_str = str[0..space];
const time_str = str[space + 1 ..];
const d = try Date.parse(date_str);
var it = std.mem.splitScalar(u8, time_str, ':');
const h = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10);
const m = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10);
const s = try std.fmt.parseInt(u8, it.next() orelse return error.InvalidFormat, 10);
var t = Time.unix(0).setDate(d);
t = t.setHour(h).setMinute(m).setSecond(s);
return t;
} else {
const d = try Date.parse(str);
return Time.unix(0).setDate(d);
}
return Time.now();
}
pub fn unix(epoch: i64) Time {
return .{ .epoch = epoch };
}
pub fn new(y: i32, m: u8, d: u8, h: ?u32, min: ?u32, sec: ?u32) Time {
var t = unix(0).setDate(.ymd(y, m, d));
if (h) |h_| t = t.setHour(h_);
if (min) |min_| t = t.setMinute(min_);
if (sec) |sec_| t = t.setSecond(sec_);
return t;
}
pub fn now() Time {
return unix(std.time.timestamp());
}
pub fn today() Time {
return unix(0).setDate(.today());
}
pub fn tomorrow() Time {
return unix(0).setDate(.tomorrow());
}
pub fn startOf(unit: TimeUnit) Time {
return Time.now().setStartOf(unit);
}
pub fn endOf(unit: TimeUnit) Time {
return Time.now().setEndOf(unit);
}
pub fn second(self: Time) u32 {
return @intCast(@mod(self.total(.seconds), 60));
}
pub fn setSecond(self: Time, sec: u32) Time {
return self.add(.seconds, @as(i64, sec) - self.second());
}
pub fn minute(self: Time) u32 {
return @intCast(@mod(self.total(.minutes), 60));
}
pub fn setMinute(self: Time, min: u32) Time {
return self.add(.minutes, @as(i64, min) - self.minute());
}
pub fn hour(self: Time) u32 {
return @intCast(@mod(self.total(.hours), 24));
}
pub fn setHour(self: Time, hr: u32) Time {
return self.add(.hours, @as(i64, hr) - self.hour());
}
pub fn date(self: Time) Date {
return rata_to_date(@divTrunc(self.epoch, std.time.s_per_day) + RATA_TO_UNIX);
}
pub fn setDate(self: Time, dat: Date) Time {
var res: i64 = @mod(self.epoch, std.time.s_per_day);
res += (date_to_rata(dat) - RATA_TO_UNIX) * std.time.s_per_day;
return unix(res);
}
pub fn time(self: Time) TimeTime {
return .{
.hour = self.hour(),
.minute = self.minute(),
.second = self.second(),
};
}
pub fn setStartOf(self: Time, unit: TimeUnit) Time {
// TODO: continue :label?
return switch (unit) {
.second => self,
.minute => self.setSecond(0),
.hour => self.setSecond(0).setMinute(0),
.day => self.setSecond(0).setMinute(0).setHour(0),
.month => {
const d = self.date();
return unix(0).setDate(.ymd(d.year, d.month, 1));
},
.year => {
const d = self.date();
return unix(0).setDate(.ymd(d.year, 1, 1));
},
};
}
// TODO: rename to startOfNext?
pub fn next(self: Time, unit: enum { second, minute, hour, day }) Time {
return switch (unit) {
.second => self.add(.seconds, 1),
.minute => self.setSecond(0).add(.minutes, 1),
.hour => self.setSecond(0).setMinute(0).add(.hours, 1),
.day => self.setSecond(0).setMinute(0).setHour(0).add(.hours, 24),
};
}
pub fn setEndOf(self: Time, unit: TimeUnit) Time {
// TODO: continue :label?
return switch (unit) {
.second => self,
.minute => self.setSecond(59),
.hour => self.setSecond(59).setMinute(59),
.day => self.setSecond(59).setMinute(59).setHour(23),
.month => {
const d = self.date();
return unix(EOD).setDate(.ymd(d.year, d.month, daysInMonth(d.year, d.month)));
},
.year => {
const d = self.date();
return unix(EOD).setDate(.ymd(d.year, 12, 31));
},
};
}
pub fn add(self: Time, part: enum { seconds, minutes, hours, days, months, years }, amount: i64) Time {
const n = switch (part) {
.seconds => amount,
.minutes => amount * std.time.s_per_min,
.hours => amount * std.time.s_per_hour,
.days => amount * std.time.s_per_day,
.months => return self.setDate(self.date().add(.month, amount)),
.years => return self.setDate(self.date().add(.year, amount)),
};
return .{ .epoch = self.epoch + n };
}
fn total(self: Time, part: enum { seconds, minutes, hours }) i64 {
return switch (part) {
.seconds => self.epoch,
.minutes => @divTrunc(self.epoch, std.time.s_per_min),
.hours => @divTrunc(self.epoch, std.time.s_per_hour),
};
}
fn year(self: Time) i32 {
return self.date().year;
}
fn month(self: Time) u8 {
return self.date().month;
}
fn day(self: Time) u8 {
return self.date().day;
}
fn monthNameLong(self: Time) []const u8 {
return MONTH_NAMES_LONG[self.date().month];
}
fn monthNameShort(self: Time) []const u8 {
return MONTH_NAMES_SHORT[self.date().month];
}
fn weekdayNameLong(self: Time) []const u8 {
return DAY_NAMES_LONG[self.date().weekday()];
}
fn weekdayNameShort(self: Time) []const u8 {
return DAY_NAMES_SHORT[self.date().weekday()];
}
pub fn format(self: Time, writer: anytype) !void {
try writer.print("{f} {d:0>2}:{d:0>2}:{d:0>2} UTC", .{
self.date(),
self.hour(),
self.minute(),
self.second(),
});
}
pub fn toStringAlloc(self: Time, alloc: std.mem.Allocator, format_str: ?[]const u8) TimeError![]u8 {
const fmt = format_str orelse "Y-m-d H:i:s";
return try formatDateTime(alloc, self, fmt);
}
pub fn toString(self: Time, format_str: ?[]const u8) TimeError![]const u8 {
return try self.toStringAlloc(std.heap.page_allocator, format_str);
}
pub fn addRelative(self: Time, delta: dlt.RelativeDelta) Time {
var d = delta;
d.normalize(); // garante que meses/horas/etc estejam normalizados
// 1. Parte calendáríca (anos + meses + dias)
var dt = self.date();
// anos primeiro (mais estável)
if (d.years != 0) {
dt = dt.add(.year, d.years);
}
// depois meses (respeita dias-in-mês)
if (d.months != 0) {
dt = dt.add(.month, d.months);
}
// por fim dias normais
if (d.days != 0) {
dt = dt.add(.day, d.days);
}
// 2. Parte do relógio (horas, minutos, segundos)
var result = self.setDate(dt);
// Podemos usar o .add() existente para segundos/minutos/horas
if (d.seconds != 0) {
result = result.add(.seconds, d.seconds);
}
if (d.minutes != 0) {
result = result.add(.minutes, d.minutes);
}
if (d.hours != 0) {
result = result.add(.hours, d.hours);
}
return result;
}
pub fn subRelative(self: Time, other: Time) dlt.RelativeDelta {
var delta = dlt.RelativeDelta{};
// Parte de tempo (horas, min, seg) igual antes
var seconds_diff: i64 = self.epoch - other.epoch;
delta.seconds = @as(i32, @intCast(@rem(seconds_diff, 60)));
seconds_diff = @divTrunc(seconds_diff, 60);
delta.minutes = @as(i32, @intCast(@rem(seconds_diff, 60)));
seconds_diff = @divTrunc(seconds_diff, 60);
delta.hours = @as(i32, @intCast(@rem(seconds_diff, 24)));
seconds_diff = @divTrunc(seconds_diff, 24);
// Parte calendárica
var later = self.date();
var earlier = other.date();
const swapped = later.cmp(earlier) == .lt;
if (swapped) {
// const temp = later;
// later = earlier;
// earlier = temp;
std.mem.swap(i32, &later.year, &earlier.year);
}
var years: i32 = later.year - earlier.year;
var months: i32 = @as(i32, later.month) - @as(i32, earlier.month);
if (months < 0) {
years -= 1;
months += 12;
}
var days: i32 = @as(i32, later.day) - @as(i32, earlier.day);
// Ajuste rigoroso para borrow (com handling de bissexto)
if (days < 0) {
const days_in_target = daysInMonth(later.year, later.month);
if (later.day == days_in_target) {
// Caso especial (ex: 28/fev não-bissexto vs 29/fev bissexto): ignora borrow, trata como período completo
days = 0;
} else {
// Borrow normal, mas ajusta se earlier.day > dias no mês anterior (para casos como 29 > 28)
const prev_month = later.add(.month, -1);
var days_borrow: i32 = @as(i32, daysInMonth(prev_month.year, prev_month.month));
if (earlier.day > @as(u8, @intCast(days_borrow))) {
days_borrow = @as(i32, earlier.day);
}
std.debug.print("days_borrow em subRelative {d}\n", .{days_borrow});
days += days_borrow;
months -= 1;
if (months < 0) {
years -= 1;
months += 12;
}
}
}
// Atribui com sinal
delta.years = if (swapped) -years else years;
delta.months = if (swapped) -months else months;
delta.days = if (swapped) -days else days;
// Normaliza ( lida com excessos, mas aqui é para meses/anos)
delta.normalize();
return delta;
}
pub fn timeSince(self: Time, alloc: std.mem.Allocator, then: Time) TimeError![]u8 {
if (self.epoch >= then.epoch) {
return try alloc.dupe(u8, "0 minutes");
}
var total_months: i64 = 0;
const delta_year: i64 = (then.year() - self.year()) * 12;
const delta_month: i32 = @as(i32, @intCast(then.month())) - @as(i32, @intCast(self.month()));
total_months = delta_year + delta_month;
if (self.day() > then.day() or (self.day() == then.day() and self.time().cmp(then.time()) == .gt)) {
total_months -= 1;
}
const months = @rem(total_months, 12);
const years = @divTrunc(total_months, 12);
var pivot_year: i64 = 0;
var pivot_month: i64 = 0;
var pivot: Time = undefined;
if (years > 0 or months > 0) {
pivot_year = @as(i64, self.year()) + years;
pivot_month = @as(i64, self.month()) + months;
if (pivot_month > 12) {
pivot_year += 1;
pivot_month -= 12;
}
const d: u8 = @min(MONTH_DAYS[@intCast(pivot_month - 1)], self.day());
pivot = Time.new(
@as(i32, @intCast(pivot_year)),
@as(u8, @intCast(pivot_month)),
d,
self.hour(),
self.minute(),
self.second(),
);
} else {
pivot = self;
}
var remaining_time = then.epoch - pivot.epoch;
var partials = std.ArrayList(i64){};
errdefer partials.deinit(alloc);
try partials.append(alloc, years);
try partials.append(alloc, months);
for (TIME_CHUNKS) |chunk| {
const count: i32 = @intCast(@divFloor(remaining_time, chunk));
try partials.append(alloc, count);
remaining_time -= count * @as(i32, @intCast(chunk));
}
const min: i64 = std.mem.min(i64, partials.items);
const max: i64 = std.mem.max(i64, partials.items);
if (min == 0 and max == 0) {
return try alloc.dupe(u8, "0 minutes");
}
var buf = std.ArrayList(u8){};
errdefer buf.deinit(alloc);
var count: i32 = 0;
for (partials.items, 0..) |partial, i| {
if (partial > 0) {
if (count >= 2) break;
try buf.appendSlice(alloc, try std.fmt.allocPrint(alloc, "{d} {s}{s}", .{ partial, TIME_STRINGS[i], if (partial > 1) "s" else "" }));
if (count == 0 and i < partials.items.len - 1) try buf.appendSlice(alloc, ", ");
count += 1;
}
}
return try buf.toOwnedSlice(alloc);
}
};
// https://github.com/cassioneri/eaf/blob/1509faf37a0e0f59f5d4f11d0456fd0973c08f85/eaf/gregorian.hpp#L42
fn rata_to_date(N: i64) Date {
checkRange(N, RATA_MIN, RATA_MAX);
// Century.
const N_1: i64 = 4 * N + 3;
const C: i64 = quotient(N_1, 146097);
const N_C: u32 = remainder(N_1, 146097) / 4;
// Year.
const N_2 = 4 * N_C + 3;
const Z: u32 = N_2 / 1461;
const N_Y: u32 = N_2 % 1461 / 4;
const Y: i64 = 100 * C + Z;
// Month and day.
const N_3: u32 = 5 * N_Y + 461;
const M: u32 = N_3 / 153;
const D: u32 = N_3 % 153 / 5;
// Map.
const J: u32 = @intFromBool(M >= 13);
return .{
.year = @intCast(Y + J),
.month = @intCast(M - 12 * J),
.day = @intCast(D + 1),
};
}
// https://github.com/cassioneri/eaf/blob/1509faf37a0e0f59f5d4f11d0456fd0973c08f85/eaf/gregorian.hpp#L88
fn date_to_rata(date: Date) i32 {
checkRange(date, Date.MIN, Date.MAX);
// Map.
const J: u32 = @intFromBool(date.month <= 2);
const Y: i32 = date.year - @as(i32, @intCast(J));
const M: u32 = date.month + 12 * J;
const D: u32 = date.day - 1;
const C: i32 = @intCast(quotient(Y, 100));
// Rata die.
const y_star: i32 = @intCast(quotient(1461 * @as(i64, Y), 4) - C + quotient(C, 4)); // n_days in all prev. years
const m_star: u32 = (153 * M - 457) / 5; // n_days in prev. months
return y_star + @as(i32, @intCast(m_star)) + @as(i32, @intCast(D));
}
fn quotient(n: i64, d: u32) i64 {
return if (n >= 0) @divTrunc(n, d) else @divTrunc((n + 1), d) - 1;
}
fn remainder(n: i64, d: u32) u32 {
return @intCast(if (n >= 0) @mod(n, d) else (n + d) - d * quotient((n + d), d));
}
// const testing = @import("testing.zig");
/// Attempts to print `arg` into a buf and then compare those strings.
pub const allocator = std.testing.allocator;
pub fn expectFmt(arg: anytype, expected: []const u8) !void {
var wb = std.io.Writer.Allocating.init(allocator);
defer wb.deinit();
try wb.writer.print("{f}", .{arg});
try std.testing.expectEqualStrings(expected, wb.written());
}
pub fn expectEqual(res: anytype, expected: meta.Const(@TypeOf(res))) TimeError!void {
if (meta.isOptional(@TypeOf(res))) {
if (expected) |e| return expectEqual(res orelse return error.ExpectedValue, e);
if (res != null) return error.ExpectedNull;
}
// TODO: find all usages of expectEqualStrings and replace it with our expectEqual
if (meta.isString(@TypeOf(res))) {
return std.testing.expectEqualStrings(expected, res);
}
return std.testing.expectEqual(expected, res);
}
// test "basic usage" {
// const t1 = Time.unix(1234567890);
// try expectFmt(t1, "2009-02-13 23:31:30 UTC");
//
// try expectEqual(t1.date(), .{
// .year = 2009,
// .month = 2,
// .day = 13,
// });
//
// try expectEqual(t1.hour(), 23);
// try expectEqual(t1.minute(), 31);
// try expectEqual(t1.second(), 30);
//
// const t2 = t1.setHour(10).setMinute(15).setSecond(45);
// try expectFmt(t2, "2009-02-13 10:15:45 UTC");
//
// const t3 = t2.add(.hours, 14).add(.minutes, 46).add(.seconds, 18);
// try expectFmt(t3, "2009-02-14 01:02:03 UTC");
//
// // t.next()
// try expectFmt(t3.next(.second), "2009-02-14 01:02:04 UTC");
// try expectFmt(t3.next(.minute), "2009-02-14 01:03:00 UTC");
// try expectFmt(t3.next(.hour), "2009-02-14 02:00:00 UTC");
// try expectFmt(t3.next(.day), "2009-02-15 00:00:00 UTC");
//
// // t.setStartOf()
// try expectFmt(t3.setStartOf(.minute), "2009-02-14 01:02:00 UTC");
// try expectFmt(t3.setStartOf(.hour), "2009-02-14 01:00:00 UTC");
// try expectFmt(t3.setStartOf(.day), "2009-02-14 00:00:00 UTC");
// try expectFmt(t3.setStartOf(.month), "2009-02-01 00:00:00 UTC");
// try expectFmt(t3.setStartOf(.year), "2009-01-01 00:00:00 UTC");
//
// // t.setEndOf()
// try expectFmt(t3.setEndOf(.minute), "2009-02-14 01:02:59 UTC");
// try expectFmt(t3.setEndOf(.hour), "2009-02-14 01:59:59 UTC");
// try expectFmt(t3.setEndOf(.day), "2009-02-14 23:59:59 UTC");
// try expectFmt(t3.setEndOf(.month), "2009-02-28 23:59:59 UTC");
// try expectFmt(t3.setEndOf(.year), "2009-12-31 23:59:59 UTC");
// }
//
// test "edge-cases" {
// const jan31 = Date.ymd(2023, 1, 31);
// try expectEqual(jan31.add(.month, 1), Date.ymd(2023, 2, 28));
// try expectEqual(jan31.add(.month, 2), Date.ymd(2023, 3, 31));
// try expectEqual(jan31.add(.month, -1), Date.ymd(2022, 12, 31));
// try expectEqual(jan31.add(.month, -2), Date.ymd(2022, 11, 30));
// try expectEqual(jan31.add(.year, 1).add(.month, 1), Date.ymd(2024, 2, 29));
//
// const feb29 = Time.unix(951782400); // 2000-02-29 00:00:00
// try expectFmt(feb29.setEndOf(.month), "2000-02-29 23:59:59 UTC");
// try expectFmt(feb29.add(.years, 1), "2001-02-28 00:00:00 UTC");
// try expectFmt(feb29.add(.years, 4), "2004-02-29 00:00:00 UTC");
// }
//
// test isLeapYear {
// try testing.expect(!isLeapYear(1999));
// try testing.expect(isLeapYear(2000));
// try testing.expect(isLeapYear(2004));
// }
//
// test daysInMonth {
// try expectEqual(daysInMonth(1999, 2), 28);
// try expectEqual(daysInMonth(2000, 2), 29);
// try expectEqual(daysInMonth(2000, 7), 31);
// try expectEqual(daysInMonth(2000, 8), 31);
// }
//
// test rata_to_date {
// try expectEqual(rata_to_date(RATA_MIN), Date.MIN);
// try expectEqual(rata_to_date(RATA_MAX), Date.MAX);
//
// try expectEqual(rata_to_date(0), .ymd(0, 3, 1));
// try expectEqual(rata_to_date(RATA_TO_UNIX), .ymd(1970, 1, 1));
// }
//
// test date_to_rata {
// try expectEqual(date_to_rata(Date.MIN), RATA_MIN);
// try expectEqual(date_to_rata(Date.MAX), RATA_MAX);
//
// try expectEqual(date_to_rata(.ymd(0, 3, 1)), 0);
// try expectEqual(date_to_rata(.ymd(1970, 1, 1)), RATA_TO_UNIX);
// }

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