Skip to content

Commit

Permalink
datetime: fix NaN handling in TimeZone conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
apjanke committed Feb 4, 2024
1 parent be2e89e commit 74b23e3
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 12 deletions.
30 changes: 22 additions & 8 deletions inst/+tblish/+internal/+chrono/+tzinfo/TzInfo.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
properties
id
formatId
# These fields are all parallel
# These fields are all parallel.
# The transitions and transitionslocal are int64s holding (I think) Unix times.
# The *Datenum suffixed ones are doubles holding datenums.
transitions
transitionsLocal
transitionsDatenum
Expand Down Expand Up @@ -154,7 +156,7 @@ function prettyprint (this)
if (ismember (this.id, tblish.internal.chrono.tzinfo.TzInfo.utcZoneAliases))
# Have to special-case this because it relies on POSIX zone rules, which
# are not implemented yet
offsets = zeros (size (dnum));
out = dnum;
else
# The time zone record is identified by finding the last record whose start
# time is less than or equal to the local time.
Expand All @@ -163,8 +165,11 @@ function prettyprint (this)
# the right transition point. (It has to be a *modified* bin search
# because the local times for transitions are not necessarily monotonic increasing.
# I think.)
#
# FIXME: Handle NaN inputs.
tf = false (size (dnum));
loc = NaN (size (dnum));
last_transition = this.transitionsLocalDatenum(end);
for i_dnum = 1:numel(dnum)
d = dnum(i_dnum);
ix = find(this.transitionsLocalDatenum <= d, 1, "last");
Expand All @@ -175,17 +180,22 @@ function prettyprint (this)
endfor
# [tf,loc] = tblish.internal.chrono.algo.binsearch (dnum, this.transitionsLocalDatenum);
ix = loc;
tfOutOfRange = isnan(ix) | ix == numel (this.transitions);
tfOutOfRange = (! isnan (dnum)) & (isnan(ix) | ix == numel (this.transitions));
tfToConvert = (! isnan (dnum)) & ! tfOutOfRange;

out = dnum;
# In-range dates take their period's gmt offset
offsets = NaN (size (dnum));
offsets(!tfOutOfRange) = this.ttinfos.gmtoffDatenum(this.timeTypes(ix(!tfOutOfRange)));
if any (tfToConvert(:))
offsets = this.ttinfos.gmtoffDatenum(this.timeTypes(ix(tfToConvert)));
out(tfToConvert) = dnum(tfToConvert) - offsets;
endif
# Out-of-range dates are handled by the POSIX look-ahead zone
if (any (tfOutOfRange(:)))
# TODO: Implement this
error ('POSIX zone rules are unimplemented');
# FIXME: Implement this
error ('datetime: POSIX zone rules are required to convert out-of-tzdb-range dates, but are unimplemented');
endif
# Anything left over should be NaNs; no change needed.
endif
out = dnum - offsets;
endfunction

function out = gmtToLocaltime (this, dnum)
Expand Down Expand Up @@ -227,6 +237,10 @@ function displayCommonInfo (this)
fprintf (' Version %s (%s time values)\n', this.formatId, time_size);
fprintf (' %d transitions, %d ttinfos, %d leap times\n', ...
numel (this.transitions), numel (this.ttinfos.gmtoff), numel (this.leapTimes));
if (! isempty (this.transitions))
fprintf (' First transition: %s Last transition: %s (UTC)\n', ...
datestr (this.transitionsDatenum(1)), datestr (this.transitionsDatenum(end)));
endif
fprintf (' %d is_stds, %d is_gmts\n', ...
numel (this.isStd), numel (this.isGmt));
if (! isempty (this.goingForwardPosixZone))
Expand Down
15 changes: 11 additions & 4 deletions inst/datetime.m
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
endproperties

properties (Access = private)
# The underlying datenum values, always in UTC
# The underlying datenum values, always in UTC when zoned, or pseudo-UTC when unzoned.
dnums = NaN % planar
endproperties
properties
Expand Down Expand Up @@ -238,13 +238,17 @@
endswitch

# Construct
# FIXME: The set.TimeZone here invokes conversion before dnums is set, which
# hits a NaN-not-supported case
if (! isempty (timeZone))
this.TimeZone = timeZone;
if (! isequal (timeZone, 'UTC'))
dnums = datetime.convertDatenumTimeZone(dnums, timeZone, 'UTC');
endif
this.dnums = dnums;
else
this.dnums = dnums;
endif
this.dnums = dnums;
if (isfield (opts, 'Format'))
this.Format = opts.Format;
endif
Expand Down Expand Up @@ -392,6 +396,9 @@
error ('Undefined TimeZone: %s', x);
endif
if (isempty (this.TimeZone) && ! isempty (x))
# Converting an unzoned date to a zoned one declares that the wall time represented by
# the unzoned dnums is a local time. Zoned datetimes store their internal dnums in UTC,
# so we need to convert *from* the new time zone to UTC for the internal representation.
this.dnums = datetime.convertDatenumTimeZone (this.dnums, x, 'UTC');
elseif (! isempty (this.TimeZone) && isempty (x))
this.dnums = datetime.convertDatenumTimeZone (this.dnums, 'UTC', this.TimeZone);
Expand Down Expand Up @@ -1598,10 +1605,10 @@ function disp (this)

%!test datetime;
%!test datetime ('2011-03-07');
%!xtest <unimplemented timezone support> datetime ('2011-03-07 12:34:56', 'TimeZone', 'America/New_York');
%!test datetime ('2011-03-07 12:34:56', 'TimeZone', 'America/New_York');
%!test
%! d = datetime;
%! d.TimeZone = 'America/New_York';
%! d2 = d;
%! d2.TimeZone = 'America/Chicago';
%! assert (abs(d.dnums - d2.dnums), (1/24), .0001)
%! assert (abs (datenum (d) - datenum (d2)), (1/24), .0001)

0 comments on commit 74b23e3

Please sign in to comment.