diff --git a/example/src/root.zig b/example/src/root.zig index 65abcaf..54119ad 100644 --- a/example/src/root.zig +++ b/example/src/root.zig @@ -53,10 +53,10 @@ pub fn run(allocator: std.mem.Allocator, io_optional: ?std.Io) !void { if (io_optional) |io| { const now_instant = try Temporal.Now.instant(io); defer now_instant.deinit(); - const now_date = try Temporal.Now.plainDateISO(io); + const now_date = try Temporal.Now.plainDateISO(allocator, io, null); defer now_date.deinit(); - const now_datetime = try Temporal.Now.plainDateTimeISO(io); - const now_time = try Temporal.Now.plainTimeISO(io); + const now_datetime = try Temporal.Now.plainDateTimeISO(allocator, io, null); + const now_time = try Temporal.Now.plainTimeISO(allocator, io, null); std.log.info( \\Now \\ - instant: {s} @@ -186,6 +186,8 @@ pub fn run(allocator: std.mem.Allocator, io_optional: ?std.Io) !void { try zdt.toString(allocator, .{}), }); + try runTimeZoneExamples(allocator, io_optional, null); + // ---- // More complex Temporal API examples // ---- @@ -523,9 +525,9 @@ pub fn run(allocator: std.mem.Allocator, io_optional: ?std.Io) !void { }); if (io_optional) |io| { - const now_zdt = try Temporal.Now.zonedDateTimeISO(allocator, io); + const now_zdt = try Temporal.Now.zonedDateTimeISO(allocator, io, null); defer now_zdt.deinit(); - const now_tz_id = try Temporal.Now.timeZoneId(allocator); + const now_tz_id = try Temporal.Now.timeZoneId(allocator, io); defer allocator.free(now_tz_id); std.log.info( \\Now Coverage @@ -814,6 +816,102 @@ pub fn run(allocator: std.mem.Allocator, io_optional: ?std.Io) !void { }); } +pub fn runTimeZoneExamples( + allocator: std.mem.Allocator, + io_optional: ?std.Io, + now_epoch_ms: ?i64, +) !void { + const epoch_ns: i128 = if (now_epoch_ms) |ms| @as(i128, ms) * 1_000_000 else 1_706_881_530_123_456_789; + + const utc_tz = try Temporal.ZonedDateTime.TimeZone.init("UTC"); + const ny_tz = try Temporal.ZonedDateTime.TimeZone.init("America/New_York"); + const dhaka_tz = try Temporal.ZonedDateTime.TimeZone.init("Asia/Dhaka"); + + const zdt_utc = try Temporal.ZonedDateTime.fromEpochNanoseconds(epoch_ns, utc_tz); + defer zdt_utc.deinit(); + const zdt_ny = try zdt_utc.withTimeZone(ny_tz); + defer zdt_ny.deinit(); + const zdt_dhaka = try zdt_utc.withTimeZone(dhaka_tz); + defer zdt_dhaka.deinit(); + + const utc_id = try zdt_utc.timeZoneId(allocator); + defer allocator.free(utc_id); + const ny_id = try zdt_ny.timeZoneId(allocator); + defer allocator.free(ny_id); + const dhaka_id = try zdt_dhaka.timeZoneId(allocator); + defer allocator.free(dhaka_id); + + std.log.info( + \\TimeZone + \\ - same instant in UTC ({s}): {s} + \\ - same instant in New York ({s}): {s} + \\ - same instant in Dhaka ({s}): {s} + \\ - UTC offset: {s} + \\ - New York offset: {s} + \\ - Dhaka offset: {s} + \\ + \\ + , .{ + utc_id, + try zdt_utc.toString(allocator, .{}), + ny_id, + try zdt_ny.toString(allocator, .{}), + dhaka_id, + try zdt_dhaka.toString(allocator, .{}), + try zdt_utc.offset(allocator), + try zdt_ny.offset(allocator), + try zdt_dhaka.offset(allocator), + }); + + if (io_optional) |io| { + const system_tz_id = try Temporal.Now.timeZoneId(allocator, io); + defer allocator.free(system_tz_id); + + const now_zdt = try Temporal.Now.zonedDateTimeISO(allocator, io, null); + defer now_zdt.deinit(); + const now_zdt_utc = try Temporal.Now.zonedDateTimeISO(allocator, io, "UTC"); + defer now_zdt_utc.deinit(); + const now_date = try Temporal.Now.plainDateISO(allocator, io, null); + defer now_date.deinit(); + + std.log.info( + \\TimeZone (system) + \\ - system timeZoneId(): {s} + \\ - Now.zonedDateTimeISO(): {s} + \\ - Now.zonedDateTimeISO("UTC"): {s} + \\ - Now.plainDateISO(): {s} + \\ + \\ + , .{ + system_tz_id, + try now_zdt.toString(allocator, .{}), + try now_zdt_utc.toString(allocator, .{}), + try now_date.toString(allocator, .{}), + }); + } else if (now_epoch_ms) |ms| { + const inst = try Temporal.Instant.fromEpochMilliseconds(ms); + defer inst.deinit(); + + const inst_dhaka_tz = try Temporal.Instant.TimeZone.init("Asia/Dhaka"); + const inst_utc_tz = try Temporal.Instant.TimeZone.init("UTC"); + const host_zdt = try inst.toZonedDateTimeISO(inst_dhaka_tz); + defer host_zdt.deinit(); + const host_zdt_utc = try inst.toZonedDateTimeISO(inst_utc_tz); + defer host_zdt_utc.deinit(); + + std.log.info( + \\TimeZone (host epoch) + \\ - Instant.toZonedDateTimeISO("Asia/Dhaka"): {s} + \\ - Instant.toZonedDateTimeISO("UTC"): {s} + \\ + \\ + , .{ + try host_zdt.toString(allocator, .{}), + try host_zdt_utc.toString(allocator, .{}), + }); + } +} + extern fn consoleLog(ptr: [*]u8, len: u32) void; pub fn logFn(comptime message_level: std.log.Level, comptime scope: @TypeOf(.enum_literal), comptime format: []const u8, args: anytype) void { if (builtin.os.tag == .freestanding) { diff --git a/src/Now.zig b/src/Now.zig index 3c4f77f..542554e 100644 --- a/src/Now.zig +++ b/src/Now.zig @@ -1,10 +1,11 @@ -const builtin = @import("builtin"); const std = @import("std"); const Instant = @import("Instant.zig"); const PlainDate = @import("PlainDate.zig"); const PlainDateTime = @import("PlainDateTime.zig"); const PlainTime = @import("PlainTime.zig"); +const SystemTimeZone = @import("SystemTimeZone.zig"); const ZonedDateTime = @import("ZonedDateTime.zig"); +const abi = @import("abi.zig"); /// # Temporal.Now /// @@ -38,112 +39,84 @@ pub fn instant(io: std.Io) !Instant { /// Returns the current date as a [`Temporal.PlainDate`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate) object, in the ISO 8601 calendar and the specified time zone. /// +/// When `time_zone_like` is null, the system time zone is used. +/// /// See: [Temporal.Now.plainDateISO](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainDateISO) -pub fn plainDateISO(io: std.Io) !PlainDate { - const now = currentParts(io); - return PlainDate.init(now.year, now.month, now.day); +pub fn plainDateISO(allocator: std.mem.Allocator, io: std.Io, time_zone_like: ?[]const u8) !PlainDate { + const time_zone = try resolveTimeZone(allocator, io, time_zone_like); + const now = try instant(io); + defer now.deinit(); + + const ptr = (try abi.extractResult(abi.c.temporal_rs_PlainDate_from_epoch_nanoseconds( + abi.toI128Nanoseconds(now.epochNanoseconds()), + abi.to.toTimeZone(time_zone), + ))) orelse return abi.TemporalError.Generic; + return .{ ._inner = ptr }; } /// Returns the current date and time as a [`Temporal.PlainDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime) object, in the ISO 8601 calendar and the specified time zone. /// +/// When `time_zone_like` is null, the system time zone is used. +/// /// See: [Temporal.Now.plainDateTimeISO](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainDateTimeISO) -pub fn plainDateTimeISO(io: std.Io) !PlainDateTime { - const now = currentParts(io); - return PlainDateTime.init( - now.year, - now.month, - now.day, - now.hour, - now.minute, - now.second, - now.millisecond, - now.microsecond, - now.nanosecond, - ); +pub fn plainDateTimeISO(allocator: std.mem.Allocator, io: std.Io, time_zone_like: ?[]const u8) !PlainDateTime { + const time_zone = try resolveTimeZone(allocator, io, time_zone_like); + const now = try instant(io); + defer now.deinit(); + + const ptr = (try abi.extractResult(abi.c.temporal_rs_PlainDateTime_from_epoch_nanoseconds( + abi.toI128Nanoseconds(now.epochNanoseconds()), + abi.to.toTimeZone(time_zone), + ))) orelse return abi.TemporalError.Generic; + return .{ ._inner = ptr }; } /// Returns the current time as a [`Temporal.PlainTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime) object, in the specified time zone. /// +/// When `time_zone_like` is null, the system time zone is used. +/// /// See: [Temporal.Now.plainTimeISO](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/plainTimeISO) -pub fn plainTimeISO(io: std.Io) !PlainTime { - const now = currentParts(io); - return PlainTime.init( - now.hour, - now.minute, - now.second, - now.millisecond, - now.microsecond, - now.nanosecond, - ); +pub fn plainTimeISO(allocator: std.mem.Allocator, io: std.Io, time_zone_like: ?[]const u8) !PlainTime { + const time_zone = try resolveTimeZone(allocator, io, time_zone_like); + const now = try instant(io); + defer now.deinit(); + + const ptr = (try abi.extractResult(abi.c.temporal_rs_PlainTime_from_epoch_nanoseconds( + abi.toI128Nanoseconds(now.epochNanoseconds()), + abi.to.toTimeZone(time_zone), + ))) orelse return abi.TemporalError.Generic; + return PlainTime{ ._inner = ptr }; } /// Returns a time zone identifier representing the system's current time zone. /// /// The caller owns the returned slice and must free it with `allocator`. /// -/// TODO: Detect the system's actual IANA time zone instead of always returning -/// "UTC". This requires platform-specific logic: -/// - Linux/macOS: resolve the `/etc/localtime` symlink to `.../zoneinfo//`. -/// - Windows: read the time zone from the registry and map the Windows zone -/// name to an IANA identifier via the CLDR `windowsZones` table. -/// - WASI: no host time zone is available; "UTC" is the only honest answer. -/// /// See: [Temporal.Now.timeZoneId](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/timeZoneId) -pub fn timeZoneId(allocator: std.mem.Allocator) ![]const u8 { - return allocator.dupe(u8, "UTC"); +pub fn timeZoneId(allocator: std.mem.Allocator, io: std.Io) ![]const u8 { + const time_zone = try SystemTimeZone.timeZone(allocator, io); + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + abi.c.temporal_rs_TimeZone_identifier(time_zone._inner, &write.inner); + return try write.toOwnedSlice(); } /// Returns the current date and time as a [`Temporal.ZonedDateTime`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime) object, in the ISO 8601 calendar and the specified time zone. /// +/// When `time_zone_like` is null, the system time zone is used. +/// /// See: [Temporal.Now.zonedDateTimeISO](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now/zonedDateTimeISO) -pub fn zonedDateTimeISO(allocator: std.mem.Allocator, io: std.Io) !ZonedDateTime { +pub fn zonedDateTimeISO(allocator: std.mem.Allocator, io: std.Io, time_zone_like: ?[]const u8) !ZonedDateTime { const now = try instant(io); - const tz_id = try timeZoneId(allocator); - defer allocator.free(tz_id); - const time_zone = try ZonedDateTime.TimeZone.init(tz_id); + defer now.deinit(); + const time_zone = try resolveTimeZone(allocator, io, time_zone_like); return ZonedDateTime.fromEpochNanoseconds(now.epochNanoseconds(), time_zone); } -const CurrentParts = struct { - year: i32, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - millisecond: u16, - microsecond: u16, - nanosecond: u16, -}; - -fn currentParts(io: std.Io) CurrentParts { - const ns = std.Io.Timestamp.now(io, .real).nanoseconds; - const seconds: i64 = @intCast(@divTrunc(ns, 1_000_000_000)); - const subsec_nanos_u64: u64 = @intCast(@rem(ns, 1_000_000_000)); - - const days_since_epoch: u64 = @intCast(@divTrunc(seconds, std.time.epoch.secs_per_day)); - const secs_of_day: u64 = @intCast(@mod(seconds, std.time.epoch.secs_per_day)); - - const epoch_day = std.time.epoch.EpochDay{ .day = @intCast(days_since_epoch) }; - const year_day = epoch_day.calculateYearDay(); - const month_day = year_day.calculateMonthDay(); - const day_seconds = std.time.epoch.DaySeconds{ .secs = @intCast(secs_of_day) }; - - const millisecond: u16 = @intCast(subsec_nanos_u64 / 1_000_000); - const microsecond: u16 = @intCast((subsec_nanos_u64 / 1_000) % 1_000); - const nanosecond: u16 = @intCast(subsec_nanos_u64 % 1_000); - - return .{ - .year = @intCast(year_day.year), - .month = @intCast(month_day.month.numeric()), - .day = @intCast(month_day.day_index + 1), - .hour = @intCast(day_seconds.getHoursIntoDay()), - .minute = @intCast(day_seconds.getMinutesIntoHour()), - .second = @intCast(day_seconds.getSecondsIntoMinute()), - .millisecond = millisecond, - .microsecond = microsecond, - .nanosecond = nanosecond, - }; +fn resolveTimeZone(allocator: std.mem.Allocator, io: std.Io, time_zone_like: ?[]const u8) !ZonedDateTime.TimeZone { + if (time_zone_like) |id| return ZonedDateTime.TimeZone.init(id); + return SystemTimeZone.timeZone(allocator, io); } test instant { @@ -155,41 +128,62 @@ test instant { test plainDateISO { const io = std.testing.io; - const date = try plainDateISO(io); + const date = try plainDateISO(std.testing.allocator, io, null); + defer date.deinit(); try std.testing.expect(date.year() >= 1970); try std.testing.expect(date.month() >= 1 and date.month() <= 12); try std.testing.expect(date.day() >= 1 and date.day() <= 31); } + test plainDateTimeISO { const io = std.testing.io; - const dt = try plainDateTimeISO(io); + var dt = try plainDateTimeISO(std.testing.allocator, io, null); + defer dt.deinit(); try std.testing.expect(dt.year() >= 1970); try std.testing.expect(dt.month() >= 1 and dt.month() <= 12); try std.testing.expect(dt.day() >= 1 and dt.day() <= 31); try std.testing.expect(dt.hour() < 24); try std.testing.expect(dt.minute() < 60); } + test plainTimeISO { const io = std.testing.io; - const t = try plainTimeISO(io); + const t = try plainTimeISO(std.testing.allocator, io, null); try std.testing.expect(t.hour() < 24); try std.testing.expect(t.minute() < 60); try std.testing.expect(t.second() < 60); } + test timeZoneId { - const tz = try timeZoneId(std.testing.allocator); + const io = std.testing.io; + const tz = try timeZoneId(std.testing.allocator, io); defer std.testing.allocator.free(tz); - try std.testing.expectEqualStrings("UTC", tz); + try std.testing.expect(tz.len > 0); + _ = try ZonedDateTime.TimeZone.init(tz); } + test zonedDateTimeISO { const io = std.testing.io; - const zdt = try zonedDateTimeISO(std.testing.allocator, io); + const zdt = try zonedDateTimeISO(std.testing.allocator, io, null); defer zdt.deinit(); - std.log.info("{d}", .{zdt.epochNanoseconds()}); - try std.testing.expect(zdt.epochNanoseconds() > 0); const tz = try zdt.timeZoneId(std.testing.allocator); defer std.testing.allocator.free(tz); - try std.testing.expectEqualStrings("UTC", tz); + const expected_tz = try timeZoneId(std.testing.allocator, io); + defer std.testing.allocator.free(expected_tz); + try std.testing.expectEqualStrings(expected_tz, tz); +} + +test "plainDateISO with explicit time zone" { + const io = std.testing.io; + const date = try plainDateISO(std.testing.allocator, io, "UTC"); + defer date.deinit(); + + const zdt = try zonedDateTimeISO(std.testing.allocator, io, "UTC"); + defer zdt.deinit(); + const plain = try zdt.toPlainDate(); + defer plain.deinit(); + + try std.testing.expect(date.equals(plain)); } diff --git a/src/SystemTimeZone.zig b/src/SystemTimeZone.zig new file mode 100644 index 0000000..0e96a9a --- /dev/null +++ b/src/SystemTimeZone.zig @@ -0,0 +1,68 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const ZonedDateTime = @import("ZonedDateTime.zig"); + +const zoneinfo_marker = "zoneinfo/"; + +/// Returns the host environment's current time zone identifier. +/// +/// Resolution order: +/// 1. `TZ` environment variable (when set and non-empty) +/// 2. Unix: `/etc/localtime` symlink target under `zoneinfo/` +/// 3. `"UTC"` +/// +/// The caller owns the returned slice. +pub fn identifier(allocator: std.mem.Allocator, io: std.Io) ![]const u8 { + if (tzFromEnv(allocator)) |tz| return tz; + + if (detectFromPlatform(allocator, io)) |tz| return tz; + + return try allocator.dupe(u8, "UTC"); +} + +/// Returns a `TimeZone` for the host environment's current time zone. +pub fn timeZone(allocator: std.mem.Allocator, io: std.Io) !ZonedDateTime.TimeZone { + const id = try identifier(allocator, io); + defer allocator.free(id); + return ZonedDateTime.TimeZone.init(id); +} + +fn tzFromEnv(allocator: std.mem.Allocator) ?[]const u8 { + if (!builtin.link_libc) return null; + + const value = std.c.getenv("TZ") orelse return null; + const tz = std.mem.span(value); + if (tz.len == 0) return null; + + return allocator.dupe(u8, tz) catch null; +} + +fn detectFromPlatform(allocator: std.mem.Allocator, io: std.Io) ?[]const u8 { + return switch (builtin.os.tag) { + .linux, .macos, .freebsd, .openbsd, .netbsd, .dragonfly => detectFromEtcLocaltime(allocator, io) catch null, + else => null, + }; +} + +fn detectFromEtcLocaltime(allocator: std.mem.Allocator, io: std.Io) ![]const u8 { + var target_buf: [std.fs.max_path_bytes]u8 = undefined; + const len = std.Io.Dir.readLinkAbsolute(io, "/etc/localtime", &target_buf) catch return error.NotFound; + const target = target_buf[0..len]; + + const idx = std.mem.lastIndexOf(u8, target, zoneinfo_marker) orelse return error.NotFound; + return try allocator.dupe(u8, target[idx + zoneinfo_marker.len ..]); +} + +test identifier { + const io = std.testing.io; + const tz = try identifier(std.testing.allocator, io); + defer std.testing.allocator.free(tz); + try std.testing.expect(tz.len > 0); + _ = try ZonedDateTime.TimeZone.init(tz); +} + +test timeZone { + const io = std.testing.io; + _ = try timeZone(std.testing.allocator, io); +}