const std = @import("std"); const Instant = @This(); inner: *CInstant, epochMilliseconds: i64, epochNanoseconds: i128, /// Construct from epoch nanoseconds (Temporal.Instant.fromEpochNanoseconds). pub fn init(epoch_ns: i128) !Instant { return fromEpochNanoseconds(epoch_ns); } /// Construct from epoch milliseconds. pub fn fromEpochMilliseconds(epoch_ms: i64) !Instant { return wrapInstant(temporal_rs_Instant_from_epoch_milliseconds(epoch_ms)); } /// Construct from epoch nanoseconds (Temporal.Instant.fromEpochNanoseconds). pub fn fromEpochNanoseconds(epoch_ns: i128) !Instant { const parts = i128ToParts(epoch_ns); return wrapInstant(temporal_rs_Instant_try_new(parts)); } /// Parse an ISO 8601 string (Temporal.Instant.from). pub fn from(text: []const u8) !Instant { const view = DiplomatStringView{ .data = text.ptr, .len = text.len }; return wrapInstant(temporal_rs_Instant_from_utf8(view)); } /// Parse an ISO 8601 UTF-16 string (Temporal.Instant.from). fn fromUtf16(text: []const u16) !Instant { const view = DiplomatString16View{ .data = text.ptr, .len = text.len }; return wrapInstant(temporal_rs_Instant_from_utf16(view)); } /// Add a Duration to this instant (Temporal.Instant.prototype.add). pub fn add(self: Instant, duration: *const Duration) !Instant { return wrapInstant(temporal_rs_Instant_add(self.inner, duration)); } /// Subtract a Duration from this instant (Temporal.Instant.prototype.subtract). pub fn subtract(self: Instant, duration: *const Duration) !Instant { return wrapInstant(temporal_rs_Instant_subtract(self.inner, duration)); } /// Difference until another instant (Temporal.Instant.prototype.until). pub fn until(self: Instant, other: Instant, settings: DifferenceSettings) !DurationHandle { return wrapDuration(temporal_rs_Instant_until(self.inner, other.inner, settings)); } /// Difference since another instant (Temporal.Instant.prototype.since). pub fn since(self: Instant, other: Instant, settings: DifferenceSettings) !DurationHandle { return wrapDuration(temporal_rs_Instant_since(self.inner, other.inner, settings)); } /// Round this instant (Temporal.Instant.prototype.round). pub fn round(self: Instant, options: RoundingOptions) !Instant { return wrapInstant(temporal_rs_Instant_round(self.inner, options)); } /// Compare two instants (Temporal.Instant.compare). pub fn compare(a: Instant, b: Instant) i8 { return temporal_rs_Instant_compare(a.inner, b.inner); } /// Equality check (Temporal.Instant.prototype.equals). pub fn equals(a: Instant, b: Instant) bool { return temporal_rs_Instant_equals(a.inner, b.inner); } /// Epoch milliseconds accessor (Temporal.Instant.prototype.epochMilliseconds). // pub fn epochMilliseconds(self: Instant) i64 { // return temporal_rs_Instant_epoch_milliseconds(self.inner); // } /// Epoch nanoseconds accessor (Temporal.Instant.prototype.epochNanoseconds). // pub fn epochNanoseconds(self: Instant) i128 { // const value = temporal_rs_Instant_epoch_nanoseconds(self.inner); // return partsToI128(value); // } /// Convert to string using compiled TZ data; caller owns returned slice. pub fn toString(self: Instant, allocator: std.mem.Allocator, opts: ToStringOptions) ![]u8 { const zone_opt = if (opts.time_zone) |z| TimeZone_option{ .ok = z, .is_ok = true } else TimeZone_option{ .ok = undefined, .is_ok = false }; const rounding = opts.rounding orelse defaultToStringRoundingOptions(); const writer = diplomat_buffer_write_create(128); defer diplomat_buffer_write_destroy(writer); const res = temporal_rs_Instant_to_ixdtf_string_with_compiled_data(self.inner, zone_opt, rounding, writer); try handleVoidResult(res); const len = diplomat_buffer_write_len(writer); const source = diplomat_buffer_write_get_bytes(writer)[0..len]; const out = try allocator.alloc(u8, len); std.mem.copyForwards(u8, out, source); return out; } /// Convert to string using an explicit provider. fn toStringWithProvider(self: Instant, allocator: std.mem.Allocator, provider: *const Provider, opts: ToStringOptions) ![]u8 { const zone_opt = if (opts.time_zone) |z| TimeZone_option{ .ok = z, .is_ok = true } else TimeZone_option{ .ok = undefined, .is_ok = false }; const rounding = opts.rounding orelse defaultToStringRoundingOptions(); const writer = diplomat_buffer_write_create(128); defer diplomat_buffer_write_destroy(writer); const res = temporal_rs_Instant_to_ixdtf_string_with_provider(self.inner, zone_opt, rounding, provider, writer); try handleVoidResult(res); const len = diplomat_buffer_write_len(writer); const source = diplomat_buffer_write_get_bytes(writer)[0..len]; const out = try allocator.alloc(u8, len); std.mem.copyForwards(u8, out, source); return out; } pub fn toJSON(self: Instant, allocator: std.mem.Allocator) ![]u8 { _ = self; _ = allocator; return error.TemporalNotImplemented; } pub fn toLocaleString(self: Instant, allocator: std.mem.Allocator) ![]u8 { _ = self; _ = allocator; return error.TemporalNotImplemented; } /// Convert to ZonedDateTime using built-in provider (Temporal.Instant.prototype.toZonedDateTimeISO). pub fn toZonedDateTimeISO(self: Instant, zone: TimeZone) !ZonedDateTimeHandle { return wrapZonedDateTime(temporal_rs_Instant_to_zoned_date_time_iso(self.inner, zone)); } /// Convert to ZonedDateTime using an explicit provider. fn toZonedDateTimeIsoWithProvider(self: Instant, zone: TimeZone, provider: *const Provider) !ZonedDateTimeHandle { return wrapZonedDateTime(temporal_rs_Instant_to_zoned_date_time_iso_with_provider(self.inner, zone, provider)); } /// Clone the underlying instant. fn clone(self: Instant) Instant { const ptr = temporal_rs_Instant_clone(self.inner); return .{ .inner = ptr, .epochMilliseconds = temporal_rs_Instant_epoch_milliseconds(ptr), .epochNanoseconds = partsToI128(temporal_rs_Instant_epoch_nanoseconds(ptr)) }; } pub fn deinit(self: Instant) void { temporal_rs_Instant_destroy(self.inner); } // --- Helpers ----------------------------------------------------------------- fn wrapInstant(res: InstantResult) !Instant { if (!res.is_ok) return error.TemporalError; const ptr = res.result.ok orelse return error.TemporalError; return .{ .inner = ptr, .epochMilliseconds = temporal_rs_Instant_epoch_milliseconds(ptr), .epochNanoseconds = partsToI128(temporal_rs_Instant_epoch_nanoseconds(ptr)) }; } fn wrapDuration(res: DurationResult) !DurationHandle { if (!res.is_ok) return error.TemporalError; const ptr = res.result.ok orelse return error.TemporalError; return .{ .ptr = ptr }; } fn wrapZonedDateTime(res: ZonedDateTimeResult) !ZonedDateTimeHandle { if (!res.is_ok) return error.TemporalError; const ptr = res.result.ok orelse return error.TemporalError; return .{ .ptr = ptr }; } fn handleVoidResult(res: VoidResult) !void { if (!res.is_ok) return error.TemporalError; } fn i128ToParts(value: i128) I128Nanoseconds { const is_neg = value < 0; const mag: u128 = if (is_neg) @intCast(@as(u128, @intCast(-value))) else @intCast(value); const mask: u64 = 1 << 63; var high: u64 = @intCast(mag >> 64); const low: u64 = @intCast(mag & 0xffff_ffff_ffff_ffff); if (is_neg) high |= mask; return .{ .high = high, .low = low }; } fn partsToI128(value: I128Nanoseconds) i128 { const mask: u64 = 1 << 63; const is_neg = (value.high & mask) != 0; const mag: u128 = ((@as(u128, value.high & ~mask)) << 64) | value.low; if (is_neg) return -@as(i128, @intCast(mag)); return @as(i128, @intCast(mag)); } fn defaultPrecision() Precision { return .{ .is_minute = false, .precision = OptionU8{ .ok = 0, .is_ok = false } }; } fn defaultToStringRoundingOptions() ToStringRoundingOptions { return .{ .precision = defaultPrecision(), .smallest_unit = Unit_option{ .ok = .Unit_Auto, .is_ok = false }, .rounding_mode = RoundingMode_option{ .ok = .RoundingMode_Trunc, .is_ok = false }, }; } fn parseDuration(text: []const u8) !DurationHandle { const view = DiplomatStringView{ .data = text.ptr, .len = text.len }; const res = temporal_rs_Duration_from_utf8(view); if (!res.is_ok) return error.TemporalError; const ptr = res.result.ok orelse return error.TemporalError; return .{ .ptr = ptr }; } // --- Public helper types ----------------------------------------------------- const ToStringOptions = struct { time_zone: ?TimeZone = null, rounding: ?ToStringRoundingOptions = null, }; const DurationHandle = struct { ptr: *Duration, pub fn deinit(self: DurationHandle) void { temporal_rs_Duration_destroy(self.ptr); } }; const ZonedDateTimeHandle = struct { ptr: *ZonedDateTime, pub fn deinit(self: ZonedDateTimeHandle) void { temporal_rs_ZonedDateTime_destroy(self.ptr); } }; // --- Extern types ------------------------------------------------------------ const CInstant = opaque {}; const Duration = opaque {}; const ZonedDateTime = opaque {}; const Provider = opaque {}; const I128Nanoseconds = extern struct { high: u64, low: u64 }; const I128Nanoseconds_option = extern struct { ok: I128Nanoseconds, is_ok: bool }; const DiplomatStringView = extern struct { data: [*c]const u8, len: usize }; const DiplomatString16View = extern struct { data: [*c]const u16, len: usize }; const OptionStringView = extern struct { ok: DiplomatStringView, is_ok: bool }; const OptionU8 = extern struct { ok: u8, is_ok: bool }; const OptionU32 = extern struct { ok: u32, is_ok: bool }; const DiplomatWrite = extern struct { context: ?*anyopaque, buf: [*c]u8, len: usize, cap: usize, grow_failed: bool, flush: ?*const fn (*DiplomatWrite) void, grow: ?*const fn (*DiplomatWrite, usize) bool, }; const TimeZone = extern struct { offset_minutes: i16, resolved_id: usize, normalized_id: usize, is_iana_id: bool, }; const TimeZone_option = extern struct { ok: TimeZone, is_ok: bool, }; const Precision = extern struct { is_minute: bool, precision: OptionU8, }; const Unit = enum(c_int) { Unit_Auto = 0, Unit_Nanosecond = 1, Unit_Microsecond = 2, Unit_Millisecond = 3, Unit_Second = 4, Unit_Minute = 5, Unit_Hour = 6, Unit_Day = 7, Unit_Week = 8, Unit_Month = 9, Unit_Year = 10, }; const Unit_option = extern struct { ok: Unit, is_ok: bool }; const RoundingMode = enum(c_int) { RoundingMode_Ceil = 0, RoundingMode_Floor = 1, RoundingMode_Expand = 2, RoundingMode_Trunc = 3, RoundingMode_HalfCeil = 4, RoundingMode_HalfFloor = 5, RoundingMode_HalfExpand = 6, RoundingMode_HalfTrunc = 7, RoundingMode_HalfEven = 8, }; const RoundingMode_option = extern struct { ok: RoundingMode, is_ok: bool }; const DifferenceSettings = extern struct { largest_unit: Unit_option, smallest_unit: Unit_option, rounding_mode: RoundingMode_option, increment: OptionU32, }; const RoundingOptions = extern struct { largest_unit: Unit_option, smallest_unit: Unit_option, rounding_mode: RoundingMode_option, increment: OptionU32, }; const ToStringRoundingOptions = extern struct { precision: Precision, smallest_unit: Unit_option, rounding_mode: RoundingMode_option, }; const ErrorKind = enum(c_int) { ErrorKind_Generic = 0, ErrorKind_Type = 1, ErrorKind_Range = 2, ErrorKind_Syntax = 3, ErrorKind_Assert = 4, }; const TemporalError = extern struct { kind: ErrorKind, msg: OptionStringView, }; const Sign = enum(c_int) { Sign_Positive = 1, Sign_Zero = 0, Sign_Negative = -1, }; // --- Result wrappers --------------------------------------------------------- const InstantResult = extern struct { result: extern union { ok: ?*CInstant, err: TemporalError, }, is_ok: bool, }; const DurationResult = extern struct { result: extern union { ok: ?*Duration, err: TemporalError, }, is_ok: bool, }; const DurationParseResult = extern struct { result: extern union { ok: ?*Duration, err: TemporalError, }, is_ok: bool, }; const ZonedDateTimeResult = extern struct { result: extern union { ok: ?*ZonedDateTime, err: TemporalError, }, is_ok: bool, }; const VoidResult = extern struct { result: extern union { err: TemporalError, }, is_ok: bool, }; // --- Extern functions ------------------------------------------------------- extern "c" fn temporal_rs_Instant_try_new(ns: I128Nanoseconds) InstantResult; extern "c" fn temporal_rs_Instant_from_epoch_milliseconds(epoch_milliseconds: i64) InstantResult; extern "c" fn temporal_rs_Instant_from_utf8(s: DiplomatStringView) InstantResult; extern "c" fn temporal_rs_Instant_from_utf16(s: DiplomatString16View) InstantResult; extern "c" fn temporal_rs_Instant_add(self: *const CInstant, duration: *const Duration) InstantResult; extern "c" fn temporal_rs_Instant_subtract(self: *const CInstant, duration: *const Duration) InstantResult; extern "c" fn temporal_rs_Instant_since(self: *const CInstant, other: *const CInstant, settings: DifferenceSettings) DurationResult; extern "c" fn temporal_rs_Instant_until(self: *const CInstant, other: *const CInstant, settings: DifferenceSettings) DurationResult; extern "c" fn temporal_rs_Instant_round(self: *const CInstant, options: RoundingOptions) InstantResult; extern "c" fn temporal_rs_Instant_compare(self: *const CInstant, other: *const CInstant) i8; extern "c" fn temporal_rs_Instant_equals(self: *const CInstant, other: *const CInstant) bool; extern "c" fn temporal_rs_Instant_epoch_milliseconds(self: *const CInstant) i64; extern "c" fn temporal_rs_Instant_epoch_nanoseconds(self: *const CInstant) I128Nanoseconds; extern "c" fn temporal_rs_Instant_to_ixdtf_string_with_compiled_data(self: *const CInstant, zone: TimeZone_option, options: ToStringRoundingOptions, write: *DiplomatWrite) VoidResult; extern "c" fn temporal_rs_Instant_to_ixdtf_string_with_provider(self: *const CInstant, zone: TimeZone_option, options: ToStringRoundingOptions, p: *const Provider, write: *DiplomatWrite) VoidResult; extern "c" fn temporal_rs_Instant_to_zoned_date_time_iso(self: *const CInstant, zone: TimeZone) ZonedDateTimeResult; extern "c" fn temporal_rs_Instant_to_zoned_date_time_iso_with_provider(self: *const CInstant, zone: TimeZone, p: *const Provider) ZonedDateTimeResult; extern "c" fn temporal_rs_Instant_clone(self: *const CInstant) *CInstant; extern "c" fn temporal_rs_Instant_destroy(self: *CInstant) void; extern "c" fn temporal_rs_Duration_destroy(self: *Duration) void; extern "c" fn temporal_rs_Duration_from_utf8(s: DiplomatStringView) DurationParseResult; extern "c" fn temporal_rs_Duration_hours(self: *const Duration) i64; extern "c" fn temporal_rs_Duration_minutes(self: *const Duration) i64; extern "c" fn temporal_rs_Duration_seconds(self: *const Duration) i64; extern "c" fn temporal_rs_Duration_milliseconds(self: *const Duration) i64; extern "c" fn temporal_rs_Duration_microseconds(self: *const Duration) f64; extern "c" fn temporal_rs_Duration_nanoseconds(self: *const Duration) f64; extern "c" fn temporal_rs_Duration_sign(self: *const Duration) Sign; extern "c" fn temporal_rs_ZonedDateTime_destroy(self: *ZonedDateTime) void; extern "c" fn diplomat_buffer_write_create(cap: usize) *DiplomatWrite; extern "c" fn diplomat_buffer_write_get_bytes(write: *DiplomatWrite) [*c]u8; extern "c" fn diplomat_buffer_write_len(write: *DiplomatWrite) usize; extern "c" fn diplomat_buffer_write_destroy(write: *DiplomatWrite) void; // --- Tests ------------------------------------------------------------------- test init { const epoch_ns: i128 = 1_704_067_200_000_000_000; // 2024-01-01T00:00:00Z const inst = try Instant.init(epoch_ns); defer inst.deinit(); try std.testing.expectEqual(epoch_ns, inst.epochNanoseconds); } test fromEpochNanoseconds { // The Rust implementation accepts values within the 100M-day window (inclusive). const max_ns: i128 = 8_640_000_000_000_000_000_000; const min_ns: i128 = -max_ns; const max_inst = try Instant.fromEpochNanoseconds(max_ns); defer max_inst.deinit(); const min_inst = try Instant.fromEpochNanoseconds(min_ns); defer min_inst.deinit(); try std.testing.expectEqual(max_ns, max_inst.epochNanoseconds); try std.testing.expectEqual(min_ns, min_inst.epochNanoseconds); try std.testing.expectError(error.TemporalError, Instant.fromEpochNanoseconds(max_ns + 1)); try std.testing.expectError(error.TemporalError, Instant.fromEpochNanoseconds(min_ns - 1)); } test from { const inst = try Instant.from("2024-03-15T14:30:45.123Z"); defer inst.deinit(); try std.testing.expectEqual(@as(i64, 1_710_513_045_123), inst.epochMilliseconds); } test fromUtf16 { const utf8 = "2024-03-15T14:30:45.123Z"; const allocator = std.testing.allocator; const utf16 = try std.unicode.utf8ToUtf16LeAlloc(allocator, utf8); defer allocator.free(utf16); const inst = try Instant.fromUtf16(utf16); defer inst.deinit(); try std.testing.expectEqual(@as(i64, 1_710_513_045_123), inst.epochMilliseconds); } test subtract { const base = try Instant.fromEpochMilliseconds(0); defer base.deinit(); var dur = try parseDuration("PT1H30M"); defer dur.deinit(); const added = try base.add(dur.ptr); defer added.deinit(); try std.testing.expectEqual(@as(i64, 5_400_000), added.epochMilliseconds); const subbed = try added.subtract(dur.ptr); defer subbed.deinit(); try std.testing.expectEqual(@as(i64, 0), subbed.epochMilliseconds); } test compare { const a = try Instant.fromEpochMilliseconds(0); defer a.deinit(); const b = try Instant.fromEpochMilliseconds(0); defer b.deinit(); const c = try Instant.fromEpochMilliseconds(1_000); defer c.deinit(); try std.testing.expectEqual(@as(i8, 0), Instant.compare(a, b)); try std.testing.expect(Instant.equals(a, b)); try std.testing.expectEqual(@as(i8, -1), Instant.compare(a, c)); try std.testing.expectEqual(@as(i8, 1), Instant.compare(c, a)); } test equals { const a = try Instant.fromEpochMilliseconds(0); defer a.deinit(); const b = try Instant.fromEpochMilliseconds(0); defer b.deinit(); const c = try Instant.fromEpochMilliseconds(1_000); defer c.deinit(); try std.testing.expectEqual(@as(i8, 0), Instant.compare(a, b)); try std.testing.expect(Instant.equals(a, b)); try std.testing.expectEqual(@as(i8, -1), Instant.compare(a, c)); try std.testing.expectEqual(@as(i8, 1), Instant.compare(c, a)); } test until { const earlier = try Instant.fromEpochMilliseconds(0); defer earlier.deinit(); const later = try Instant.fromEpochMilliseconds(3_600_000); defer later.deinit(); const settings = DifferenceSettings{ .largest_unit = Unit_option{ .ok = .Unit_Hour, .is_ok = true }, .smallest_unit = Unit_option{ .ok = .Unit_Second, .is_ok = true }, .rounding_mode = RoundingMode_option{ .ok = .RoundingMode_Trunc, .is_ok = true }, .increment = OptionU32{ .ok = 0, .is_ok = false }, }; var until_handle = try earlier.until(later, settings); defer until_handle.deinit(); try std.testing.expectEqual(Sign.Sign_Positive, temporal_rs_Duration_sign(until_handle.ptr)); try std.testing.expectEqual(@as(i64, 1), temporal_rs_Duration_hours(until_handle.ptr)); var since_handle = try later.since(earlier, settings); defer since_handle.deinit(); try std.testing.expectEqual(Sign.Sign_Positive, temporal_rs_Duration_sign(since_handle.ptr)); try std.testing.expectEqual(@as(i64, 1), temporal_rs_Duration_hours(since_handle.ptr)); } test since { const earlier = try Instant.fromEpochMilliseconds(0); defer earlier.deinit(); const later = try Instant.fromEpochMilliseconds(3_600_000); defer later.deinit(); const settings = DifferenceSettings{ .largest_unit = Unit_option{ .ok = .Unit_Hour, .is_ok = true }, .smallest_unit = Unit_option{ .ok = .Unit_Second, .is_ok = true }, .rounding_mode = RoundingMode_option{ .ok = .RoundingMode_Trunc, .is_ok = true }, .increment = OptionU32{ .ok = 0, .is_ok = false }, }; var until_handle = try earlier.until(later, settings); defer until_handle.deinit(); try std.testing.expectEqual(Sign.Sign_Positive, temporal_rs_Duration_sign(until_handle.ptr)); try std.testing.expectEqual(@as(i64, 1), temporal_rs_Duration_hours(until_handle.ptr)); var since_handle = try later.since(earlier, settings); defer since_handle.deinit(); try std.testing.expectEqual(Sign.Sign_Positive, temporal_rs_Duration_sign(since_handle.ptr)); try std.testing.expectEqual(@as(i64, 1), temporal_rs_Duration_hours(since_handle.ptr)); } test round { const inst = try Instant.fromEpochNanoseconds(1_609_459_245_123_456_789); defer inst.deinit(); const opts = RoundingOptions{ .largest_unit = Unit_option{ .ok = .Unit_Auto, .is_ok = false }, .smallest_unit = Unit_option{ .ok = .Unit_Second, .is_ok = true }, .rounding_mode = RoundingMode_option{ .ok = .RoundingMode_HalfExpand, .is_ok = true }, .increment = OptionU32{ .ok = 0, .is_ok = false }, }; const rounded = try inst.round(opts); defer rounded.deinit(); const ns = rounded.epochNanoseconds; try std.testing.expectEqual(@as(i128, 1_609_459_245_000_000_000), ns); } test clone { const inst = try Instant.fromEpochMilliseconds(42); defer inst.deinit(); const cloned = inst.clone(); defer cloned.deinit(); try std.testing.expectEqual(inst.epochMilliseconds, cloned.epochMilliseconds); } test toString { const epoch_ns: i128 = 1704067200000000000; // 2024-01-01 00:00:00 UTC const inst = try Instant.init(epoch_ns); defer inst.deinit(); const allocator = std.testing.allocator; const instant_str = try inst.toString(allocator, .{}); defer allocator.free(instant_str); try std.testing.expectEqualStrings(instant_str, "2024-01-01T00:00:00Z"); }