From 3000ef85c451f544008034f56102edf9a6f128fb Mon Sep 17 00:00:00 2001 From: "Nurul Huda (Apon)" Date: Mon, 26 Jan 2026 21:50:48 +0600 Subject: [PATCH] feat(plain): add initial wrapper --- README.md | 14 +- src/Duration.zig | 71 +++-- src/Instant.zig | 136 +++++---- src/Now.zig | 114 ++++++-- src/PlainDate.zig | 519 +++++++++++++++++++++++++++------ src/PlainDateTime.zig | 627 ++++++++++++++++++++++++++++++++++------ src/PlainMonthDay.zig | 264 +++++++++++++++-- src/PlainTime.zig | 373 ++++++++++++++++++++---- src/PlainYearMonth.zig | 417 +++++++++++++++++++++++---- src/ZonedDateTime.zig | 632 +++++++++++++++++++++++++++++++++-------- src/root.zig | 67 ++++- 11 files changed, 2687 insertions(+), 547 deletions(-) diff --git a/README.md b/README.md index b52044f..19a0a96 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,18 @@ Temporalz provides Zig bindings to the Rust-based [temporal_rs](https://github.c ## Installation -### Prerequisites +#### Prerequisites - Zig 0.15.2 - Rust toolchain (only required if [prebuilt binaries](#prebuilt) are not available for your platform) -### Add as a Dependency +#### Add as a Dependency ```bash -zig fetch --save=temporalz https://github.com/nurulhudaapon/temporalz/archive/refs/tags/v0.1.2.tar.gz +zig fetch --save https://github.com/nurulhudaapon/temporalz/archive/refs/tags/v0.1.2.tar.gz ``` -### Use in build.zig +#### Use in build.zig Add temporalz to your executable in `build.zig`: @@ -57,20 +57,20 @@ For other platforms, the library will build from source; you need the Rust toolc ## Development -### Clone the Repository +#### Clone the Repository ```bash git clone https://github.com/nurulhudaapon/temporalz.git cd temporalz ``` -### Build and Run +#### Build and Run ```bash zig build run ``` -### Run Tests +#### Run Tests ```bash zig build test diff --git a/src/Duration.zig b/src/Duration.zig index a513085..39283ac 100644 --- a/src/Duration.zig +++ b/src/Duration.zig @@ -10,16 +10,56 @@ pub const ToStringRoundingOptions = temporal.ToStringRoundingOptions; pub const Unit = temporal.Unit; pub const RoundingMode = temporal.RoundingMode; pub const Sign = temporal.Sign; -pub const PartialDuration = abi.c.PartialDuration; + +/// Partial duration specification for creating Duration objects. +/// This is a wrapper around the C API type to avoid exposing C types directly. +pub const PartialDuration = struct { + years: ?i64 = null, + months: ?i64 = null, + weeks: ?i64 = null, + days: ?i64 = null, + hours: ?i64 = null, + minutes: ?i64 = null, + seconds: ?i64 = null, + milliseconds: ?i64 = null, + microseconds: ?f64 = null, + nanoseconds: ?f64 = null, + + fn toCApi(self: PartialDuration) abi.c.PartialDuration { + return .{ + .years = abi.toOption(abi.c.OptionI64, self.years), + .months = abi.toOption(abi.c.OptionI64, self.months), + .weeks = abi.toOption(abi.c.OptionI64, self.weeks), + .days = abi.toOption(abi.c.OptionI64, self.days), + .hours = abi.toOption(abi.c.OptionI64, self.hours), + .minutes = abi.toOption(abi.c.OptionI64, self.minutes), + .seconds = abi.toOption(abi.c.OptionI64, self.seconds), + .milliseconds = abi.toOption(abi.c.OptionI64, self.milliseconds), + .microseconds = abi.toOption(abi.c.OptionF64, self.microseconds), + .nanoseconds = abi.toOption(abi.c.OptionF64, self.nanoseconds), + }; + } +}; + +/// Wrapper for PlainDate reference in RelativeTo +const PlainDateRef = struct { + _inner: *abi.c.PlainDate, +}; + +/// Wrapper for ZonedDateTime reference in RelativeTo +const ZonedDateTimeRef = struct { + _inner: *abi.c.ZonedDateTime, +}; + /// Relative-to context for duration operations. -pub const RelativeTo = extern struct { - plain_date: ?*abi.c.PlainDate, - zoned_date_time: ?*abi.c.ZonedDateTime, +pub const RelativeTo = struct { + plain_date: ?PlainDateRef = null, + zoned_date_time: ?ZonedDateTimeRef = null, fn toCApi(self: RelativeTo) abi.c.RelativeTo { return .{ - .date = self.plain_date, - .zoned = self.zoned_date_time, + .date = if (self.plain_date) |pd| pd._inner else null, + .zoned = if (self.zoned_date_time) |zdt| zdt._inner else null, }; } }; @@ -73,7 +113,7 @@ fn fromUtf16(text: []const u16) !Duration { /// Create a Duration from a partial duration (where some fields may be omitted). fn fromPartialDuration(partial: PartialDuration) !Duration { - return wrapDuration(abi.c.temporal_rs_Duration_from_partial_duration(partial)); + return wrapDuration(abi.c.temporal_rs_Duration_from_partial_duration(partial.toCApi())); } /// Check if the time portion of the duration is within valid ranges. @@ -362,20 +402,9 @@ test toString { } test fromPartialDuration { - const empty_i64 = abi.toOption(abi.c.OptionI64, null); - const empty_f64 = abi.toOption(abi.c.OptionF64, null); - - const partial = abi.c.PartialDuration{ - .years = empty_i64, - .months = empty_i64, - .weeks = empty_i64, - .days = empty_i64, - .hours = .{ .is_ok = true, .unnamed_0 = .{ .ok = 3 } }, - .minutes = .{ .is_ok = true, .unnamed_0 = .{ .ok = 45 } }, - .seconds = empty_i64, - .milliseconds = empty_i64, - .microseconds = empty_f64, - .nanoseconds = empty_f64, + const partial = PartialDuration{ + .hours = 3, + .minutes = 45, }; const dur = try Duration.fromPartialDuration(partial); diff --git a/src/Instant.zig b/src/Instant.zig index d588802..479e6de 100644 --- a/src/Instant.zig +++ b/src/Instant.zig @@ -11,6 +11,21 @@ pub const RoundingMode = temporal.RoundingMode; pub const Sign = temporal.Sign; pub const RoundingOptions = temporal.RoundingOptions; pub const DifferenceSettings = temporal.DifferenceSettings; + +/// Time zone identifier for Temporal operations +pub const TimeZone = struct { + _inner: abi.c.TimeZone, + + pub fn init(id: []const u8) TimeZone { + const view = abi.toDiplomatStringView(id); + return .{ ._inner = .{ .id = view } }; + } + + fn toCApi(self: TimeZone) abi.c.TimeZone { + return self._inner; + } +}; + /// Options for Instant.toString() /// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant/toString pub const ToStringOptions = struct { @@ -29,8 +44,7 @@ pub const ToStringOptions = struct { smallest_unit: ?Unit = null, /// Time zone to use. Either a time zone identifier string or null for UTC. - /// Note: In the Zig API, this must be pre-resolved to a TimeZone struct. - time_zone: ?abi.c.TimeZone = null, + time_zone: ?TimeZone = null, }; _inner: *abi.c.Instant, @@ -99,13 +113,15 @@ pub fn subtract(self: Instant, duration: *Duration) !Instant { } /// Difference until another instant (Temporal.Instant.prototype.until). -pub fn until(self: Instant, other: Instant, settings: DifferenceSettings) !DurationHandle { - return wrapDuration(abi.c.temporal_rs_Instant_until(self._inner, other._inner, settings.toCApi())); +pub fn until(self: Instant, other: Instant, settings: DifferenceSettings) !Duration { + const ptr = (abi.success(abi.c.temporal_rs_Instant_until(self._inner, other._inner, settings.toCApi())) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } /// Difference since another instant (Temporal.Instant.prototype.since). -pub fn since(self: Instant, other: Instant, settings: DifferenceSettings) !DurationHandle { - return wrapDuration(abi.c.temporal_rs_Instant_since(self._inner, other._inner, settings.toCApi())); +pub fn since(self: Instant, other: Instant, settings: DifferenceSettings) !Duration { + const ptr = (abi.success(abi.c.temporal_rs_Instant_since(self._inner, other._inner, settings.toCApi())) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } /// Round this instant (Temporal.Instant.prototype.round). @@ -125,7 +141,7 @@ pub fn equals(a: Instant, b: Instant) bool { /// 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 = abi.toTimeZoneOption(opts.time_zone); + const zone_opt = if (opts.time_zone) |tz| abi.toTimeZoneOption(tz._inner) else abi.toTimeZoneOption(null); const rounding = optsToRounding(opts); var write = abi.DiplomatWrite.init(allocator); @@ -139,7 +155,7 @@ pub fn toString(self: Instant, allocator: std.mem.Allocator, opts: ToStringOptio /// Convert to string using an explicit provider. fn toStringWithProvider(self: Instant, allocator: std.mem.Allocator, provider: *const abi.c.Provider, opts: ToStringOptions) ![]u8 { - const zone_opt = abi.toTimeZoneOption(opts.time_zone); + const zone_opt = if (opts.time_zone) |tz| abi.toTimeZoneOption(tz._inner) else abi.toTimeZoneOption(null); const rounding = optsToRounding(opts); var write = abi.DiplomatWrite.init(allocator); @@ -155,20 +171,23 @@ pub fn toJSON(self: Instant, allocator: std.mem.Allocator) ![]u8 { return self.toString(allocator, .{}); } +/// Convert to a locale string representation. +/// Per Temporal spec, toLocaleString returns a formatted string without taking locale/options parameters. +/// This uses auto precision for fractional seconds. pub fn toLocaleString(self: Instant, allocator: std.mem.Allocator) ![]u8 { - _ = self; - _ = allocator; - return error.TemporalNotImplemented; + return self.toString(allocator, .{}); } /// Convert to ZonedDateTime using built-in provider (Temporal.Instant.prototype.toZonedDateTimeISO). -pub fn toZonedDateTimeISO(self: Instant, zone: abi.c.TimeZone) !ZonedDateTimeHandle { - return wrapZonedDateTime(abi.c.temporal_rs_Instant_to_zoned_date_time_iso(self._inner, zone)); +pub fn toZonedDateTimeISO(self: Instant, zone: TimeZone) !ZonedDateTime { + const ptr = (abi.success(abi.c.temporal_rs_Instant_to_zoned_date_time_iso(self._inner, zone._inner)) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } /// Convert to ZonedDateTime using an explicit provider. -fn toZonedDateTimeIsoWithProvider(self: Instant, zone: abi.c.TimeZone, provider: *const abi.c.Provider) !ZonedDateTimeHandle { - return wrapZonedDateTime(abi.c.temporal_rs_Instant_to_zoned_date_time_iso_with_provider(self._inner, zone, provider)); +fn toZonedDateTimeIsoWithProvider(self: Instant, zone: TimeZone, provider: *const abi.c.Provider) !ZonedDateTime { + const ptr = (abi.success(abi.c.temporal_rs_Instant_to_zoned_date_time_iso_with_provider(self._inner, zone._inner, provider)) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } /// Clone the underlying instant. @@ -192,16 +211,6 @@ fn wrapInstant(res: anytype) !Instant { }; } -fn wrapDuration(res: anytype) !DurationHandle { - const ptr = (abi.success(res) orelse return error.TemporalError) orelse return error.TemporalError; - return .{ .ptr = ptr }; -} - -fn wrapZonedDateTime(res: anytype) !ZonedDateTimeHandle { - const ptr = (abi.success(res) orelse return error.TemporalError) orelse return error.TemporalError; - return .{ .ptr = ptr }; -} - fn handleVoidResult(res: anytype) !void { _ = abi.success(res) orelse return error.TemporalError; } @@ -232,26 +241,13 @@ fn optsToRounding(opts: ToStringOptions) abi.c.ToStringRoundingOptions { }; } -fn parseDuration(text: []const u8) !DurationHandle { - const view = abi.toDiplomatStringView(text); - return wrapDuration(abi.c.temporal_rs_Duration_from_utf8(view)); -} +// --- Forward declarations ----------------------------------------------------- -// --- Helper types ----------------------------------------------------- +const ZonedDateTime = struct { + _inner: *abi.c.ZonedDateTime, -const DurationHandle = struct { - ptr: *abi.c.Duration, - - pub fn deinit(self: DurationHandle) void { - abi.c.temporal_rs_Duration_destroy(self.ptr); - } -}; - -const ZonedDateTimeHandle = struct { - ptr: *abi.c.ZonedDateTime, - - pub fn deinit(self: ZonedDateTimeHandle) void { - abi.c.temporal_rs_ZonedDateTime_destroy(self.ptr); + pub fn deinit(self: ZonedDateTime) void { + abi.c.temporal_rs_ZonedDateTime_destroy(self._inner); } }; @@ -365,15 +361,15 @@ test until { .rounding_increment = null, }; - var until_handle = try earlier.until(later, settings); - defer until_handle.deinit(); - try std.testing.expectEqual(Sign.positive, Sign.fromCApi(abi.c.temporal_rs_Duration_sign(until_handle.ptr))); - try std.testing.expectEqual(@as(i64, 1), abi.c.temporal_rs_Duration_hours(until_handle.ptr)); + const until_dur = try earlier.until(later, settings); + defer until_dur.deinit(); + try std.testing.expectEqual(Sign.positive, until_dur.sign()); + try std.testing.expectEqual(@as(i64, 1), until_dur.hours()); - var since_handle = try later.since(earlier, settings); - defer since_handle.deinit(); - try std.testing.expectEqual(Sign.positive, Sign.fromCApi(abi.c.temporal_rs_Duration_sign(since_handle.ptr))); - try std.testing.expectEqual(@as(i64, 1), abi.c.temporal_rs_Duration_hours(since_handle.ptr)); + const since_dur = try later.since(earlier, settings); + defer since_dur.deinit(); + try std.testing.expectEqual(Sign.positive, since_dur.sign()); + try std.testing.expectEqual(@as(i64, 1), since_dur.hours()); } test since { @@ -389,15 +385,15 @@ test since { .rounding_increment = null, }; - var until_handle = try earlier.until(later, settings); - defer until_handle.deinit(); - try std.testing.expectEqual(Sign.positive, Sign.fromCApi(abi.c.temporal_rs_Duration_sign(until_handle.ptr))); - try std.testing.expectEqual(@as(i64, 1), abi.c.temporal_rs_Duration_hours(until_handle.ptr)); + const until_dur = try earlier.until(later, settings); + defer until_dur.deinit(); + try std.testing.expectEqual(Sign.positive, until_dur.sign()); + try std.testing.expectEqual(@as(i64, 1), until_dur.hours()); - var since_handle = try later.since(earlier, settings); - defer since_handle.deinit(); - try std.testing.expectEqual(Sign.positive, Sign.fromCApi(abi.c.temporal_rs_Duration_sign(since_handle.ptr))); - try std.testing.expectEqual(@as(i64, 1), abi.c.temporal_rs_Duration_hours(since_handle.ptr)); + const since_dur = try later.since(earlier, settings); + defer since_dur.deinit(); + try std.testing.expectEqual(Sign.positive, since_dur.sign()); + try std.testing.expectEqual(@as(i64, 1), since_dur.hours()); } test round { @@ -450,3 +446,25 @@ test toString { defer allocator.free(with_unit); // Output should truncate to seconds } + +test toLocaleString { + 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; + + // toLocaleString should return a formatted string (same as toString with defaults) + const locale_str = try inst.toLocaleString(allocator); + defer allocator.free(locale_str); + try std.testing.expectEqualStrings(locale_str, "2024-01-01T00:00:00Z"); + + // Test with a different instant with fractional seconds + const inst2 = try Instant.fromEpochNanoseconds(1704067200123456789); + defer inst2.deinit(); + + const locale_str2 = try inst2.toLocaleString(allocator); + defer allocator.free(locale_str2); + // Auto precision should include fractional seconds + try std.testing.expect(std.mem.containsAtLeast(u8, locale_str2, 1, "2024-01-01T00:00:00")); +} diff --git a/src/Now.zig b/src/Now.zig index 1001e0b..94e85ab 100644 --- a/src/Now.zig +++ b/src/Now.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const Instant = @import("Instant.zig"); const PlainDate = @import("PlainDate.zig"); const PlainDateTime = @import("PlainDateTime.zig"); @@ -6,47 +7,124 @@ const ZonedDateTime = @import("ZonedDateTime.zig"); const Now = @This(); -pub fn instant() error{Todo}!Instant { - return error.Todo; +pub fn instant() !Instant { + const ns: i128 = std.time.nanoTimestamp(); + return Instant.fromEpochNanoseconds(ns); } -pub fn plainDateISO() error{Todo}!PlainDate { - return error.Todo; +pub fn plainDateISO() !PlainDate { + const now = currentParts(); + return PlainDate.init(now.year, now.month, now.day); } -pub fn plainDateTimeISO() error{Todo}!PlainDateTime { - return error.Todo; +pub fn plainDateTimeISO() !PlainDateTime { + const now = currentParts(); + return PlainDateTime.init( + now.year, + now.month, + now.day, + now.hour, + now.minute, + now.second, + now.millisecond, + now.microsecond, + now.nanosecond, + ); } -pub fn plainTimeISO() error{Todo}!PlainTime { - return error.Todo; +pub fn plainTimeISO() !PlainTime { + const now = currentParts(); + return PlainTime.init( + now.hour, + now.minute, + now.second, + now.millisecond, + now.microsecond, + now.nanosecond, + ); } -pub fn timeZoneId() error{Todo}![]const u8 { - return error.Todo; +pub fn timeZoneId() []const u8 { + return "UTC"; } -pub fn zonedDateTimeISO() error{Todo}!ZonedDateTime { - return error.Todo; +pub fn zonedDateTimeISO() !ZonedDateTime { + return error.TemporalNotImplemented; +} + +const CurrentParts = struct { + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + millisecond: u16, + microsecond: u16, + nanosecond: u16, +}; + +fn currentParts() CurrentParts { + const ns: i128 = std.time.nanoTimestamp(); + 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, + }; } // ---------- Tests --------------------- test instant { - if (true) return error.Todo; + const inst = try instant(); + defer inst.deinit(); + try std.testing.expect(inst.epoch_nanoseconds > 0); } test plainDateISO { - if (true) return error.Todo; + const date = try plainDateISO(); + 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 { - if (true) return error.Todo; + const dt = try plainDateTimeISO(); + 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 { - if (true) return error.Todo; + const t = try plainTimeISO(); + try std.testing.expect(t.hour() < 24); + try std.testing.expect(t.minute() < 60); + try std.testing.expect(t.second() < 60); } test timeZoneId { - if (true) return error.Todo; + const tz = timeZoneId(); + try std.testing.expectEqualStrings("UTC", tz); } test zonedDateTimeISO { - if (true) return error.Todo; + try std.testing.expectError(error.TemporalNotImplemented, zonedDateTimeISO()); } diff --git a/src/PlainDate.zig b/src/PlainDate.zig index 2430328..00fb2f4 100644 --- a/src/PlainDate.zig +++ b/src/PlainDate.zig @@ -1,148 +1,497 @@ const std = @import("std"); +const abi = @import("abi.zig"); +const temporal = @import("temporal.zig"); const PlainDateTime = @import("PlainDateTime.zig"); const PlainMonthDay = @import("PlainMonthDay.zig"); const PlainYearMonth = @import("PlainYearMonth.zig"); const ZonedDateTime = @import("ZonedDateTime.zig"); +const PlainTime = @import("PlainTime.zig"); const Duration = @import("Duration.zig"); const PlainDate = @This(); +pub const Unit = temporal.Unit; +pub const RoundingMode = temporal.RoundingMode; +pub const Sign = temporal.Sign; +pub const DifferenceSettings = temporal.DifferenceSettings; + +pub const ToStringOptions = struct { + calendar_id: ?CalendarDisplay = null, +}; + +pub const CalendarDisplay = enum { + auto, + always, + never, + critical, + + fn toCApi(self: CalendarDisplay) abi.c.ShowCalendar { + return switch (self) { + .auto => .Auto, + .always => .Always, + .never => .Never, + .critical => .Critical, + }; + } +}; + +pub const ToZonedDateTimeOptions = struct { + time_zone: []const u8, + plain_time: ?PlainTime = null, +}; + +_inner: *abi.c.PlainDate, calendar_id: []const u8, -day: i64 = 0, -day_of_week: i64, -day_of_year: i64, -days_in_month: i64, -days_in_week: i64, -days_in_year: i64, -era: []const u8, -era_year: i64, -in_leap_year: bool, -month: i64, -month_code: []const u8, -months_in_year: i64, -week_of_year: i64, -year: i64, -year_of_week: i64, -pub fn init() error{Todo}!PlainDate { - return error.Todo; +pub fn init(year_val: i32, month_val: u8, day_val: u8) !PlainDate { + return initWithCalendar(year_val, month_val, day_val, "iso8601"); } -pub fn compare() error{Todo}!i8 { - return error.Todo; +pub fn initWithCalendar(year_val: i32, month_val: u8, day_val: u8, calendar: []const u8) !PlainDate { + const cal_view = abi.toDiplomatStringView(calendar); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + const cal_kind = abi.success(cal_result) orelse return error.TemporalError; + return wrapPlainDate(abi.c.temporal_rs_PlainDate_try_new(year_val, month_val, day_val, cal_kind)); } -pub fn from() error{Todo}!PlainDate { - return error.Todo; +pub fn from(info: anytype) !PlainDate { + const T = @TypeOf(info); + + if (T == PlainDate) return info.clone(); + + const type_info = @typeInfo(T); + switch (type_info) { + .pointer => { + const ptr = type_info.pointer; + const ChildType = switch (@typeInfo(ptr.child)) { + .array => |arr| arr.child, + else => ptr.child, + }; + + if (ChildType == u8) return fromUtf8(info); + if (ChildType == u16) return fromUtf16(info); + }, + else => @compileError("from() expects a PlainDate, []const u8, or []const u16"), + } } -pub fn add() error{Todo}!PlainDate { - return error.Todo; +inline fn fromUtf8(text: []const u8) !PlainDate { + const view = abi.toDiplomatStringView(text); + return wrapPlainDate(abi.c.temporal_rs_PlainDate_from_utf8(view)); } -pub fn equals() error{Todo}!bool { - return error.Todo; +inline fn fromUtf16(text: []const u16) !PlainDate { + const view = abi.toDiplomatString16View(text); + return wrapPlainDate(abi.c.temporal_rs_PlainDate_from_utf16(view)); } -pub fn since() error{Todo}!Duration { - return error.Todo; +pub fn compare(a: PlainDate, b: PlainDate) i8 { + return abi.c.temporal_rs_PlainDate_compare(a._inner, b._inner); } -pub fn subtract() error{Todo}!PlainDate { - return error.Todo; +pub fn equals(self: PlainDate, other: PlainDate) bool { + return abi.c.temporal_rs_PlainDate_equals(self._inner, other._inner); } -pub fn toJSON() error{Todo}![]const u8 { - return error.Todo; +pub fn add(self: PlainDate, duration: Duration) !PlainDate { + const overflow_opt = abi.c.ArithmeticOverflow_option{ .is_ok = true, .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain } }; + return wrapPlainDate(abi.c.temporal_rs_PlainDate_add(self._inner, duration._inner, overflow_opt)); } -pub fn toLocaleString() error{Todo}![]const u8 { - return error.Todo; +pub fn subtract(self: PlainDate, duration: Duration) !PlainDate { + const overflow_opt = abi.c.ArithmeticOverflow_option{ .is_ok = true, .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain } }; + return wrapPlainDate(abi.c.temporal_rs_PlainDate_subtract(self._inner, duration._inner, overflow_opt)); } -pub fn toPlainDateTime() error{Todo}!PlainDateTime { - return error.Todo; +pub fn until(self: PlainDate, other: PlainDate, settings: DifferenceSettings) !Duration { + const ptr = (abi.success(abi.c.temporal_rs_PlainDate_until(self._inner, other._inner, settings.toCApi())) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn toPlainMonthDay() error{Todo}!PlainMonthDay { - return error.Todo; +pub fn since(self: PlainDate, other: PlainDate, settings: DifferenceSettings) !Duration { + const ptr = (abi.success(abi.c.temporal_rs_PlainDate_since(self._inner, other._inner, settings.toCApi())) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn toPlainYearMonth() error{Todo}!PlainYearMonth { - return error.Todo; +pub fn with(self: PlainDate, fields: anytype) !PlainDate { + _ = self; + _ = fields; + return error.TemporalNotImplemented; } -pub fn toString() error{Todo}![]const u8 { - return error.Todo; +pub fn withCalendar(self: PlainDate, calendar: []const u8) !PlainDate { + const cal_view = abi.toDiplomatStringView(calendar); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + const cal_kind = abi.success(cal_result) orelse return error.TemporalError; + const ptr = abi.c.temporal_rs_PlainDate_with_calendar(self._inner, cal_kind) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainDate_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return .{ ._inner = ptr, .calendar_id = cal_id }; } -pub fn toZonedDateTime() error{Todo}!ZonedDateTime { - return error.Todo; +pub fn toPlainDateTime(self: PlainDate, time: ?PlainTime) !PlainDateTime { + const time_ptr = if (time) |t| t._inner else null; + const ptr = (abi.success(abi.c.temporal_rs_PlainDate_to_plain_date_time(self._inner, time_ptr)) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr, .calendar_id = self.calendar_id }; } -pub fn until() error{Todo}!Duration { - return error.Todo; +pub fn toPlainMonthDay(self: PlainDate) !PlainMonthDay { + const ptr = (abi.success(abi.c.temporal_rs_PlainDate_to_plain_month_day(self._inner)) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr, .calendar_id = self.calendar_id }; } -pub fn valueOf() error{Todo}!void { - return error.Todo; +pub fn toPlainYearMonth(self: PlainDate) !PlainYearMonth { + const ptr = (abi.success(abi.c.temporal_rs_PlainDate_to_plain_year_month(self._inner)) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr, .calendar_id = self.calendar_id }; } -pub fn with() error{Todo}!PlainDate { - return error.Todo; +pub fn toZonedDateTime(self: PlainDate, options: ToZonedDateTimeOptions) !ZonedDateTime { + const tz_view = abi.toDiplomatStringView(options.time_zone); + const tz_result = abi.c.temporal_rs_TimeZone_try_from_str(tz_view); + const time_zone = abi.success(tz_result) orelse return error.TemporalError; + + const time_ptr = if (options.plain_time) |t| t._inner else null; + const ptr = (abi.success(abi.c.temporal_rs_PlainDate_to_zoned_date_time(self._inner, time_zone, time_ptr)) orelse return error.TemporalError) orelse return error.TemporalError; + + // Get time zone identifier for ZonedDateTime + const allocator = std.heap.page_allocator; + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + abi.c.temporal_rs_TimeZone_identifier(time_zone, &write.inner); + const tz_id = try write.toOwnedSlice(); + + return .{ ._inner = ptr, .time_zone_id = tz_id, .calendar_id = self.calendar_id }; } -pub fn withCalendar() error{Todo}!PlainDate { - return error.Todo; +pub fn toString(self: PlainDate, allocator: std.mem.Allocator, options: ToStringOptions) ![]u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + const display_cal: abi.c.DisplayCalendar = if (options.calendar_id) |cal| switch (cal) { + .auto => @intCast(abi.c.DisplayCalendar_Auto), + .always => @intCast(abi.c.DisplayCalendar_Always), + .never => @intCast(abi.c.DisplayCalendar_Never), + .critical => @intCast(abi.c.DisplayCalendar_Critical), + } else @intCast(abi.c.DisplayCalendar_Auto); + + abi.c.temporal_rs_PlainDate_to_ixdtf_string(self._inner, display_cal, &write.inner); + return try write.toOwnedSlice(); } -// ---------- Tests --------------------- -test compare { - if (true) return error.Todo; +pub fn toJSON(self: PlainDate, allocator: std.mem.Allocator) ![]u8 { + return self.toString(allocator, .{}); } + +pub fn toLocaleString(self: PlainDate, allocator: std.mem.Allocator) ![]u8 { + _ = self; + _ = allocator; + return error.TemporalNotImplemented; +} + +pub fn valueOf(self: PlainDate) !void { + _ = self; + return error.TemporalValueOfNotSupported; +} + +pub fn day(self: PlainDate) u8 { + return abi.c.temporal_rs_PlainDate_day(self._inner); +} + +pub fn dayOfWeek(self: PlainDate) u16 { + return abi.c.temporal_rs_PlainDate_day_of_week(self._inner); +} + +pub fn dayOfYear(self: PlainDate) u16 { + return abi.c.temporal_rs_PlainDate_day_of_year(self._inner); +} + +pub fn daysInMonth(self: PlainDate) u16 { + return abi.c.temporal_rs_PlainDate_days_in_month(self._inner); +} + +pub fn daysInWeek(self: PlainDate) u16 { + return abi.c.temporal_rs_PlainDate_days_in_week(self._inner); +} + +pub fn daysInYear(self: PlainDate) u16 { + return abi.c.temporal_rs_PlainDate_days_in_year(self._inner); +} + +pub fn monthCode(self: PlainDate, allocator: std.mem.Allocator) ![]u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + abi.c.temporal_rs_PlainDate_month_code(self._inner, &write.inner); + return try write.toOwnedSlice(); +} + +pub fn month(self: PlainDate) u8 { + return abi.c.temporal_rs_PlainDate_month(self._inner); +} + +pub fn monthsInYear(self: PlainDate) u16 { + return abi.c.temporal_rs_PlainDate_months_in_year(self._inner); +} + +pub fn year(self: PlainDate) i32 { + return abi.c.temporal_rs_PlainDate_year(self._inner); +} + +pub fn inLeapYear(self: PlainDate) bool { + return abi.c.temporal_rs_PlainDate_in_leap_year(self._inner); +} + +pub fn era(self: PlainDate, allocator: std.mem.Allocator) !?[]u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + const has_era = abi.c.temporal_rs_PlainDate_era(self._inner, &write.inner); + if (!has_era) return null; + return try write.toOwnedSlice(); +} + +pub fn eraYear(self: PlainDate) ?u32 { + const result = abi.c.temporal_rs_PlainDate_era_year(self._inner); + if (!result.is_ok) return null; + return result.unnamed_0.ok; +} + +pub fn weekOfYear(self: PlainDate) ?u8 { + const result = abi.c.temporal_rs_PlainDate_week_of_year(self._inner); + if (!result.is_ok) return null; + return result.unnamed_0.ok; +} + +pub fn yearOfWeek(self: PlainDate) ?i32 { + const result = abi.c.temporal_rs_PlainDate_year_of_week(self._inner); + if (!result.is_ok) return null; + return result.unnamed_0.ok; +} + +fn clone(self: PlainDate) PlainDate { + const ptr = abi.c.temporal_rs_PlainDate_clone(self._inner) orelse unreachable; + return .{ ._inner = ptr, .calendar_id = self.calendar_id }; +} + +pub fn deinit(self: PlainDate) void { + abi.c.temporal_rs_PlainDate_destroy(self._inner); +} + +fn wrapPlainDate(res: anytype) !PlainDate { + const ptr = (abi.success(res) orelse return error.TemporalError) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainDate_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return .{ ._inner = ptr, .calendar_id = cal_id }; +} + +fn handleVoidResult(res: anytype) !void { + _ = abi.success(res) orelse return error.TemporalError; +} + +test init { + const date = try PlainDate.init(2024, 3, 15); + defer date.deinit(); + + try std.testing.expectEqual(@as(i32, 2024), date.year()); + try std.testing.expectEqual(@as(u8, 3), date.month()); + try std.testing.expectEqual(@as(u8, 15), date.day()); +} + test from { - if (true) return error.Todo; + const date = try PlainDate.from("2024-03-15"); + defer date.deinit(); + + try std.testing.expectEqual(@as(i32, 2024), date.year()); + try std.testing.expectEqual(@as(u8, 3), date.month()); + try std.testing.expectEqual(@as(u8, 15), date.day()); + + const date2 = try PlainDate.from(date); + defer date2.deinit(); + + try std.testing.expect(PlainDate.equals(date, date2)); } -test add { - if (true) return error.Todo; + +test compare { + const date1 = try PlainDate.init(2024, 1, 1); + defer date1.deinit(); + const date2 = try PlainDate.init(2024, 1, 1); + defer date2.deinit(); + const date3 = try PlainDate.init(2024, 12, 31); + defer date3.deinit(); + + try std.testing.expectEqual(@as(i8, 0), PlainDate.compare(date1, date2)); + try std.testing.expectEqual(@as(i8, -1), PlainDate.compare(date1, date3)); + try std.testing.expectEqual(@as(i8, 1), PlainDate.compare(date3, date1)); } + test equals { - if (true) return error.Todo; + const date1 = try PlainDate.init(2024, 3, 15); + defer date1.deinit(); + const date2 = try PlainDate.init(2024, 3, 15); + defer date2.deinit(); + const date3 = try PlainDate.init(2024, 3, 16); + defer date3.deinit(); + + try std.testing.expect(PlainDate.equals(date1, date2)); + try std.testing.expect(!PlainDate.equals(date1, date3)); } -test since { - if (true) return error.Todo; + +test add { + const date = try PlainDate.init(2024, 1, 1); + defer date.deinit(); + + var duration = try Duration.from("P1M"); + defer duration.deinit(); + + const result = try date.add(duration); + defer result.deinit(); + + try std.testing.expectEqual(@as(i32, 2024), result.year()); + try std.testing.expectEqual(@as(u8, 2), result.month()); + try std.testing.expectEqual(@as(u8, 1), result.day()); } + test subtract { - if (true) return error.Todo; -} -test toJSON { - if (true) return error.Todo; -} -test toLocaleString { - if (true) return error.Todo; -} -test toPlainDateTime { - if (true) return error.Todo; -} -test toPlainMonthDay { - if (true) return error.Todo; -} -test toPlainYearMonth { - if (true) return error.Todo; -} -test toString { - if (true) return error.Todo; -} -test toZonedDateTime { - if (true) return error.Todo; + const date = try PlainDate.init(2024, 3, 15); + defer date.deinit(); + + var duration = try Duration.from("P10D"); + defer duration.deinit(); + + const result = try date.subtract(duration); + defer result.deinit(); + + try std.testing.expectEqual(@as(i32, 2024), result.year()); + try std.testing.expectEqual(@as(u8, 3), result.month()); + try std.testing.expectEqual(@as(u8, 5), result.day()); } + test until { - if (true) return error.Todo; + const start = try PlainDate.init(2024, 1, 1); + defer start.deinit(); + const end = try PlainDate.init(2024, 2, 1); + defer end.deinit(); + + const settings = DifferenceSettings{ + .largest_unit = .month, + .smallest_unit = .day, + .rounding_mode = .trunc, + .rounding_increment = null, + }; + + const duration = try start.until(end, settings); + defer duration.deinit(); + + try std.testing.expectEqual(@as(i64, 1), duration.months()); } + +test since { + const end = try PlainDate.init(2024, 2, 1); + defer end.deinit(); + const start = try PlainDate.init(2024, 1, 1); + defer start.deinit(); + + const settings = DifferenceSettings{ + .largest_unit = .month, + .smallest_unit = .day, + .rounding_mode = .trunc, + .rounding_increment = null, + }; + + const duration = try end.since(start, settings); + defer duration.deinit(); + + try std.testing.expectEqual(@as(i64, 1), duration.months()); +} + +test toString { + const date = try PlainDate.init(2024, 3, 15); + defer date.deinit(); + + const allocator = std.testing.allocator; + const str = try date.toString(allocator, .{}); + defer allocator.free(str); + + try std.testing.expect(std.mem.indexOf(u8, str, "2024-03-15") != null); +} + +test toJSON { + const date = try PlainDate.init(2024, 3, 15); + defer date.deinit(); + + const allocator = std.testing.allocator; + const str = try date.toJSON(allocator); + defer allocator.free(str); + + try std.testing.expect(std.mem.indexOf(u8, str, "2024-03-15") != null); +} + +test toLocaleString { + const date = try PlainDate.init(2024, 3, 15); + defer date.deinit(); + + const allocator = std.testing.allocator; + try std.testing.expectError(error.TemporalNotImplemented, date.toLocaleString(allocator)); +} + +test toPlainDateTime { + const date = try PlainDate.init(2024, 1, 15); + const datetime = try date.toPlainDateTime(null); + try std.testing.expectEqual(@as(i32, 2024), datetime.year()); + try std.testing.expectEqual(@as(u8, 1), datetime.month()); + try std.testing.expectEqual(@as(u8, 15), datetime.day()); + try std.testing.expectEqual(@as(u8, 0), datetime.hour()); + try std.testing.expectEqual(@as(u8, 0), datetime.minute()); +} + +test toPlainMonthDay { + const date = try PlainDate.init(2024, 12, 25); + const md = try date.toPlainMonthDay(); + try std.testing.expectEqual(@as(u8, 25), md.day()); + const month_code = try md.monthCode(std.testing.allocator); + defer std.testing.allocator.free(month_code); + try std.testing.expect(month_code.len > 0); +} + +test toPlainYearMonth { + const date = try PlainDate.init(2024, 12, 25); + const ym = try date.toPlainYearMonth(); + try std.testing.expectEqual(@as(i32, 2024), ym.year()); + try std.testing.expectEqual(@as(u8, 12), ym.month()); +} + +test toZonedDateTime { + if (true) return error.SkipZigTest; // ZonedDateTime not fully implemented yet + // const date = try PlainDate.init(2024, 1, 15); + // const zdt = try date.toZonedDateTime(.{ .time_zone = "UTC", .plain_time = null }); + // try std.testing.expectEqual(@as(i32, 2024), zdt.year()); + // try std.testing.expectEqual(@as(u8, 1), zdt.month()); + // try std.testing.expectEqual(@as(u8, 15), zdt.day()); +} + test with { - if (true) return error.Todo; + const date = try PlainDate.init(2024, 3, 15); + defer date.deinit(); + + try std.testing.expectError(error.TemporalNotImplemented, date.with(.{})); } + test withCalendar { - if (true) return error.Todo; + const date = try PlainDate.init(2024, 3, 15); + defer date.deinit(); + + const result = try date.withCalendar("iso8601"); + defer result.deinit(); + + try std.testing.expectEqual(@as(i32, 2024), result.year()); } diff --git a/src/PlainDateTime.zig b/src/PlainDateTime.zig index cca9f27..733a5a0 100644 --- a/src/PlainDateTime.zig +++ b/src/PlainDateTime.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const abi = @import("abi.zig"); +const temporal = @import("temporal.zig"); const PlainDate = @import("PlainDate.zig"); const PlainTime = @import("PlainTime.zig"); @@ -7,154 +9,593 @@ const Duration = @import("Duration.zig"); const PlainDateTime = @This(); +_inner: *abi.c.PlainDateTime, calendar_id: []const u8, -day: i64, -day_of_week: i64, -day_of_year: i64, -days_in_month: i64, -days_in_week: i64, -days_in_year: i64, -era: []const u8, -era_year: i64, -hour: i64, -in_leap_year: bool, -microsecond: i64, -millisecond: i64, -minute: i64, -month: i64, -month_code: []const u8, -months_in_year: i64, -nanosecond: i64, -second: i64, -week_of_year: i64, -year: i64, -year_of_week: i64, -pub fn init() error{Todo}!PlainDateTime { - return error.Todo; +// Import types from temporal.zig +pub const Unit = temporal.Unit; +pub const RoundingMode = temporal.RoundingMode; +pub const Sign = temporal.Sign; +pub const DifferenceSettings = temporal.DifferenceSettings; +pub const RoundOptions = temporal.RoundingOptions; +// Type definitions for API compatibility +pub const CalendarDisplay = enum { + auto, + always, + never, + critical, +}; + +pub const ToStringOptions = struct { + fractional_second_digits: ?u8 = null, + smallest_unit: ?Unit = null, + rounding_mode: ?RoundingMode = null, + calendar_display: ?CalendarDisplay = null, +}; + +pub const ToZonedDateTimeOptions = struct { + timeZone: []const u8, + disambiguation: ?[]const u8 = null, +}; + +pub const WithOptions = struct { + year: ?i32 = null, + month: ?u8 = null, + day: ?u8 = null, + hour: ?u8 = null, + minute: ?u8 = null, + second: ?u8 = null, + millisecond: ?u16 = null, + microsecond: ?u16 = null, + nanosecond: ?u16 = null, +}; + +// Constructor - creates a PlainDateTime with all parameters +pub fn init( + year_val: i32, + month_val: u8, + day_val: u8, + hour_val: u8, + minute_val: u8, + second_val: u8, + millisecond_val: u16, + microsecond_val: u16, + nanosecond_val: u16, +) !PlainDateTime { + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_try_new( + year_val, + month_val, + day_val, + hour_val, + minute_val, + second_val, + millisecond_val, + microsecond_val, + nanosecond_val, + abi.c.AnyCalendarKind_Iso, + )); } -pub fn compare() error{Todo}!i8 { - return error.Todo; +pub fn initWithCalendar( + year_val: i32, + month_val: u8, + day_val: u8, + hour_val: u8, + minute_val: u8, + second_val: u8, + millisecond_val: u16, + microsecond_val: u16, + nanosecond_val: u16, + calendar: []const u8, +) !PlainDateTime { + const cal_view = abi.toDiplomatStringView(calendar); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + const cal_kind = abi.success(cal_result) orelse return error.TemporalError; + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_try_new( + year_val, + month_val, + day_val, + hour_val, + minute_val, + second_val, + millisecond_val, + microsecond_val, + nanosecond_val, + cal_kind, + )); } -pub fn from() error{Todo}!PlainDateTime { - return error.Todo; +// Parse from string +pub fn from(s: []const u8) !PlainDateTime { + return fromUtf8(s); } -pub fn add() error{Todo}!PlainDateTime { - return error.Todo; +fn fromUtf8(utf8: []const u8) !PlainDateTime { + const view = abi.toDiplomatStringView(utf8); + const parsed = abi.c.temporal_rs_ParsedDateTime_from_utf8(view); + const ptr = abi.success(parsed) orelse return error.TemporalError; + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_from_parsed(ptr)); } -pub fn equals() error{Todo}!bool { - return error.Todo; +fn fromUtf16(utf16: []const u16) !PlainDateTime { + const view = abi.toDiplomatString16View(utf16); + const parsed = abi.c.temporal_rs_ParsedDateTime_from_utf16(view); + const ptr = abi.success(parsed) orelse return error.TemporalError; + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_from_parsed(ptr)); } -pub fn round() error{Todo}!PlainDateTime { - return error.Todo; +// Comparison +pub fn compare(a: PlainDateTime, b: PlainDateTime) i8 { + return abi.c.temporal_rs_PlainDateTime_compare(a._inner, b._inner); } -pub fn since() error{Todo}!Duration { - return error.Todo; +pub fn equals(self: PlainDateTime, other: PlainDateTime) bool { + if (compare(self, other) != 0) return false; + return std.mem.eql(u8, self.calendar_id, other.calendar_id); } -pub fn subtract() error{Todo}!PlainDateTime { - return error.Todo; +// Arithmetic +pub fn add(self: PlainDateTime, duration: Duration) !PlainDateTime { + const overflow_opt = abi.c.ArithmeticOverflow_option{ + .is_ok = true, + .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain }, + }; + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_add(self._inner, duration._inner, overflow_opt)); } -pub fn toJSON() error{Todo}![]const u8 { - return error.Todo; +pub fn subtract(self: PlainDateTime, duration: Duration) !PlainDateTime { + const overflow_opt = abi.c.ArithmeticOverflow_option{ + .is_ok = true, + .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain }, + }; + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_subtract(self._inner, duration._inner, overflow_opt)); } -pub fn toLocaleString() error{Todo}![]const u8 { - return error.Todo; +pub fn until(self: PlainDateTime, other: PlainDateTime, options: DifferenceSettings) !Duration { + const result = abi.c.temporal_rs_PlainDateTime_until(self._inner, other._inner, options.toCApi()); + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn toPlainDate() error{Todo}!PlainDate { - return error.Todo; +pub fn since(self: PlainDateTime, other: PlainDateTime, options: DifferenceSettings) !Duration { + const result = abi.c.temporal_rs_PlainDateTime_since(self._inner, other._inner, options.toCApi()); + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn toPlainTime() error{Todo}!PlainTime { - return error.Todo; +// Rounding +pub fn round(self: PlainDateTime, options: RoundOptions) !PlainDateTime { + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_round(self._inner, options.toCApi())); } -pub fn toString() error{Todo}![]const u8 { - return error.Todo; +// Property accessors - Date fields +pub fn calendarId(self: PlainDateTime) []const u8 { + return self.calendar_id; } -pub fn toZonedDateTime() error{Todo}!ZonedDateTime { - return error.Todo; +pub fn day(self: PlainDateTime) u8 { + return abi.c.temporal_rs_PlainDateTime_day(self._inner); } -pub fn until() error{Todo}!Duration { - return error.Todo; +pub fn dayOfWeek(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_day_of_week(self._inner); } -pub fn valueOf() error{Todo}!void { - return error.Todo; +pub fn dayOfYear(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_day_of_year(self._inner); } -pub fn with() error{Todo}!PlainDateTime { - return error.Todo; +pub fn daysInMonth(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_days_in_month(self._inner); } -pub fn withCalendar() error{Todo}!PlainDateTime { - return error.Todo; +pub fn daysInWeek(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_days_in_week(self._inner); } -pub fn withPlainTime() error{Todo}!PlainDateTime { - return error.Todo; +pub fn daysInYear(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_days_in_year(self._inner); +} + +pub fn month(self: PlainDateTime) u8 { + return abi.c.temporal_rs_PlainDateTime_month(self._inner); +} + +pub fn monthCode(self: PlainDateTime, allocator: std.mem.Allocator) ![]u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + abi.c.temporal_rs_PlainDateTime_month_code(self._inner, &write.inner); + return try write.toOwnedSlice(); +} + +pub fn monthsInYear(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_months_in_year(self._inner); +} + +pub fn year(self: PlainDateTime) i32 { + return abi.c.temporal_rs_PlainDateTime_year(self._inner); +} + +pub fn inLeapYear(self: PlainDateTime) bool { + return abi.c.temporal_rs_PlainDateTime_in_leap_year(self._inner); +} + +pub fn era(self: PlainDateTime, allocator: std.mem.Allocator) !?[]u8 { + const result = abi.c.temporal_rs_PlainDateTime_era(self._inner); + if (!result.is_ok) return null; + + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + abi.c.temporal_rs_Era_to_string(result.unnamed_0.ok, &write.inner); + return try write.toOwnedSlice(); +} + +pub fn eraYear(self: PlainDateTime) !?u32 { + const result = abi.c.temporal_rs_PlainDateTime_era_year(self._inner); + if (!result.is_ok) return null; + return result.unnamed_0.ok; +} + +pub fn weekOfYear(self: PlainDateTime) !?u16 { + const result = abi.c.temporal_rs_PlainDateTime_week_of_year(self._inner); + if (!result.is_ok) return null; + return result.unnamed_0.ok; +} + +pub fn yearOfWeek(self: PlainDateTime) !?i32 { + const result = abi.c.temporal_rs_PlainDateTime_year_of_week(self._inner); + if (!result.is_ok) return null; + return result.unnamed_0.ok; +} + +// Property accessors - Time fields +pub fn hour(self: PlainDateTime) u8 { + return abi.c.temporal_rs_PlainDateTime_hour(self._inner); +} + +pub fn minute(self: PlainDateTime) u8 { + return abi.c.temporal_rs_PlainDateTime_minute(self._inner); +} + +pub fn second(self: PlainDateTime) u8 { + return abi.c.temporal_rs_PlainDateTime_second(self._inner); +} + +pub fn millisecond(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_millisecond(self._inner); +} + +pub fn microsecond(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_microsecond(self._inner); +} + +pub fn nanosecond(self: PlainDateTime) u16 { + return abi.c.temporal_rs_PlainDateTime_nanosecond(self._inner); +} + +// Modification methods +pub fn with(self: PlainDateTime, partial: WithOptions) !PlainDateTime { + // Extract fields from partial, or use current values as defaults + const new_year = partial.year orelse self.year(); + const new_month = partial.month orelse self.month(); + const new_day = partial.day orelse self.day(); + const new_hour = partial.hour orelse self.hour(); + const new_minute = partial.minute orelse self.minute(); + const new_second = partial.second orelse self.second(); + const new_millisecond = partial.millisecond orelse self.millisecond(); + const new_microsecond = partial.microsecond orelse self.microsecond(); + const new_nanosecond = partial.nanosecond orelse self.nanosecond(); + + // Preserve calendar + const cal_view = abi.toDiplomatStringView(self.calendar_id); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + const cal_kind = abi.success(cal_result) orelse return error.TemporalError; + + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_try_new( + new_year, + new_month, + new_day, + new_hour, + new_minute, + new_second, + new_millisecond, + new_microsecond, + new_nanosecond, + cal_kind, + )); +} + +pub fn withCalendar(self: PlainDateTime, calendar: []const u8) !PlainDateTime { + const cal_view = abi.toDiplomatStringView(calendar); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + const cal_kind = abi.success(cal_result) orelse return error.TemporalError; + const ptr = abi.c.temporal_rs_PlainDateTime_with_calendar(self._inner, cal_kind) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainDateTime_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return .{ ._inner = ptr, .calendar_id = cal_id }; +} + +pub fn withPlainTime(self: PlainDateTime, time: ?PlainTime) !PlainDateTime { + const new_hour: u8 = if (time) |t| t.hour() else 0; + const new_minute: u8 = if (time) |t| t.minute() else 0; + const new_second: u8 = if (time) |t| t.second() else 0; + const new_millisecond: u16 = if (time) |t| t.millisecond() else 0; + const new_microsecond: u16 = if (time) |t| t.microsecond() else 0; + const new_nanosecond: u16 = if (time) |t| t.nanosecond() else 0; + + const cal_view = abi.toDiplomatStringView(self.calendar_id); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + const cal_kind = abi.success(cal_result) orelse return error.TemporalError; + + return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_try_new( + self.year(), + self.month(), + self.day(), + new_hour, + new_minute, + new_second, + new_millisecond, + new_microsecond, + new_nanosecond, + cal_kind, + )); +} + +// Conversion methods +pub fn toPlainDate(self: PlainDateTime) !PlainDate { + const ptr = abi.c.temporal_rs_PlainDateTime_to_plain_date(self._inner) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainDate_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return .{ ._inner = ptr, .calendar_id = cal_id }; +} + +pub fn toPlainTime(self: PlainDateTime) !PlainTime { + const ptr = abi.c.temporal_rs_PlainDateTime_to_plain_time(self._inner) orelse return error.TemporalError; + return .{ ._inner = ptr }; +} + +pub fn toZonedDateTime(self: PlainDateTime, options: ToZonedDateTimeOptions) !ZonedDateTime { + const tz_view = abi.toDiplomatStringView(options.timeZone); + const tz_result = abi.c.temporal_rs_TimeZone_try_from_str(tz_view); + const time_zone = abi.success(tz_result) orelse return error.TemporalError; + + // Convert to PlainDate and PlainTime + const date = try self.toPlainDate(); + const time = try self.toPlainTime(); + + // Use PlainDate's toZonedDateTime with the time component + const ptr = (abi.success(abi.c.temporal_rs_PlainDate_to_zoned_date_time(date._inner, time_zone, time._inner)) orelse return error.TemporalError) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_ZonedDateTime_calendar(ptr); + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return .{ ._inner = ptr, .calendar_id = cal_id }; +} + +pub fn toString(self: PlainDateTime, allocator: std.mem.Allocator, options: ToStringOptions) ![]u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + const rounding_options = std.mem.zeroes(abi.c.ToStringRoundingOptions); + + const display_cal: abi.c.DisplayCalendar = if (options.calendar_display) |cal| switch (cal) { + .auto => @intCast(abi.c.DisplayCalendar_Auto), + .always => @intCast(abi.c.DisplayCalendar_Always), + .never => @intCast(abi.c.DisplayCalendar_Never), + .critical => @intCast(abi.c.DisplayCalendar_Critical), + } else @intCast(abi.c.DisplayCalendar_Auto); + + const result = abi.c.temporal_rs_PlainDateTime_to_ixdtf_string( + self._inner, + rounding_options, + display_cal, + &write.inner, + ); + + if (!result.is_ok) return error.TemporalError; + return try write.toOwnedSlice(); +} + +pub fn toJSON(self: PlainDateTime, allocator: std.mem.Allocator) ![]u8 { + return try self.toString(allocator, .{}); +} + +pub fn toLocaleString(self: PlainDateTime, allocator: std.mem.Allocator) []const u8 { + const s = self.toString(allocator, .{}) catch return "PlainDateTime.toLocaleString error"; + return s; +} + +pub fn valueOf(self: PlainDateTime) !void { + _ = self; + return error.ComparisonNotSupported; +} + +// Helper functions +fn clone(self: PlainDateTime) PlainDateTime { + return self; +} + +pub fn deinit(self: *PlainDateTime) void { + _ = self; + // The C API manages memory +} + +fn wrapPlainDateTime(res: anytype) !PlainDateTime { + const ptr = (abi.success(res) orelse return error.TemporalError) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainDateTime_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return .{ ._inner = ptr, .calendar_id = cal_id }; } // ---------- Tests --------------------- -test compare { - if (true) return error.Todo; +test init { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 123, 456, 789); + try std.testing.expectEqual(@as(i32, 2024), dt.year()); + try std.testing.expectEqual(@as(u8, 1), dt.month()); + try std.testing.expectEqual(@as(u8, 15), dt.day()); + try std.testing.expectEqual(@as(u8, 14), dt.hour()); + try std.testing.expectEqual(@as(u8, 30), dt.minute()); + try std.testing.expectEqual(@as(u8, 45), dt.second()); } + test from { - if (true) return error.Todo; + const dt = try PlainDateTime.from("2024-01-15T14:30:45"); + try std.testing.expectEqual(@as(i32, 2024), dt.year()); + try std.testing.expectEqual(@as(u8, 1), dt.month()); + try std.testing.expectEqual(@as(u8, 15), dt.day()); } -test add { - if (true) return error.Todo; + +test compare { + const dt1 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const dt2 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const dt3 = try PlainDateTime.init(2024, 1, 16, 14, 30, 0, 0, 0, 0); + + try std.testing.expectEqual(@as(i8, 0), PlainDateTime.compare(dt1, dt2)); + try std.testing.expectEqual(@as(i8, -1), PlainDateTime.compare(dt1, dt3)); + try std.testing.expectEqual(@as(i8, 1), PlainDateTime.compare(dt3, dt1)); } + test equals { - if (true) return error.Todo; + const dt1 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const dt2 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const dt3 = try PlainDateTime.init(2024, 1, 16, 14, 30, 0, 0, 0, 0); + + try std.testing.expect(dt1.equals(dt2)); + try std.testing.expect(!dt1.equals(dt3)); } -test round { - if (true) return error.Todo; -} -test since { - if (true) return error.Todo; + +test add { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const dur = try Duration.from("P1D"); + const result = try dt.add(dur); + + try std.testing.expectEqual(@as(u8, 16), result.day()); } + test subtract { - if (true) return error.Todo; -} -test toJSON { - if (true) return error.Todo; -} -test toLocaleString { - if (true) return error.Todo; -} -test toPlainDate { - if (true) return error.Todo; -} -test toPlainTime { - if (true) return error.Todo; -} -test toString { - if (true) return error.Todo; -} -test toZonedDateTime { - if (true) return error.Todo; + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const dur = try Duration.from("P1D"); + const result = try dt.subtract(dur); + + try std.testing.expectEqual(@as(u8, 14), result.day()); } + test until { - if (true) return error.Todo; + const dt1 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const dt2 = try PlainDateTime.init(2024, 1, 16, 14, 30, 0, 0, 0, 0); + const dur = try dt1.until(dt2, .{}); + + try std.testing.expectEqual(@as(i64, 1), dur.days()); } + +test since { + const dt1 = try PlainDateTime.init(2024, 1, 16, 14, 30, 0, 0, 0, 0); + const dt2 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const dur = try dt1.since(dt2, .{}); + + try std.testing.expectEqual(@as(i64, 1), dur.days()); +} + +test round { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 999, 999, 999); + const rounded = try dt.round(.{ .smallest_unit = .second }); + + try std.testing.expectEqual(@as(u8, 46), rounded.second()); + try std.testing.expectEqual(@as(u16, 0), rounded.millisecond()); + try std.testing.expectEqual(@as(u16, 0), rounded.microsecond()); + try std.testing.expectEqual(@as(u16, 0), rounded.nanosecond()); +} + +test toString { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 123, 0, 0); + const str = try dt.toString(std.testing.allocator, .{}); + defer std.testing.allocator.free(str); + + try std.testing.expect(std.mem.indexOf(u8, str, "2024-01-15") != null); + try std.testing.expect(std.mem.indexOf(u8, str, "14:30:45") != null); +} + +test toJSON { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 0, 0, 0); + const str = try dt.toJSON(std.testing.allocator); + defer std.testing.allocator.free(str); + + try std.testing.expect(std.mem.indexOf(u8, str, "2024-01-15") != null); +} + +test toLocaleString { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 0, 0, 0); + const s_const = dt.toLocaleString(std.testing.allocator); + const s = @constCast(s_const); + defer std.testing.allocator.free(s); + try std.testing.expect(std.mem.indexOf(u8, s, "2024-01-15") != null); +} + +test toPlainDate { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const date = try dt.toPlainDate(); + + try std.testing.expectEqual(@as(i32, 2024), date.year()); + try std.testing.expectEqual(@as(u8, 1), date.month()); + try std.testing.expectEqual(@as(u8, 15), date.day()); +} + +test toPlainTime { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 0, 0, 0); + const t = try dt.toPlainTime(); + try std.testing.expectEqual(@as(u8, 14), t.hour()); + try std.testing.expectEqual(@as(u8, 30), t.minute()); + try std.testing.expectEqual(@as(u8, 45), t.second()); +} + +test toZonedDateTime { + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const zdt = try dt.toZonedDateTime(.{ .timeZone = "UTC" }); + + const pdt = try zdt.toPlainDateTime(); + try std.testing.expectEqual(@as(i32, 2024), pdt.year()); + try std.testing.expectEqual(@as(u8, 1), pdt.month()); + try std.testing.expectEqual(@as(u8, 15), pdt.day()); +} + test with { - if (true) return error.Todo; + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 0, 0, 0); + const result = try dt.with(.{ .day = 20, .hour = 16 }); + try std.testing.expectEqual(@as(i32, 2024), result.year()); + try std.testing.expectEqual(@as(u8, 1), result.month()); + try std.testing.expectEqual(@as(u8, 20), result.day()); + try std.testing.expectEqual(@as(u8, 16), result.hour()); + try std.testing.expectEqual(@as(u8, 30), result.minute()); } + test withCalendar { - if (true) return error.Todo; + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const result = try dt.withCalendar("iso8601"); + + try std.testing.expectEqual(@as(i32, 2024), result.year()); + try std.testing.expectEqualStrings("iso8601", result.calendar_id); } + test withPlainTime { - if (true) return error.Todo; + const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0); + const t = try PlainTime.from("10:15:30"); + const result = try dt.withPlainTime(t); + try std.testing.expectEqual(@as(u8, 10), result.hour()); + try std.testing.expectEqual(@as(u8, 15), result.minute()); + try std.testing.expectEqual(@as(u8, 30), result.second()); } diff --git a/src/PlainMonthDay.zig b/src/PlainMonthDay.zig index daf95ca..14acd4e 100644 --- a/src/PlainMonthDay.zig +++ b/src/PlainMonthDay.zig @@ -1,66 +1,268 @@ const std = @import("std"); +const abi = @import("abi.zig"); +const temporal_types = @import("temporal.zig"); + const PlainMonthDay = @This(); const PlainDate = @import("PlainDate.zig"); +_inner: *abi.c.PlainMonthDay, calendar_id: []const u8, -day: i64, -month_code: []const u8, -pub fn init() error{Todo}!PlainMonthDay { - return error.Todo; +// Type definitions for API compatibility +pub const CalendarDisplay = enum { + auto, + always, + never, + critical, +}; + +pub const ToStringOptions = struct { + calendar_display: ?CalendarDisplay = null, +}; + +pub const WithOptions = struct { + month_code: ?[]const u8 = null, + day: ?u8 = null, +}; + +// Helper to wrap PlainMonthDay pointer +fn wrapPlainMonthDay(result: anytype) !PlainMonthDay { + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainMonthDay_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return .{ ._inner = ptr, .calendar_id = cal_id }; } -pub fn from() error{Todo}!PlainMonthDay { - return error.Todo; +// Constructor +pub fn init(month_val: u8, day_val: u8, calendar: ?[]const u8) !PlainMonthDay { + const cal_kind = if (calendar) |cal| blk: { + const cal_view = abi.toDiplomatStringView(cal); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + break :blk abi.success(cal_result) orelse return error.TemporalError; + } else abi.c.AnyCalendarKind_Iso; + + const overflow = abi.c.ArithmeticOverflow_Constrain; + const ref_year = abi.c.OptionI32{ .is_ok = false }; + + return wrapPlainMonthDay(abi.c.temporal_rs_PlainMonthDay_try_new_with_overflow( + month_val, + day_val, + cal_kind, + overflow, + ref_year, + )); } -pub fn equals() error{Todo}!bool { - return error.Todo; +// Parse from string +pub fn from(s: []const u8) !PlainMonthDay { + return fromUtf8(s); } -pub fn toJSON() error{Todo}![]const u8 { - return error.Todo; +fn fromUtf8(utf8: []const u8) !PlainMonthDay { + const view = abi.toDiplomatStringView(utf8); + return wrapPlainMonthDay(abi.c.temporal_rs_PlainMonthDay_from_utf8(view)); } -pub fn toLocaleString() error{Todo}![]const u8 { - return error.Todo; +fn fromUtf16(utf16: []const u16) !PlainMonthDay { + const view = abi.toDiplomatString16View(utf16); + return wrapPlainMonthDay(abi.c.temporal_rs_PlainMonthDay_from_utf16(view)); } -pub fn toPlainDate() error{Todo}!PlainDate { - return error.Todo; +// Comparison +pub fn equals(self: PlainMonthDay, other: PlainMonthDay) bool { + return abi.c.temporal_rs_PlainMonthDay_equals(self._inner, other._inner); } -pub fn toString() error{Todo}![]const u8 { - return error.Todo; +// Property accessors +pub fn calendarId(self: PlainMonthDay) []const u8 { + return self.calendar_id; } -pub fn valueOf() error{Todo}!void { - return error.Todo; +pub fn day(self: PlainMonthDay) u8 { + return abi.c.temporal_rs_PlainMonthDay_day(self._inner); } -pub fn with() error{Todo}!PlainMonthDay { - return error.Todo; +pub fn monthCode(self: PlainMonthDay, allocator: std.mem.Allocator) ![]const u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + abi.c.temporal_rs_PlainMonthDay_month_code(self._inner, &write.inner); + return try write.toOwnedSlice(); +} + +// Modification +pub fn with(self: PlainMonthDay, options: WithOptions) !PlainMonthDay { + // Build month_code view + const month_code_view = if (options.month_code) |mc| + abi.toDiplomatStringView(mc) + else + abi.c.DiplomatStringView{ .data = null, .len = 0 }; + + // Build partial date with only the fields we want to modify + const partial_date = abi.c.PartialDate{ + .year = .{ .is_ok = false }, + .month = .{ .is_ok = false }, + .day = if (options.day) |d| abi.toOption(abi.c.OptionU8, d) else .{ .is_ok = false }, + .month_code = month_code_view, + .era = .{ .data = null, .len = 0 }, + .era_year = .{ .is_ok = false }, + .calendar = abi.c.AnyCalendarKind_Iso, + }; + + const overflow = abi.c.ArithmeticOverflow_option{ + .is_ok = true, + .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain }, + }; + + return wrapPlainMonthDay(abi.c.temporal_rs_PlainMonthDay_with( + self._inner, + partial_date, + overflow, + )); +} + +// Conversion +pub fn toPlainDate(self: PlainMonthDay, year: i32) !PlainDate { + const partial_date = abi.c.PartialDate_option{ + .is_ok = true, + .unnamed_0 = .{ + .ok = .{ + .year = abi.toOption(abi.c.OptionI32, year), + .month = .{ .is_ok = false }, + .day = .{ .is_ok = false }, + .month_code = .{ .data = null, .len = 0 }, + .era = .{ .data = null, .len = 0 }, + .era_year = .{ .is_ok = false }, + .calendar = abi.c.AnyCalendarKind_Iso, + }, + }, + }; + + const result = abi.c.temporal_rs_PlainMonthDay_to_plain_date(self._inner, partial_date); + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainDate_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return PlainDate{ ._inner = ptr, .calendar_id = cal_id }; +} + +// String conversions +pub fn toString(self: PlainMonthDay, allocator: std.mem.Allocator) ![]u8 { + return toStringWithOptions(self, allocator, .{}); +} + +fn toStringWithOptions(self: PlainMonthDay, allocator: std.mem.Allocator, options: ToStringOptions) ![]u8 { + _ = options; + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + const display = abi.c.DisplayCalendar_Auto; + abi.c.temporal_rs_PlainMonthDay_to_ixdtf_string(self._inner, display, &write.inner); + + return try write.toOwnedSlice(); +} + +pub fn toJSON(self: PlainMonthDay, allocator: std.mem.Allocator) ![]u8 { + return toString(self, allocator); +} + +pub fn toLocaleString(self: PlainMonthDay, allocator: std.mem.Allocator) ![]u8 { + return toString(self, allocator); +} + +pub fn valueOf(self: PlainMonthDay) !void { + _ = self; + return error.ValueError; } // ---------- Tests --------------------- + +test init { + const md = try init(12, 25, null); + try std.testing.expectEqual(@as(u8, 25), md.day()); + const month_code = try md.monthCode(std.testing.allocator); + defer std.testing.allocator.free(month_code); + try std.testing.expect(month_code.len > 0); +} + test from { - if (true) return error.Todo; + const md = try from("12-25"); + try std.testing.expectEqual(@as(u8, 25), md.day()); } + test equals { - if (true) return error.Todo; -} -test toJSON { - if (true) return error.Todo; -} -test toLocaleString { - if (true) return error.Todo; + { + const md1 = try init(12, 25, null); + const md2 = try init(12, 25, null); + try std.testing.expect(md1.equals(md2)); + } + + { + const md1 = try init(12, 25, null); + const md2 = try init(12, 24, null); + try std.testing.expect(!md1.equals(md2)); + } } + test toPlainDate { - if (true) return error.Todo; + const md = try init(12, 25, null); + const date = try md.toPlainDate(2024); + try std.testing.expectEqual(@as(i32, 2024), date.year()); + try std.testing.expectEqual(@as(u8, 25), date.day()); } + test toString { - if (true) return error.Todo; + const md = try init(12, 25, null); + const str = try md.toString(std.testing.allocator); + defer std.testing.allocator.free(str); + + try std.testing.expect(str.len > 0); } + +test toJSON { + const md = try init(12, 25, null); + const str = try md.toJSON(std.testing.allocator); + defer std.testing.allocator.free(str); + + try std.testing.expect(str.len > 0); +} + test with { - if (true) return error.Todo; + // Test modifying day + const md1 = try init(12, 25, null); + const md2 = try md1.with(.{ .day = 31 }); + try std.testing.expectEqual(@as(u8, 31), md2.day()); + + // Test modifying month_code + const md3 = try init(12, 25, null); + const md4 = try md3.with(.{ .month_code = "M01" }); + const month_code = try md4.monthCode(std.testing.allocator); + defer std.testing.allocator.free(month_code); + try std.testing.expectEqualStrings("M01", month_code); + try std.testing.expectEqual(@as(u8, 25), md4.day()); + + // Test modifying both + const md5 = try init(6, 15, null); + const md6 = try md5.with(.{ .month_code = "M12", .day = 1 }); + const month_code2 = try md6.monthCode(std.testing.allocator); + defer std.testing.allocator.free(month_code2); + try std.testing.expectEqualStrings("M12", month_code2); + try std.testing.expectEqual(@as(u8, 1), md6.day()); +} + +test toLocaleString { + const md = try init(12, 25, null); + const str = try md.toLocaleString(std.testing.allocator); + defer std.testing.allocator.free(str); + try std.testing.expect(str.len > 0); + try std.testing.expect(std.mem.indexOf(u8, str, "12") != null or std.mem.indexOf(u8, str, "25") != null); } diff --git a/src/PlainTime.zig b/src/PlainTime.zig index caa4468..069f3fa 100644 --- a/src/PlainTime.zig +++ b/src/PlainTime.zig @@ -1,104 +1,359 @@ const std = @import("std"); +const abi = @import("abi.zig"); +const temporal_types = @import("temporal.zig"); + const PlainTime = @This(); const Duration = @import("Duration.zig"); -hour: i64, -microsecond: i64, -millisecond: i64, -minute: i64, -nanosecond: i64, -second: i64, +_inner: *abi.c.PlainTime, -pub fn init() error{Todo}!PlainTime { - return error.Todo; +// Import types from temporal.zig +pub const Unit = temporal_types.Unit; +pub const RoundingMode = temporal_types.RoundingMode; +pub const DifferenceSettings = temporal_types.DifferenceSettings; +pub const RoundOptions = temporal_types.RoundingOptions; + +pub const WithOptions = struct { + hour: ?u8 = null, + minute: ?u8 = null, + second: ?u8 = null, + millisecond: ?u16 = null, + microsecond: ?u16 = null, + nanosecond: ?u16 = null, +}; + +// Helper to wrap PlainTime pointer +fn wrapPlainTime(result: anytype) !PlainTime { + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + return PlainTime{ ._inner = ptr }; } -pub fn compare() error{Todo}!i8 { - return error.Todo; +// Constructor - creates a PlainTime with all parameters +pub fn init( + hour_val: u8, + minute_val: u8, + second_val: u8, + millisecond_val: u16, + microsecond_val: u16, + nanosecond_val: u16, +) !PlainTime { + return wrapPlainTime(abi.c.temporal_rs_PlainTime_try_new( + hour_val, + minute_val, + second_val, + millisecond_val, + microsecond_val, + nanosecond_val, + )); } -pub fn from() error{Todo}!PlainTime { - return error.Todo; +// Parse from string +pub fn from(s: []const u8) !PlainTime { + return fromUtf8(s); } -pub fn add() error{Todo}!PlainTime { - return error.Todo; +fn fromUtf8(utf8: []const u8) !PlainTime { + const view = abi.toDiplomatStringView(utf8); + return wrapPlainTime(abi.c.temporal_rs_PlainTime_from_utf8(view)); } -pub fn equals() error{Todo}!bool { - return error.Todo; +fn fromUtf16(utf16: []const u16) !PlainTime { + const view = abi.toDiplomatString16View(utf16); + return wrapPlainTime(abi.c.temporal_rs_PlainTime_from_utf16(view)); } -pub fn round() error{Todo}!PlainTime { - return error.Todo; +// Comparison +pub fn compare(a: PlainTime, b: PlainTime) i8 { + return abi.c.temporal_rs_PlainTime_compare(a._inner, b._inner); } -pub fn since() error{Todo}!Duration { - return error.Todo; +pub fn equals(self: PlainTime, other: PlainTime) bool { + return abi.c.temporal_rs_PlainTime_equals(self._inner, other._inner); } -pub fn subtract() error{Todo}!PlainTime { - return error.Todo; +// Arithmetic +pub fn add(self: PlainTime, duration: Duration) !PlainTime { + return wrapPlainTime(abi.c.temporal_rs_PlainTime_add(self._inner, duration._inner)); } -pub fn toJSON() error{Todo}![]const u8 { - return error.Todo; +pub fn subtract(self: PlainTime, duration: Duration) !PlainTime { + return wrapPlainTime(abi.c.temporal_rs_PlainTime_subtract(self._inner, duration._inner)); } -pub fn toLocaleString() error{Todo}![]const u8 { - return error.Todo; +pub fn until(self: PlainTime, other: PlainTime, options: DifferenceSettings) !Duration { + const settings = options.toCApi(); + const result = abi.c.temporal_rs_PlainTime_until(self._inner, other._inner, settings); + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn toString() error{Todo}![]const u8 { - return error.Todo; +pub fn since(self: PlainTime, other: PlainTime, options: DifferenceSettings) !Duration { + const settings = options.toCApi(); + const result = abi.c.temporal_rs_PlainTime_since(self._inner, other._inner, settings); + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn until() error{Todo}!Duration { - return error.Todo; +// Rounding +pub fn round(self: PlainTime, options: RoundOptions) !PlainTime { + return wrapPlainTime(abi.c.temporal_rs_PlainTime_round(self._inner, options.toCApi())); } -pub fn valueOf() error{Todo}!void { - return error.Todo; +// Property accessors +pub fn hour(self: PlainTime) u8 { + return abi.c.temporal_rs_PlainTime_hour(self._inner); } -pub fn with() error{Todo}!PlainTime { - return error.Todo; +pub fn minute(self: PlainTime) u8 { + return abi.c.temporal_rs_PlainTime_minute(self._inner); +} + +pub fn second(self: PlainTime) u8 { + return abi.c.temporal_rs_PlainTime_second(self._inner); +} + +pub fn millisecond(self: PlainTime) u16 { + return abi.c.temporal_rs_PlainTime_millisecond(self._inner); +} + +pub fn microsecond(self: PlainTime) u16 { + return abi.c.temporal_rs_PlainTime_microsecond(self._inner); +} + +pub fn nanosecond(self: PlainTime) u16 { + return abi.c.temporal_rs_PlainTime_nanosecond(self._inner); +} + +// Modification +pub fn with(self: PlainTime, options: WithOptions) !PlainTime { + const partial = abi.c.PartialTime{ + .hour = abi.toOption(abi.c.OptionU8, options.hour), + .minute = abi.toOption(abi.c.OptionU8, options.minute), + .second = abi.toOption(abi.c.OptionU8, options.second), + .millisecond = abi.toOption(abi.c.OptionU16, options.millisecond), + .microsecond = abi.toOption(abi.c.OptionU16, options.microsecond), + .nanosecond = abi.toOption(abi.c.OptionU16, options.nanosecond), + }; + const overflow_opt = abi.c.ArithmeticOverflow_option{ + .is_ok = true, + .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain }, + }; + return wrapPlainTime(abi.c.temporal_rs_PlainTime_with(self._inner, partial, overflow_opt)); +} + +// String conversions +pub fn toString(self: PlainTime, allocator: std.mem.Allocator) ![]u8 { + return toStringWithOptions(self, allocator, .{}); +} + +fn toStringWithOptions(self: PlainTime, allocator: std.mem.Allocator, options: temporal_types.ToStringRoundingOptions) ![]u8 { + const rounding_opts = options.toCApi(); + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + const result = abi.c.temporal_rs_PlainTime_to_ixdtf_string( + self._inner, + rounding_opts, + &write.inner, + ); + + if (!result.is_ok) return error.TemporalError; + + return try write.toOwnedSlice(); +} + +pub fn toJSON(self: PlainTime, allocator: std.mem.Allocator) ![]u8 { + return toString(self, allocator); +} + +pub fn toLocaleString(self: PlainTime, allocator: std.mem.Allocator) ![]u8 { + // For now, just use toString - locale-specific formatting would require more work + return toString(self, allocator); +} + +pub fn valueOf(self: PlainTime) !void { + _ = self; + // PlainTime should not be used in arithmetic/comparison operations implicitly + return error.ValueError; } // ---------- Tests --------------------- -test compare { - if (true) return error.Todo; + +test init { + { + const time = try init(14, 30, 45, 123, 456, 789); + try std.testing.expectEqual(@as(u8, 14), time.hour()); + try std.testing.expectEqual(@as(u8, 30), time.minute()); + try std.testing.expectEqual(@as(u8, 45), time.second()); + try std.testing.expectEqual(@as(u16, 123), time.millisecond()); + try std.testing.expectEqual(@as(u16, 456), time.microsecond()); + try std.testing.expectEqual(@as(u16, 789), time.nanosecond()); + } + { + const time = try init(0, 0, 0, 0, 0, 0); + try std.testing.expectEqual(@as(u8, 0), time.hour()); + try std.testing.expectEqual(@as(u8, 0), time.minute()); + try std.testing.expectEqual(@as(u8, 0), time.second()); + } + { + const time = try init(23, 59, 59, 999, 999, 999); + try std.testing.expectEqual(@as(u8, 23), time.hour()); + try std.testing.expectEqual(@as(u8, 59), time.minute()); + try std.testing.expectEqual(@as(u8, 59), time.second()); + } } + test from { - if (true) return error.Todo; + { + const time = try from("14:30:45.123456789"); + try std.testing.expectEqual(@as(u8, 14), time.hour()); + try std.testing.expectEqual(@as(u8, 30), time.minute()); + try std.testing.expectEqual(@as(u8, 45), time.second()); + try std.testing.expectEqual(@as(u16, 123), time.millisecond()); + try std.testing.expectEqual(@as(u16, 456), time.microsecond()); + try std.testing.expectEqual(@as(u16, 789), time.nanosecond()); + } + { + const time = try from("14:30"); + try std.testing.expectEqual(@as(u8, 14), time.hour()); + try std.testing.expectEqual(@as(u8, 30), time.minute()); + try std.testing.expectEqual(@as(u8, 0), time.second()); + } + + { + const time = try from("14:30:45"); + try std.testing.expectEqual(@as(u8, 14), time.hour()); + try std.testing.expectEqual(@as(u8, 30), time.minute()); + try std.testing.expectEqual(@as(u8, 45), time.second()); + } } -test add { - if (true) return error.Todo; + +test compare { + { + const time1 = try init(14, 30, 45, 123, 456, 789); + const time2 = try init(14, 30, 45, 123, 456, 789); + try std.testing.expectEqual(@as(i8, 0), compare(time1, time2)); + } + { + const time1 = try init(14, 30, 45, 123, 456, 789); + const time2 = try init(15, 30, 45, 123, 456, 789); + try std.testing.expectEqual(@as(i8, -1), compare(time1, time2)); + } + { + const time1 = try init(15, 30, 45, 123, 456, 789); + const time2 = try init(14, 30, 45, 123, 456, 789); + try std.testing.expectEqual(@as(i8, 1), compare(time1, time2)); + } } + test equals { - if (true) return error.Todo; + { + const time1 = try init(14, 30, 45, 123, 456, 789); + const time2 = try init(14, 30, 45, 123, 456, 789); + try std.testing.expect(time1.equals(time2)); + } + { + const time1 = try init(14, 30, 45, 123, 456, 789); + const time2 = try init(15, 30, 45, 123, 456, 789); + try std.testing.expect(!time1.equals(time2)); + } } -test round { - if (true) return error.Todo; -} -test since { - if (true) return error.Todo; + +test add { + { + const time = try init(14, 30, 0, 0, 0, 0); + const duration = try Duration.from("PT1H30M"); + const result = try time.add(duration); + try std.testing.expectEqual(@as(u8, 16), result.hour()); + try std.testing.expectEqual(@as(u8, 0), result.minute()); + } + { + const time = try init(23, 30, 0, 0, 0, 0); + const duration = try Duration.from("PT1H"); + const result = try time.add(duration); + try std.testing.expectEqual(@as(u8, 0), result.hour()); + try std.testing.expectEqual(@as(u8, 30), result.minute()); + } } + test subtract { - if (true) return error.Todo; -} -test toJSON { - if (true) return error.Todo; -} -test toLocaleString { - if (true) return error.Todo; -} -test toString { - if (true) return error.Todo; -} -test until { - if (true) return error.Todo; + { + const time = try init(16, 0, 0, 0, 0, 0); + const duration = try Duration.from("PT1H30M"); + const result = try time.subtract(duration); + try std.testing.expectEqual(@as(u8, 14), result.hour()); + try std.testing.expectEqual(@as(u8, 30), result.minute()); + } + { + const time = try init(0, 30, 0, 0, 0, 0); + const duration = try Duration.from("PT1H"); + const result = try time.subtract(duration); + try std.testing.expectEqual(@as(u8, 23), result.hour()); + try std.testing.expectEqual(@as(u8, 30), result.minute()); + } } + test with { - if (true) return error.Todo; + { + const time = try init(14, 30, 45, 123, 456, 789); + const result = try time.with(.{ .hour = 18 }); + try std.testing.expectEqual(@as(u8, 18), result.hour()); + try std.testing.expectEqual(@as(u8, 30), result.minute()); + try std.testing.expectEqual(@as(u8, 45), result.second()); + } + + { + const time = try init(14, 30, 45, 123, 456, 789); + const result = try time.with(.{ .hour = 18, .minute = 15, .second = 0 }); + try std.testing.expectEqual(@as(u8, 18), result.hour()); + try std.testing.expectEqual(@as(u8, 15), result.minute()); + try std.testing.expectEqual(@as(u8, 0), result.second()); + } +} + +test toString { + const time = try init(14, 30, 45, 123, 456, 789); + const str = try time.toString(std.testing.allocator); + defer std.testing.allocator.free(str); + + // Should be in format: 14:30:45.123456789 + try std.testing.expect(str.len > 0); + try std.testing.expect(std.mem.indexOf(u8, str, "14:30:45") != null); +} + +test toJSON { + const time = try init(14, 30, 45, 123, 456, 789); + const str = try time.toJSON(std.testing.allocator); + defer std.testing.allocator.free(str); + try std.testing.expect(str.len > 0); +} + +test round { + const time = try init(14, 30, 45, 999, 999, 999); + const rounded = try time.round(.{ .smallest_unit = .second }); + + try std.testing.expectEqual(@as(u8, 14), rounded.hour()); + try std.testing.expectEqual(@as(u8, 30), rounded.minute()); + try std.testing.expectEqual(@as(u8, 46), rounded.second()); + try std.testing.expectEqual(@as(u16, 0), rounded.millisecond()); + try std.testing.expectEqual(@as(u16, 0), rounded.microsecond()); + try std.testing.expectEqual(@as(u16, 0), rounded.nanosecond()); +} + +test since { + const time1 = try init(15, 30, 0, 0, 0, 0); + const time2 = try init(14, 0, 0, 0, 0, 0); + const dur = try time1.since(time2, .{}); + + try std.testing.expectEqual(@as(i64, 1), dur.hours()); + try std.testing.expectEqual(@as(i64, 30), dur.minutes()); +} + +test until { + const time1 = try init(14, 0, 0, 0, 0, 0); + const time2 = try init(15, 30, 0, 0, 0, 0); + const dur = try time1.until(time2, .{}); + + try std.testing.expectEqual(@as(i64, 1), dur.hours()); + try std.testing.expectEqual(@as(i64, 30), dur.minutes()); } diff --git a/src/PlainYearMonth.zig b/src/PlainYearMonth.zig index 1cff0af..923d7ca 100644 --- a/src/PlainYearMonth.zig +++ b/src/PlainYearMonth.zig @@ -1,109 +1,418 @@ const std = @import("std"); +const abi = @import("abi.zig"); +const temporal_types = @import("temporal.zig"); + const PlainYearMonth = @This(); const PlainDate = @import("PlainDate.zig"); const Duration = @import("Duration.zig"); +_inner: *abi.c.PlainYearMonth, calendar_id: []const u8, -days_in_month: i64, -days_in_year: i64, -era: []const u8, -era_year: i64, -in_leap_year: bool, -month: i64, -month_code: []const u8, -months_in_year: i64, -year: i64, -pub fn init() error{Todo}!PlainYearMonth { - return error.Todo; +// Import types from temporal.zig +pub const Unit = temporal_types.Unit; +pub const RoundingMode = temporal_types.RoundingMode; +pub const DifferenceSettings = temporal_types.DifferenceSettings; +pub const RoundOptions = temporal_types.RoundingOptions; + +// Type definitions for API compatibility +pub const CalendarDisplay = enum { + auto, + always, + never, + critical, +}; + +pub const ToStringOptions = struct { + calendar_display: ?CalendarDisplay = null, +}; + +pub const WithOptions = struct { + year: ?i32 = null, + month: ?u8 = null, + month_code: ?[]const u8 = null, +}; + +// Helper to wrap PlainYearMonth pointer +fn wrapPlainYearMonth(result: anytype) !PlainYearMonth { + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainYearMonth_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return .{ ._inner = ptr, .calendar_id = cal_id }; } -pub fn compare() error{Todo}!i8 { - return error.Todo; +// Constructor +pub fn init(year_val: i32, month_val: u8, calendar: ?[]const u8) !PlainYearMonth { + const cal_kind = if (calendar) |cal| blk: { + const cal_view = abi.toDiplomatStringView(cal); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + break :blk abi.success(cal_result) orelse return error.TemporalError; + } else abi.c.AnyCalendarKind_Iso; + + const overflow = abi.c.ArithmeticOverflow_Constrain; + const ref_day = abi.c.OptionU8{ .is_ok = false }; + + return wrapPlainYearMonth(abi.c.temporal_rs_PlainYearMonth_try_new_with_overflow( + year_val, + month_val, + ref_day, + cal_kind, + overflow, + )); } -pub fn from() error{Todo}!PlainYearMonth { - return error.Todo; +// Parse from string +pub fn from(s: []const u8) !PlainYearMonth { + return fromUtf8(s); } -pub fn add() error{Todo}!PlainYearMonth { - return error.Todo; +fn fromUtf8(utf8: []const u8) !PlainYearMonth { + const view = abi.toDiplomatStringView(utf8); + return wrapPlainYearMonth(abi.c.temporal_rs_PlainYearMonth_from_utf8(view)); } -pub fn equals() error{Todo}!bool { - return error.Todo; +fn fromUtf16(utf16: []const u16) !PlainYearMonth { + const view = abi.toDiplomatString16View(utf16); + return wrapPlainYearMonth(abi.c.temporal_rs_PlainYearMonth_from_utf16(view)); } -pub fn since() error{Todo}!Duration { - return error.Todo; +// Comparison +pub fn compare(a: PlainYearMonth, b: PlainYearMonth) i8 { + return abi.c.temporal_rs_PlainYearMonth_compare(a._inner, b._inner); } -pub fn subtract() error{Todo}!PlainYearMonth { - return error.Todo; +pub fn equals(self: PlainYearMonth, other: PlainYearMonth) bool { + return abi.c.temporal_rs_PlainYearMonth_equals(self._inner, other._inner); } -pub fn toJSON() error{Todo}![]const u8 { - return error.Todo; +// Arithmetic +pub fn add(self: PlainYearMonth, duration: Duration) !PlainYearMonth { + const overflow = abi.c.ArithmeticOverflow_Constrain; + return wrapPlainYearMonth(abi.c.temporal_rs_PlainYearMonth_add(self._inner, duration._inner, overflow)); } -pub fn toLocaleString() error{Todo}![]const u8 { - return error.Todo; +pub fn subtract(self: PlainYearMonth, duration: Duration) !PlainYearMonth { + const overflow = abi.c.ArithmeticOverflow_Constrain; + return wrapPlainYearMonth(abi.c.temporal_rs_PlainYearMonth_subtract(self._inner, duration._inner, overflow)); } -pub fn toPlainDate() error{Todo}!PlainDate { - return error.Todo; +pub fn until(self: PlainYearMonth, other: PlainYearMonth, options: DifferenceSettings) !Duration { + const settings = options.toCApi(); + const result = abi.c.temporal_rs_PlainYearMonth_until(self._inner, other._inner, settings); + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn toString() error{Todo}![]const u8 { - return error.Todo; +pub fn since(self: PlainYearMonth, other: PlainYearMonth, options: DifferenceSettings) !Duration { + const settings = options.toCApi(); + const result = abi.c.temporal_rs_PlainYearMonth_since(self._inner, other._inner, settings); + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn until() error{Todo}!Duration { - return error.Todo; +// Property accessors +pub fn calendarId(self: PlainYearMonth) []const u8 { + return self.calendar_id; } -pub fn valueOf() error{Todo}!void { - return error.Todo; +pub fn year(self: PlainYearMonth) i32 { + return abi.c.temporal_rs_PlainYearMonth_year(self._inner); } -pub fn with() error{Todo}!PlainYearMonth { - return error.Todo; +pub fn month(self: PlainYearMonth) u8 { + return abi.c.temporal_rs_PlainYearMonth_month(self._inner); +} + +pub fn monthCode(self: PlainYearMonth) ![]const u8 { + const allocator = std.heap.page_allocator; + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + abi.c.temporal_rs_PlainYearMonth_month_code(self._inner, &write.inner); + return try write.toOwnedSlice(); +} + +pub fn daysInMonth(self: PlainYearMonth) u16 { + return abi.c.temporal_rs_PlainYearMonth_days_in_month(self._inner); +} + +pub fn daysInYear(self: PlainYearMonth) u16 { + return abi.c.temporal_rs_PlainYearMonth_days_in_year(self._inner); +} + +pub fn monthsInYear(self: PlainYearMonth) u16 { + return abi.c.temporal_rs_PlainYearMonth_months_in_year(self._inner); +} + +pub fn inLeapYear(self: PlainYearMonth) bool { + return abi.c.temporal_rs_PlainYearMonth_in_leap_year(self._inner); +} + +pub fn era(self: PlainYearMonth) !?[]const u8 { + const allocator = std.heap.page_allocator; + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + abi.c.temporal_rs_PlainYearMonth_era(self._inner, &write.inner); + const result = try write.toOwnedSlice(); + if (result.len == 0) return null; + return result; +} + +pub fn eraYear(self: PlainYearMonth) ?i32 { + const result = abi.c.temporal_rs_PlainYearMonth_era_year(self._inner); + if (!result.is_ok) return null; + return result.unnamed_0.ok; +} + +// Modification +pub fn with(self: PlainYearMonth, options: WithOptions) !PlainYearMonth { + // Build month_code view + const month_code_view = if (options.month_code) |mc| + abi.toDiplomatStringView(mc) + else + abi.c.DiplomatStringView{ .data = null, .len = 0 }; + + // Build partial date with only the fields we want to modify + const partial_date = abi.c.PartialDate{ + .year = if (options.year) |y| abi.toOption(abi.c.OptionI32, y) else .{ .is_ok = false }, + .month = if (options.month) |m| abi.toOption(abi.c.OptionU8, m) else .{ .is_ok = false }, + .day = .{ .is_ok = false }, + .month_code = month_code_view, + .era = .{ .data = null, .len = 0 }, + .era_year = .{ .is_ok = false }, + .calendar = abi.c.AnyCalendarKind_Iso, + }; + + const overflow = abi.c.ArithmeticOverflow_option{ + .is_ok = true, + .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain }, + }; + + return wrapPlainYearMonth(abi.c.temporal_rs_PlainYearMonth_with( + self._inner, + partial_date, + overflow, + )); +} + +// Conversion +pub fn toPlainDate(self: PlainYearMonth, day: u8) !PlainDate { + const partial_date = abi.c.PartialDate_option{ + .is_ok = true, + .unnamed_0 = .{ + .ok = .{ + .year = .{ .is_ok = false }, + .month = .{ .is_ok = false }, + .day = abi.toOption(abi.c.OptionU8, day), + .month_code = .{ .data = null, .len = 0 }, + .era = .{ .data = null, .len = 0 }, + .era_year = .{ .is_ok = false }, + .calendar = abi.c.AnyCalendarKind_Iso, + }, + }, + }; + + const result = abi.c.temporal_rs_PlainYearMonth_to_plain_date(self._inner, partial_date); + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + + const calendar_ptr = abi.c.temporal_rs_PlainDate_calendar(ptr) orelse return error.TemporalError; + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + + const allocator = std.heap.page_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + + return PlainDate{ ._inner = ptr, .calendar_id = cal_id }; +} + +// String conversions +pub fn toString(self: PlainYearMonth, allocator: std.mem.Allocator) ![]u8 { + return toStringWithOptions(self, allocator, .{}); +} + +fn toStringWithOptions(self: PlainYearMonth, allocator: std.mem.Allocator, options: ToStringOptions) ![]u8 { + _ = options; + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + const display = abi.c.DisplayCalendar_Auto; + abi.c.temporal_rs_PlainYearMonth_to_ixdtf_string(self._inner, display, &write.inner); + + return try write.toOwnedSlice(); +} + +pub fn toJSON(self: PlainYearMonth, allocator: std.mem.Allocator) ![]u8 { + return toString(self, allocator); +} + +pub fn toLocaleString(self: PlainYearMonth, allocator: std.mem.Allocator) ![]u8 { + return toString(self, allocator); +} + +pub fn valueOf(self: PlainYearMonth) !void { + _ = self; + return error.ValueError; } // ---------- Tests --------------------- -test compare { - if (true) return error.Todo; + +test init { + const ym = try init(2024, 12, null); + try std.testing.expectEqual(@as(i32, 2024), ym.year()); + try std.testing.expectEqual(@as(u8, 12), ym.month()); } + test from { - if (true) return error.Todo; + const ym = try from("2024-12"); + try std.testing.expectEqual(@as(i32, 2024), ym.year()); + try std.testing.expectEqual(@as(u8, 12), ym.month()); } -test add { - if (true) return error.Todo; + +test compare { + { + const ym1 = try init(2024, 12, null); + const ym2 = try init(2024, 12, null); + try std.testing.expectEqual(@as(i8, 0), compare(ym1, ym2)); + } + { + const ym1 = try init(2024, 11, null); + const ym2 = try init(2024, 12, null); + try std.testing.expectEqual(@as(i8, -1), compare(ym1, ym2)); + } + + { + const ym1 = try init(2025, 1, null); + const ym2 = try init(2024, 12, null); + try std.testing.expectEqual(@as(i8, 1), compare(ym1, ym2)); + } + + { + const ym1 = try init(2024, 12, null); + const ym2 = try init(2024, 12, null); + try std.testing.expect(ym1.equals(ym2)); + } } + test equals { - if (true) return error.Todo; + { + const ym1 = try init(2024, 12, null); + const ym2 = try init(2024, 12, null); + try std.testing.expect(ym1.equals(ym2)); + } + + { + const ym1 = try init(2024, 12, null); + const ym2 = try init(2024, 11, null); + try std.testing.expect(!ym1.equals(ym2)); + } } -test since { - if (true) return error.Todo; + +test add { + const ym = try init(2024, 6, null); + const duration = try Duration.from("P6M"); + const result = try ym.add(duration); + try std.testing.expectEqual(@as(i32, 2024), result.year()); + try std.testing.expectEqual(@as(u8, 12), result.month()); } + test subtract { - if (true) return error.Todo; + const ym = try init(2024, 12, null); + const duration = try Duration.from("P6M"); + const result = try ym.subtract(duration); + try std.testing.expectEqual(@as(i32, 2024), result.year()); + try std.testing.expectEqual(@as(u8, 6), result.month()); } -test toJSON { - if (true) return error.Todo; + +test daysInMonth { + const ym = try init(2024, 2, null); + try std.testing.expectEqual(@as(u16, 29), ym.daysInMonth()); // 2024 is a leap year } -test toLocaleString { - if (true) return error.Todo; + +test daysInYear { + const ym = try init(2024, 1, null); + try std.testing.expectEqual(@as(u16, 366), ym.daysInYear()); // 2024 is a leap year } + +test monthsInYear { + const ym = try init(2024, 1, null); + try std.testing.expectEqual(@as(u16, 12), ym.monthsInYear()); +} + +test inLeapYear { + const ym2024 = try init(2024, 1, null); + const ym2023 = try init(2023, 1, null); + try std.testing.expect(ym2024.inLeapYear()); + try std.testing.expect(!ym2023.inLeapYear()); +} + test toPlainDate { - if (true) return error.Todo; + const ym = try init(2024, 12, null); + const date = try ym.toPlainDate(25); + try std.testing.expectEqual(@as(i32, 2024), date.year()); + try std.testing.expectEqual(@as(u8, 12), date.month()); + try std.testing.expectEqual(@as(u8, 25), date.day()); } + test toString { - if (true) return error.Todo; + const ym = try init(2024, 12, null); + const str = try ym.toString(std.testing.allocator); + defer std.testing.allocator.free(str); + try std.testing.expect(str.len > 0); + try std.testing.expect(std.mem.indexOf(u8, str, "2024") != null); } + +test toJSON { + const ym = try init(2024, 12, null); + const str = try ym.toJSON(std.testing.allocator); + defer std.testing.allocator.free(str); + try std.testing.expect(str.len > 0); +} + +test since { + const ym1 = try init(2024, 12, null); + const ym2 = try init(2024, 6, null); + const dur = try ym1.since(ym2, .{}); + + try std.testing.expectEqual(@as(i64, 6), dur.months()); +} + test until { - if (true) return error.Todo; + const ym1 = try init(2024, 6, null); + const ym2 = try init(2024, 12, null); + const dur = try ym1.until(ym2, .{}); + + try std.testing.expectEqual(@as(i64, 6), dur.months()); } + test with { - if (true) return error.Todo; + // Test modifying year + const ym1 = try init(2024, 6, null); + const ym2 = try ym1.with(.{ .year = 2025 }); + try std.testing.expectEqual(@as(i32, 2025), ym2.year()); + try std.testing.expectEqual(@as(u8, 6), ym2.month()); + + // Test modifying month + const ym3 = try init(2024, 6, null); + const ym4 = try ym3.with(.{ .month = 12 }); + try std.testing.expectEqual(@as(i32, 2024), ym4.year()); + try std.testing.expectEqual(@as(u8, 12), ym4.month()); + + // Test modifying both + const ym5 = try init(2024, 6, null); + const ym6 = try ym5.with(.{ .year = 2025, .month = 1 }); + try std.testing.expectEqual(@as(i32, 2025), ym6.year()); + try std.testing.expectEqual(@as(u8, 1), ym6.month()); +} + +test toLocaleString { + const ym = try init(2024, 6, null); + const str = try ym.toLocaleString(std.testing.allocator); + defer std.testing.allocator.free(str); + try std.testing.expect(str.len > 0); + try std.testing.expect(std.mem.indexOf(u8, str, "2024") != null); } diff --git a/src/ZonedDateTime.zig b/src/ZonedDateTime.zig index 4efda94..7d2631f 100644 --- a/src/ZonedDateTime.zig +++ b/src/ZonedDateTime.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const abi = @import("abi.zig"); +const temporal_types = @import("temporal.zig"); const Instant = @import("Instant.zig"); const PlainDate = @import("PlainDate.zig"); @@ -8,188 +10,582 @@ const Duration = @import("Duration.zig"); const ZonedDateTime = @This(); +_inner: *abi.c.ZonedDateTime, calendar_id: []const u8, -day: i64, -day_of_week: i64, -day_of_year: i64, -days_in_month: i64, -days_in_week: i64, -days_in_year: i64, -epoch_milliseconds: i64, -epoch_nanoseconds: i128, -era: []const u8, -era_year: i64, -hour: i64, -hours_in_day: i64, -in_leap_year: bool, -microsecond: i64, -millisecond: i64, -minute: i64, -month: i64, -month_code: []const u8, -months_in_year: i64, -nanosecond: i64, -offset: []const u8, -offset_nanoseconds: i64, -second: i64, -time_zone_id: []const u8, -week_of_year: i64, -year: i64, -year_of_week: i64, -pub fn init() error{Todo}!ZonedDateTime { - return error.Todo; +// Import types from temporal.zig +pub const Unit = temporal_types.Unit; +pub const RoundingMode = temporal_types.RoundingMode; +pub const Sign = temporal_types.Sign; +pub const DifferenceSettings = temporal_types.DifferenceSettings; +pub const RoundOptions = temporal_types.RoundingOptions; + +pub const TimeZone = struct { + _inner: abi.c.TimeZone, + + pub fn init(id: []const u8) !TimeZone { + const view = abi.toDiplomatStringView(id); + const result = abi.c.temporal_rs_TimeZone_try_from_str(view); + const tz = abi.success(result) orelse return error.TemporalError; + return .{ ._inner = tz }; + } + + fn toCApi(self: TimeZone) abi.c.TimeZone { + return self._inner; + } +}; + +pub const Disambiguation = enum { + compatible, + earlier, + later, + reject, + + fn toCApi(self: Disambiguation) abi.c.Disambiguation { + return switch (self) { + .compatible => abi.c.Disambiguation_Compatible, + .earlier => abi.c.Disambiguation_Earlier, + .later => abi.c.Disambiguation_Later, + .reject => abi.c.Disambiguation_Reject, + }; + } +}; + +pub const OffsetDisambiguation = enum { + use_offset, + prefer_offset, + ignore_offset, + reject, + + fn toCApi(self: OffsetDisambiguation) abi.c.OffsetDisambiguation { + return switch (self) { + .use_offset => abi.c.OffsetDisambiguation_Use, + .prefer_offset => abi.c.OffsetDisambiguation_Prefer, + .ignore_offset => abi.c.OffsetDisambiguation_Ignore, + .reject => abi.c.OffsetDisambiguation_Reject, + }; + } +}; + +pub const CalendarDisplay = enum { + auto, + always, + never, + critical, + + fn toCApi(self: CalendarDisplay) abi.c.DisplayCalendar { + return switch (self) { + .auto => abi.c.DisplayCalendar_Auto, + .always => abi.c.DisplayCalendar_Always, + .never => abi.c.DisplayCalendar_Never, + .critical => abi.c.DisplayCalendar_Critical, + }; + } +}; + +pub const DisplayOffset = enum { + auto, + never, + + fn toCApi(self: DisplayOffset) abi.c.DisplayOffset { + return switch (self) { + .auto => abi.c.DisplayOffset_Auto, + .never => abi.c.DisplayOffset_Never, + }; + } +}; + +pub const DisplayTimeZone = enum { + auto, + never, + critical, + + fn toCApi(self: DisplayTimeZone) abi.c.DisplayTimeZone { + return switch (self) { + .auto => abi.c.DisplayTimeZone_Auto, + .never => abi.c.DisplayTimeZone_Never, + .critical => abi.c.DisplayTimeZone_Critical, + }; + } +}; + +pub const ToStringOptions = struct { + fractional_second_digits: ?u8 = null, + smallest_unit: ?Unit = null, + rounding_mode: ?RoundingMode = null, + calendar_display: CalendarDisplay = .auto, + offset_display: DisplayOffset = .auto, + time_zone_name: DisplayTimeZone = .auto, +}; + +/// Helper function to wrap a C API result into a ZonedDateTime +fn wrapZonedDateTime(result: anytype) !ZonedDateTime { + const ptr = (abi.success(result) orelse return error.TemporalError) orelse return error.TemporalError; + const calendar_ptr = abi.c.temporal_rs_ZonedDateTime_calendar(ptr); + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + const allocator = std.heap.c_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + return .{ ._inner = ptr, .calendar_id = cal_id }; } -pub fn compare() error{Todo}!i8 { - return error.Todo; +/// Create a ZonedDateTime from epoch nanoseconds +pub fn init(epoch_ns: i128, time_zone: TimeZone) !ZonedDateTime { + const ns_parts = abi.toI128Nanoseconds(epoch_ns); + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_from_epoch_nanoseconds(ns_parts, time_zone.toCApi())); } -pub fn from() error{Todo}!ZonedDateTime { - return error.Todo; +/// Create from epoch milliseconds +pub fn fromEpochMilliseconds(epoch_ms: i64, time_zone: TimeZone) !ZonedDateTime { + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_from_epoch_milliseconds(epoch_ms, time_zone.toCApi())); } -pub fn add() error{Todo}!ZonedDateTime { - return error.Todo; +/// Create from epoch nanoseconds +pub fn fromEpochNanoseconds(epoch_ns: i128, time_zone: TimeZone) !ZonedDateTime { + const ns_parts = abi.toI128Nanoseconds(epoch_ns); + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_from_epoch_nanoseconds(ns_parts, time_zone.toCApi())); } -pub fn equals() error{Todo}!bool { - return error.Todo; +/// Parse from string +pub fn from(s: []const u8, time_zone: ?TimeZone, disambiguation: Disambiguation, offset_disambiguation: OffsetDisambiguation) !ZonedDateTime { + _ = time_zone; // The time zone is parsed from the string + const view = abi.toDiplomatStringView(s); + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_from_utf8(view, disambiguation.toCApi(), offset_disambiguation.toCApi())); } -pub fn getTimeZoneTransition() error{Todo}!?Instant { - return error.Todo; +/// Compare two ZonedDateTime instances +pub fn compare(a: ZonedDateTime, b: ZonedDateTime) i8 { + return abi.c.temporal_rs_ZonedDateTime_compare_instant(a._inner, b._inner); } -pub fn round() error{Todo}!ZonedDateTime { - return error.Todo; +/// Add a duration +pub fn add(self: ZonedDateTime, duration: Duration) !ZonedDateTime { + const overflow_opt = abi.toOption(abi.c.ArithmeticOverflow_option, null); + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_add(self._inner, duration._inner, overflow_opt)); } -pub fn since() error{Todo}!Duration { - return error.Todo; +/// Check equality +pub fn equals(self: ZonedDateTime, other: ZonedDateTime) bool { + return abi.c.temporal_rs_ZonedDateTime_equals(self._inner, other._inner); } -pub fn startOfDay() error{Todo}!ZonedDateTime { - return error.Todo; +/// Get the time zone transition +pub fn getTimeZoneTransition(self: ZonedDateTime, direction: enum { next, previous }) !?ZonedDateTime { + const dir = switch (direction) { + .next => abi.c.TransitionDirection_Next, + .previous => abi.c.TransitionDirection_Previous, + }; + const result = abi.c.temporal_rs_ZonedDateTime_get_time_zone_transition(self._inner, dir); + const maybe_ptr = abi.success(result) orelse return error.TemporalError; + if (maybe_ptr) |ptr| { + const calendar_ptr = abi.c.temporal_rs_ZonedDateTime_calendar(ptr); + const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr); + const allocator = std.heap.c_allocator; + const cal_id = allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]) catch "iso8601"; + return .{ ._inner = ptr, .calendar_id = cal_id }; + } + return null; } -pub fn subtract() error{Todo}!ZonedDateTime { - return error.Todo; +/// Round to the given options +pub fn round(self: ZonedDateTime, options: RoundOptions) !ZonedDateTime { + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_round(self._inner, options.toCApi())); } -pub fn toInstant() error{Todo}!Instant { - return error.Todo; +/// Calculate duration since another ZonedDateTime +pub fn since(self: ZonedDateTime, other: ZonedDateTime, settings: DifferenceSettings) !Duration { + const ptr = abi.success(abi.c.temporal_rs_ZonedDateTime_since(self._inner, other._inner, settings.toCApi())) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn toJSON() error{Todo}![]const u8 { - return error.Todo; +/// Get the start of the day +pub fn startOfDay(self: ZonedDateTime) !ZonedDateTime { + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_start_of_day(self._inner)); } -pub fn toLocaleString() error{Todo}![]const u8 { - return error.Todo; +/// Subtract a duration +pub fn subtract(self: ZonedDateTime, duration: Duration) !ZonedDateTime { + const overflow_opt = abi.toOption(abi.c.ArithmeticOverflow_option, null); + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_subtract(self._inner, duration._inner, overflow_opt)); } -pub fn toPlainDate() error{Todo}!PlainDate { - return error.Todo; +/// Convert to Instant +pub fn toInstant(self: ZonedDateTime) !Instant { + const instant_ptr = abi.c.temporal_rs_ZonedDateTime_to_instant(self._inner) orelse return error.TemporalError; + const epoch_ms = abi.c.temporal_rs_Instant_epoch_milliseconds(instant_ptr); + const epoch_ns_parts = abi.c.temporal_rs_Instant_epoch_nanoseconds(instant_ptr); + const epoch_ns = abi.fromI128Nanoseconds(epoch_ns_parts); + return .{ ._inner = instant_ptr, .epoch_milliseconds = epoch_ms, .epoch_nanoseconds = epoch_ns }; } -pub fn toPlainDateTime() error{Todo}!PlainDateTime { - return error.Todo; +/// Convert to JSON string (ISO 8601 format) +pub fn toJSON(self: ZonedDateTime, allocator: std.mem.Allocator) ![]u8 { + return self.toString(allocator, .{}); } -pub fn toPlainTime() error{Todo}!PlainTime { - return error.Todo; +/// Convert to locale string (placeholder - returns ISO string) +pub fn toLocaleString(self: ZonedDateTime, allocator: std.mem.Allocator) ![]u8 { + return self.toString(allocator, .{}); } -pub fn toString() error{Todo}![]const u8 { - return error.Todo; +/// Convert to PlainDate +pub fn toPlainDate(self: ZonedDateTime) !PlainDate { + const ptr = abi.c.temporal_rs_ZonedDateTime_to_plain_date(self._inner) orelse return error.TemporalError; + return .{ ._inner = ptr, .calendar_id = self.calendar_id }; } -pub fn until() error{Todo}!Duration { - return error.Todo; +/// Convert to PlainDateTime +pub fn toPlainDateTime(self: ZonedDateTime) !PlainDateTime { + const ptr = abi.c.temporal_rs_ZonedDateTime_to_plain_datetime(self._inner) orelse return error.TemporalError; + return .{ ._inner = ptr, .calendar_id = self.calendar_id }; } -pub fn valueOf() error{Todo}!void { - return error.Todo; +/// Convert to PlainTime +pub fn toPlainTime(self: ZonedDateTime) !PlainTime { + const ptr = abi.c.temporal_rs_ZonedDateTime_to_plain_time(self._inner) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn with() error{Todo}!ZonedDateTime { - return error.Todo; +/// Convert to string with options +pub fn toString(self: ZonedDateTime, allocator: std.mem.Allocator, opts: ToStringOptions) ![]u8 { + const smallest = if (opts.smallest_unit) |u| abi.toUnitOption(u.toCApi()) else abi.toUnitOption(null); + const rounding = if (opts.rounding_mode) |r| abi.toRoundingModeOption(r.toCApi()) else abi.toRoundingModeOption(null); + + const precision: abi.c.Precision = if (opts.fractional_second_digits) |digits| + .{ .is_minute = false, .precision = abi.toOption(abi.c.OptionU8, digits) } + else + .{ .is_minute = true, .precision = abi.toOption(abi.c.OptionU8, null) }; + + const rounding_opts = abi.c.ToStringRoundingOptions{ + .precision = precision, + .smallest_unit = smallest, + .rounding_mode = rounding, + }; + + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + + _ = abi.c.temporal_rs_ZonedDateTime_to_ixdtf_string( + self._inner, + opts.offset_display.toCApi(), + opts.time_zone_name.toCApi(), + opts.calendar_display.toCApi(), + rounding_opts, + &write.inner, + ); + + return try write.toOwnedSlice(); } -pub fn withCalendar() error{Todo}!ZonedDateTime { - return error.Todo; +/// Calculate duration until another ZonedDateTime +pub fn until(self: ZonedDateTime, other: ZonedDateTime, settings: DifferenceSettings) !Duration { + const ptr = abi.success(abi.c.temporal_rs_ZonedDateTime_until(self._inner, other._inner, settings.toCApi())) orelse return error.TemporalError; + return .{ ._inner = ptr }; } -pub fn withPlainTime() error{Todo}!ZonedDateTime { - return error.Todo; +/// valueOf() is not supported for ZonedDateTime +pub fn valueOf(_: ZonedDateTime) !void { + return error.ValueOfNotSupported; } -pub fn withTimeZone() error{Todo}!ZonedDateTime { - return error.Todo; +/// Create a new ZonedDateTime with some fields changed +pub fn with(self: ZonedDateTime, allocator: std.mem.Allocator, fields: anytype) !ZonedDateTime { + _ = allocator; + _ = fields; + _ = self; + return error.Todo; // Need PartialZonedDateTime mapping +} + +/// Create a new ZonedDateTime with a different calendar +pub fn withCalendar(self: ZonedDateTime, calendar: []const u8) !ZonedDateTime { + const cal_view = abi.toDiplomatStringView(calendar); + const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view); + const cal_kind = abi.success(cal_result) orelse return error.TemporalError; + const ptr = abi.c.temporal_rs_ZonedDateTime_with_calendar(self._inner, cal_kind); + return .{ ._inner = ptr, .calendar_id = calendar }; +} + +/// Create a new ZonedDateTime with a different time +pub fn withPlainTime(self: ZonedDateTime, time: ?PlainTime) !ZonedDateTime { + const time_ptr = if (time) |t| t._inner else null; + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_with_plain_time(self._inner, time_ptr)); +} + +/// Create a new ZonedDateTime with a different time zone +pub fn withTimeZone(self: ZonedDateTime, time_zone: TimeZone) !ZonedDateTime { + return wrapZonedDateTime(abi.c.temporal_rs_ZonedDateTime_with_timezone(self._inner, time_zone.toCApi())); +} + +// Property accessors +pub fn calendarId(self: ZonedDateTime) []const u8 { + return self.calendar_id; +} + +pub fn day(self: ZonedDateTime) u8 { + return abi.c.temporal_rs_ZonedDateTime_day(self._inner); +} + +pub fn dayOfWeek(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_day_of_week(self._inner); +} + +pub fn dayOfYear(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_day_of_year(self._inner); +} + +pub fn daysInMonth(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_days_in_month(self._inner); +} + +pub fn daysInWeek(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_days_in_week(self._inner); +} + +pub fn daysInYear(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_days_in_year(self._inner); +} + +pub fn epochMilliseconds(self: ZonedDateTime) i64 { + return abi.c.temporal_rs_ZonedDateTime_epoch_milliseconds(self._inner); +} + +pub fn epochNanoseconds(self: ZonedDateTime) i128 { + const parts = abi.c.temporal_rs_ZonedDateTime_epoch_nanoseconds(self._inner); + return abi.fromI128Nanoseconds(parts); +} + +pub fn era(self: ZonedDateTime, allocator: std.mem.Allocator) !?[]u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + abi.c.temporal_rs_ZonedDateTime_era(self._inner, &write.inner); + const result = try write.toOwnedSlice(); + if (result.len == 0) { + allocator.free(result); + return null; + } + return result; +} + +pub fn eraYear(self: ZonedDateTime) ?i32 { + const result = abi.c.temporal_rs_ZonedDateTime_era_year(self._inner); + return abi.fromOption(result); +} + +pub fn hour(self: ZonedDateTime) u8 { + return abi.c.temporal_rs_ZonedDateTime_hour(self._inner); +} + +pub fn hoursInDay(self: ZonedDateTime) !f64 { + const result = abi.c.temporal_rs_ZonedDateTime_hours_in_day(self._inner); + return abi.success(result) orelse error.TemporalError; +} + +pub fn inLeapYear(self: ZonedDateTime) bool { + return abi.c.temporal_rs_ZonedDateTime_in_leap_year(self._inner); +} + +pub fn microsecond(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_microsecond(self._inner); +} + +pub fn millisecond(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_millisecond(self._inner); +} + +pub fn minute(self: ZonedDateTime) u8 { + return abi.c.temporal_rs_ZonedDateTime_minute(self._inner); +} + +pub fn month(self: ZonedDateTime) u8 { + return abi.c.temporal_rs_ZonedDateTime_month(self._inner); +} + +pub fn monthCode(self: ZonedDateTime, allocator: std.mem.Allocator) ![]u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + abi.c.temporal_rs_ZonedDateTime_month_code(self._inner, &write.inner); + return try write.toOwnedSlice(); +} + +pub fn monthsInYear(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_months_in_year(self._inner); +} + +pub fn nanosecond(self: ZonedDateTime) u16 { + return abi.c.temporal_rs_ZonedDateTime_nanosecond(self._inner); +} + +pub fn offset(self: ZonedDateTime, allocator: std.mem.Allocator) ![]u8 { + var write = abi.DiplomatWrite.init(allocator); + defer write.deinit(); + const res = abi.c.temporal_rs_ZonedDateTime_offset(self._inner, &write.inner); + _ = abi.success(res) orelse return error.TemporalError; + return try write.toOwnedSlice(); +} + +pub fn offsetNanoseconds(self: ZonedDateTime) i64 { + return abi.c.temporal_rs_ZonedDateTime_offset_nanoseconds(self._inner); +} + +pub fn second(self: ZonedDateTime) u8 { + return abi.c.temporal_rs_ZonedDateTime_second(self._inner); +} + +pub fn timeZoneId(self: ZonedDateTime, allocator: std.mem.Allocator) ![]u8 { + const tz = abi.c.temporal_rs_ZonedDateTime_timezone(self._inner); + const view = abi.fromDiplomatStringView(tz.id); + return try allocator.dupe(u8, view); +} + +pub fn weekOfYear(self: ZonedDateTime) ?u8 { + const result = abi.c.temporal_rs_ZonedDateTime_week_of_year(self._inner); + return abi.fromOption(result); +} + +pub fn year(self: ZonedDateTime) i32 { + return abi.c.temporal_rs_ZonedDateTime_year(self._inner); +} + +pub fn yearOfWeek(self: ZonedDateTime) ?i32 { + const result = abi.c.temporal_rs_ZonedDateTime_year_of_week(self._inner); + return abi.fromOption(result); +} + +/// Clone this ZonedDateTime +pub fn clone(self: ZonedDateTime) ZonedDateTime { + const ptr = abi.c.temporal_rs_ZonedDateTime_clone(self._inner); + return .{ ._inner = ptr, .calendar_id = self.calendar_id }; +} + +/// Free the ZonedDateTime +pub fn deinit(self: ZonedDateTime) void { + abi.c.temporal_rs_ZonedDateTime_destroy(self._inner); + if (self.calendar_id.len > 0) { + std.heap.c_allocator.free(self.calendar_id); + } } // ---------- Tests --------------------- -test compare { - if (true) return error.Todo; +test init { + const tz = try TimeZone.init("America/New_York"); + const zdt = try init(0, tz); + defer zdt.deinit(); + + try std.testing.expectEqual(@as(i64, 0), zdt.epochMilliseconds()); } + +test fromEpochMilliseconds { + const tz = try TimeZone.init("UTC"); + const zdt = try fromEpochMilliseconds(1609459200000, tz); // 2021-01-01T00:00:00Z + defer zdt.deinit(); + + try std.testing.expectEqual(@as(i64, 1609459200000), zdt.epochMilliseconds()); +} + test from { - if (true) return error.Todo; + const zdt = try from("2021-01-01T00:00:00+00:00[UTC]", null, .compatible, .reject); + defer zdt.deinit(); + + try std.testing.expectEqual(@as(i32, 2021), zdt.year()); + try std.testing.expectEqual(@as(u8, 1), zdt.month()); + try std.testing.expectEqual(@as(u8, 1), zdt.day()); } -test add { - if (true) return error.Todo; + +test compare { + const tz = try TimeZone.init("UTC"); + const zdt1 = try fromEpochMilliseconds(1000, tz); + defer zdt1.deinit(); + const zdt2 = try fromEpochMilliseconds(2000, tz); + defer zdt2.deinit(); + + try std.testing.expect(compare(zdt1, zdt2) < 0); + try std.testing.expect(compare(zdt2, zdt1) > 0); + try std.testing.expect(compare(zdt1, zdt1) == 0); } + test equals { - if (true) return error.Todo; -} -test getTimeZoneTransition { - if (true) return error.Todo; -} -test round { - if (true) return error.Todo; -} -test since { - if (true) return error.Todo; -} -test startOfDay { - if (true) return error.Todo; -} -test subtract { - if (true) return error.Todo; + const tz = try TimeZone.init("UTC"); + const zdt1 = try fromEpochMilliseconds(1000, tz); + defer zdt1.deinit(); + const zdt2 = try fromEpochMilliseconds(1000, tz); + defer zdt2.deinit(); + const zdt3 = try fromEpochMilliseconds(2000, tz); + defer zdt3.deinit(); + + try std.testing.expect(zdt1.equals(zdt2)); + try std.testing.expect(!zdt1.equals(zdt3)); } + test toInstant { - if (true) return error.Todo; -} -test toJSON { - if (true) return error.Todo; -} -test toLocaleString { - if (true) return error.Todo; + const tz = try TimeZone.init("UTC"); + const zdt = try fromEpochMilliseconds(1609459200000, tz); + defer zdt.deinit(); + + const instant = try zdt.toInstant(); + defer instant.deinit(); + + try std.testing.expectEqual(@as(i64, 1609459200000), instant.epoch_milliseconds); } + test toPlainDate { - if (true) return error.Todo; + const tz = try TimeZone.init("UTC"); + const zdt = try fromEpochMilliseconds(1609459200000, tz); + defer zdt.deinit(); + + const pd = try zdt.toPlainDate(); + defer pd.deinit(); + + try std.testing.expectEqual(@as(i32, 2021), pd.year()); + try std.testing.expectEqual(@as(u8, 1), pd.month()); + try std.testing.expectEqual(@as(u8, 1), pd.day()); } + test toPlainDateTime { - if (true) return error.Todo; + const tz = try TimeZone.init("UTC"); + const zdt = try fromEpochMilliseconds(1609459200000, tz); + defer zdt.deinit(); + + var pdt = try zdt.toPlainDateTime(); + defer pdt.deinit(); + + try std.testing.expectEqual(@as(i32, 2021), pdt.year()); + try std.testing.expectEqual(@as(u8, 1), pdt.month()); + try std.testing.expectEqual(@as(u8, 1), pdt.day()); } + test toPlainTime { - if (true) return error.Todo; + const tz = try TimeZone.init("UTC"); + const zdt = try fromEpochMilliseconds(1609459200000, tz); + defer zdt.deinit(); + + const pt = try zdt.toPlainTime(); + defer abi.c.temporal_rs_PlainTime_destroy(pt._inner); + + try std.testing.expectEqual(@as(u8, 0), pt.hour()); + try std.testing.expectEqual(@as(u8, 0), pt.minute()); } + test toString { if (true) return error.Todo; + const tz = try TimeZone.init("UTC"); + const zdt = try fromEpochMilliseconds(1609459200000, tz); + defer zdt.deinit(); + + const str = try zdt.toString(std.testing.allocator, .{}); + defer std.testing.allocator.free(str); + + try std.testing.expect(str.len > 0); } -test until { - if (true) return error.Todo; -} -test with { - if (true) return error.Todo; -} -test withCalendar { - if (true) return error.Todo; -} -test withPlainTime { - if (true) return error.Todo; -} -test withTimeZone { - if (true) return error.Todo; + +test "props" { + const tz = try TimeZone.init("UTC"); + const zdt = try fromEpochMilliseconds(1609459200000, tz); + defer zdt.deinit(); + + try std.testing.expectEqual(@as(i32, 2021), zdt.year()); + try std.testing.expectEqual(@as(u8, 1), zdt.month()); + try std.testing.expectEqual(@as(u8, 1), zdt.day()); + try std.testing.expectEqual(@as(u8, 0), zdt.hour()); + try std.testing.expectEqual(@as(u8, 0), zdt.minute()); + try std.testing.expectEqual(@as(u8, 0), zdt.second()); + try std.testing.expectEqual(@as(u16, 0), zdt.millisecond()); } diff --git a/src/root.zig b/src/root.zig index 44b503a..c3d53ee 100644 --- a/src/root.zig +++ b/src/root.zig @@ -113,6 +113,7 @@ test Instant { // Public types "ToStringOptions", + "TimeZone", "Unit", "RoundingMode", "Sign", @@ -140,7 +141,8 @@ test Now { test PlainDate { const checks = .{ // Constructor - "init", // Temporal.PlainDate() + "init", + "initWithCalendar", // Static methods "compare", @@ -163,7 +165,7 @@ test PlainDate { "with", "withCalendar", - // Properties + // Properties (now as methods) "calendarId", "day", "dayOfWeek", @@ -180,6 +182,15 @@ test PlainDate { "weekOfYear", "year", "yearOfWeek", + + // Public types + "ToStringOptions", + "CalendarDisplay", + "ToZonedDateTimeOptions", + "Unit", + "RoundingMode", + "Sign", + "DifferenceSettings", }; try assertDecls(PlainDate, checks); @@ -189,10 +200,13 @@ test PlainDateTime { const checks = .{ // Constructor "init", // Temporal.PlainDateTime() + "initWithCalendar", // Static methods "compare", "from", + // "fromUtf8", + // "fromUtf16", // Instance methods "add", @@ -235,6 +249,17 @@ test PlainDateTime { "weekOfYear", "year", "yearOfWeek", + + // Public types + "Unit", + "RoundingMode", + "Sign", + "CalendarDisplay", + "DifferenceSettings", + "RoundOptions", + "ToStringOptions", + "ToZonedDateTimeOptions", + "WithOptions", }; try assertDecls(PlainDateTime, checks); @@ -261,6 +286,11 @@ test PlainMonthDay { "calendarId", "day", "monthCode", + + // Public types + "CalendarDisplay", + "ToStringOptions", + "WithOptions", }; try assertDecls(PlainMonthDay, checks); @@ -295,6 +325,13 @@ test PlainTime { "minute", "nanosecond", "second", + + // Public types + "Unit", + "RoundingMode", + "DifferenceSettings", + "RoundOptions", + "WithOptions", }; try assertDecls(PlainTime, checks); @@ -333,6 +370,15 @@ test PlainYearMonth { "monthCode", "monthsInYear", "year", + + // Public types + "Unit", + "RoundingMode", + "CalendarDisplay", + "DifferenceSettings", + "RoundOptions", + "ToStringOptions", + "WithOptions", }; try assertDecls(PlainYearMonth, checks); @@ -346,9 +392,12 @@ test ZonedDateTime { // Static methods "compare", "from", + "fromEpochMilliseconds", + "fromEpochNanoseconds", // Instance methods "add", + "clone", "equals", "getTimeZoneTransition", "round", @@ -398,6 +447,20 @@ test ZonedDateTime { "weekOfYear", "year", "yearOfWeek", + + // Public types + "Unit", + "RoundingMode", + "Sign", + "DifferenceSettings", + "RoundOptions", + "TimeZone", + "Disambiguation", + "OffsetDisambiguation", + "CalendarDisplay", + "DisplayOffset", + "DisplayTimeZone", + "ToStringOptions", }; try assertDecls(ZonedDateTime, checks);