Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timezone revision: separation of rules and offset #35

Merged
merged 12 commits into from
Oct 27, 2024
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@ Types of changes

- datetime parser: option to use a modifier in parsing directives
- allow parsing of English month/day names, independent of the current locale
- method to test whether a datetime with seconds == 60 actually is a leap second datetime
- method 'validateLeap' to test whether a datetime with seconds == 60 actually is a leap second datetime
- method 'diffLeap' to calculate difference in leap seconds between two datetimes
- new type / struct 'UTCoffset', which is used to specify a concrete offset from UTC for a zoned datetime
- new helper struct 'tz_options', to provide either a time zone or a UTC offset for functions that set or convert time zones of a datetime

### Changed

- (internal) improve Timezone.format method, remove usage of `@constCast` and reduce usage of `.?` on optionals - by @Ratakor
- (internal) Timezone handling as a tagged union for different sources of rules (IANA db TZif or POSIX; POSIX only prepared, not implemented yet)
- API changes:
- datetime creation: methods 'fromFields' and 'fromUnix' now take an optional 'tz_options', which is a tagged union that either can be a UTC offset or a Timezone
- time zone set / change: methods 'tzLocalize' and 'tzConvert' also now take 'tz_options'
- 'Datetime.now': also takes 'tz_options' now instead of a Timezone

### Removed

- method 'fromTzfile' (Timezone creation). A Timezone should either be created from the embedded tzdata (method 'fromTzdata', comptime or runtime) or at runtime from the system's tzdata (method 'runtimeFromTzfile')

## 2024-10-12, v0.3.5

Expand Down
91 changes: 47 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,51 +13,54 @@
### [Demo](https://github.com/FObersteiner/zdt/blob/master/examples/ex_demo.zig)

```zig
// need an allocator for the time zones since the size of the rule-files varies
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

// zdt embeds the IANA tz database:
var tz_LA = try zdt.Timezone.fromTzdata("America/Los_Angeles", allocator);
defer tz_LA.deinit();

// you can also use your system's tz data if it provides it:
var tz_Paris = try zdt.Timezone.fromTzfile("Europe/Paris", allocator);
defer tz_Paris.deinit();

// ISO8601 parser on-board, accepts wide variety of compatible formats
const a_datetime = try zdt.Datetime.fromISO8601("2022-03-07");
const this_time_LA = try a_datetime.tzLocalize(tz_LA);

// string output also requires allocation...
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
try this_time_LA.toString("%I %p, %Z", buf.writer());

const this_time_Paris = try this_time_LA.tzConvert(tz_Paris);

// '{s}' directive gives ISO8601 format by default;
std.debug.print(
"Time, LA : {s} ({s})\n... that's {s} in Paris\n\n",
.{ this_time_LA, buf.items, this_time_Paris },
);
// Time, LA : 2022-03-07T00:00:00-08:00 (12 am, PST)
// ... that's 2022-03-07T09:00:00+01:00 in Paris

const wall_diff = try this_time_Paris.diffWall(this_time_LA);
const abs_diff = this_time_Paris.diff(this_time_LA);

std.debug.print(
"Wall clock time difference: {s}\nAbsolute time difference: {s}\n",
.{ wall_diff, abs_diff },
);
// Wall clock time difference: PT09H00M00S
// Absolute time difference: PT00H00M00S

// need an allocator for the time zones since the size of the rule-files varies.
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

// zdt embeds the IANA tz database:
var tz_LA = try zdt.Timezone.fromTzdata("America/Los_Angeles", allocator);
defer tz_LA.deinit();

// you can also use your system's tz data at runtime;
// this will very likely not work on Windows, wo we can use the embedded version.
var tz_Paris = switch (builtin.os.tag) {
.windows => try zdt.Timezone.fromTzdata("Europe/Paris", allocator),
else => try zdt.Timezone.runtimeFromTzfile("Europe/Paris", zdt.Timezone.tzdb_prefix, allocator),
};
defer tz_Paris.deinit();

// ISO8601 parser on-board, accepts wide variety of compatible formats
const a_datetime = try zdt.Datetime.fromISO8601("2022-03-07");
const this_time_LA = try a_datetime.tzLocalize(&tz_LA);

// string output also requires allocation...
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
try this_time_LA.toString("%I %p, %Z", buf.writer());

const this_time_Paris = try this_time_LA.tzConvert(&tz_Paris);

// '{s}' directive gives ISO8601 format by default;
std.debug.print(
"Time, LA : {s} ({s})\n... that's {s} in Paris ({s})\n\n",
.{ this_time_LA, buf.items, this_time_Paris, this_time_Paris.tzAbbreviation() },
);
// Time, LA : 2022-03-07T00:00:00-08:00 (12 am, PST)
// ... that's 2022-03-07T09:00:00+01:00 in Paris (CET)

const wall_diff = try this_time_Paris.diffWall(this_time_LA);
const abs_diff = this_time_Paris.diff(this_time_LA);

std.debug.print(
"Wall clock time difference: {s}\nAbsolute time difference: {s}\n",
.{ wall_diff, abs_diff },
);
// Wall clock time difference: PT09H00M00S
// Absolute time difference: PT00H00M00S
```

More examples in the `./examples` directory. There's a build-step to build them all; EX:
More examples in the `./examples` directory. There's a build-step to build them all;

```zig
zig build examples && ./zig-out/bin/ex_datetime
Expand All @@ -73,7 +76,7 @@ See [changelog](https://github.com/FObersteiner/zdt/blob/master/change.log)

## Zig version

This library is developed with Zig `0.14.0-dev` aka 'master', might not compile with older versions. As of 2024-10-19, Zig-0.13 stable or higher should work.
This library is developed with Zig `0.14.0-dev` aka 'master', might not compile with older versions. As of 2024-10-27, Zig-0.13 stable or higher should work.

## IANA timezone database version

Expand Down
2 changes: 1 addition & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const std = @import("std");
const builtin = @import("builtin");
const log = std.log.scoped(.zdt_build);

const zdt_version = std.SemanticVersion{ .major = 0, .minor = 3, .patch = 5 };
const zdt_version = std.SemanticVersion{ .major = 0, .minor = 4, .patch = 0 };

const example_files = [_][]const u8{
"ex_demo",
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = "zdt",
.version = "0.3.5",
.version = "0.4.0",
.paths = .{
"zdt.zig",
"lib", // anything from lib
Expand Down
4 changes: 2 additions & 2 deletions examples/ex_datetime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ pub fn main() !void {
println("---> (usage) Unix epoch: datetime from timestamp", .{});
const unix_epoch_naive = try Datetime.fromUnix(0, Duration.Resolution.second, null);
println("'Unix epoch', naive datetime : {s}", .{unix_epoch_naive});
var unix_epoch_correct = try Datetime.fromUnix(0, Duration.Resolution.second, Tz.UTC);
var unix_epoch_correct = try Datetime.fromUnix(0, Duration.Resolution.second, .{ .tz = &Tz.UTC });
println("'Unix epoch', aware datetime : {s}", .{unix_epoch_correct});
println("'Unix epoch', tz name : {s}", .{unix_epoch_correct.tzinfo.?.name()});
println("'Unix epoch', tz name : {s}", .{unix_epoch_correct.tzName()});

println("", .{});
println("---> (usage) Now: datetime from system time", .{});
Expand Down
21 changes: 14 additions & 7 deletions examples/ex_demo.zig
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
const std = @import("std");
const builtin = @import("builtin");

const zdt = @import("zdt");

pub fn main() !void {
// need an allocator for the time zones since the size of the rule-files varies
// need an allocator for the time zones since the size of the rule-files varies.
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

// zdt embeds the IANA tz database:
var tz_LA = try zdt.Timezone.fromTzdata("America/Los_Angeles", allocator);
defer tz_LA.deinit();
// you can also use your system's tz data if it provides it:
var tz_Paris = try zdt.Timezone.fromTzfile("Europe/Paris", allocator);

// you can also use your system's tz data at runtime;
// this will very likely not work on Windows, wo we can use the embedded version.
var tz_Paris = switch (builtin.os.tag) {
.windows => try zdt.Timezone.fromTzdata("Europe/Paris", allocator),
else => try zdt.Timezone.runtimeFromTzfile("Europe/Paris", zdt.Timezone.tzdb_prefix, allocator),
};
defer tz_Paris.deinit();

// ISO8601 parser on-board, accepts wide variety of compatible formats
const a_datetime = try zdt.Datetime.fromISO8601("2022-03-07");
const this_time_LA = try a_datetime.tzLocalize(tz_LA);
const this_time_LA = try a_datetime.tzLocalize(.{ .tz = &tz_LA });

// string output also requires allocation...
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
try this_time_LA.toString("%I %p, %Z", buf.writer());

const this_time_Paris = try this_time_LA.tzConvert(tz_Paris);
const this_time_Paris = try this_time_LA.tzConvert(.{ .tz = &tz_Paris });

// '{s}' directive gives ISO8601 format by default;
std.debug.print(
"Time, LA : {s} ({s})\n... that's {s} in Paris\n\n",
.{ this_time_LA, buf.items, this_time_Paris },
"Time, LA : {s} ({s})\n... that's {s} in Paris ({s})\n\n",
.{ this_time_LA, buf.items, this_time_Paris, this_time_Paris.tzAbbreviation() },
);
// Time, LA : 2022-03-07T00:00:00-08:00 (12 am, PST)
// ... that's 2022-03-07T09:00:00+01:00 in Paris
Expand Down
4 changes: 3 additions & 1 deletion examples/ex_locale.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ pub fn main() !void {
const loc = "de_DE.UTF-8";
const new_loc = c_locale.setlocale(time_mask, loc);
if (new_loc == null) {
std.process.fatal("skip example, failed to set locale", .{});
// Zig 0.14:
// std.process.fatal("skip example, failed to set locale", .{});
std.log.err("skip example, failed to set locale", .{});
}

const dt = try Datetime.fromISO8601("2024-10-12");
Expand Down
14 changes: 7 additions & 7 deletions examples/ex_offsetTz.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ const builtin = @import("builtin");

const zdt = @import("zdt");
const Datetime = zdt.Datetime;
const Tz = zdt.Timezone;
const UTCoffset = zdt.UTCoffset;

pub fn main() !void {
println("---> UTC offset time zone example", .{});
println("OS / architecture: {s} / {s}", .{ @tagName(builtin.os.tag), @tagName(builtin.cpu.arch) });
println("Zig version: {s}\n", .{builtin.zig_version_string});

const tzinfo = try Tz.fromOffset(3600, "UTC+1");
var a_date = try Datetime.fromFields(.{ .year = 1970, .tzinfo = tzinfo });
const offset = try UTCoffset.fromSeconds(3600, "UTC+1");
var a_date = try Datetime.fromFields(.{ .year = 1970, .tz_options = .{ .utc_offset = offset } });
println("datetime: {s}", .{a_date});
println("tz name: {s}", .{a_date.tzinfo.?.name()});
println("offset name: {s}", .{a_date.tzAbbreviation()});

const other_tzinfo = try Tz.fromOffset(-5 * 3600, "UTC-5");
var a_date_other_tz = try a_date.tzConvert(other_tzinfo);
const other_offset = try UTCoffset.fromSeconds(-5 * 3600, "UTC-5");
var a_date_other_tz = try a_date.tzConvert(.{ .utc_offset = other_offset });
println("datetime in other tz: {s}", .{a_date_other_tz});
println("other tz name: {s}", .{a_date_other_tz.tzinfo.?.name()});
println("other offset name: {s}", .{a_date_other_tz.tzAbbreviation()});
}

fn println(comptime fmt: []const u8, args: anytype) void {
Expand Down
2 changes: 1 addition & 1 deletion examples/ex_strings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn main() !void {
const leap_datetime = "2016-12-31T23:59:60Z";
parsed = try Datetime.fromISO8601(leap_datetime);
assert(parsed.second == 60);
assert(std.meta.eql(parsed.tzinfo.?, Timezone.UTC));
assert(std.meta.eql(parsed.tz.?.*, Timezone.UTC));
println("parsed '{s}'\n to {s}", .{ leap_datetime, parsed });

// The format might be less-standard, so we need to provide parsing directives à la strptime
Expand Down
62 changes: 47 additions & 15 deletions examples/ex_timezones.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,55 +21,87 @@ pub fn main() !void {
defer _ = gpa.deinit();
const allocator = gpa.allocator();

// TODO : example with UTC as time zone

println("time zone database version: {s}\n", .{Tz.tzdb_version});

var tz_berlin: Tz = try Tz.fromTzdata("Europe/Berlin", allocator);
defer tz_berlin.deinit();
var now_berlin: Datetime = try Datetime.now(tz_berlin);
var now_berlin: Datetime = try Datetime.now(.{ .tz = &tz_berlin });
const now_utc: Datetime = Datetime.nowUTC();
println("Now, UTC time : {s}", .{now_utc});
println("Now, Berlin time : {s} ({s})", .{ now_berlin, now_berlin.tzinfo.?.abbreviation() });
println("Datetimes have timezone? {}, {}\n", .{ now_utc.isAware(), now_berlin.isAware() });
println("Now, Berlin time : {s} ({s})", .{ now_berlin, now_berlin.tzAbbreviation() });
println("Datetimes have UTC offset / time zone? : {}, {}\n", .{ now_utc.isAware(), now_berlin.isAware() });

var my_tz: Tz = try Tz.tzLocal(allocator);
defer my_tz.deinit();
var now_local = try now_berlin.tzConvert(my_tz);
var now_local = try now_berlin.tzConvert(.{ .tz = &my_tz });
println("My time zone : {s}", .{my_tz.name()});

println("Now, my time zone : {s} ({s})", .{ now_local, now_local.tzinfo.?.abbreviation() });
println("Now, my time zone : {s} ({s})", .{ now_local, now_local.tzAbbreviation() });
println("", .{});

var tz_ny = try Tz.fromTzdata("America/New_York", allocator);
defer tz_ny.deinit();
var now_ny: Datetime = try now_local.tzConvert(tz_ny);
println("Now in New York : {s} ({s})", .{ now_ny, now_ny.tzinfo.?.abbreviation() });
var now_ny: Datetime = try now_local.tzConvert(.{ .tz = &tz_ny });
println("Now in New York : {s} ({s})", .{ now_ny, now_ny.tzAbbreviation() });
println("Wall time difference, local vs. NY: {}", .{try now_ny.diffWall(now_local)});
println("", .{});

println("New York has DST currently? : {}", .{now_ny.tzinfo.?.tzOffset.?.is_dst});
println("New York has DST currently? : {}", .{now_ny.isDST()});
var ny_summer_2023: Datetime = try Datetime.fromFields(.{
.year = 2023,
.month = 8,
.day = 9,
.tzinfo = tz_ny,
.tz_options = .{ .tz = &tz_ny },
});
println("New York, summer : {s} ({s})", .{ ny_summer_2023, ny_summer_2023.tzinfo.?.abbreviation() });
println("New York has DST in summer? : {}", .{ny_summer_2023.tzinfo.?.tzOffset.?.is_dst});
println("New York, summer : {s} ({s})", .{ ny_summer_2023, ny_summer_2023.tzAbbreviation() });
println("New York has DST in summer? : {}", .{ny_summer_2023.isDST()});
println("", .{});

// non-existing datetime: DST gap
// always errors:
const err_ne = Datetime.fromFields(.{ .year = 2024, .month = 3, .day = 10, .hour = 2, .minute = 30, .tzinfo = tz_ny });
const err_ne = Datetime.fromFields(.{
.year = 2024,
.month = 3,
.day = 10,
.hour = 2,
.minute = 30,
.tz_options = .{ .tz = &tz_ny },
});
println("Attempt to create non-existing datetime: {any}", .{err_ne});

// ambiguous datetime: DST fold
// errors if 'dst_fold' is undefined:
const err_amb = Datetime.fromFields(.{ .year = 2024, .month = 11, .day = 3, .hour = 1, .minute = 30, .tzinfo = tz_ny });
const err_amb = Datetime.fromFields(.{
.year = 2024,
.month = 11,
.day = 3,
.hour = 1,
.minute = 30,
.tz_options = .{ .tz = &tz_ny },
});
println("Attempt to create ambiguous datetime: {any}", .{err_amb});
// we can specify on which side of the fold the datetime should fall:
const amb_dt_early = try Datetime.fromFields(.{ .year = 2024, .month = 11, .day = 3, .hour = 1, .minute = 30, .dst_fold = 0, .tzinfo = tz_ny });
const amb_dt_early = try Datetime.fromFields(.{
.year = 2024,
.month = 11,
.day = 3,
.hour = 1,
.minute = 30,
.dst_fold = 0,
.tz_options = .{ .tz = &tz_ny },
});
println("Ambiguous datetime, early side of fold: {s}", .{amb_dt_early});
const amb_dt_late = try Datetime.fromFields(.{ .year = 2024, .month = 11, .day = 3, .hour = 1, .minute = 30, .dst_fold = 1, .tzinfo = tz_ny });
const amb_dt_late = try Datetime.fromFields(.{
.year = 2024,
.month = 11,
.day = 3,
.hour = 1,
.minute = 30,
.dst_fold = 1,
.tz_options = .{ .tz = &tz_ny },
});
println("Ambiguous datetime, late side of fold: {s}", .{amb_dt_late});
}

Expand Down
Loading