diff --git a/inst/+tblish/+internal/+chrono/+tzinfo/TzInfo.m b/inst/+tblish/+internal/+chrono/+tzinfo/TzInfo.m index 6ee02243..6d2a31b2 100644 --- a/inst/+tblish/+internal/+chrono/+tzinfo/TzInfo.m +++ b/inst/+tblish/+internal/+chrono/+tzinfo/TzInfo.m @@ -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 @@ -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. @@ -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"); @@ -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) @@ -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)) diff --git a/inst/datetime.m b/inst/datetime.m index 3190c1e8..1f538a3f 100644 --- a/inst/datetime.m +++ b/inst/datetime.m @@ -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 @@ -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 @@ -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); @@ -1598,10 +1605,10 @@ function disp (this) %!test datetime; %!test datetime ('2011-03-07'); -%!xtest 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)