## Copyright (C) 2024-2026 Andreas Bertsatos <abertsatos@biol.uoa.gr>
##
## This file is part of the datatypes package for GNU Octave.
##
## This program is free software; you can redistribute it and/or modify it under
## the terms of the GNU General Public License as published by the Free Software
## Foundation; either version 3 of the License, or (at your option) any later
## version.
##
## This program is distributed in the hope that it will be useful, but WITHOUT
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
## details.
##
## You should have received a copy of the GNU General Public License along with
## this program; if not, see <http://www.gnu.org/licenses/>.

################################################################################
##                 ** Create and convert 'datetime' type **                   ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'datetime'         'dispstrings'      'cellstr'          'char'            ##
## 'ymd'              'hms'              'year'             'quarter'         ##
## 'month'            'week'             'day'              'hour'            ##
## 'minute'           'second'           'timeofday'        'tzoffset'        ##
##                                                                            ##
################################################################################

## The default display formats set by 'datetime.setDefaultFormats' persist
## across sessions, so a user who has set one would otherwise break every
## assertion below that renders a datetime.  Save whatever is stored, run the
## file at the factory defaults, and put it back in the closing block.  A format
## that was never set is restored as absent, not as its factory value, so that
## running the tests leaves no preference behind.
%!shared savedDefaultFormat, savedDefaultDateFormat
%! savedDefaultFormat = '';
%! savedDefaultDateFormat = '';
%! if (ispref ('datatypes', 'datetime_DefaultFormat'))
%!   savedDefaultFormat = getpref ('datatypes', 'datetime_DefaultFormat');
%! endif
%! if (ispref ('datatypes', 'datetime_DefaultDateFormat'))
%!   savedDefaultDateFormat = getpref ('datatypes', 'datetime_DefaultDateFormat');
%! endif
%! datetime.setDefaultFormats ('reset');

## Test 'datetime' constructor -- no-argument and basic construction
%!assert_equal (class (datetime), 'datetime')
%!assert_equal (isa (datetime, 'datetime'), true)
%!assert_equal (isscalar (datetime ()), true)
%!assert_equal (isscalar (datetime (2020, 1, 1)), true)
%!assert_equal (class (datetime (2020, 1, 1)), 'datetime')

## Test 'datetime' constructor -- Y,M,D components and broadcasting
%!assert_equal (year (datetime (2020, 1, 1)), 2020)
%!assert_equal (month (datetime (2020, 6, 1)), 6)
%!assert_equal (day (datetime (2020, 6, 15)), 15)
%!assert_equal (size (datetime (2020, 1, [1; 2; 3])), [3, 1])
%!assert_equal (size (datetime (2020, 1, [1, 2, 3])), [1, 3])
## size with no dimension named folds the trailing dimensions into the last
## output when fewer outputs are asked for than the array has dimensions, and
## pads with ones when more are asked for, as core Octave and MATLAB both do.
%!test
%! A = datetime (2024, 1, ones (2, 2, 2));
%! [r, c] = size (A);
%! assert_equal ([r, c], [2, 4]);
%!test
%! A = datetime (2024, 1, ones (2, 2));
%! [a, b, c, d] = size (A);
%! assert_equal ([a, b, c, d], [2, 2, 1, 1]);
%!assert_equal (size (datetime (2020, [1, 2; 3, 4], 1)), [2, 2])
%!assert_equal (year (datetime (2020, 1, [1; 2])), [2020; 2020])
%!assert_equal (day (datetime (2020, 1, [1; 2; 3])), [1; 2; 3])
%!assert_equal (size (datetime ([2019, 2020], 1, 1)), [1, 2])

## Test 'datetime' constructor -- month/day rollover normalisation
%!assert_equal (year (datetime (2020, 13, 1)), 2021)
%!assert_equal (month (datetime (2020, 13, 1)), 1)
%!assert_equal (month (datetime (2020, 0, 1)), 12)
%!assert_equal (year (datetime (2020, 0, 1)), 2019)
%!assert_equal (month (datetime (2020, 1, 32)), 2)
%!assert_equal (day (datetime (2020, 1, 32)), 1)
%!assert_equal (day (datetime (2020, 3, 0)), 29)
%!assert_equal (month (datetime (2020, 3, 0)), 2)

## Test 'datetime' constructor -- H,MI,S components
%!assert_equal (hour (datetime (2020, 1, 1, 13, 0, 0)), 13)
%!assert_equal (minute (datetime (2020, 1, 1, 13, 45, 0)), 45)
%!assert_equal (second (datetime (2020, 1, 1, 13, 45, 30)), 30)
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 30.5)), 30.5)
%!assert_equal (hour (datetime (2020, 1, 1, 25, 0, 0)), 1)
%!assert_equal (day (datetime (2020, 1, 1, 25, 0, 0)), 2)
%!assert_equal (minute (datetime (2020, 1, 1, 0, 75, 0)), 15)
%!assert_equal (hour (datetime (2020, 1, 1, 0, 75, 0)), 1)

## Test 'datetime' constructor -- milliseconds component
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 0, 500)), 0.5)
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 1, 250)), 1.25)

## Test 'datetime' constructor -- microsecond precision (stored precision is
## microseconds; construction must not truncate to milliseconds).
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45.123456)), 45.123456, 1e-9)
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45.000001)), 45.000001, 1e-9)
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45.123456789)), 45.123457, 1e-9)
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45.123456) + seconds (0)), ...
%!              45.123456, 1e-9)
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45.123456, ...
%!              'TimeZone', 'America/New_York')), 45.123456, 1e-9)
%!assert_equal (second (datetime (1704067200.123456, ...
%!              'ConvertFrom', 'posixtime')), 0.123456, 1e-9)

## Test 'datetime' constructor -- from datevec matrix (3 or 6 columns)
%!assert_equal (year (datetime ([2019, 6, 1; 2020, 7, 2])), [2019; 2020])
%!assert_equal (month (datetime ([2019, 6, 1; 2020, 7, 2])), [6; 7])
%!assert_equal (hour (datetime ([2020, 1, 1, 5, 0, 0])), 5)
%!assert_equal (size (datetime ([2019, 6, 1; 2020, 7, 2])), [2, 1])

## Test 'datetime' constructor -- from date strings
%!assert_equal (year (datetime ('2020-06-15')), 2020)
%!assert_equal (month (datetime ('2020-06-15')), 6)
%!assert_equal (day (datetime ('2020-06-15')), 15)
%!assert_equal (year (datetime ({'2019-01-01'; '2020-01-01'})), [2019; 2020])

## Test 'datetime' constructor -- ConvertFrom posixtime/datenum/excel
%!assert_equal (cellstr (datetime (0, 'ConvertFrom', 'posixtime')), {'01-Jan-1970'})
%!assert_equal (cellstr (datetime (86400, 'ConvertFrom', 'posixtime')), {'02-Jan-1970'})
%!assert_equal (cellstr (datetime (719529, 'ConvertFrom', 'datenum')), {'01-Jan-1970'})
%!assert_equal (year (datetime (0, 'ConvertFrom', 'posixtime')), 1970)
%!assert_equal (isnat (datetime (NaN, 'ConvertFrom', 'datenum')), true)

## Test 'datetime' constructor -- NaT and Inf propagation
%!assert_equal (isnat (datetime (NaN, NaN, NaN)), true)
%!assert_equal (isnat (datetime (2020, 1, 1)), false)
%!assert_equal (isinf (datetime (Inf, Inf, Inf)), true)
%!assert_equal (year (datetime (Inf, Inf, Inf)), Inf)
%!assert_equal (year (datetime (-Inf, -Inf, -Inf)), -Inf)

## Test 'datetime' constructor -- second underflow/overflow normalisation
%!assert_equal (char (datetime (2024, 3, 1, 0, 0, -1), 'yyyy-MM-dd HH:mm:ss'), ...
%!              '2024-02-29 23:59:59')
%!assert_equal (char (datetime (2024, 1, 1, 0, 0, 90), 'yyyy-MM-dd HH:mm:ss'), ...
%!              '2024-01-01 00:01:30')
%!assert_equal (char (datetime (2024, 14, 32, 25, 61, 61), 'yyyy-MM-dd HH:mm:ss'), ...
%!              '2025-03-05 02:02:01')

## Test 'datetime' constructor -- leap-day validity and non-leap rollover
%!assert_equal (char (datetime (2024, 2, 29), 'yyyy-MM-dd'), '2024-02-29')
%!assert_equal (char (datetime (2023, 2, 29), 'yyyy-MM-dd'), '2023-03-01')

## Test 'datetime' constructor -- year 0, negative, and far-future years
%!assert_equal (datevec (datetime (0, 1, 1)), [0, 1, 1, 0, 0, 0])
%!assert_equal (datevec (datetime (-1, 6, 15)), [-1, 6, 15, 0, 0, 0])
%!assert_equal (datevec (datetime (10000, 1, 1)), [10000, 1, 1, 0, 0, 0])

## Test 'datetime' constructor -- date strings with and without a time
%!assert_equal (char (datetime ('2024-03-09 14:30:15'), 'yyyy-MM-dd HH:mm:ss'), ...
%!              '2024-03-09 14:30:15')
%!assert_equal (char (datetime ('15-Jun-2024'), 'yyyy-MM-dd'), '2024-06-15')

## Test 'datetime' constructor -- datevec matrix with a time-of-day row
%!assert_equal (char (datetime ([2024, 3, 9, 14, 30, 15]), 'yyyy-MM-dd HH:mm:ss'), ...
%!              '2024-03-09 14:30:15')

## Test 'datetime' constructor -- ConvertFrom excel and datenum values
%!assert_equal (char (datetime (45360, 'ConvertFrom', 'excel'), 'yyyy-MM-dd'), ...
%!              '2024-03-09')
%!assert_equal (char (datetime (739320, 'ConvertFrom', 'datenum'), 'yyyy-MM-dd'), ...
%!              '2024-03-09')
%!assert_equal (class (datetime (Inf, 'ConvertFrom', 'datenum')), 'datetime')

## Test 'datetime' constructor -- ConvertFrom posixtime honours the time zone
%!assert_equal (char (datetime (0, 'ConvertFrom', 'posixtime', 'TimeZone', ...
%!              'America/New_York'), 'yyyy-MM-dd HH:mm:ss'), '1969-12-31 19:00:00')
%!assert_equal (char (datetime (0, 'ConvertFrom', 'posixtime', 'TimeZone', ...
%!              'Asia/Tokyo'), 'yyyy-MM-dd HH:mm:ss'), '1970-01-01 09:00:00')
%!assert_equal (isnat (datetime (NaN, 'ConvertFrom', 'posixtime', ...
%!              'TimeZone', 'UTC')), true)

## Test 'datetime.empty' -- empty datetime arrays
%!assert_equal (size (datetime.empty), [0, 0])
%!assert_equal (class (datetime.empty), 'datetime')
%!assert_equal (isempty (datetime.empty), true)
%!assert_equal (size (datetime.empty (2, 0)), [2, 0])
%!assert_equal (size (datetime.empty (0, 3)), [0, 3])
%!error <datetime.empty: at least one dimension must be zero for an empty array.> ...
%! datetime.empty (3)
## Test 'datetime.empty' -- size arguments follow the rules 'zeros' follows: a
## negative dimension counts as zero, a size vector holding none names no size
%!assert_equal (size (datetime.empty (0)), [0, 0])
%!assert_equal (size (datetime.empty (-1)), [0, 0])
%!assert_equal (size (datetime.empty (0, -1)), [0, 0])
%!assert_equal (size (datetime.empty (-2, -3)), [0, 0])
%!assert_equal (size (datetime.empty ([])), [0, 0])
%!assert_equal (size (datetime.empty (2, 0, 3)), [2, 0, 3])
%!error <datetime.empty: dimensions must be integer values.> ...
%! datetime.empty (1.5)

## Test 'datetime' constructor -- year/month/day/hour/minute must be integer
%!error <datetime: Year, Month, Day, Hour, and Minute components must be integer values.> ...
%! datetime (2024, 1, 1, 0, 30.5, 0)
%!error <datetime: Year, Month, Day, Hour, and Minute components must be integer values.> ...
%! datetime (2024.5, 1, 1)
%!error <datetime: Year, Month, Day, Hour, and Minute components must be integer values.> ...
%! datetime ([2024, 1, 15.5])
%!assert_equal (second (datetime ([2024, 1, 1, 0, 0, 30.5])), 30.5)

## Test 'datetime' constructor -- ConvertFrom yyyymmdd
%!assert_equal (char (datetime (20240309, 'ConvertFrom', 'yyyymmdd'), ...
%!              'yyyy-MM-dd'), '2024-03-09')
%!assert_equal (cellstr (datetime ([20240309, 20241231], ...
%!              'ConvertFrom', 'yyyymmdd')), {'09-Mar-2024', '31-Dec-2024'})
%!assert_equal (isnat (datetime (NaN, 'ConvertFrom', 'yyyymmdd')), true)
%!error <datetime: Year, Month, Day, Hour, and Minute components must be integer values.> ...
%! datetime (20240309.5, 'ConvertFrom', 'yyyymmdd')

## Test 'datetime' constructor -- TimeZone and Format properties
%!assert_equal (datetime (2020, 1, 1, 'TimeZone', 'UTC').TimeZone, 'UTC')
%!assert_equal (datetime (2020, 1, 1, 'Format', 'default').Format, 'dd-MMM-uuuu')
%!assert_equal (datetime (2020, 1, 1).TimeZone, '')

## Test 'datetime' constructor -- error branches
%!error <datetime: 'Format' must be a character vector.> ...
%! datetime (2020, 1, 1, 'Format', 5)
%!error <datetime: 'InputFormat' must be a character vector.> ...
%! datetime ('2020', 'InputFormat', 5)
%!error <datetime: 'PivotYear' can only be used with 'InputFormat'.> ...
%! datetime ('2020', 'PivotYear', 50)
%!error <datetime: 'PivotYear' must be a numeric scalar.> ...
%! datetime ('20', 'InputFormat', 'yy', 'PivotYear', 'x')
%!error <datetime: 'PivotYear' must be an integer value.> ...
%! datetime ('20', 'InputFormat', 'yy', 'PivotYear', 50.5)

## Test 'datetime' constructor -- 'InputFormat' LDML parsing (MATLAB-verified)
%!assert_equal (datevec (datetime ('09.03.2024', 'InputFormat', 'dd.MM.yyyy')), ...
%!              [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('2024/03/09 14:30', ...
%!              'InputFormat', 'yyyy/MM/dd HH:mm')), [2024, 3, 9, 14, 30, 0])
%!assert_equal (datevec (datetime ('09-Mar-2024', ...
%!              'InputFormat', 'dd-MMM-yyyy')), [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('March 9, 2024', ...
%!              'InputFormat', 'MMMM d, yyyy')), [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('9/3/24', 'InputFormat', 'd/M/yy')), ...
%!              [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('02:30 PM 09-Mar-2024', ...
%!              'InputFormat', 'hh:mm a dd-MMM-yyyy')), [2024, 3, 9, 14, 30, 0])
%!assert_equal (datevec (datetime ('12:00 AM 09-Mar-2024', ...
%!              'InputFormat', 'hh:mm a dd-MMM-yyyy')), [2024, 3, 9, 0, 0, 0])
%!assert_equal (second (datetime ('2024-03-09T14:30:45.678', ...
%!              'InputFormat', 'yyyy-MM-dd''T''HH:mm:ss.SSS')), 45.678, 1e-9)
%!assert_equal (datevec (datetime ('9.3.2024', 'InputFormat', 'dd.MM.yyyy')), ...
%!              [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('49', 'InputFormat', 'yy', ...
%!              'PivotYear', 2000)), [2049, 1, 1, 0, 0, 0])
%!assert_equal (year (datetime ('99', 'InputFormat', 'yy', ...
%!              'PivotYear', 1950)), 1999)
%!assert_equal (month (datetime ('49', 'InputFormat', 'yy')), 1)
%!error <datetime: could not parse the date/time string> ...
%! datetime ('09/03/2024', 'InputFormat', 'dd.MM.yyyy')
%!error <datetime: could not parse the date/time string> ...
%! datetime ('Foo 9, 2024', 'InputFormat', 'MMMM d, yyyy')
%!error <datetime: could not parse the date/time string> ...
%! datetime ('09.03.2024 extra', 'InputFormat', 'dd.MM.yyyy')

## Test 'datetime' constructor -- 'InputFormat' rejects a date that does not
## exist.  A lone string errors; within an array only that element is lost,
## becoming NaT (MATLAB-verified against R2024a).
%!error <datetime: could not parse the date/time string '2024-04-31' with 'InputFormat' 'yyyy-MM-dd'.> ...
%! datetime ('2024-04-31', 'InputFormat', 'yyyy-MM-dd')
%!error <datetime: could not parse the date/time string '2023-02-29' with 'InputFormat' 'yyyy-MM-dd'.> ...
%! datetime ('2023-02-29', 'InputFormat', 'yyyy-MM-dd')
%!error <datetime: could not parse the date/time string '2024-13-01' with 'InputFormat' 'yyyy-MM-dd'.> ...
%! datetime ('2024-13-01', 'InputFormat', 'yyyy-MM-dd')
%!error <datetime: could not parse the date/time string '2024-01-00' with 'InputFormat' 'yyyy-MM-dd'.> ...
%! datetime ('2024-01-00', 'InputFormat', 'yyyy-MM-dd')
%!error <datetime: could not parse the date/time string '2024-04-31' with 'InputFormat' 'yyyy-MM-dd'.> ...
%! datetime ({'2024-04-31'}, 'InputFormat', 'yyyy-MM-dd')
%!error <datetime: could not parse the date/time string '2024-01-01 25:00:00' with 'InputFormat' 'yyyy-MM-dd HH:mm:ss'.> ...
%! datetime ('2024-01-01 25:00:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss')
%!error <datetime: could not parse the date/time string '2024-01-01 12:60:00' with 'InputFormat' 'yyyy-MM-dd HH:mm:ss'.> ...
%! datetime ('2024-01-01 12:60:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss')
%!error <datetime: could not parse the date/time string '2024-01-01 12:00:60' with 'InputFormat' 'yyyy-MM-dd HH:mm:ss'.> ...
%! datetime ('2024-01-01 12:00:60', 'InputFormat', 'yyyy-MM-dd HH:mm:ss')
%!assert_equal (char (datetime ('2024-02-29', 'InputFormat', 'yyyy-MM-dd')), ...
%!              '29-Feb-2024')
%!test
%! d = datetime ({'2024-01-15', '2024-04-31'}, 'InputFormat', 'yyyy-MM-dd');
%! assert_equal (isnat (d), [false, true])
%! assert_equal (cellstr (d), {'15-Jan-2024', 'NaT'})
%!test
%! d = datetime ({'xx', '2024-01-15'}, 'InputFormat', 'yyyy-MM-dd');
%! assert_equal (isnat (d), [true, false])
%!test
%! ## A zone does not change the verdict.
%! d = datetime ({'2024-01-15', '2024-04-31'}, 'InputFormat', 'yyyy-MM-dd', ...
%!               'TimeZone', 'America/New_York');
%! assert_equal (isnat (d), [false, true])
%!test
%! ## A NaT carries no time of day, so a date-only array keeps its date-only
%! ## rendering when one sits beside it.
%! assert_equal (cellstr ([datetime(2024, 1, 15); NaT]), ...
%!               {'15-Jan-2024'; 'NaT'})
%! assert_equal (cellstr ([datetime(2024, 1, 15, 13, 30, 0); NaT]), ...
%!               {'15-Jan-2024 13:30:00'; 'NaT'})
%!test
%! ## An infinite datetime carries no time of day either, on the same grounds.
%! pinf = datetime (Inf, 'ConvertFrom', 'datenum');
%! ninf = datetime (-Inf, 'ConvertFrom', 'datenum');
%! assert_equal (cellstr ([datetime(2024, 1, 15); pinf]), ...
%!               {'15-Jan-2024'; 'Inf'})
%! assert_equal (cellstr ([datetime(2024, 1, 15); ninf]), ...
%!               {'15-Jan-2024'; '-Inf'})
%! assert_equal (cellstr ([datetime(2024, 1, 15, 13, 30, 0); pinf]), ...
%!               {'15-Jan-2024 13:30:00'; 'Inf'})
%! assert_equal ([datetime(2024, 1, 15); pinf].Format, 'dd-MMM-uuuu')
%!test
%! ## Beside a NaT as well, and an array of nothing but non-finite elements
%! ## says nothing either way, keeping the date and time pattern.
%! pinf = datetime (Inf, 'ConvertFrom', 'datenum');
%! assert_equal (cellstr ([datetime(2024, 1, 15); NaT; pinf]), ...
%!               {'15-Jan-2024'; 'NaT'; 'Inf'})
%! assert_equal ([pinf; pinf].Format, 'dd-MMM-uuuu HH:mm:ss')
%! assert_equal ([NaT; pinf].Format, 'dd-MMM-uuuu HH:mm:ss')

## Test 'datetime' constructor -- a datetime input is copied, components, time
## zone and display format all carrying over
%!test
%! d = datetime (2024, 1, 1, 12, 0, 0);
%! r = datetime (d);
%! assert_equal (datevec (r), datevec (d));
%! assert_equal (isempty (r.TimeZone), true);
%! assert_equal (r.Format, d.Format);
%!test
%! z = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! r = datetime (z);
%! assert_equal (r.TimeZone, 'America/New_York');
%! assert_equal (posixtime (r), posixtime (z));
%!test
%! ls = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! r = datetime (ls);
%! assert_equal (r.Second, 60);
%! assert_equal (r.TimeZone, 'UTCLeapSeconds');
%! assert_equal (r.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'");
%!assert_equal (isnat (datetime (NaT)), true)
%!assert_equal (size (datetime (datetime (2024, 1, [1, 2, 3]))), [1, 3])

## Test 'datetime' constructor -- copying accepts 'Format' and 'TimeZone', the
## zone attaching to an unzoned array and converting a zoned one
%!test
%! d = datetime (2024, 1, 1, 12, 0, 0);
%! r = datetime (d, 'Format', 'uuuu-MM-dd');
%! assert_equal (r.Format, 'uuuu-MM-dd');
%! assert_equal (char (r), '2024-01-01');
%!test
%! d = datetime (2024, 1, 1, 12, 0, 0);
%! r = datetime (d, 'TimeZone', 'UTC');
%! assert_equal (r.TimeZone, 'UTC');
%! assert_equal (r.Hour, 12);
%! assert_equal (posixtime (r), 1704110400);
%!test
%! z = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! r = datetime (z, 'TimeZone', 'UTC');
%! assert_equal (r.Hour, 17);
%! assert_equal (posixtime (r), 1704128400);
%!test
%! z = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! r = datetime (z, 'TimeZone', '');
%! assert_equal (isempty (r.TimeZone), true);
%! assert_equal (r.Hour, 12);

## Test 'datetime' constructor -- a change of zone while copying follows the
## same rule as assigning the property, so the leap-second zone may be both
## entered and left (MATLAB's constructor and property disagree here)
%!test
%! ls = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! r = datetime (ls, 'TimeZone', 'UTC');
%! assert_equal (datevec (r), [2016, 12, 31, 23, 59, 59]);
%! assert_equal (r.Format, 'dd-MMM-uuuu HH:mm:ss');
%!test
%! d = datetime (2024, 1, 1, 12, 0, 0);
%! r = datetime (d, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (r.TimeZone, 'UTCLeapSeconds');
%! assert_equal (r.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'");

## Test 'datetime' constructor -- the text-reading options have nothing to act
## on when copying and are ignored, while 'ConvertFrom' is an error
%!test
%! d = datetime (2024, 1, 1, 12, 0, 0);
%! assert_equal (datevec (datetime (d, 'InputFormat', 'uuuu-MM-dd')), datevec (d));
%! assert_equal (datevec (datetime (d, 'Locale', 'fr_FR')), datevec (d));
%! assert_equal (datevec (datetime (d, 'PivotYear', 1900)), datevec (d));
%!error <datetime: 'ConvertFrom' cannot be used with a datetime array input.> ...
%! datetime (datetime (2024, 1, 1), 'ConvertFrom', 'datenum')
%!error <datetime: a datetime array must be the only positional input.> ...
%! datetime (datetime (2024, 1, 1), datetime (2024, 1, 2))

## Test 'datetime' constructor -- elapsed time names no instant
%!error <datetime: input data must be a numeric array, a string array, a cell array of character vectors, or a character vector.> ...
%! datetime (days (3))
%!error <datetime: input data must be a numeric array, a string array, a cell array of character vectors, or a character vector.> ...
%! datetime (calmonths (3))

## Test 'datetime' constructor -- empty text names no date and becomes NaT,
## whereas a container holding no text at all gives an empty datetime
%!test
%! assert_equal (size (datetime ('')), [1, 1]);
%! assert_equal (isnat (datetime ('')), true);
%! assert_equal (isnat (datetime ({''})), true);
%! assert_equal (isnat (datetime (string (''))), true);
%! assert_equal (isnat (datetime (char (zeros (1, 0)))), true);
%!test
%! assert_equal (size (datetime ({})), [0, 0]);
%! assert_equal (isempty (datetime ({})), true);

## Test 'datetime' constructor -- a blank among readable dates costs only
## itself, so a blank field in imported data does not lose the column
%!test
%! d = datetime ({'2024-01-01', '', '2024-01-03'});
%! assert_equal (size (d), [1, 3]);
%! assert_equal (isnat (d), logical ([0, 1, 0]));
%! assert_equal (d.Day, [1, NaN, 3]);
%!test
%! d = datetime ({'2024-01-01'; ''});
%! assert_equal (size (d), [2, 1]);
%! assert_equal (isnat (d), logical ([0; 1]));
%!test
%! d = datetime ({'2024-01-01', ''}, 'InputFormat', 'uuuu-MM-dd');
%! assert_equal (isnat (d), logical ([0, 1]));
%!test
%! d = datetime ({'2024-01-01', ''}, 'TimeZone', 'America/New_York');
%! assert_equal (isnat (d), logical ([0, 1]));
%! assert_equal (d.TimeZone, 'America/New_York');
%!test
%! d = datetime ({'2016-12-31T23:59:60Z', ''}, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (isnat (d), logical ([0, 1]));
%! assert_equal (d.Second(1), 60);

## Test 'datetime' constructor -- a character matrix holds one date per row,
## giving a column, with trailing blanks trimmed
%!test
%! d = datetime (['2024-01-01'; '2024-01-02']);
%! assert_equal (size (d), [2, 1]);
%! assert_equal (d.Day, [1; 2]);
%!test
%! d = datetime (['01-Jan-2024'; '02-Jan-2024'; '03-Jan-2024']);
%! assert_equal (size (d), [3, 1]);
%! assert_equal (d.Day, [1; 2; 3]);
%!assert_equal (size (datetime (['2024-01-01'])), [1, 1])
%!test
%! d = datetime (['2024-01-01  '; '2024-01-02  ']);
%! assert_equal (d.Day, [1; 2]);
%!test
%! d = datetime (['2024-01-01'; '2024-01-02'], 'InputFormat', 'uuuu-MM-dd');
%! assert_equal (d.Day, [1; 2]);
%!test
%! d = datetime (['2024-01-01'; '2024-01-02'], 'TimeZone', 'UTC');
%! assert_equal (size (d), [2, 1]);
%! assert_equal (d.TimeZone, 'UTC');

## Test 'datetime' constructor -- a blank row of a character matrix names no
## date, while a matrix with no rows holds no text at all
%!test
%! d = datetime (['2024-01-01'; '          ']);
%! assert_equal (size (d), [2, 1]);
%! assert_equal (isnat (d), logical ([0; 1]));
%!assert_equal (size (datetime (char (zeros (0, 10)))), [0, 1])
%!assert_equal (size (datetime ('')), [1, 1])

## Test 'datetime' constructor -- a character array of more than two dimensions
## is refused
%!error <datetime: invalid type for 'DateStrings'.> ...
%! datetime (reshape (repmat ('a', 1, 8), 2, 2, 2))

## Test 'cat' -- empty text adds one missing element, an empty container none
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! assert_equal (isnat ([ny, '']), logical ([0, 1]));
%! assert_equal (isnat ([ny, {''}]), logical ([0, 1]));
%! assert_equal ([ny, ''].TimeZone, 'America/New_York');
%! assert_equal (size ([ny, {}]), [1, 1]);
%! assert_equal (size ([ny, []]), [1, 1]);

## Test 'datetime' constructor -- format auto-detection (no 'InputFormat')
%!assert_equal (datevec (datetime ('2024-03-09')), [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('2024-03-09 14:30:45')), ...
%!              [2024, 3, 9, 14, 30, 45])
%!assert_equal (datevec (datetime ('2024/03/09')), [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('03/09/2024')), [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('09-Mar-2024')), [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('09-March-2024')), [2024, 3, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ('09-Mar-2024 14:30:45')), ...
%!              [2024, 3, 9, 14, 30, 45])
%!assert_equal (datevec (datetime ('2024-2-9')), [2024, 2, 9, 0, 0, 0])
%!assert_equal (datevec (datetime ({'2024-03-09'; '2024-12-31'})), ...
%!              [2024, 3, 9, 0, 0, 0; 2024, 12, 31, 0, 0, 0])
%!error <datetime: could not recognize date/time format from input.> ...
%! datetime ('2024-13-09')
%!error <datetime: could not recognize date/time format from input.> ...
%! datetime ('2024-03-32')
%!error <datetime: could not recognize date/time format from input.> ...
%! datetime ('2024-03-09 25:00:00')

## Test 'datetime' constructor -- the auto-detected repertoire, widened toward
## MATLAB's: a 'T' separator, fractional seconds, a time without seconds, and
## the 'Month d, yyyy' form are read
%!assert_equal (datevec (datetime ('2024-03-09T14:30:45')), ...
%!              [2024, 3, 9, 14, 30, 45])
%!assert_equal (datevec (datetime ('2024-03-09T14:30:45.678')), ...
%!              [2024, 3, 9, 14, 30, 45.678])
%!assert_equal (datevec (datetime ('2024-03-09 13:45:00.123')), ...
%!              [2024, 3, 9, 13, 45, 0.123])
%!assert_equal (datevec (datetime ('2024-03-09 14:30')), ...
%!              [2024, 3, 9, 14, 30, 0])
%!assert_equal (datevec (datetime ('March 15, 2024')), [2024, 3, 15, 0, 0, 0])

## Test 'datetime' constructor -- one format is detected and read for the whole
## array, so text in another format is lost rather than read on its own terms
%!test
%! d = datetime ({'2024-03-09'; '09-Mar-2024'});
%! assert_equal (isnat (d), logical ([0; 1]));
%! assert_equal (d.Day(1), 9);
%!test
%! d = datetime ({'2024-03-09 14:30:45'; '2024-03-10'});
%! assert_equal (isnat (d), logical ([0; 1]));
%!assert_equal (datevec (datetime ({'2024-03-09'; '2024-12-31'})), ...
%!              [2024, 3, 9, 0, 0, 0; 2024, 12, 31, 0, 0, 0])

## Test 'datetime' constructor -- the format is detected from the first string
## that is not blank, and text none can be detected from is refused
%!test
%! d = datetime ({''; '2024-03-09'});
%! assert_equal (isnat (d), logical ([1; 0]));
%! assert_equal (d.Day(2), 9);
%!error <datetime: could not recognize the date/time format of 'not-a-date'.> ...
%! datetime ({'not-a-date'; '2024-03-09'})

## Test 'datetime' constructor -- a date that does not exist is refused under
## the detected format, as it is under an explicit one
%!error <datetime: could not recognize date/time format from input.> ...
%! datetime ('2024-04-31')
%!error <datetime: could not recognize date/time format from input.> ...
%! datetime ('2024-02-30')
%!error <datetime: could not recognize date/time format from input.> ...
%! datetime ('2024-03-09 13:45:60')

## Test 'datetime' constructor -- 'MixedFormats' restores per-string detection,
## an Octave extension for text gathered from several sources
%!test
%! d = datetime ({'2024-03-09'; '09-Mar-2024'}, 'MixedFormats', true);
%! assert_equal (isnat (d), logical ([0; 0]));
%! assert_equal (datevec (d), [2024, 3, 9, 0, 0, 0; 2024, 3, 9, 0, 0, 0]);
%!assert_equal (datevec (datetime ('2024-04-31', 'MixedFormats', true)), ...
%!              [2024, 5, 1, 0, 0, 0])
%!assert_equal (datevec (datetime ('2024-02-30', 'MixedFormats', true)), ...
%!              [2024, 3, 1, 0, 0, 0])
%!assert_equal (datevec (datetime ('2024-03-09 13:45:60', ...
%!              'MixedFormats', true)), [2024, 3, 9, 13, 46, 0])
%!error <datetime: 'MixedFormats' must be a logical scalar.> ...
%! datetime ('2024-03-09', 'MixedFormats', 'yes')

## Test 'datetime' constructor -- a slash date is ambiguous unless the text
## settles it, and the whole array is consulted
%!test
%! warning ('off', 'Octave:datetime:ambiguous-format', 'local');
%! assert_equal (datevec (datetime ('03/09/2024')), [2024, 3, 9, 0, 0, 0]);
%! assert_equal (datevec (datetime ({'01/02/2024'; '15/03/2024'})), ...
%!               [2024, 2, 1, 0, 0, 0; 2024, 3, 15, 0, 0, 0]);
%! assert_equal (datevec (datetime ({'15/03/2024'; '01/02/2024'})), ...
%!               [2024, 3, 15, 0, 0, 0; 2024, 2, 1, 0, 0, 0]);
%!assert_equal (datevec (datetime ('13/09/2024')), [2024, 9, 13, 0, 0, 0])
%!warning <datetime: the text was read with the format 'MM/dd/yyyy', but 'dd/MM/yyyy' would read it just as well; give 'InputFormat' to say which is meant.> ...
%! datetime ('03/09/2024');

## Test 'datetime' constructor -- 'Locale' localized month names (MATLAB-verified)
%!assert_equal (month (datetime ('9 février 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'fr_FR')), 2)
%!assert_equal (month (datetime ('9 janv. 2024', ...
%!              'InputFormat', 'd MMM yyyy', 'Locale', 'fr_FR')), 1)
%!assert_equal (month (datetime ('9 März 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'de_DE')), 3)
%!assert_equal (month (datetime ('9 Sept. 2024', ...
%!              'InputFormat', 'd MMM yyyy', 'Locale', 'de_DE')), 9)
%!assert_equal (month (datetime ('9 septiembre 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'es_ES')), 9)
%!assert_equal (month (datetime ('9 sept 2024', ...
%!              'InputFormat', 'd MMM yyyy', 'Locale', 'es_ES')), 9)
%!assert_equal (month (datetime ('9 maggio 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'it_IT')), 5)
%!assert_equal (month (datetime ('9 set 2024', ...
%!              'InputFormat', 'd MMM yyyy', 'Locale', 'it_IT')), 9)
%!assert_equal (month (datetime ('9 março 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'pt_BR')), 3)
%!assert_equal (month (datetime ('9 dez. 2024', ...
%!              'InputFormat', 'd MMM yyyy', 'Locale', 'pt_BR')), 12)
%!assert_equal (month (datetime ('MARS', 'InputFormat', 'MMMM', ...
%!              'Locale', 'fr_FR')), 3)
%!assert_equal (month (datetime ('March', 'InputFormat', 'MMMM', ...
%!              'Locale', 'system')), 3)
%!error <datetime: could not parse the date/time string> ...
%! datetime ('9 mars 2024', 'InputFormat', 'd MMMM yyyy')
%!error <datetime: unsupported 'Locale' 'ja_JP'; supported languages are en, fr, de, es, it, pt, and el.> ...
%! datetime ('9 marzo 2024', 'InputFormat', 'd MMMM yyyy', 'Locale', 'ja_JP')
%!error <datetime: 'Locale' must be a character vector.> ...
%! datetime ('2024', 'InputFormat', 'yyyy', 'Locale', 5)
%!test
%! ## Locale name tables are memoized, so alternating locales within a session
%! ## must keep resolving against their own tables.
%! a = month (datetime ('9 mars 2024', 'InputFormat', 'd MMMM yyyy', ...
%!                      'Locale', 'fr'));
%! b = month (datetime ('9 März 2024', 'InputFormat', 'd MMMM yyyy', ...
%!                      'Locale', 'de'));
%! c = month (datetime ('9 mars 2024', 'InputFormat', 'd MMMM yyyy', ...
%!                      'Locale', 'fr'));
%! d = month (datetime ('9 March 2024', 'InputFormat', 'd MMMM yyyy'));
%! assert_equal ([a, b, c, d], [3, 3, 3, 3])

## Test 'datetime' constructor -- weekday name fields (consumed, but validated)
%!assert_equal (day (datetime ('7 09-Mar-2024', 'InputFormat', 'e dd-MMM-yyyy')), 9)
%!assert_equal (day (datetime ('07 09-Mar-2024', ...
%!              'InputFormat', 'ee dd-MMM-yyyy')), 9)
%!assert_equal (day (datetime ('Saturday, 09-Mar-2024', ...
%!              'InputFormat', 'eeee, dd-MMM-yyyy')), 9)
%!assert_equal (day (datetime ('Sat, 09-Mar-2024', ...
%!              'InputFormat', 'eee, dd-MMM-yyyy')), 9)
%!assert_equal (day (datetime ('vendredi 09.03.2024', ...
%!              'InputFormat', 'eeee dd.MM.yyyy', 'Locale', 'fr_FR')), 9)
%!assert_equal (day (datetime ('Sa. 09.03.2024', ...
%!              'InputFormat', 'eee dd.MM.yyyy', 'Locale', 'de_DE')), 9)
%!assert_equal (day (datetime ('sábado 09.03.2024', ...
%!              'InputFormat', 'eeee dd.MM.yyyy', 'Locale', 'es_ES')), 9)
%!assert_equal (day (datetime ('venerdì 09.03.2024', ...
%!              'InputFormat', 'eeee dd.MM.yyyy', 'Locale', 'it_IT')), 9)
%!assert_equal (day (datetime ('segunda-feira, 11.03.2024', ...
%!              'InputFormat', 'eeee, dd.MM.yyyy', 'Locale', 'pt_BR')), 11)
%!error <datetime: could not parse the date/time string> ...
%! datetime ('Xyzzy, 09-Mar-2024', 'InputFormat', 'eeee, dd-MMM-yyyy')

## Test 'datetime' constructor -- localized day-period ('a') markers.  Most
## locales use 'AM'/'PM'; Spanish uses "a. m."/"p. m." with a UTF-8 no-break
## space (U+00A0 = bytes 194 160).
%!assert_equal (hour (datetime ('02:30 PM 09-Mar-2024', ...
%!              'InputFormat', 'hh:mm a dd-MMM-yyyy')), 14)
%!assert_equal (hour (datetime ('02:30 PM 09.03.2024', ...
%!              'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'fr_FR')), 14)
%!assert_equal (hour (datetime ('02:30 AM 09.03.2024', ...
%!              'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'de_DE')), 2)
%!assert_equal (hour (datetime (['02:30 p.', char([194, 160]), 'm. 09.03.2024'], ...
%!              'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'es_ES')), 14)
%!assert_equal (hour (datetime (['02:30 a.', char([194, 160]), 'm. 09.03.2024'], ...
%!              'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'es_ES')), 2)
%!error <datetime: could not parse the date/time string> ...
%! datetime ('02:30 PM 09.03.2024', 'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'es_ES')

## Test 'datetime' constructor -- Greek locale ('el').  Month names are the
## genitive (format) forms; day-period is 'π.μ.'/'μ.μ.'.
%!assert_equal (month (datetime ('9 Μαρτίου 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 3)
%!assert_equal (month (datetime ('9 Μαΐου 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 5)
%!assert_equal (month (datetime ('9 Αυγ 2024', ...
%!              'InputFormat', 'd MMM yyyy', 'Locale', 'el_GR')), 8)
%!assert_equal (day (datetime ('Σάββατο, 09.03.2024', ...
%!              'InputFormat', 'eeee, dd.MM.yyyy', 'Locale', 'el_GR')), 9)
%!assert_equal (day (datetime ('Κυρ, 10.03.2024', ...
%!              'InputFormat', 'eee, dd.MM.yyyy', 'Locale', 'el_GR')), 10)
%!assert_equal (hour (datetime ('02:30 μ.μ. 09.03.2024', ...
%!              'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'el_GR')), 14)
%!assert_equal (hour (datetime ('02:30 π.μ. 09.03.2024', ...
%!              'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'el_GR')), 2)
%!error <datetime: could not parse the date/time string> ...
%! datetime ('9 Μάρτιος 2024', 'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')

## Test 'datetime' constructor -- case/accent folding of names.  Latin names are
## case-insensitive; Greek names are case- AND accent-insensitive, so the
## accentless all-caps spelling (idiomatic in Greek) is accepted.
%!assert_equal (month (datetime ('9 MARCH 2024', 'InputFormat', 'd MMMM yyyy')), 3)
%!assert_equal (month (datetime ('9 février 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'fr_FR')), 2)
%!assert_equal (month (datetime ('9 μαρτίου 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 3)
%!assert_equal (month (datetime ('9 ΜΑΡΤΊΟΥ 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 3)
%!assert_equal (month (datetime ('9 ΜΑΡΤΙΟΥ 2024', ...
%!              'InputFormat', 'd MMMM yyyy', 'Locale', 'el_GR')), 3)
%!assert_equal (day (datetime ('ΣΑΒΒΑΤΟ, 09.03.2024', ...
%!              'InputFormat', 'eeee, dd.MM.yyyy', 'Locale', 'el_GR')), 9)
%!assert_equal (hour (datetime ('02:30 Μ.Μ. 09.03.2024', ...
%!              'InputFormat', 'hh:mm a dd.MM.yyyy', 'Locale', 'el_GR')), 14)
%!error <datetime: could not parse the date/time string> ...
%! datetime ('ΞΞΞ, 09.03.2024', 'InputFormat', 'eeee, dd.MM.yyyy', 'Locale', 'el_GR')

## Test 'dispstrings' and 'cellstr'
%!assert_equal (dispstrings (datetime (2020, 1, 1)), {'01-Jan-2020'})
%!assert_equal (dispstrings (datetime (2020, 1, 1, 12, 30, 0)), {'01-Jan-2020 12:30:00'})
%!assert_equal (dispstrings (datetime (2020, 1, 1, 12, 30, 45.5)), {'01-Jan-2020 12:30:45'})
%!assert_equal (cellstr (datetime (2020, 1, 1)), {'01-Jan-2020'})
%!assert_equal (cellstr (datetime (2020, 12, 25)), {'25-Dec-2020'})
%!assert_equal (cellstr (NaT), {'NaT'})
%!assert_equal (class (cellstr (datetime (2020, 1, 1))), 'cell')
%!assert_equal (size (cellstr (datetime (2020, 1, [1; 2; 3]))), [3, 1])

## Test 'char'
%!assert_equal (char (datetime (2020, 1, 1)), '01-Jan-2020')
%!assert_equal (class (char (datetime (2020, 1, 1))), 'char')
%!assert_equal (rows (char (datetime (2020, 1, [1; 2]))), 2)

## Test Format seam -- 'default' is a data-dependent sentinel (date-only when
## every element sits at midnight, date+time when any time-of-day is nonzero)
%!assert_equal (datetime (2026, 3, 9).Format, 'dd-MMM-uuuu')
%!assert_equal (datetime (2026, 3, 9, 0, 0, 0).Format, 'dd-MMM-uuuu')
%!assert_equal (datetime (2026, 3, 9, 0, 0, 0.5).Format, 'dd-MMM-uuuu HH:mm:ss')
%!assert_equal (datetime (2026, 3, 9, 13, 0, 0).Format, 'dd-MMM-uuuu HH:mm:ss')
%!assert_equal (datetime (NaN, 1, 1).Format, 'dd-MMM-uuuu HH:mm:ss')
%!test
%! m = [datetime(2026,3,9,0,0,0); datetime(2026,3,9,13,0,0)];
%! assert_equal (m.Format, 'dd-MMM-uuuu HH:mm:ss');
%! assert_equal (dispstrings (m), {'09-Mar-2026 00:00:00'; '09-Mar-2026 13:00:00'});
%!test
%! m = [datetime(2026,3,9,0,0,0); datetime(2026,3,10,0,0,0)];
%! assert_equal (m.Format, 'dd-MMM-uuuu');

## Test Format seam -- 'default' re-resolves after arithmetic; 'defaultdate'
## is always date-only regardless of the data
%!test
%! d = datetime (2026, 3, 9) + hours (5);
%! assert_equal (d.Format, 'dd-MMM-uuuu HH:mm:ss');
%! assert_equal (char (d), '09-Mar-2026 05:00:00');
%!test
%! d = datetime (2026, 3, 9, 1, 2, 3);
%! d.Format = 'defaultdate';
%! assert_equal (d.Format, 'dd-MMM-uuuu');
%! assert_equal (char (d), '09-Mar-2026');

## Test Format seam -- default display omits sub-seconds (whole second shown)
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3.75)), '09-Mar-2026 14:05:03')

## Test Format seam -- LDML year/month/day fields
%!assert_equal (char (datetime (2026, 3, 9), 'y'), '2026')
%!assert_equal (char (datetime (2026, 3, 9), 'yy'), '26')
%!assert_equal (char (datetime (2026, 3, 9), 'yyyy'), '2026')
%!assert_equal (char (datetime (26, 3, 9), 'yyyy'), '0026')
%!assert_equal (char (datetime (2026, 3, 9), 'uuuuu'), '02026')
%!assert_equal (char (datetime (2026, 3, 9), 'M'), '3')
%!assert_equal (char (datetime (2026, 3, 9), 'MM'), '03')
%!assert_equal (char (datetime (2026, 3, 9), 'MMM'), 'Mar')
%!assert_equal (char (datetime (2026, 3, 9), 'MMMM'), 'March')
%!assert_equal (char (datetime (2026, 3, 9), 'MMMMM'), 'M')
%!assert_equal (char (datetime (2026, 3, 9), 'd'), '9')
%!assert_equal (char (datetime (2026, 3, 9), 'dd'), '09')
%!assert_equal (char (datetime (2026, 3, 9), 'D'), '68')
%!assert_equal (char (datetime (2026, 3, 9), 'DDD'), '068')

## Test Format seam -- LDML time, fractional, and meridiem fields
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'H'), '14')
%!assert_equal (char (datetime (2026, 3, 9, 9, 5, 3), 'H'), '9')
%!assert_equal (char (datetime (2026, 3, 9, 9, 5, 3), 'HH'), '09')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'h'), '2')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'hh'), '02')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'mm'), '05')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'ss'), '03')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3.75), 'ss'), '03')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3.5), 'ss.SSS'), '03.500')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'a'), 'PM')
%!assert_equal (char (datetime (2026, 3, 9, 9, 5, 3), 'a'), 'AM')

## Test Format seam -- 12-hour clock edges (midnight and noon)
%!assert_equal (char (datetime (2026, 3, 9, 0, 5, 3), 'hh:mm a'), '12:05 AM')
%!assert_equal (char (datetime (2026, 3, 9, 12, 5, 3), 'hh:mm a'), '12:05 PM')
%!assert_equal (char (datetime (2026, 3, 9, 23, 5, 3), 'hh:mm a'), '11:05 PM')

## Test Format seam -- weekday (Sunday = 1), quarter, era, week of month
%!assert_equal (char (datetime (2026, 3, 9), 'e'), '2')
%!assert_equal (char (datetime (2026, 3, 9), 'ee'), '02')
%!assert_equal (char (datetime (2026, 3, 9), 'eee'), 'Mon')
%!assert_equal (char (datetime (2026, 3, 9), 'eeee'), 'Monday')
%!assert_equal (char (datetime (2026, 3, 8), 'eee'), 'Sun')
%!assert_equal (char (datetime (2026, 3, 9), 'Q'), '1')
%!assert_equal (char (datetime (2026, 7, 9), 'Q'), '3')
%!assert_equal (char (datetime (2026, 3, 9), 'QQQ'), 'Q1')
%!assert_equal (char (datetime (2026, 3, 9), 'QQQQ'), '1st quarter')
%!assert_equal (char (datetime (2026, 3, 9), 'G'), 'CE')
%!assert_equal (char (datetime (2026, 3, 9), 'W'), '2')

## Test Format seam -- quoted literals and full mixed patterns
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'yyyy-MM-dd''T''HH:mm:ss'), ...
%!              '2026-03-09T14:05:03')
%!assert_equal (char (datetime (2026, 3, 9), 'MM/dd/yyyy'), '03/09/2026')
%!assert_equal (char (datetime (2026, 3, 9), '''Day ''d'' of ''MMMM'), 'Day 9 of March')
%!assert_equal (char (datetime (2026, 3, 9), 'eeee, MMMM d, yyyy'), 'Monday, March 9, 2026')

## Test Format seam -- time-zone offset fields (DST-aware; unzoned -> asterisks)
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', ...
%!              'America/New_York'), 'Z'), '-0400')
%!assert_equal (char (datetime (2026, 1, 9, 14, 5, 3, 'TimeZone', ...
%!              'America/New_York'), 'Z'), '-0500')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', ...
%!              'America/New_York'), 'ZZZZ'), 'UTC-04:00')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', ...
%!              'America/New_York'), 'XXX'), '-04:00')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', 'UTC'), 'X'), 'Z')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', 'UTC'), 'x'), '+00')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3, 'TimeZone', 'UTC'), 'Z'), '+0000')
%!assert_equal (char (datetime (2026, 3, 9, 14, 5, 3), 'ZZZZ'), '****')

## Test Format seam -- unsupported format symbols are rejected
%!error <datetime: the format 'yyyy-QQ-EE' contains an unsupported symbol: 'E'.> ...
%! datetime (2026, 3, 9, 'Format', 'yyyy-QQ-EE')
%!error <datetime: the format 'bad' contains an unsupported symbol: 'b'.> ...
%! d = datetime (2026, 3, 9); d.Format = 'bad';

## Test __datetime__ 'zoneabbrev' builtin mode -- the compiled foundation for
## the datetime 'z' format field: DST-aware IANA zone abbreviations, empty for
## NaT/Inf, and a cell array matching the input size.
%!assert_equal (__datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', ...
%!              'zoneabbrev', 'TimeZone', 'America/New_York', ...
%!              'Precision', 'microseconds'), {'EDT'})
%!assert_equal (__datetime__ (2026, 1, 9, 14, 0, 0, 'ConvertTo', ...
%!              'zoneabbrev', 'TimeZone', 'America/New_York', ...
%!              'Precision', 'microseconds'), {'EST'})
%!assert_equal (__datetime__ (2026, 7, 9, 12, 0, 0, 'ConvertTo', ...
%!              'zoneabbrev', 'TimeZone', 'Europe/Athens', ...
%!              'Precision', 'microseconds'), {'EEST'})
%!assert_equal (__datetime__ (2026, 1, 9, 12, 0, 0, 'ConvertTo', ...
%!              'zoneabbrev', 'TimeZone', 'Europe/Athens', ...
%!              'Precision', 'microseconds'), {'EET'})
%!assert_equal (__datetime__ (2026, 3, 9, 14, 0, 0, 'ConvertTo', ...
%!              'zoneabbrev', 'TimeZone', 'UTC', ...
%!              'Precision', 'microseconds'), {'UTC'})
%!assert_equal (__datetime__ (2026, [3; 7], 9, 14, 0, 0, 'ConvertTo', ...
%!              'zoneabbrev', 'TimeZone', 'America/New_York', ...
%!              'Precision', 'microseconds'), {'EDT'; 'EDT'})
%!assert_equal (__datetime__ (NaN, 1, 1, 0, 0, 0, 'ConvertTo', ...
%!              'zoneabbrev', 'TimeZone', 'UTC', ...
%!              'Precision', 'microseconds'), {''})
%!assert_equal (__datetime__ (Inf, 1, 1, 0, 0, 0, 'ConvertTo', ...
%!              'zoneabbrev', 'TimeZone', 'UTC', ...
%!              'Precision', 'microseconds'), {''})

## Test Format seam -- time-zone name style: default is the Octave-specific
## 'iana', unzoned renders asterisks, invalid style is rejected.
%!assert_equal (datetime.zoneNameStyle (), 'iana')
%!assert_equal (char (datetime (2026, 3, 9), 'z'), '*')
%!error <datetime.zoneNameStyle: STYLE must be 'iana' or 'matlab'.> ...
%! datetime.zoneNameStyle ('bogus')

## Test Format seam -- 'iana' style shows the IANA abbreviation for every zone.
%!test
%! old = datetime.zoneNameStyle ();
%! unwind_protect
%!   datetime.zoneNameStyle ('iana');
%!   assert_equal (datetime.zoneNameStyle (), 'iana');
%!   assert_equal (char (datetime (2026, 3, 9, 14, 0, 0, 'TimeZone', ...
%!                 'America/New_York'), 'z'), 'EDT');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'Europe/Athens'), 'z'), 'EEST');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'Asia/Tokyo'), 'z'), 'JST');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'Asia/Shanghai'), 'z'), 'CST');
%! unwind_protect_cleanup
%!   datetime.zoneNameStyle (old);
%! end_unwind_protect

## Test Format seam -- 'matlab' style: named abbreviation only for North
## American zones (plus GMT/UTC), numeric UTC offset elsewhere, with the
## (abbrev, offset) key resolving collisions (Shanghai/Havana).
%!test
%! old = datetime.zoneNameStyle ();
%! unwind_protect
%!   datetime.zoneNameStyle ('matlab');
%!   assert_equal (char (datetime (2026, 3, 9, 14, 0, 0, 'TimeZone', ...
%!                 'America/New_York'), 'z'), 'EDT');
%!   assert_equal (char (datetime (2026, 1, 9, 14, 0, 0, 'TimeZone', ...
%!                 'America/New_York'), 'z'), 'EST');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'America/Mexico_City'), 'z'), 'CST');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'Pacific/Honolulu'), 'z'), 'HST');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'Europe/Athens'), 'z'), 'UTC+3');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'Asia/Tokyo'), 'z'), 'UTC+9');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'Asia/Kolkata'), 'z'), 'UTC+5:30');
%!   assert_equal (char (datetime (2026, 1, 15, 12, 0, 0, 'TimeZone', ...
%!                 'America/St_Johns'), 'z'), 'UTC-3:30');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'Asia/Shanghai'), 'z'), 'UTC+8');
%!   assert_equal (char (datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', ...
%!                 'America/Havana'), 'z'), 'UTC-4');
%!   assert_equal (char (datetime (2026, 3, 9, 14, 0, 0, 'TimeZone', ...
%!                 'UTC'), 'z'), 'UTC');
%! unwind_protect_cleanup
%!   datetime.zoneNameStyle (old);
%! end_unwind_protect

## Test Format seam -- per-format overrides ignore the session style: 'zzzz'
## forces 'iana', 'zzzzz' forces 'matlab'.
%!test
%! old = datetime.zoneNameStyle ();
%! unwind_protect
%!   ath = datetime (2026, 7, 9, 12, 0, 0, 'TimeZone', 'Europe/Athens');
%!   datetime.zoneNameStyle ('matlab');
%!   assert_equal (char (ath, 'zzzz'), 'EEST');
%!   datetime.zoneNameStyle ('iana');
%!   assert_equal (char (ath, 'zzzzz'), 'UTC+3');
%! unwind_protect_cleanup
%!   datetime.zoneNameStyle (old);
%! end_unwind_protect

## Test Format seam -- a NaT in the array renders under every field, including
## the weekday fields, which used to abort the whole array
%!test
%! dt = [datetime(2024, 3, 9); NaT];
%! assert_equal (cellstr (dt, 'eeee'), {'Saturday'; 'NaT'})
%! assert_equal (cellstr (dt, 'eee, dd MMM yyyy'), ...
%!               {'Sat, 09 Mar 2024'; 'NaT'})
%! assert_equal (cellstr (dt, 'e'), {'7'; 'NaT'})
%! assert_equal (cellstr (dt, 'D'), {'69'; 'NaT'})
%! assert_equal (cellstr (dt, 'W'), {'2'; 'NaT'})
%!test
%! dt = [datetime(2024, 3, 9); NaT; datetime(2024, 12, 25)];
%! assert_equal (cellstr (dt, 'eeee'), {'Saturday'; 'NaT'; 'Wednesday'})

## Test Format seam -- every LDML date/time field renders MATLAB-exactly
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'yyyy'), '2024')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'yy'), '24')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'MMMM'), 'March')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'MMM'), 'Mar')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'MMMMM'), 'M')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'dd'), '09')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'D'), '69')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'eeee'), 'Saturday')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'eee'), 'Sat')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'a'), 'PM')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'hh:mm a'), '02:30 PM')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'HH'), '14')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'QQQ'), 'Q1')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'G'), 'CE')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'ss'), '45')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), ...
%!              'eee, dd MMM yyyy HH:mm:ss'), 'Sat, 09 Mar 2024 14:30:45')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45.678), 'ss.S'), '45.6')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45.678), 'ss.SS'), '45.67')
%!assert_equal (char (datetime (2024, 1, 1, 0, 0, 45.678), 'ss.SSS'), '45.678')
%!assert_equal (char (datetime (2024, 1, 1, 0, 0, 45.001), 'ss.SSS'), '45.001')
%!assert_equal (char (datetime (2024, 1, 1, 0, 0, 45.999), 'ss.SSS'), '45.999')
%!assert_equal (char (datetime (2024, 1, 1, 0, 0, 12.5), 'ss.SSS'), '12.500')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'h:mm a'), '2:30 PM')
%!assert_equal (char (datetime (2024, 1, 1, 0, 0, 0), 'h a'), '12 AM')
%!assert_equal (char (datetime (2024, 1, 1, 12, 0, 0), 'h a'), '12 PM')
%!assert_equal (char (datetime (2024, 1, 1, 0, 0, 0), 'HH'), '00')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'e'), '7')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'ee'), '07')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'u'), '2024')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'QQQQ'), '1st quarter')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'QQ'), '01')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), 'MM'), '03')
%!assert_equal (char (datetime (2024, 3, 9, 14, 30, 45), ...
%!              'yyyy/MM/dd''T''HH:mm'), '2024/03/09T14:30')

## Test 'ymd'
%!test
%! [y, m, d] = ymd (datetime (2020, 7, 4));
%! assert_equal (y, 2020);
%! assert_equal (m, 7);
%! assert_equal (d, 4);
%!test
%! [y, m, d] = ymd (datetime (2020, 1, [1; 2]));
%! assert_equal (y, [2020; 2020]);
%! assert_equal (d, [1; 2]);

## Test 'hms'
%!test
%! [h, m, s] = hms (datetime (2020, 1, 1, 13, 45, 30.5));
%! assert_equal (h, 13);
%! assert_equal (m, 45);
%! assert_equal (s, 30.5);

## Test 'year'
%!assert_equal (year (datetime (2020, 1, 1)), 2020)
%!assert_equal (year (datetime ([1999, 2, 1; 2021, 3, 1])), [1999; 2021])
%!assert_equal (year (NaT), NaN)

## Test 'quarter'
%!assert_equal (quarter (datetime (2020, 1, 1)), 1)
%!assert_equal (quarter (datetime (2020, 4, 1)), 2)
%!assert_equal (quarter (datetime (2020, 7, 1)), 3)
%!assert_equal (quarter (datetime (2020, 10, 1)), 4)
%!assert_equal (quarter (datetime (2020, 12, 31)), 4)
%!assert_equal (quarter (datetime (2020, [1, 4, 7, 10], 1)), [1, 2, 3, 4])

## Test 'month' with type options
%!assert_equal (month (datetime (2020, 6, 1)), 6)
%!assert_equal (month (datetime (2020, 6, 1), 'monthofyear'), 6)
%!assert_equal (month (datetime (2020, 7, 1), 'name'), {'July'})
%!assert_equal (month (datetime (2020, 7, 1), 'shortname'), {'Jul'})
%!assert_equal (month (datetime (2020, 1, 1), 'name'), {'January'})
%!assert_equal (month (datetime (2020, 12, 1), 'shortname'), {'Dec'})
%!error <datetime: unrecognized MONTHTYPE.> month (datetime (2020, 1, 1), 'bogus')

## Test 'day' with dayofmonth (default)
%!assert_equal (day (datetime (2020, 6, 15)), 15)
%!assert_equal (day (datetime (2020, 6, 15), 'dayofmonth'), 15)
%!assert_equal (day (datetime (2020, 1, [1; 15; 31])), [1; 15; 31])

## Test 'day' with dayofweek (1=Sunday..7=Saturday)
%!assert_equal (day (datetime (2020, 1, 1), 'dayofweek'), 4)
%!assert_equal (day (datetime (2000, 1, 1), 'dayofweek'), 7)
%!assert_equal (day (datetime (2020, 2, 29), 'dayofweek'), 7)
%!assert_equal (day (datetime (2024, 2, 29), 'dayofweek'), 5)
%!assert_equal (day (datetime (1900, 1, 1), 'dayofweek'), 2)
%!assert_equal (day (datetime (2020, 3, 1), 'dayofweek'), 1)
%!assert_equal (day (datetime (2020, 1, [1; 8; 15]), 'dayofweek'), [4; 4; 4])
%!assert_equal (day (datetime (2020, [1, 2], [1, 29]), 'dayofweek'), [4, 7])

## Test 'day' with iso-dayofweek (1=Monday..7=Sunday)
%!assert_equal (day (datetime (2020, 1, 1), 'iso-dayofweek'), 3)
%!assert_equal (day (datetime (2020, 1, 5), 'iso-dayofweek'), 7)
%!assert_equal (day (datetime (2020, 1, 6), 'iso-dayofweek'), 1)

## Test 'day' with name/shortname
%!assert_equal (day (datetime (2020, 1, 1), 'name'), {'Wednesday'})
%!assert_equal (day (datetime (2000, 1, 1), 'name'), {'Saturday'})
%!assert_equal (day (datetime (2020, 1, 1), 'shortname'), {'Wed'})
%!assert_equal (day (datetime (2020, 3, 1), 'shortname'), {'Sun'})

## Test 'day' with dayofyear (regression: shape + leap-year borrow)
%!assert_equal (day (datetime (2020, 1, 1), 'dayofyear'), 1)
%!assert_equal (day (datetime (2020, 12, 31), 'dayofyear'), 366)
%!assert_equal (day (datetime (2021, 12, 31), 'dayofyear'), 365)
%!assert_equal (day (datetime (2020, 3, 1), 'dayofyear'), 61)
%!assert_equal (day (datetime (2021, 3, 1), 'dayofyear'), 60)
%!assert_equal (day (datetime (2020, 2, 29), 'dayofyear'), 60)
%!assert_equal (day (datetime (2020, 3, [1; 2]), 'dayofyear'), [61; 62])
%!assert_equal (day (datetime (2020, [1, 3; 6, 12], [1, 1; 1, 31]), 'dayofyear'), [1, 61; 153, 366])
%!assert_equal (day (NaT, 'dayofyear'), NaN)
%!error <datetime: unrecognized DAYTYPE.> day (datetime (2020, 1, 1), 'bogus')

## Test component accessors -- year-boundary, leap, and quarter/dow edges
%!assert_equal (day (datetime (2024, 12, 31), 'dayofyear'), 366)
%!assert_equal (day (datetime (2023, 12, 31), 'dayofyear'), 365)
%!assert_equal (week (datetime (2024, 12, 31)), 53)
%!assert_equal (week (datetime (2025, 1, 1)), 1)
%!assert_equal (quarter (datetime (2024, [3 4 6 7 9 10 12], 1)), [1 2 2 3 3 4 4])
%!assert_equal (second (datetime (2024, 1, 1, 23, 59, 59), 'secondofday'), 86399)
%!assert_equal (day (datetime ([2024 2000 1900], 1, 1), 'dayofweek'), [2 7 2])

## Test component accessors -- NaT yields NaN, arrays preserve shape
%!assert_equal (isnan (year (NaT)), true)
%!assert_equal (isnan (quarter (NaT)), true)
%!assert_equal (isnan (week (NaT)), true)
%!assert_equal (year (datetime ([2020 2021 2022], 1, 1)), [2020 2021 2022])
%!assert_equal (month (datetime (2024, [1 6 12], 1)), [1 6 12])
%!assert_equal (hour (datetime (2024, 1, 1, [0 6 12 18], 0, 0)), [0 6 12 18])
%!assert_equal (ismissing ([datetime(2024, 1, 1), NaT]), logical ([0 1]))

## Test 'dateshift' -- 'dayofweek' next and previous occurrences
%!assert_equal (char (dateshift (datetime (2024, 3, 13), 'dayofweek', ...
%!              'Sunday'), 'yyyy-MM-dd'), '2024-03-17')
%!assert_equal (char (dateshift (datetime (2024, 3, 13), 'dayofweek', ...
%!              'Sunday', 'previous'), 'yyyy-MM-dd'), '2024-03-10')

## Test conversions and calendar -- array shapes and further edges
%!assert_equal (yyyymmdd (datetime (2024, [3 12], [9 31])), [20240309 20241231])
%!assert_equal (datenum (datetime (2024, 3, 9, 12, 0, 0)), 739320.5)
%!assert_equal (hours (diff (datetime (2024, 1, [1 3 7]), 1)), [48 96])
%!assert_equal (cellstr (between (datetime (2020, 3, 15), ...
%!              datetime (2024, 3, 15), 'years')), {'4y'})
%!assert_equal (isregular (datetime (2020:2:2026, 1, 1), 'years'), true)
%!assert_equal (posixtime (datetime (1970, 1, [1 2], 'TimeZone', 'UTC')), [0 86400])
%!assert_equal (exceltime (datetime (1900, 1, [1 2])), [1 2])

## Test 'caldiff' -- single-component differences over a series
%!assert_equal (cellstr (caldiff (datetime ([2020 2022 2025], 1, 1), 'Years')), ...
%!              {'2y', '3y'})
%!assert_equal (cellstr (caldiff (datetime (2024, 1, [1 5 12]), 'Days')), {'4d', '7d'})
%!assert_equal (cellstr (caldiff (datetime (2024, 1, [1 8 22]), 'Weeks')), {'1w', '2w'})

## Test 'caldiff' -- month-end clamp counts match MATLAB (values, not display;
## a zero single component renders as '0d' here vs MATLAB '0mo' -- a documented
## calendarDuration rendering deviation, the counts are identical)
%!assert_equal (calmonths (caldiff (datetime (2024, [1 2 3], 31), 'Months')), [1 0])
%!assert_equal (split (caldiff (datetime (2024, [1 2 3], 31), 'Months'), 'days'), [0 0])
%!assert_equal (calmonths (caldiff (datetime (2024, [2 3], [29 31]), 'Months')), 1)
%!assert_equal (calmonths (caldiff (datetime (2024, [3 4], [31 30]), 'Months')), 1)
%!assert_equal (calmonths (caldiff (datetime (2024, [1 3], [31 31]), 'Months')), 2)

## Test 'dateshift' -- 'dayofweek' with next/previous/nth-occurrence rules
%!assert_equal (char (dateshift (datetime (2024, 3, 13), 'dayofweek', 'Monday', ...
%!              'next'), 'yyyy-MM-dd'), '2024-03-18')
%!assert_equal (char (dateshift (datetime (2024, 3, 13), 'dayofweek', 'Friday', ...
%!              'previous'), 'yyyy-MM-dd'), '2024-03-08')
%!assert_equal (char (dateshift (datetime (2024, 3, 1), 'dayofweek', 'Sunday', ...
%!              2), 'yyyy-MM-dd'), '2024-03-10')

## Test 'colon' -- hour and calendar-week steps count correctly
%!assert_equal (numel (datetime (2024, 1, 1):hours (12):datetime (2024, 1, 2, ...
%!              12, 0, 0)), 4)
%!assert_equal (numel (datetime (2024, 1, 1):calweeks (1):datetime (2024, 1, 22)), 4)

## Test arithmetic -- chained calendar addition and duration subtraction
%!assert_equal (char (datetime (2024, 1, 31) + calmonths (1) + caldays (5), ...
%!              'yyyy-MM-dd'), '2024-03-05')
%!assert_equal (char (datetime (2024, 1, 1) - minutes (90), ...
%!              'yyyy-MM-dd HH:mm:ss'), '2023-12-31 22:30:00')

## Test conversions -- round-trip through posixtime and datenum
%!assert_equal (char (datetime (posixtime (datetime (2024, 6, 15, 10, 30, 0, ...
%!              'TimeZone', 'UTC')), 'ConvertFrom', 'posixtime', 'TimeZone', ...
%!              'UTC'), 'yyyy-MM-dd HH:mm:ss'), '2024-06-15 10:30:00')
%!assert_equal (char (datetime (datenum (datetime (2024, 6, 15)), ...
%!              'ConvertFrom', 'datenum'), 'yyyy-MM-dd'), '2024-06-15')

## Test 'week' -- week 1 always contains January 1; year-end weeks
%!assert_equal (week (datetime (2020, 1, 1)), 1)
%!assert_equal (week (datetime (2021, 1, 1)), 1)
%!assert_equal (week (datetime (2022, 1, 1)), 1)
%!assert_equal (week (datetime (2023, 1, 1)), 1)
%!assert_equal (week (datetime (2025, 1, 1)), 1)
%!assert_equal (week (datetime (2026, 1, 1)), 1)
%!assert_equal (week (datetime (2021, 12, 31)), 53)
%!assert_equal (week (datetime (2022, 12, 31)), 53)
%!assert_equal (week (datetime (2023, 12, 31)), 53)

## Test 'tzoffset' -- whole-hour, half-hour, and southern-hemisphere zones
%!assert_equal (minutes (tzoffset (datetime (2024, 6, 15, ...
%!              'TimeZone', 'Asia/Kolkata'))), 330)
%!assert_equal (minutes (tzoffset (datetime (2024, 6, 15, ...
%!              'TimeZone', 'Asia/Tokyo'))), 540)
%!assert_equal (minutes (tzoffset (datetime (2024, 1, 15, ...
%!              'TimeZone', 'Australia/Sydney'))), 660)
%!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, ...
%!              'TimeZone', 'Australia/Sydney'))), 600)
%!assert_equal (minutes (tzoffset (datetime (2024, 6, 15, ...
%!              'TimeZone', 'Pacific/Marquesas'))), -570)

## Test 'isdst' -- southern-hemisphere and European DST windows
%!assert_equal (isdst (datetime (2024, 1, 15, 'TimeZone', 'Australia/Sydney')), true)
%!assert_equal (isdst (datetime (2024, 7, 15, 'TimeZone', 'Europe/London')), true)
%!assert_equal (isdst (datetime (2024, 1, 15, 'TimeZone', 'Europe/London')), false)

## Test DST transitions -- a wall clock that is ambiguous (the hour the clock
## repeats) resolves to the later, standard-time offset; one that does not
## exist (the gap the clock skips) moves ahead by the length of that gap.
## Both used to abort Octave with an uncaught date.h exception.
## All values verified against MATLAB R2024a.
%!assert_equal (char (datetime (2026, 11, 1, 1, 30, 0, 'TimeZone', ...
%!              'America/New_York'), 'yyyy-MM-dd HH:mm:ss z'), ...
%!              '2026-11-01 01:30:00 EST')
%!assert_equal (posixtime (datetime (2026, 11, 1, 1, 30, 0, 'TimeZone', ...
%!              'America/New_York')), 1793514600)
%!assert_equal (isdst (datetime (2026, 11, 1, 1, 30, 0, 'TimeZone', ...
%!              'America/New_York')), false)
%!assert_equal (seconds (tzoffset (datetime (2026, 11, 1, 1, 30, 0, ...
%!              'TimeZone', 'America/New_York'))), -5 * 3600)
%!assert_equal (char (datetime (2026, 3, 8, 2, 30, 0, 'TimeZone', ...
%!              'America/New_York'), 'yyyy-MM-dd HH:mm:ss z'), ...
%!              '2026-03-08 03:30:00 EDT')
%!assert_equal (posixtime (datetime (2026, 3, 8, 2, 30, 0, 'TimeZone', ...
%!              'America/New_York')), 1772955000)
%!assert_equal (isdst (datetime (2026, 3, 8, 2, 30, 0, 'TimeZone', ...
%!              'America/New_York')), true)
%!assert_equal (seconds (tzoffset (datetime (2026, 3, 8, 2, 30, 0, ...
%!              'TimeZone', 'America/New_York'))), -4 * 3600)
%!assert_equal (char (datetime (2026, 3, 8, 2, 0, 0, 'TimeZone', ...
%!              'America/New_York')), '08-Mar-2026 03:00:00')
%!assert_equal (char (datetime (2026, 3, 8, 2, 59, 59, 'TimeZone', ...
%!              'America/New_York')), '08-Mar-2026 03:59:59')
%!assert_equal (char (datetime (2026, 3, 29, 3, 30, 0, 'TimeZone', ...
%!              'Europe/Athens')), '29-Mar-2026 04:30:00')
%!assert_equal (seconds (tzoffset (datetime (2026, 10, 25, 3, 30, 0, ...
%!              'TimeZone', 'Europe/Athens'))), 2 * 3600)
%!assert_equal (char (datetime (2026, 10, 4, 2, 30, 0, 'TimeZone', ...
%!              'Australia/Sydney')), '04-Oct-2026 03:30:00')
%!assert_equal (seconds (tzoffset (datetime (2026, 4, 5, 2, 30, 0, ...
%!              'TimeZone', 'Australia/Sydney'))), 10 * 3600)
%!test
%! ## Lord Howe shifts the clock by half an hour, so the gap is half an hour
%! ## and 02:15 moves to 02:45 rather than to the transition itself.
%! d = datetime (2026, 10, 4, 2, 15, 0, 'TimeZone', 'Australia/Lord_Howe');
%! assert_equal (char (d), '04-Oct-2026 02:45:00')
%! assert_equal (seconds (tzoffset (d)), 11 * 3600)
%!test
%! ## Attaching a zone to an unzoned array applies the same rules.
%! d = datetime (2026, 3, 8, 2, 30, 0);
%! d.TimeZone = 'America/New_York';
%! assert_equal (char (d), '08-Mar-2026 03:30:00')
%! a = datetime (2026, 11, 1, 1, 30, 0);
%! a.TimeZone = 'America/New_York';
%! assert_equal (char (a, 'yyyy-MM-dd HH:mm:ss z'), '2026-11-01 01:30:00 EST')
%!test
%! ## A NaT alongside a skipped wall clock stays NaT.
%! d = [datetime(2026, 3, 8, 2, 30, 0); NaT];
%! d.TimeZone = 'America/New_York';
%! assert_equal (cellstr (d), {'08-Mar-2026 03:30:00'; 'NaT'})
## Test DST transitions -- text naming a wall clock its zone never shows is
## rejected, whereas the same wall clock given as numeric components is moved
## ahead by the gap.  MATLAB draws exactly this distinction (R2024a).  Under an
## 'InputFormat' a lone string errors and an array loses only that element to
## NaT; without one the whole input is rejected, as when no format is detected.
%!error <datetime: could not parse the date/time string '2026-03-08 02:30:00' with 'InputFormat' 'yyyy-MM-dd HH:mm:ss'.> ...
%! datetime ('2026-03-08 02:30:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss', 'TimeZone', 'America/New_York')
%!error <datetime: could not parse the date/time string '2026-10-04 02:15:00' with 'InputFormat' 'yyyy-MM-dd HH:mm:ss'.> ...
%! datetime ('2026-10-04 02:15:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss', 'TimeZone', 'Australia/Lord_Howe')
%!error <datetime: could not recognize date/time format from input.> ...
%! datetime ('2026-03-08 02:30:00', 'TimeZone', 'America/New_York')
%!error <datetime: could not recognize date/time format from input.> ...
%! datetime ({'2026-03-08 02:30:00', '2026-06-01 12:00:00'}, 'TimeZone', 'America/New_York')
%!test
%! d = datetime ({'2026-03-08 02:30:00', '2026-06-01 12:00:00'}, ...
%!               'InputFormat', 'yyyy-MM-dd HH:mm:ss', ...
%!               'TimeZone', 'America/New_York');
%! assert_equal (isnat (d), [true, false])
%! assert_equal (cellstr (d), {'NaT', '01-Jun-2026 12:00:00'})
%!test
%! ## A repeated hour is not skipped, so it stays acceptable, and the boundaries
%! ## of the gap itself are real wall clocks.
%! a = datetime ('2026-11-01 01:30:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss', ...
%!               'TimeZone', 'America/New_York');
%! assert_equal (char (a, 'yyyy-MM-dd HH:mm:ss z'), '2026-11-01 01:30:00 EST')
%! b = datetime ('2026-03-08 01:59:59', 'InputFormat', 'yyyy-MM-dd HH:mm:ss', ...
%!               'TimeZone', 'America/New_York');
%! assert_equal (char (b), '08-Mar-2026 01:59:59')
%! c = datetime ('2026-03-08 03:00:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss', ...
%!               'TimeZone', 'America/New_York');
%! assert_equal (char (c), '08-Mar-2026 03:00:00')
%!test
%! ## Unzoned text names no instant, so nothing is rejected.
%! d = datetime ('2026-03-08 02:30:00', 'InputFormat', 'yyyy-MM-dd HH:mm:ss');
%! assert_equal (char (d), '08-Mar-2026 02:30:00')

## Test 'month'/'day' -- full names
%!assert_equal (month (datetime (2024, 7, 1), 'name'), {'July'})
%!assert_equal (day (datetime (2024, 3, 10), 'name'), {'Sunday'})

## Test Format seam -- numeric zone-offset fields (Z, x, X)
%!assert_equal (char (datetime (2024, 7, 15, 14, 30, 0, 'TimeZone', ...
%!              'America/New_York'), 'Z'), '-0400')
%!assert_equal (char (datetime (2024, 7, 15, 14, 30, 0, 'TimeZone', ...
%!              'America/New_York'), 'x'), '-04')
%!assert_equal (char (datetime (2024, 7, 15, 14, 30, 0, 'TimeZone', ...
%!              'America/New_York'), 'xxx'), '-04:00')
%!assert_equal (char (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 'X'), 'Z')
%!assert_equal (char (datetime (2024, 1, 1, 'TimeZone', 'Asia/Kolkata'), ...
%!              'xxx'), '+05:30')

## Test Format seam -- ISO-8601 zone doublings and the UTC 'UTC'/'Z' forms
%!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'UTC'), 'ZZZZ'), 'UTC')
%!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'UTC'), 'ZZZZZ'), 'Z')
%!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'America/New_York'), ...
%!              'ZZZZZ'), '-05:00')
%!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'Asia/Kolkata'), ...
%!              'ZZZZ'), 'UTC+05:30')
%!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'UTC'), 'XXXXX'), 'Z')
%!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'UTC'), 'xxxxx'), '+00:00')
%!assert_equal (char (datetime (2024, 1, 15, 'TimeZone', 'America/New_York'), ...
%!              'xxxx'), '-0500')

## Test 'string'/'cellstr'/'char' -- array shapes
%!assert_equal (cellstr (string (datetime (2024, 1, [1 2]), 'yyyy-MM-dd')), ...
%!              {'2024-01-01', '2024-01-02'})
%!assert_equal (cellstr (datetime (2024, 1, [1 2])), {'01-Jan-2024', '02-Jan-2024'})
%!assert_equal (size (char (datetime (2024, 1, [1; 2]), 'yyyy-MM-dd')), [2, 10])

## Test component accessors -- N-D array preserves shape
%!assert_equal (year (datetime (2024, 1, reshape (1:4, 2, 2))), ...
%!              [2024 2024; 2024 2024])
%!assert_equal (size (day (datetime (2024, 1, reshape (1:8, 2, 2, 2)))), [2 2 2])

## Test 'convertTo' -- every numeric type on one fractional-day value
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'datenum'), 739320.25)
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'excel'), 45360.25)
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'excel1904'), 43898.25)
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'juliandate'), 2460378.75)
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'modifiedjuliandate'), ...
%!              60378.25)
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'yyyymmdd'), 20240309)

## Test 'min'/'max' -- the returned index of the extreme element
%!test
%! [m, i] = min (datetime (2024, 3, [9 2 20]));
%! assert_equal (i, 2);
%!test
%! [m, i] = max (datetime (2024, 3, [9 2 20]));
%! assert_equal (i, 3);

## Test 'unique'/'sort' -- stable order and column-wise matrix sort
%!assert_equal (cellstr (unique (datetime (2024, 1, [3 1 3 2]), 'stable')), ...
%!              {'03-Jan-2024', '01-Jan-2024', '02-Jan-2024'})
%!assert_equal (cellstr (sort (datetime (2024, 1, [3 1; 2 4]))), ...
%!              {'02-Jan-2024', '01-Jan-2024'; '03-Jan-2024', '04-Jan-2024'})

## Test 'diff'/'caldiff' -- second-order and Time-component differences
%!assert_equal (hours (diff (datetime (2024, 1, [1 2 4 8]), 2)), [24 48])
%!assert_equal (cellstr (caldiff (datetime (2024, 1, 1, [0 6 12], 0, 0), 'Time')), ...
%!              {'6h 0m 0s', '6h 0m 0s'})

## Test conversions -- pre-epoch second and array datevec shape
%!assert_equal (posixtime (datetime (1969, 12, 31, 23, 59, 59, 'TimeZone', 'UTC')), -1)
%!assert_equal (size (datevec (datetime (2024, 1, [1;2;3]))), [3 6])

## Test calendar diffs -- mixed default, day counts across leap February
%!assert_equal (cellstr (caldiff (datetime ([2020 2024], [1 3], [15 20]))), ...
%!              {'4y 2mo 5d'})
%!assert_equal (cellstr (between (datetime (2024, 2, 28), ...
%!              datetime (2024, 3, 1), 'days')), {'2d'})
%!assert_equal (cellstr (between (datetime (2023, 2, 28), ...
%!              datetime (2023, 3, 1), 'days')), {'1d'})

## Test 'dateshift' -- end of February respects leap years
%!assert_equal (char (dateshift (datetime (2024, 2, 15), 'end', 'month'), ...
%!              'yyyy-MM-dd'), '2024-02-29')
%!assert_equal (char (dateshift (datetime (2023, 2, 15), 'end', 'month'), ...
%!              'yyyy-MM-dd'), '2023-02-28')

## Test 'week'/'quarter' -- week-of-month span and all twelve months
%!assert_equal (week (datetime (2024, 3, 1), 'weekofmonth'), 1)
%!assert_equal (week (datetime (2024, 3, 31), 'weekofmonth'), 6)
%!assert_equal (quarter (datetime (2024, 1:12, 1)), [1 1 1 2 2 2 3 3 3 4 4 4])

## Test reductions -- DST-aware time of day, even-count median, N-normalisation
%!assert_equal (hours (timeofday (datetime (2024, 3, 10, 3, 30, 0, 'TimeZone', ...
%!              'America/New_York'))), 2.5)
%!assert_equal (char (mean (datetime (2024, 1, [1 3])), 'yyyy-MM-dd'), '2024-01-02')
%!assert_equal (char (median (datetime (2024, 1, [1 2 3 4])), ...
%!              'yyyy-MM-dd HH:mm:ss'), '2024-01-02 12:00:00')
%!assert_equal (char (std (datetime (2024, 1, [1 3 5]), 1)), '39:11:30')

## Test arithmetic -- fixed durations and calendar-unit additions
%!assert_equal (char (datetime (2024, 1, 1) + hours (25), ...
%!              'yyyy-MM-dd HH:mm:ss'), '2024-01-02 01:00:00')
%!assert_equal (char (datetime (2024, 1, 1) + seconds (3661), ...
%!              'yyyy-MM-dd HH:mm:ss'), '2024-01-01 01:01:01')
%!assert_equal (char (datetime (2024, 2, 29) + calyears (4), 'yyyy-MM-dd'), ...
%!              '2028-02-29')
%!assert_equal (char (datetime (2024, 1, 15) + calquarters (2), 'yyyy-MM-dd'), ...
%!              '2024-07-15')
%!assert_equal (char (datetime (2024, 1, 1) + calweeks (3), 'yyyy-MM-dd'), ...
%!              '2024-01-22')

## Test conversions -- array outputs and the numeric-year construction
%!assert_equal (exceltime (datetime (2024, [1 6 12], 1)), [45292 45444 45627])
%!assert_equal (juliandate (datetime ([2000 2024], 1, 1)), [2451544.5 2460310.5])
%!assert_equal (datenum (datetime ([2020 2024], 1, 1)), [737791 739252])
%!assert_equal (char (datetime (49, 1, 1), 'yyyy-MM-dd'), '0049-01-01')

## Test 'isbetween'/'min'/'max' -- text bounds and matrix reductions
%!assert_equal (isbetween (datetime (2024, 6, 15), '2024-06-01', '2024-06-30'), true)
%!assert_equal (cellstr (min (datetime (2024, 1, [3 1; 2 4]))), ...
%!              {'02-Jan-2024', '01-Jan-2024'})
%!assert_equal (cellstr (max (datetime (2024, 1, [3 1; 2 4]), [], 2)), ...
%!              {'03-Jan-2024'; '04-Jan-2024'})

## Test 'caldiff'/'dateshift'/'string' -- quarters, week start, literal format
%!assert_equal (cellstr (caldiff (datetime (2024, [1 4 7], 1), 'Quarters')), ...
%!              {'1q', '1q'})
%!assert_equal (char (dateshift (datetime (2024, 3, 13), 'start', 'week'), ...
%!              'yyyy-MM-dd'), '2024-03-10')
%!assert_equal (char (datetime (2024, 3, 9), '''Day'' d ''of'' MMMM'), ...
%!              'Day 9 of March')

## Test 'between'/'isregular' -- quarter counts and weekly regularity
%!assert_equal (cellstr (between (datetime (2024, 1, 1), ...
%!              datetime (2025, 1, 1), 'quarters')), {'4q'})
%!assert_equal (isregular (datetime (2024, 1, 1:7:22), 'weeks'), true)

## Test 'dateshift' -- start of quarter and end of day
%!assert_equal (char (dateshift (datetime (2024, 5, 15), 'start', 'quarter'), ...
%!              'yyyy-MM-dd'), '2024-04-01')
%!assert_equal (char (dateshift (datetime (2024, 11, 15), 'start', 'quarter'), ...
%!              'yyyy-MM-dd'), '2024-10-01')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 0, 0), 'end', ...
%!              'day'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-16 00:00:00')

## Test conversions/components -- fractional posix, elapsed day, quarter edges
%!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0.5, 'TimeZone', 'UTC')), ...
%!              1704067200.5)
%!assert_equal (seconds (timeofday (datetime (2024, 1, 1, 1, 1, 1))), 3661)
%!assert_equal (exceltime (datetime (1904, 1, 1), '1904'), 0)
%!assert_equal (quarter (datetime (2024, [3 4], 31)), [1 2])
%!assert_equal (second (datetime (2024, 1, 1, 0, 1, 30)), 30)
%!assert_equal (minute (datetime (2024, 1, 1, 5, 45, 0)), 45)

## Test 'mode'/'diff'/'isweekend' -- most frequent, column diff, weekend run
%!assert_equal (char (mode (datetime (2024, 1, [5 5 2 2 2])), 'yyyy-MM-dd'), ...
%!              '2024-01-02')
%!assert_equal (hours (diff (datetime (2024, 1, [1;3;7]))), [48; 96])
%!assert_equal (isweekend (datetime (2024, 3, [16 17 18])), logical ([1 1 0]))

## Test component accessors -- assorted fixed values across the calendar
%!assert_equal (year (datetime ([1999 2000 2001], 6, 15)), [1999 2000 2001])
%!assert_equal (char (datetime (2024, 12, 1), 'MMM'), 'Dec')
%!assert_equal (char (datetime (2024, 3, 10), 'eee'), 'Sun')
%!assert_equal (char (datetime (50, 1, 1), 'yyyy'), '0050')
%!assert_equal (day (datetime (2024, 1, [1 15 31])), [1 15 31])
%!assert_equal (day (datetime (2024, 3, 1), 'dayofyear'), 61)
%!assert_equal (day (datetime (2023, 3, 1), 'dayofyear'), 60)
%!assert_equal (week (datetime (2024, 6, 15)), 24)
%!assert_equal (quarter (datetime (2024, 7, 1)), 3)
%!assert_equal (second (datetime (2024, 1, 1, 12, 0, 0), 'secondofday'), 43200)
%!assert_equal (day (datetime (2024, 3, 11), 'iso-dayofweek'), 1)
%!assert_equal (month (datetime (2024, [1 6 12], 1), 'shortname'), {'Jan', 'Jun', 'Dec'})

## Test 'hour'
%!assert_equal (hour (datetime (2020, 1, 1, 23, 0, 0)), 23)
%!assert_equal (hour (datetime (2020, 1, 1, 0, 0, 0)), 0)
%!assert_equal (hour (NaT), NaN)

## Test 'minute'
%!assert_equal (minute (datetime (2020, 1, 1, 0, 59, 0)), 59)
%!assert_equal (minute (NaT), NaN)

## Test 'second' with type options
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45)), 45)
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 45.75)), 45.75)
%!assert_equal (second (datetime (2020, 1, 1, 0, 0, 0), 'secondofminute'), 0)
%!assert_equal (second (datetime (2020, 1, 1, 2, 3, 4), 'secondofday'), 7384)
%!assert_equal (second (datetime (2020, 1, 1, 1, 0, 0), 'secondofday'), 3600)
%!error <datetime.second: unrecognized SECONDTYPE.> ...
%! second (datetime (2020, 1, 1), 'bogus')

## Test 'week' (weekofyear default; Sunday-Saturday weeks, week 1 holds Jan 1)
%!assert_equal (week (datetime (2023, 1, 1)), 1)
%!assert_equal (week (datetime (2023, 1, 8)), 2)
%!assert_equal (week (datetime (2023, 1, 31)), 5)
%!assert_equal (week (datetime (2024, 1, 1)), 1)
%!assert_equal (week (datetime (2024, 1, 7)), 2)
%!assert_equal (week (datetime (2024, 12, 29)), 53)
%!assert_equal (week (datetime (2024, 12, 31)), 53)
%!assert_equal (week (datetime (2025, 1, 1)), 1)
%!assert_equal (week (datetime (2020, 2, 29)), 9)
%!assert_equal (week (datetime (2020, 3, 1)), 10)
%!assert_equal (week (datetime (2023, 1, [1, 8, 31])), [1, 2, 5])
%!assert_equal (week (datetime (2020, 1, 1), 'weekofyear'), 1)

## Test 'week' with weekofmonth
%!assert_equal (week (datetime (2023, 1, 1), 'weekofmonth'), 1)
%!assert_equal (week (datetime (2023, 1, 31), 'weekofmonth'), 5)
%!assert_equal (week (datetime (2024, 12, 29), 'weekofmonth'), 5)
%!assert_equal (week (datetime (2020, 2, 29), 'weekofmonth'), 5)
%!assert_equal (week (datetime (2020, 3, 1), 'weekofmonth'), 1)
%!assert_equal (week (datetime (2023, 1, [1, 8, 15, 22, 29]), 'weekofmonth'), [1, 2, 3, 4, 5])

## Test 'week' NaT and error branch
%!assert_equal (week (NaT), NaN)
%!assert_equal (week ([datetime(2020, 1, 1); NaT]), [1; NaN])
%!error <datetime.week: unrecognized WEEKTYPE.> ...
%! week (datetime (2020, 1, 1), 'bogus')

## Test 'timeofday' -- elapsed time since midnight, returned as a duration
%!assert_equal (seconds (timeofday (datetime (2024, 6, 15, 14, 45, 30))), 53130)
%!assert_equal (seconds (timeofday (datetime (2024, 6, 15, 14, 45, 30.5))), 53130.5)
%!assert_equal (seconds (timeofday (datetime (2024, 3, 10, 0, 0, 0))), 0)
%!assert_equal (hours (timeofday (datetime (2024, 3, 10, 3, 30, 0))), 3.5)
%!assert_equal (seconds (timeofday ([datetime(2024, 6, 15, 1, 0, 0), ...
%!                                   datetime(2024, 6, 15, 2, 0, 0)])), [3600, 7200])
%!assert_equal (isnan (seconds (timeofday (NaT))), true)
%!test
%! d = timeofday (datetime (2024, 6, 15, 5, 0, 0));
%! assert_equal (class (d), 'duration');
%! assert_equal (d.Format, 'hh:mm:ss');

## Test 'timeofday' -- zoned result is DST-aware (physical time since midnight)
%!assert_equal (hours (timeofday (datetime (2024, 3, 10, 3, 30, 0, ...
%!                                'TimeZone', 'America/New_York'))), 2.5)
%!assert_equal (seconds (timeofday (datetime (2024, 6, 15, 14, 45, 30.5, ...
%!                                  'TimeZone', 'America/New_York'))), 53130.5)

## Test 'tzoffset' -- offset from UTC as a duration (positive east, DST-aware)
%!assert_equal (minutes (tzoffset (datetime (2024, 1, 15, ...
%!                                 'TimeZone', 'America/New_York'))), -300)
%!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, ...
%!                                 'TimeZone', 'America/New_York'))), -240)
%!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, ...
%!                                 'TimeZone', 'Asia/Kolkata'))), 330)
%!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, 'TimeZone', 'UTC'))), 0)
%!assert_equal (minutes (tzoffset (datetime (2024, 7, 15, ...
%!                                 'TimeZone', 'Pacific/Marquesas'))), -570)
%!assert_equal (minutes (tzoffset (datetime (2024, 1, [15, NaN], ...
%!                                 'TimeZone', 'America/New_York'))), [-300, NaN])
%!assert_equal (isnan (minutes (tzoffset (datetime (2024, 7, 15)))), true)
%!test
%! d = tzoffset (datetime (2024, 1, 15, 'TimeZone', 'America/New_York'));
%! assert_equal (class (d), 'duration');
%! assert_equal (d.Format, 'hh:mm');

## Test 'isdst' -- logical DST flag, false when unzoned or NaT
%!assert_equal (class (isdst (datetime (2024, 7, 15, ...
%!                            'TimeZone', 'America/New_York'))), 'logical')
%!assert_equal (isdst (datetime (2024, 1, 15, ...
%!                     'TimeZone', 'America/New_York')), false)
%!assert_equal (isdst (datetime (2024, 7, 15, ...
%!                     'TimeZone', 'America/New_York')), true)
%!assert_equal (isdst (datetime (2024, 7, 15, 'TimeZone', 'UTC')), false)
%!assert_equal (isdst (datetime (2024, 1, 15, ...
%!                     'TimeZone', 'Australia/Sydney')), true)
%!assert_equal (isdst (datetime (2024, 7, 15, ...
%!                     'TimeZone', 'Australia/Sydney')), false)
%!assert_equal (isdst (datetime (2024, 7, 15, 'TimeZone', 'Asia/Kolkata')), false)
%!assert_equal (isdst (datetime (2024, [1, 7], 15, ...
%!                     'TimeZone', 'America/New_York')), [false, true])
%!assert_equal (isdst (datetime (2024, [1, NaN], 15, ...
%!                     'TimeZone', 'America/New_York')), [false, false])
%!assert_equal (isdst (datetime (2024, 7, 15)), false)
%!assert_equal (isdst (NaT), false)

## Test 'isweekend' -- logical, true on Saturday or Sunday, false for NaT
%!assert_equal (class (isweekend (datetime (2024, 1, 6))), 'logical')
%!assert_equal (isweekend (datetime (2024, 1, 6)), true)
%!assert_equal (isweekend (datetime (2024, 1, 7)), true)
%!assert_equal (isweekend (datetime (2024, 1, 8)), false)
%!assert_equal (isweekend (datetime (2024, 1, 1:7)), ...
%!              logical ([0, 0, 0, 0, 0, 1, 1]))
%!assert_equal (isweekend (datetime (2024, 1, [6, 7; 8, 13])), ...
%!              logical ([1, 1; 0, 1]))
%!assert_equal (isweekend (datetime (2024, 1, 6, 'TimeZone', 'UTC')), true)
%!assert_equal (isweekend (NaT), false)
%!assert_equal (isweekend ([datetime(2024, 1, 6); NaT]), logical ([1; 0]))

## Test 'isregular' -- a vector that does not move has no spacing, so repeated
## instants are not regularly spaced, for time or for any calendar unit
%!test
%! d = datetime (2024, 1, [1, 1, 1]);
%! [tf, dt] = isregular (d);
%! assert_equal (tf, false);
%! assert_equal (isnan (dt), true);
%! assert_equal (isregular (d, 'days'), false);
%! assert_equal (isregular (d, 'months'), false);
%! assert_equal (isregular (d, 'years'), false);
%!assert_equal (isregular (datetime (2024, [1, 1, 1], 1), 'months'), false)
%!assert_equal (isregular (datetime (2024, 1, [5, 5]), 'time'), false)
## Test 'isregular' -- a moving vector is still regular, in either direction
%!assert_equal (isregular (datetime (2024, 1, [1, 2, 3]), 'days'), true)
%!assert_equal (isregular (datetime (2024, 1, [3, 2, 1]), 'days'), true)
%!assert_equal (isregular (datetime (2024, [1, 2, 3], 1), 'months'), true)
%!assert_equal (isregular (datetime (2024, [3, 2, 1], 1), 'months'), true)

## Test 'isregular' -- time-based (default) regularity
%!assert_equal (isregular (datetime (2024, 1, 1) + days (0:3)), true)
%!assert_equal (isregular (datetime (2024, 1, 1) + hours (0:3)), true)
%!assert_equal (isregular (datetime (2024, 1, [1 2 4])), false)
%!assert_equal (isregular (datetime (2024, 1, [1 5])), true)
%!assert_equal (isregular (fliplr (datetime (2024, 1, 1) + days (0:3))), true)
%!assert_equal (isregular ((datetime (2024, 1, 1) + days (0:3))'), true)
%!assert_equal (isregular (datetime (2024, 1, 1)), false)
%!assert_equal (isregular ([datetime(2024, 1, 1), NaT, datetime(2024, 1, 3)]), false)
%!test
%! e = datetime (2024, 1, 1);
%! e(1) = [];
%! assert_equal (isregular (e), false);

## Test 'isregular' -- monthly data is irregular in time but regular in months
%!assert_equal (isregular (datetime (2024, 1, 1):calmonths (1):datetime (2024, 6, 1)), false)
%!assert_equal (isregular (datetime (2024, 1, 1):calmonths (1):datetime (2024, 6, 1), 'months'), true)

## Test 'isregular' -- calendar units
%!assert_equal (isregular (datetime (2024, 1, 1) + days (0:3), 'days'), true)
%!assert_equal (isregular (datetime (2024, 1, 1) + days (0:3), 'weeks'), false)
%!assert_equal (isregular (datetime (2024, 1, 1:7:22), 'weeks'), true)
%!assert_equal (isregular (datetime (2024, 1:4, 15), 'months'), true)
%!assert_equal (isregular (datetime (2024, [1 3 5], 31), 'months'), true)
%!assert_equal (isregular (datetime (2024, 1, [1 2 3]), 'months'), false)
%!assert_equal (isregular (datetime (2024, 1, 1) + calquarters (0:3), 'quarters'), true)
%!assert_equal (isregular (datetime (2020, 2, 29) + calyears (0:3), 'years'), true)

## Test 'isregular' -- clamp series with varying day-of-month is irregular
%!assert_equal (isregular (datetime (2024, 1, 31) + calmonths (0:3), 'months'), false)

## Test 'isregular' -- calendar-day spacing across DST is regular in days, not time
%!assert_equal (isregular (datetime (2024, 3, 9, ...
%!              'TimeZone', 'America/New_York') + caldays (0:3), 'days'), true)
%!assert_equal (isregular (datetime (2024, 3, 9, ...
%!              'TimeZone', 'America/New_York') + caldays (0:3), 'time'), false)

## Test 'isregular' -- second output DT (common step)
%!test
%! [tf, dt] = isregular (datetime (2024, 1, 1) + days (0:3));
%! assert_equal (tf, true);
%! assert_equal (class (dt), 'duration');
%! assert_equal (seconds (dt), 86400);
%!test
%! [tf, dt] = isregular (datetime (2024, [1 3 5], 31), 'months');
%! assert_equal (class (dt), 'calendarDuration');
%! assert_equal (calmonths (dt), 2);
%!test
%! [tf, dt] = isregular (datetime (2024, 1, 1:7:22), 'weeks');
%! assert_equal (caldays (dt), 7);
%!test
%! [tf, dt] = isregular (datetime (2020, 2, 29) + calyears (0:3), 'years');
%! assert_equal (calmonths (dt), 12);
%!test
%! [tf, dt] = isregular (datetime (2024, 1, [1 2 4]));
%! assert_equal (tf, false);
%! assert_equal (class (dt), 'duration');
%! assert_equal (isnan (seconds (dt)), true);
%!test
%! [tf, dt] = isregular (datetime (2024, 1, [1 2 5]), 'months');
%! assert_equal (class (dt), 'calendarDuration');
%! assert_equal (isnan (calmonths (dt)), true);
%!test
%! [tf, dt] = isregular (datetime (2024, 1, 1), 'months');
%! assert_equal (tf, false);
%! assert_equal (isnan (calmonths (dt)), true);

## Test 'isregular' -- errors
%!error <datetime.isregular: UNIT must be 'time', 'years', 'quarters', 'months', 'weeks', or 'days'.> ...
%! isregular (datetime (2024, 1, 1) + days (0:3), 'bogus')
%!error <datetime.isregular: input must be a datetime vector.> ...
%! isregular (reshape (datetime (2024, 1, 1) + days (0:5), 2, 3))

################################################################################
##                         ** Summary Information **                          ##
################################################################################

## Test 'size'
%!assert_equal (size (datetime (2020, 1, 1)), [1, 1])
%!assert_equal (size (datetime (2020, 1, [1; 2; 3])), [3, 1])
%!assert_equal (size (datetime (2020, 1, [1, 2, 3])), [1, 3])
%!assert_equal (size (datetime (2020, ones (2, 4, 3), 1)), [2, 4, 3])
%!test
%! [r, c] = size (datetime (2020, 1, [1, 2, 3]));
%! assert_equal (r, 1);
%! assert_equal (c, 3);
%!assert_equal (size (datetime (2020, 1, [1; 2; 3]), 1), 3)
%!assert_equal (size (datetime (2020, 1, [1; 2; 3]), 2), 1)

## Test 'ndims'
%!assert_equal (ndims (datetime (2020, 1, 1)), 2)
%!assert_equal (ndims (datetime (2020, 1, [1; 2])), 2)
%!assert_equal (ndims (datetime (2020, ones (2, 3, 4), 1)), 3)

## Test indexing -- linear, logical, end, and N-D element access
%!assert_equal (char (datetime (2024, 1, 1:5)(2), 'yyyy-MM-dd'), '2024-01-02')
%!assert_equal (char (datetime (2024, 1, 1:5)(end), 'yyyy-MM-dd'), '2024-01-05')
%!assert_equal (numel (datetime (2024, 1, 1:5)(logical ([1 0 1 0 1]))), 3)
%!assert_equal (size (datetime (2024, 1, reshape (1:8, 2, 2, 2))), [2, 2, 2])
%!assert_equal (char (datetime (2024, 1, reshape (1:8, 2, 2, 2))(2, 2, 2), ...
%!              'yyyy-MM-dd'), '2024-01-08')

## Test 'subsasgn' -- assignment grows the array, and deletion shrinks it
%!test
%! t = datetime (2024, 1, 1:3);
%! t(5) = datetime (2024, 1, 10);
%! assert_equal (numel (t), 5);
%!test
%! t = datetime (2024, 1, 1:5);
%! t(2) = [];
%! assert_equal (numel (t), 4);
%! assert_equal (char (t(2), 'yyyy-MM-dd'), '2024-01-03');

## Test 'subsasgn' -- an out-of-range assignment fills the gap with NaT
%!test
%! t = datetime (2024, 1, 1:3);
%! t(5) = datetime (2024, 1, 10);
%! assert_equal (isnat (t(4)), true);
%! assert_equal (char (t(3), 'yyyy-MM-dd'), '2024-01-03');
%! assert_equal (char (t(5), 'yyyy-MM-dd'), '2024-01-10');
%! assert_equal (cellstr (t(4)), {'NaT'});
%!test
%! m = datetime (2024, 1, 1:3);
%! m(3, 3) = datetime (2024, 1, 20);
%! assert_equal (size (m), [3, 3]);
%! assert_equal (sum (isnat (m(:))), 5);
%!test
%! z = datetime (2024, 1, 1:2, 'TimeZone', 'UTC');
%! z(4) = datetime (2024, 1, 9, 'TimeZone', 'UTC');
%! assert_equal (isnat (z(3)), true);
%! assert_equal (z.TimeZone, 'UTC');

## Test 'numel'
%!assert_equal (numel (datetime (2020, 1, 1)), 1)
%!assert_equal (numel (datetime (2020, 1, [1; 2; 3])), 3)
%!assert_equal (numel (datetime (2020, ones (2, 3), 1)), 6)

## Test 'length'
%!assert_equal (length (datetime (2020, 1, 1)), 1)
%!assert_equal (length (datetime (2020, 1, [1; 2; 3])), 3)
%!assert_equal (length (datetime (2020, ones (2, 5), 1)), 5)

## Test 'keyHash' (regression: previously read a non-existent property)
%!assert_equal (class (keyHash (datetime (2020, 1, 1))), 'uint64')
%!assert_equal (isscalar (keyHash (datetime (2020, 1, [1; 2; 3]))), true)
%!assert_equal (keyHash (datetime (2020, 1, 1)), keyHash (datetime (2020, 1, 1)))
%!assert_equal (keyHash (datetime (2020, 1, 1)) != keyHash (datetime (2020, 1, 2)), true)
%!assert_equal (class (keyHash (datetime (2020, 1, 1), uint64 (7))), 'uint64')
%!error <datetime.keyHash: BASE must be a UINT64 scalar.> ...
%! keyHash (datetime (2020, 1, 1), 7)

################################################################################
##              ** Convert to other Numeric Representations **                ##
################################################################################

## Test 'datevec'
%!assert_equal (datevec (datetime (2020, 7, 4, 1, 2, 3)), [2020, 7, 4, 1, 2, 3])
%!assert_equal (size (datevec (datetime (2020, 1, [1; 2]))), [2, 6])
%!test
%! [y, m, d, h, mi, s] = datevec (datetime (2020, 7, 4, 1, 2, 3));
%! assert_equal ([y, m, d, h, mi, s], [2020, 7, 4, 1, 2, 3]);

## Test 'datestr' -- legacy string conversion delegating to core datestr
%!assert_equal (datestr (datetime (2024, 3, 9, 14, 5, 7)), '09-Mar-2024 14:05:07')
%!assert_equal (datestr (datetime (2024, 3, 9, 14, 5, 7), ...
%!              'yyyy-mm-dd HH:MM:SS'), '2024-03-09 14:05:07')
%!assert_equal (datestr (datetime (2024, 3, 9), 'dd-mmm-yyyy'), '09-Mar-2024')
%!assert_equal (datestr (datetime (2024, 3, 9, 14, 5, 7, ...
%!              'TimeZone', 'America/New_York')), '09-Mar-2024 14:05:07')
%!test
%! s = datestr ([datetime(2024, 3, 9, 14, 5, 7); datetime(2024, 3, 10, 14, 5, 7)]);
%! assert_equal (size (s), [2, 20]);
%! assert_equal (s(1,:), '09-Mar-2024 14:05:07');
%! assert_equal (s(2,:), '10-Mar-2024 14:05:07');

## Test 'string' -- datetime to string array via the Format seam
%!assert_equal (class (string (datetime (2024, 3, 9))), 'string')
%!assert_equal (cellstr (string (datetime (2024, 3, 9, 14, 5, 7))), ...
%!              {'09-Mar-2024 14:05:07'})
%!assert_equal (cellstr (string (datetime (2024, 3, 9), 'yyyy-MM-dd')), ...
%!              {'2024-03-09'})
%!assert_equal (cellstr (string (datetime (2024, 3, 9, 'TimeZone', ...
%!              'America/New_York'), 'yyyy-MM-dd')), {'2024-03-09'})
%!assert_equal (ismissing (string (NaT)), true)

## Test 'string' -- Format property, shape, and NaT -> missing
%!test
%! t = datetime (2024, 3, 9);
%! t.Format = 'yyyy-MM-dd';
%! assert_equal (cellstr (string (t)), {'2024-03-09'});
%!test
%! t = [datetime(2024, 3, 9, 14, 5, 7), NaT];
%! s = string (t);
%! assert_equal (size (s), [1, 2]);
%! assert_equal (ismissing (s), logical ([0, 1]));
%!test
%! t = [datetime(2024, 1, 1, 5, 0, 0); datetime(2024, 2, 2, 6, 0, 0)];
%! assert_equal (cellstr (string (t)), cellstr (t));

## Test 'yyyymmdd' -- YYYYMMDD numeric date, time ignored, NaT -> NaN
%!assert_equal (yyyymmdd (datetime (2024, 3, 9)), 20240309)
%!assert_equal (yyyymmdd (datetime (2024, 3, 9, 14, 5, 7)), 20240309)
%!assert_equal (yyyymmdd (datetime (2024, 12, 31)), 20241231)
%!assert_equal (yyyymmdd (datetime (1999, 1, 1)), 19990101)
%!assert_equal (class (yyyymmdd (datetime (2024, 1, 1))), 'double')
%!assert_equal (yyyymmdd (datetime (2024, 3, 9, 23, 0, 0, ...
%!              'TimeZone', 'America/New_York')), 20240309)
%!assert_equal (yyyymmdd (datetime (2024, 1, [1; 2])), [20240101; 20240102])
%!assert_equal (isnan (yyyymmdd (NaT)), true)

## Test 'datenum' -- serial date number, wall-clock, NaT -> NaN, Inf preserved
%!assert_equal (datenum (datetime (2024, 3, 9)), 739320)
%!assert_equal (datenum (datetime (2024, 1, 1)), 739252)
%!assert_equal (datenum (datetime (2024, 3, 9, 6, 0, 0)), 739320.25)
%!assert_equal (datenum (datetime (2024, 3, 9, 6, 30, 15.5)), ...
%!              datenum (2024, 3, 9, 6, 30, 15.5))
%!assert_equal (class (datenum (datetime (2024, 1, 1))), 'double')
%!assert_equal (datenum (datetime (2024, 3, 9, 23, 0, 0, ...
%!              'TimeZone', 'America/New_York')), datenum (2024, 3, 9, 23, 0, 0))
%!assert_equal (datenum (datetime (2024, 1, [1; 2])), [739252; 739253])
%!assert_equal (isnan (datenum (NaT)), true)
%!assert_equal (datenum ([datetime(2024, 1, 1), NaT]), [739252, NaN])
%!assert_equal (datenum (datetime (Inf, 'ConvertFrom', 'datenum')), Inf)
%!assert_equal (datenum (datetime (-Inf, 'ConvertFrom', 'datenum')), -Inf)

## Test 'exceltime' -- Excel serial date; 1900 (default) and 1904 systems
%!assert_equal (exceltime (datetime (2024, 3, 9)), 45360)
%!assert_equal (exceltime (datetime (2024, 3, 9, 6, 0, 0)), 45360.25)
%!assert_equal (exceltime (datetime (1900, 1, 1)), 1)
%!assert_equal (exceltime (datetime (1900, 2, 28)), 59)
%!assert_equal (exceltime (datetime (1900, 3, 1)), 61)
%!assert_equal (exceltime (datetime (1899, 12, 30)), -1)
%!assert_equal (exceltime (datetime (2024, 3, 9), '1904'), 43898)
%!assert_equal (class (exceltime (datetime (2024, 1, 1))), 'double')
%!assert_equal (exceltime (datetime (2024, 3, 9, 23, 0, 0, ...
%!              'TimeZone', 'America/New_York')), ...
%!              exceltime (datetime (2024, 3, 9, 23, 0, 0)))
%!assert_equal (exceltime (datetime (2024, 1, [1; 2])), [45292; 45293])
%!assert_equal (isnan (exceltime (NaT)), true)
%!assert_equal (exceltime ([datetime(2024, 1, 1), NaT]), [45292, NaN])
%!error <datetime.exceltime: DATETYPE must be '1900' or '1904'.> ...
%! exceltime (datetime (2024, 1, 1), 'bogus')

## Test 'juliandate' -- Julian date and modified variant, instant-based
%!assert_equal (juliandate (datetime (2000, 1, 1, 12, 0, 0)), 2451545)
%!assert_equal (juliandate (datetime (2000, 1, 1, 12, 0, 0, ...
%!              'TimeZone', 'UTC')), 2451545)
%!assert_equal (juliandate (datetime (2024, 3, 9)), 2460378.5)
%!assert_equal (juliandate (datetime (2024, 3, 9), 'juliandate'), 2460378.5)
%!assert_equal (juliandate (datetime (2024, 3, 9), 'modifiedjuliandate'), 60378)
%!assert_equal (class (juliandate (datetime (2024, 1, 1))), 'double')
%!assert_equal (juliandate (datetime (2024, 1, [1, 2])), [2460310.5, 2460311.5])
%!assert_equal (isnan (juliandate (NaT)), true)
%!assert_equal (juliandate (datetime (Inf, 'ConvertFrom', 'datenum')), Inf)

## Test 'juliandate' -- zoned result uses the absolute UTC instant
%!assert_equal (juliandate (datetime (2000, 1, 1, 12, 0, 0, ...
%!              'TimeZone', 'America/New_York')), ...
%!              juliandate (datetime (2000, 1, 1, 17, 0, 0, 'TimeZone', 'UTC')))

## Test 'juliandate' -- error branch
%!error <datetime.juliandate: DATETYPE must be 'juliandate' or 'modifiedjuliandate'.> ...
%! juliandate (datetime (2024, 1, 1), 'bogus')

## Test conversions -- pre-epoch, epoch, Excel epoch, and instant-based edges
%!assert_equal (posixtime (datetime (1969, 12, 31, 23, 0, 0)), -3600)
%!assert_equal (datenum (datetime (1970, 1, 1)), 719529)
%!assert_equal (exceltime (datetime (1900, 1, 1)), 1)
%!assert_equal (juliandate (datetime (2000, 1, 1, 7, 0, 0, ...
%!              'TimeZone', 'America/New_York')), 2451545)

## Test 'convertTo' -- umbrella dispatcher to the numeric conversions
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC'), ...
%!              'datenum'), 739320.25)
%!assert_equal (convertTo (datetime (2024, 3, 9), 'excel'), 45360)
%!assert_equal (convertTo (datetime (2024, 3, 9), 'excel1900'), 45360)
%!assert_equal (convertTo (datetime (2024, 3, 9), 'excel1904'), 43898)
%!assert_equal (convertTo (datetime (2024, 3, 9), 'juliandate'), 2460378.5)
%!assert_equal (convertTo (datetime (2024, 3, 9), 'modifiedjuliandate'), 60378)
%!assert_equal (convertTo (datetime (2024, 1, 1, 'TimeZone', 'UTC'), ...
%!              'posixtime'), 1704067200)
%!assert_equal (convertTo (datetime (2024, 3, 9), 'yyyymmdd'), 20240309)
%!assert_equal (convertTo (datetime (2024, 3, 9), 'DATENUM'), 739320)
%!assert_equal (isnan (convertTo (NaT, 'datenum')), true)

## Test 'convertTo' -- delegation matches the standalone methods
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'datenum'), ...
%!              datenum (datetime (2024, 3, 9, 6, 0, 0)))
%!assert_equal (convertTo (datetime (2024, 3, 9), 'excel1904'), ...
%!              exceltime (datetime (2024, 3, 9), '1904'))

## Test 'convertTo' -- error branches
%!error <datetime.convertTo: not enough input arguments.> ...
%! convertTo (datetime (2024, 1, 1))
%!error <datetime.convertTo: unrecognized conversion type 'bogus'.> ...
%! convertTo (datetime (2024, 1, 1), 'bogus')
%!error <datetime.convertTo: too many input arguments.> ...
%! convertTo (datetime (2024, 1, 1), 'datenum', 5)

## Test 'convertTo' -- ntp/ntfs/.net integer epochs (uint64), instant-based
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC'), ...
%!              'ntp'), uint64 (3918952800) * uint64 (4294967296))
%!assert_equal (class (convertTo (datetime (2024, 3, 9), 'ntp')), 'uint64')
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0.5, 'TimeZone', 'UTC'), ...
%!              'ntp'), ...
%!              uint64 (3918952800) * uint64 (4294967296) + uint64 (2147483648))
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, ...
%!              'TimeZone', 'America/New_York'), 'ntp'), ...
%!              uint64 (3918952800 + 18000) * uint64 (4294967296))
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0), 'ntp'), ...
%!              uint64 (3918952800) * uint64 (4294967296))
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC'), ...
%!              'ntfs'), uint64 (13354437600) * uint64 (10000000))
%!assert_equal (class (convertTo (datetime (2024, 3, 9), 'ntfs')), 'uint64')
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0.001, 'TimeZone', 'UTC'), ...
%!              'ntfs'), uint64 (13354437600) * uint64 (10000000) + uint64 (10000))
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC'), ...
%!              '.net'), uint64 (63845560800) * uint64 (10000000))
%!assert_equal (class (convertTo (datetime (2024, 3, 9), '.net')), 'uint64')

## Test 'convertTo' -- ntp/ntfs array preserves shape and values
%!test
%! t = datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC');
%! a = convertTo ([t, t + hours(1)], 'ntfs');
%! assert_equal (size (a), [1, 2]);
%! assert_equal (a(1), uint64 (13354437600) * uint64 (10000000));
%! assert_equal (a(2), uint64 (13354437600 + 3600) * uint64 (10000000));

## Test 'convertTo' -- epochtime (int64) with Epoch and TicksPerSecond
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC'), ...
%!              'epochtime'), int64 (1709964000))
%!assert_equal (class (convertTo (datetime (2024, 3, 9), 'epochtime')), 'int64')
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC'), ...
%!              'epochtime', 'TicksPerSecond', 1000), int64 (1709964000000))
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0, 'TimeZone', 'UTC'), ...
%!              'epochtime', 'Epoch', datetime (2000, 1, 1, 'TimeZone', 'UTC'), ...
%!              'TicksPerSecond', 1000), int64 (763279200000))
%!assert_equal (convertTo (datetime (2024, 3, 9, 6, 0, 0.5, 'TimeZone', 'UTC'), ...
%!              'epochtime'), int64 (1709964001))

## Test 'convertTo' -- integer epochs reject NaT, Inf, and out-of-range
%!error <datetime.convertTo: 'NTP' conversion is not supported for missing values, infinite datetimes, or datetimes outside the representable range.> ...
%! convertTo (NaT, 'ntp')
%!error <datetime.convertTo: 'NTFS' conversion is not supported for missing values, infinite datetimes, or datetimes outside the representable range.> ...
%! convertTo (NaT, 'ntfs')
%!error <datetime.convertTo: '.NET' conversion is not supported for missing values, infinite datetimes, or datetimes outside the representable range.> ...
%! convertTo (datetime (-1, 1, 1), '.net')
%!error <datetime.convertTo: 'NTP' conversion is not supported for missing values, infinite datetimes, or datetimes outside the representable range.> ...
%! convertTo (datetime (1850, 1, 1, 'TimeZone', 'UTC'), 'ntp')
%!error <datetime.convertTo: 'NTP' conversion is not supported for missing values, infinite datetimes, or datetimes outside the representable range.> ...
%! convertTo (datetime (2040, 1, 1, 'TimeZone', 'UTC'), 'ntp')
%!error <datetime.convertTo: 'NTP' conversion is not supported for missing values, infinite datetimes, or datetimes outside the representable range.> ...
%! convertTo ([datetime(2024, 1, 1, 'TimeZone', 'UTC'), ...
%!            NaT('TimeZone', 'UTC')], 'ntp')
%!error <datetime.convertTo: 'epochtime' conversion is not supported for missing values, infinite datetimes, or datetimes outside the int64 range for the given epoch and TicksPerSecond.> ...
%! convertTo (NaT, 'epochtime')
%!error <datetime.convertTo: the epoch and the input datetime array must both have a time zone, or must both be unzoned.> ...
%! convertTo (datetime (2024, 1, 1, 'TimeZone', 'UTC'), 'epochtime', 'Epoch', datetime (2000, 1, 1))
%!error <datetime.convertTo: 'TicksPerSecond' must be a positive scalar.> ...
%! convertTo (datetime (2024, 1, 1), 'epochtime', 'TicksPerSecond', -5)
%!error <datetime.convertTo: to convert datetimes to tt2000 times, the 'TimeZone' property of the input must be 'UTCLeapSeconds'.> ...
%! convertTo (datetime (2024, 1, 1), 'tt2000')

## Test 'posixtime' -- seconds since the Unix epoch, unzoned treated as UTC
%!assert_equal (posixtime (datetime (1970, 1, 1, 0, 0, 0)), 0)
%!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0)), 1704067200)
%!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0, 'TimeZone', 'UTC')), ...
%!              1704067200)
%!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0, ...
%!                         'TimeZone', 'America/New_York')), 1704085200)
%!assert_equal (posixtime (datetime (2024, 1, 1, 0, 0, 0.5)), 1704067200.5)
%!assert_equal (class (posixtime (datetime (2024, 1, 1))), 'double')
%!assert_equal (posixtime (datetime (2024, 1, [1; 2])), [1704067200; 1704153600])
%!assert_equal (isnan (posixtime (NaT)), true)

## Test 'interp1' -- datetime sample points, numeric values
%!test
%! t = datetime (2024, 1, 1) + days (0:4);
%! v = [10 20 30 40 50];
%! tq = datetime (2024, 1, 1) + days ([1.5 3.25]);
%! assert_equal (interp1 (t, v, tq), [25 42.5]);
%! assert_equal (class (interp1 (t, v, tq)), 'double');
%! assert_equal (interp1 (t, v, tq, 'nearest'), [30 40]);
%! assert_equal (interp1 (t, v, tq, 'previous'), [20 40]);
%!test
%! t = datetime (2024, 1, 1) + days (0:4);
%! v = [10 20 30 40 50];
%! assert_equal (isnan (interp1 (t, v, datetime (2024, 1, 10))), true);
%! assert_equal (interp1 (t, v, datetime (2024, 1, 10), 'linear', 'extrap'), 100);
%! assert_equal (interp1 (t, v, datetime (2024, 1, 10), 'linear', -1), -1);
%! assert_equal (isnan (interp1 (t, v, NaT)), true);

## Test 'interp1' -- datetime values yield a datetime result (zone preserved)
%!test
%! V = datetime (2024, 1, 1) + days (0:4);
%! r = interp1 (0:4, V, [1.5 3.25]);
%! assert_equal (class (r), 'datetime');
%! assert_equal (cellstr (r), {'02-Jan-2024 12:00:00', '04-Jan-2024 06:00:00'});
%! assert_equal (isnat (interp1 (0:4, V, 9)), true);
%!test
%! t = datetime (2024, 1, 1) + days (0:4);
%! V = datetime (2024, 1, 1) + days (0:4);
%! tq = datetime (2024, 1, 1) + days ([1.5 3.25]);
%! assert_equal (cellstr (interp1 (t, V, tq)), ...
%!               {'02-Jan-2024 12:00:00', '04-Jan-2024 06:00:00'});
%!test
%! Vz = datetime (2024, 1, 1, 'TimeZone', 'America/New_York') + days (0:4);
%! r = interp1 (0:4, Vz, 1.5);
%! assert_equal (r.TimeZone, 'America/New_York');
%! assert_equal (char (r, 'yyyy-MM-dd HH:mm:ss'), '2024-01-02 12:00:00');

## Test 'interp1' -- sample and query points must be the same kind
%!error <datetime.interp1: query points must be a datetime array when the sample points are datetime.> ...
%! interp1 (datetime (2024, 1, 1) + days (0:4), [1 2 3 4 5], 2)
%!error <datetime.interp1: query points must be numeric when the sample points are numeric.> ...
%! interp1 (0:4, [1 2 3 4 5], datetime (2024, 1, 2))

################################################################################
##                          ** Query Operations **                            ##
################################################################################

## Test 'iscolumn' / 'isrow' / 'isscalar' / 'isvector' / 'ismatrix'
%!assert_equal (iscolumn (datetime (2020, 1, [1; 2; 3])), true)
%!assert_equal (iscolumn (datetime (2020, 1, [1, 2, 3])), false)
%!assert_equal (isrow (datetime (2020, 1, [1, 2, 3])), true)
%!assert_equal (isrow (datetime (2020, 1, [1; 2; 3])), false)
%!assert_equal (isscalar (datetime (2020, 1, 1)), true)
%!assert_equal (isscalar (datetime (2020, 1, [1; 2])), false)
%!assert_equal (isvector (datetime (2020, 1, [1; 2; 3])), true)
%!assert_equal (isvector (datetime (2020, ones (2, 2), 1)), false)
%!assert_equal (ismatrix (datetime (2020, ones (2, 2), 1)), true)

## Test 'isempty'
%!assert_equal (isempty (datetime (2020, 1, 1)), false)
%!assert_equal (isempty (datetime (zeros (0, 3))), true)

## Test 'isfinite' / 'isinf' / 'isnat'
%!assert_equal (isfinite (datetime (2020, 1, 1)), true)
%!assert_equal (isfinite (NaT), false)
%!assert_equal (isfinite (datetime (Inf, Inf, Inf)), false)
%!assert_equal (isinf (datetime (Inf, Inf, Inf)), true)
%!assert_equal (isinf (datetime (2020, 1, 1)), false)
%!assert_equal (isnat (NaT), true)
%!assert_equal (isnat (datetime (2020, 1, 1)), false)
%!assert_equal (isnat (datetime (2020, 1, [1; 2])), [false; false])

## Test 'ismissing'
%!assert_equal (ismissing (datetime (2020, 1, 1)), false)
%!assert_equal (ismissing (NaT), true)
%!test
%! d = datetime (2020, 1, [1; 2; 3]);
%! d(2) = datetime (NaN, NaN, NaN);
%! assert_equal (ismissing (d), [false; true; false]);
%!assert_equal (ismissing (datetime (2020, 1, [1; 2; 3]), datetime (2020, 1, 3)), [false; false; true])
%!error <datetime.ismissing: too many input arguments.> ...
%! ismissing (datetime (2020, 1, 1), datetime (2020, 1, 1), 3)
%!error <datetime.ismissing: INDICATOR must be a 'datetime' array.> ...
%! ismissing (datetime (2020, 1, 1), 5)

## Test 'isbetween' -- the default interval is closed and returns a logical
%!test
%! x = datetime (2026, 1, [1 2 3 4 5]);
%! tf = isbetween (x, datetime (2026, 1, 2), datetime (2026, 1, 4));
%! assert_equal (class (tf), 'logical');
%! assert_equal (tf, logical ([0 1 1 1 0]));

## Test 'isbetween' -- the four interval types include the right endpoints
%!test
%! x = datetime (2026, 1, [1 2 3 4 5]);
%! lo = datetime (2026, 1, 2);
%! hi = datetime (2026, 1, 4);
%! assert_equal (isbetween (x, lo, hi, 'closed'), logical ([0 1 1 1 0]));
%! assert_equal (isbetween (x, lo, hi, 'open'), logical ([0 0 1 0 0]));
%! assert_equal (isbetween (x, lo, hi, 'openleft'), logical ([0 0 1 1 0]));
%! assert_equal (isbetween (x, lo, hi, 'openright'), logical ([0 1 1 0 0]));

## Test 'isbetween' -- Not-A-Time in any input is false; inverted bounds are false
%!assert_equal (isbetween ([NaT datetime(2026,1,3)], datetime (2026, 1, 2), ...
%!              datetime (2026, 1, 4)), logical ([0 1]))
%!assert_equal (isbetween (datetime (2026, 1, [1 2 3 4 5]), NaT, ...
%!              datetime (2026, 1, 4)), logical ([0 0 0 0 0]))
%!assert_equal (isbetween (datetime (2026, 1, [1 2 3 4 5]), datetime (2026, 1, 4), ...
%!              datetime (2026, 1, 2)), logical ([0 0 0 0 0]))

## Test 'isbetween' -- bounds broadcast and may be date/time text
%!assert_equal (isbetween (datetime (2026, 1, [1 2 3 4 5]), ...
%!              datetime (2026, 1, [1 2 3 4 5]), datetime (2026, 1, 4)), ...
%!              logical ([1 1 1 1 0]))
%!assert_equal (isbetween (datetime (2026, 1, [1 2 3 4 5]), '02-Jan-2026', ...
%!              '04-Jan-2026'), logical ([0 1 1 1 0]))

## Test 'isbetween' -- comparison is by absolute instant across time zones
%!assert_equal (isbetween (datetime (2026, 1, 1, 5, 0, 0, 'TimeZone', 'UTC'), ...
%!              datetime (2026, 1, 1, 0, 0, 0, 'TimeZone', 'America/New_York'), ...
%!              datetime (2026, 1, 1, 1, 0, 0, 'TimeZone', 'America/New_York')), true)

## Test 'isbetween' -- invalid arguments are rejected
%!error <datetime.isbetween: not enough input arguments.> ...
%! isbetween (datetime (2026, 1, 1), datetime (2026, 1, 2));
%!error <datetime.isbetween: too many input arguments.> ...
%! isbetween (datetime (2026, 1, 1), datetime (2026, 1, 1), datetime (2026, 1, 2), 'closed', 1);
%!error <datetime.isbetween: interval type must be 'closed', 'open', 'openleft', or 'openright'.> ...
%! isbetween (datetime (2026, 1, 1), datetime (2026, 1, 1), datetime (2026, 1, 2), 'bogus');
%!error <datetime.isbetween: cannot combine a datetime array with a time zone with one without a time zone.> ...
%! isbetween (datetime (2026, 1, 2), datetime (2026, 1, 1, 'TimeZone', 'UTC'), ...
%!            datetime (2026, 1, 3, 'TimeZone', 'UTC'));
%!error <datetime.isbetween: LOWER and UPPER must be datetime arrays or date/time text.> ...
%! isbetween (datetime (2026, 1, 1), days (1), days (2));

## Test 'isbetween' -- the four interval types at the boundaries
%!assert_equal (isbetween (datetime (2024, 1, [5 10 15]), datetime (2024, 1, 5), ...
%!              datetime (2024, 1, 15)), logical ([1 1 1]))
%!assert_equal (isbetween (datetime (2024, 1, [5 10 15]), datetime (2024, 1, 5), ...
%!              datetime (2024, 1, 15), 'open'), logical ([0 1 0]))
%!assert_equal (isbetween (datetime (2024, 1, [5 10 15]), datetime (2024, 1, 5), ...
%!              datetime (2024, 1, 15), 'openleft'), logical ([0 1 1]))
%!assert_equal (isbetween (datetime (2024, 1, [5 10 15]), datetime (2024, 1, 5), ...
%!              datetime (2024, 1, 15), 'openright'), logical ([1 1 0]))

## Test array ops -- concatenation and reshape preserve the time zone
%!test
%! c = [datetime(2024, 1, 1, 'TimeZone', 'UTC'), ...
%!      datetime(2024, 1, 2, 'TimeZone', 'UTC')];
%! assert_equal (c.TimeZone, 'UTC');
%!test
%! r = reshape (datetime (2024, 1, 1:4, 'TimeZone', 'Asia/Tokyo'), 2, 2);
%! assert_equal (r.TimeZone, 'Asia/Tokyo');
%! assert_equal (size (r), [2, 2]);

## Test 'colon' -- a negative step counts down
%!assert_equal (cellstr (datetime (2024, 1, 5):-caldays (1):datetime (2024, 1, 1)), ...
%!              {'05-Jan-2024', '04-Jan-2024', '03-Jan-2024', '02-Jan-2024', ...
%!               '01-Jan-2024'})

## Test 'unique' -- sorted order with the mapping indices
%!test
%! [b, ia, ib] = unique (datetime (2021, 1, [1 1 2]));
%! assert_equal (b.Day, [1 2]);
%! assert_equal (ia, [1; 3]);
%! assert_equal (ib, [1; 1; 2]);

## Test 'unique' -- round-trips B = A(ia) and A = B(ib)
%!test
%! a = datetime (2021, 1, [3 1 2 1]);
%! [b, ia, ib] = unique (a);
%! assert_equal (isequaln (b, a(ia)), true);
%! assert_equal (isequaln (a, b(ib)), true);

## Test 'unique' -- each Not-A-Time is kept as a distinct value, sorted last
%!test
%! a = datetime (2021, 1, [3 1 NaN 1 NaN 2]);
%! [b, ia, ib] = unique (a);
%! assert_equal (size (b), [1 5]);
%! assert_equal (b.Day(1:3), [1 2 3]);
%! assert_equal (isnat (b), logical ([0 0 0 1 1]));
%! assert_equal (ia, [2; 6; 1; 3; 5]);
%! assert_equal (ib, [3; 1; 4; 1; 5; 2]);

## Test 'unique' -- an all-NaT array keeps every element
%!assert_equal (sum (isnat (unique (datetime ([NaN NaN NaN], 1, 1)))), 3)

## Test 'unique' -- 'stable' keeps first-seen order, NaT included
%!test
%! a = datetime (2021, 1, [3 1 NaN 1 NaN 2]);
%! [b, ia, ib] = unique (a, 'stable');
%! assert_equal (b.Day([1 2 5]), [3 1 2]);
%! assert_equal (isnat (b), logical ([0 0 1 1 0]));
%! assert_equal (ia, [1; 2; 3; 5; 6]);
%! assert_equal (ib, [1; 2; 3; 2; 4; 5]);

## Test 'unique' -- 'first' (default) and 'last' occurrence indices
%!test
%! [~, ia] = unique (datetime (2020, 1, [2 1 2 1]), 'first');
%! assert_equal (ia, [2; 1]);
%! [~, ia] = unique (datetime (2020, 1, [2 1 2 1]), 'last');
%! assert_equal (ia, [4; 3]);

## Test 'unique' -- 'rows' treats each row as a single value
%!test
%! [b, ia, ib] = unique (datetime (2021, 1, [1 2; 3 4; 1 2]), 'rows');
%! assert_equal (b.Day, [1 2; 3 4]);
%! assert_equal (ia, [1; 2]);
%! assert_equal (ib, [1; 2; 1]);

## Test 'unique' -- orientation follows the input, empties give a column
%!assert_equal (size (unique (datetime (2021, 1, [3 1 2]))), [1 3])
%!assert_equal (size (unique (datetime (2021, 1, [3; 1; 2]))), [3 1])
%!assert_equal (size (unique (datetime (2021, 1, [3 1; 2 1]))), [3 1])
%!assert_equal (size (unique (datetime (2021, 1, []))), [0 1])

## Test 'unique' -- values are compared by absolute instant; the zone is kept
%!test
%! z = unique (datetime (2021, 1, [2 1 2], 'TimeZone', 'America/New_York'));
%! assert_equal (z.Day, [1 2]);
%! assert_equal (z.TimeZone, 'America/New_York');

## Test 'unique' -- the 'legacy' option is rejected
%!error <datetime.unique: 'legacy' option is not supported.> ...
%! unique (datetime (2021, 1, [1 2 1]), 'legacy');

## Test 'intersect' -- common values, sorted, carrying A's format
%!test
%! a = datetime (2026, 1, [1 2 3 4]);  a.Format = 'dd/MM/uuuu';
%! b = datetime (2026, 1, [3 4 5 6]);  b.Format = 'uuuu-MM-dd';
%! c = intersect (a, b);
%! assert_equal (day (c), [3 4]);
%! assert_equal (c.Format, 'dd/MM/uuuu');
%! assert_equal (intersect (b, a).Format, 'uuuu-MM-dd');

## Test 'intersect' -- index outputs satisfy C = A(ixA) = B(ixB)
%!test
%! a = datetime (2026, 1, [1 2 3 4]);
%! b = datetime (2026, 1, [3 4 5 6]);
%! [c, ixA, ixB] = intersect (a, b);
%! assert_equal (isequaln (c, a(ixA)), true);
%! assert_equal (isequaln (c, b(ixB)), true);

## Test 'intersect' -- membership is by absolute instant, result keeps A's zone
%!test
%! a = datetime (2026, 1, 1, 12, 0, 0, 'TimeZone', 'UTC');
%! b = datetime (2026, 1, 1, 7, 0, 0, 'TimeZone', 'America/New_York');
%! c = intersect (a, b);
%! assert_equal (numel (c), 1);
%! assert_equal (c.TimeZone, 'UTC');
%! assert_equal (hour (c), 12);

## Test 'intersect' -- two zones whose wall clocks never share an instant
%!test
%! z1 = datetime (2026, 1, [1 2 3], 'TimeZone', 'America/New_York');
%! z2 = datetime (2026, 1, [2 3 4], 'TimeZone', 'Europe/Athens');
%! c = intersect (z1, z2);
%! assert_equal (numel (c), 0);
%! assert_equal (c.TimeZone, 'America/New_York');

## Test 'intersect' -- each NaT is distinct and never matches
%!test
%! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT];
%! n2 = [NaT, datetime(2026,1,2)];
%! c = intersect (n1, n2);
%! assert_equal (day (c), 2);
%! assert_equal (numel (c), 1);

## Test 'intersect' -- orientation: column unless both inputs are rows
%!assert_equal (size (intersect (datetime(2026,1,(1:4)'), datetime(2026,1,[3 4 5 6]))), [2 1])
%!assert_equal (size (intersect (datetime(2026,1,[1 2 3 4]), datetime(2026,1,(3:6)'))), [2 1])
%!assert_equal (size (intersect (datetime(2026,1,[1 2 3 4]), datetime(2026,1,[3 4 5 6]))), [1 2])

## Test 'intersect' -- 'stable' returns values in order of appearance in A
%!test
%! a = datetime (2026, 1, [4 3 2 1]);
%! b = datetime (2026, 1, [3 4 5 6]);
%! assert_equal (day (intersect (a, b, 'stable')), [4 3]);

## Test 'intersect' -- 'rows' matches whole rows
%!test
%! m1 = datetime (2026, 1, [1 2; 3 4]);
%! m2 = datetime (2026, 1, [3 4; 5 6]);
%! assert_equal (day (intersect (m1, m2, 'rows')), [3 4]);

## Test 'intersect' -- empty operand gives a 0-by-1 result
%!assert_equal (size (intersect (datetime (2026, 1, []), datetime (2026, 1, [3 4]))), [0 1])

## Test 'intersect' -- infinite elements compare as ordinary sortable values
%!test
%! p = datetime (2026, 1, 1) + days (Inf);
%! a = [datetime(2026,1,1), p, datetime(2026,1,1) - days(Inf)];
%! b = [p, datetime(2026,1,9)];
%! c = intersect (a, b);
%! assert_equal (numel (c), 1);
%! assert_equal (isinf (c) & ! isnat (c), true);

## Test 'intersect' -- text operands are promoted to datetime
%!assert_equal (day (intersect (datetime (2026, 1, [1 2 3 4]), '02-Jan-2026')), 2)
%!assert_equal (day (intersect (datetime (2026, 1, [1 2 3 4]), ...
%!                              {'02-Jan-2026', '03-Jan-2026'})), [2 3])

## Test 'intersect' -- invalid operands and options are rejected
%!error <datetime.intersect: comparison is not defined between datetime and numeric arrays.> ...
%! intersect (datetime (2026, 1, [1 2]), [736000 736001]);
%!error <datetime.intersect: comparison is not defined between datetime and duration arrays.> ...
%! intersect (datetime (2026, 1, [1 2]), days (1));
%!error <datetime.intersect: cannot combine a datetime array with a time zone with one without a time zone.> ...
%! intersect (datetime (2026, 1, 1), datetime (2026, 1, 1, 'TimeZone', 'UTC'));
%!error <datetime.intersect: 'legacy' option is not supported.> ...
%! intersect (datetime (2026, 1, [1 2]), datetime (2026, 1, [2 3]), 'legacy');

## Test set-ops -- cross-zone results are instant-based and NaT never collapses
%!assert_equal (cellstr (intersect (datetime (2024, 1, 1, 12, 0, 0, ...
%!              'TimeZone', 'America/New_York'), datetime (2024, 1, 1, 17, 0, 0, ...
%!              'TimeZone', 'UTC'))), {'01-Jan-2024 12:00:00'})
%!assert_equal (numel (union (datetime (2024, 1, 1, [12 13], 0, 0, 'TimeZone', ...
%!              'America/New_York'), datetime (2024, 1, 1, [17 20], 0, 0, ...
%!              'TimeZone', 'UTC'))), 3)
%!assert_equal (numel (setdiff (datetime (2024, 1, 1, [12 13], 0, 0, 'TimeZone', ...
%!              'America/New_York'), datetime (2024, 1, 1, [17 20], 0, 0, ...
%!              'TimeZone', 'UTC'))), 1)
%!assert_equal (numel (unique ([datetime(2024, 1, 1), NaT, NaT])), 3)

## Test 'union' -- all unique values, sorted, carrying A's format
%!test
%! a = datetime (2026, 1, [1 2 3 4]);  a.Format = 'dd/MM/uuuu';
%! b = datetime (2026, 1, [3 4 5 6]);
%! c = union (a, b);
%! assert_equal (day (c), [1 2 3 4 5 6]);
%! assert_equal (c.Format, 'dd/MM/uuuu');

## Test 'union' -- cross-zone values are interleaved by absolute instant
%!test
%! z1 = datetime (2026, 1, [1 2 3], 'TimeZone', 'America/New_York');
%! z2 = datetime (2026, 1, [2 3 4], 'TimeZone', 'Europe/Athens');
%! c = union (z1, z2);
%! assert_equal (day (c), [1 1 2 2 3 3]);
%! assert_equal (hour (c), [0 17 0 17 0 17]);
%! assert_equal (c.TimeZone, 'America/New_York');

## Test 'union' -- every distinct NaT is retained, sorted last
%!test
%! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT];
%! n2 = [NaT, datetime(2026,1,2)];
%! c = union (n1, n2);
%! assert_equal (numel (c), 5);
%! assert_equal (day (c)(1:2), [1 2]);
%! assert_equal (isnat (c), logical ([0 0 1 1 1]));

## Test 'union' -- 'stable' keeps A's values then B's new values in order
%!assert_equal (day (union (datetime (2026, 1, [4 1]), ...
%!                          datetime (2026, 1, [1 7]), 'stable')), [4 1 7])

## Test 'union' -- 'rows' combines unique rows
%!test
%! m1 = datetime (2026, 1, [1 2; 3 4]);
%! m2 = datetime (2026, 1, [3 4; 5 6]);
%! assert_equal (day (union (m1, m2, 'rows')), [1 2; 3 4; 5 6]);

## Test 'union' -- orientation and an empty operand
%!assert_equal (size (union (datetime(2026,1,(1:2)'), datetime(2026,1,[2 3]))), [3 1])
%!assert_equal (size (union (datetime (2026, 1, []), datetime (2026, 1, [3 4]))), [2 1])

## Test 'union' -- index outputs and text promotion
%!assert_equal (day (union (datetime (2026, 1, [1 2]), '03-Jan-2026')), [1 2 3])
%!error <datetime.union: cannot combine a datetime array with a time zone with one without a time zone.> ...
%! union (datetime (2026, 1, 1), datetime (2026, 1, 1, 'TimeZone', 'UTC'));

## Test 'setdiff' -- values in A not in B, with the index output
%!test
%! a = datetime (2026, 1, [1 2 3 4]);
%! b = datetime (2026, 1, [3 4 5 6]);
%! [c, ixA] = setdiff (a, b);
%! assert_equal (day (c), [1 2]);
%! assert_equal (isequaln (c, a(ixA)), true);

## Test 'setdiff' -- A's NaT elements are all retained
%!test
%! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT];
%! n2 = [NaT, datetime(2026,1,2)];
%! c = setdiff (n1, n2);
%! assert_equal (numel (c), 3);
%! assert_equal (day (c)(1), 1);
%! assert_equal (isnat (c), logical ([0 1 1]));

## Test 'setdiff' -- 'stable', 'rows', orientation
%!assert_equal (day (setdiff (datetime (2026, 1, [4 1 2]), ...
%!                            datetime (2026, 1, 2), 'stable')), [4 1])
%!test
%! m1 = datetime (2026, 1, [1 2; 3 4]);
%! m2 = datetime (2026, 1, [3 4; 5 6]);
%! assert_equal (day (setdiff (m1, m2, 'rows')), [1 2]);
%!assert_equal (size (setdiff (datetime (2026, 1, []), datetime (2026, 1, [3 4]))), [0 1])

## Test 'setxor' -- values in exactly one input, sorted
%!test
%! a = datetime (2026, 1, [1 2 3 4]);
%! b = datetime (2026, 1, [3 4 5 6]);
%! assert_equal (day (setxor (a, b)), [1 2 5 6]);

## Test 'setxor' -- distinct NaT elements from both inputs are retained
%!test
%! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT];
%! n2 = [NaT, datetime(2026,1,2)];
%! c = setxor (n1, n2);
%! assert_equal (numel (c), 4);
%! assert_equal (day (c)(1), 1);
%! assert_equal (isnat (c), logical ([0 1 1 1]));

## Test 'setxor' -- 'stable' and 'rows'
%!assert_equal (day (setxor (datetime (2026, 1, [4 1]), ...
%!                           datetime (2026, 1, [1 7]), 'stable')), [4 7])
%!test
%! m1 = datetime (2026, 1, [1 2; 3 4]);
%! m2 = datetime (2026, 1, [3 4; 5 6]);
%! assert_equal (day (setxor (m1, m2, 'rows')), [1 2; 5 6]);

## Test 'ismember' -- membership, index, and same-instant across zones
%!assert_equal (ismember (datetime (2020, 1, [1; 2; 3]), datetime (2020, 1, [2; 3])), ...
%!              [false; true; true])
%!test
%! [tf, idx] = ismember (datetime (2020, 1, [1; 2; 3]), datetime (2020, 1, [2; 3]));
%! assert_equal (tf, [false; true; true]);
%! assert_equal (idx, [0; 1; 2]);
%!assert_equal (ismember (datetime (2026, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'), ...
%!                        datetime (2026, 1, 1, 7, 0, 0, 'TimeZone', 'America/New_York')), true)

## Test 'ismember' -- NaT never matches; text first operand is promoted
%!test
%! n1 = [datetime(2026,1,1), NaT, datetime(2026,1,2), NaT];
%! n2 = [NaT, datetime(2026,1,2)];
%! [tf, idx] = ismember (n1, n2);
%! assert_equal (tf, logical ([0 0 1 0]));
%! assert_equal (idx, [0 0 2 0]);
%!assert_equal (ismember ('02-Jan-2026', datetime (2026, 1, [1 2 3 4])), true)

## Test 'ismember' -- 'rows' and an empty first operand (TF follows A's size)
%!test
%! m1 = datetime (2026, 1, [1 2; 3 4]);
%! m2 = datetime (2026, 1, [3 4; 5 6]);
%! [tf, idx] = ismember (m1, m2, 'rows');
%! assert_equal (tf, [false; true]);
%! assert_equal (idx, [0; 1]);
%!assert_equal (size (ismember (datetime (2026, 1, []), datetime (2026, 1, [3 4]))), [0 0])

## Test 'ismember' -- invalid options and zone mismatch are rejected
%!error <datetime.ismember: 'rows' applies only to 2-D matrices.> ...
%! ismember (datetime (2026, 1, reshape (1:8, [2 2 2])), ...
%!           datetime (2026, 1, [1 2; 3 4]), 'rows');
%!error <datetime.ismember: 'rows' requires same number of columns.> ...
%! ismember (datetime (2026, 1, [1 2; 3 4]), ...
%!           datetime (2026, 1, [1 2 3; 4 5 6]), 'rows');
%!error <datetime.ismember: invalid optional argument.> ...
%! ismember (datetime (2026, 1, [1 2]), datetime (2026, 1, [2 3]), 'bogus');
%!error <datetime.ismember: cannot combine a datetime array with a time zone with one without a time zone.> ...
%! ismember (datetime (2026, 1, 1), datetime (2026, 1, 1, 'TimeZone', 'UTC'));

################################################################################
##                           ** Array Operations **                           ##
################################################################################

## Test 'cat' / 'horzcat' / 'vertcat'
%!assert_equal (size ([datetime(2020,1,1), datetime(2020,1,2)]), [1, 2])
%!assert_equal (size ([datetime(2020,1,1); datetime(2020,1,2)]), [2, 1])
%!assert_equal (day ([datetime(2020,1,1), datetime(2020,1,2)]), [1, 2])
%!assert_equal (size (cat (1, datetime (2020, 1, [1; 2]), datetime (2020, 1, 3))), [3, 1])
%!assert_equal (day (vertcat (datetime (2020, 1, 1), datetime (2020, 1, 2))), [1; 2])
%!error <datetime.cat: all inputs must be datetime arrays or date/time text.> ...
%! cat (2, datetime (2020, 1, 1), 5)

## Test 'cat' -- a 0-by-0 [] or {} contributes nothing and is dropped, so an
## array can be accumulated from an empty start
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! assert_equal (size ([ny, []]), [1, 1]);
%! assert_equal ([ny, []].TimeZone, 'America/New_York');
%! assert_equal (size ([[], ny]), [1, 1]);
%! assert_equal ([[], ny].TimeZone, 'America/New_York');
%! assert_equal (size ([ny, {}]), [1, 1]);
%! assert_equal (size ([ny, [], ny]), [1, 2]);
%! assert_equal (size (cat (2, [], ny)), [1, 1]);
%!test
%! d = datetime.empty;
%! for k = 1:3
%!   d = [d, datetime(2024, 1, k)];
%! endfor
%! assert_equal (d.Day, [1, 2, 3]);
%!test
%! d = [];
%! for k = 1:3
%!   d = [d, datetime(2024, 1, k)];
%! endfor
%! assert_equal (d.Day, [1, 2, 3]);
%!assert_equal (size ([datetime(2024, 1, 1), []]), [1, 1])

## Test 'cat' -- date/time text is promoted, and is read as a wall clock in the
## time zone of the first datetime operand rather than converted into it
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! r = [ny, '2024-01-01'];
%! assert_equal (size (r), [1, 2]);
%! assert_equal (r.TimeZone, 'America/New_York');
%! assert_equal (r.Hour, [12, 0]);
%! assert_equal (posixtime (r), [1704128400, 1704085200]);
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! r = [ny, '2024-01-01 06:00:00'];
%! assert_equal (posixtime (r), [1704128400, 1704106800]);
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! assert_equal ([ny, {'2024-01-01'}].Hour, [12, 0]);
%! assert_equal ([ny, string('2024-01-01')].Hour, [12, 0]);
%! assert_equal (size ([ny, {'2024-01-01', '2024-01-02'}]), [1, 3]);
%! assert_equal ([ny; '2024-01-02'].Day, [1; 2]);
%!test
%! un = datetime (2024, 1, 1, 12, 0, 0);
%! r = [un, '2024-01-01'];
%! assert_equal (isempty (r.TimeZone), true);
%! assert_equal (r.Hour, [12, 0]);

## Test 'cat' -- text may come first, the first datetime operand still giving
## the result its time zone
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! r = ['2024-01-01', ny];
%! assert_equal (r.TimeZone, 'America/New_York');
%! assert_equal (r.Hour, [0, 12]);
%!test
%! un = datetime (2024, 1, 1, 12, 0, 0);
%! r = ['2024-01-01', un];
%! assert_equal (isempty (r.TimeZone), true);
%! assert_equal (r.Hour, [0, 12]);

## Test 'cat' -- text promoted into the leap-second zone can name an inserted
## second
%!test
%! d = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! r = [d, '2016-12-31T23:59:60Z'];
%! assert_equal (r.Second, [60, 60]);

## Test 'cat' -- an empty [] is dropped before the time-zone test, so a mismatch
## behind it is still caught
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), [], datetime (2024, 1, 1))

## Test 'cat' -- anything that is neither a datetime nor date/time text is
## refused, an empty numeric of non-zero size included
%!error <datetime.cat: all inputs must be datetime arrays or date/time text.> ...
%! horzcat (datetime (2024, 1, 1), zeros (0, 3))
%!error <datetime.cat: all inputs must be datetime arrays or date/time text.> ...
%! horzcat (datetime (2024, 1, 1), NaN)
%!error <datetime.cat: all inputs must be datetime arrays or date/time text.> ...
%! horzcat (datetime (2024, 1, 1), true)
%!error <datetime.cat: all inputs must be datetime arrays or date/time text.> ...
%! horzcat (datetime (2024, 1, 1), int32 (5))
%!error <datetime.cat: all inputs must be datetime arrays or date/time text.> ...
%! horzcat (datetime (2024, 1, 1), days (1))
%!error <datetime.cat: all inputs must be datetime arrays or date/time text.> ...
%! horzcat (datetime (2024, 1, 1), calmonths (1))

## Test 'cat' -- zoned operands need not share a zone; each is converted into
## the zone of the first, so the instant each element names is preserved
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! ut = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC');
%! r = [ny, ut];
%! assert_equal (r.TimeZone, 'America/New_York');
%! assert_equal (r.Hour, [12, 7]);
%! assert_equal (posixtime (r), [posixtime(ny), posixtime(ut)]);
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! ut = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC');
%! r = [ut, ny];
%! assert_equal (r.TimeZone, 'UTC');
%! assert_equal (r.Hour, [12, 17]);
%! assert_equal (posixtime (r), [posixtime(ut), posixtime(ny)]);
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! lh = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! r = [ny, lh];
%! assert_equal (datevec (r)(2,:), [2023, 12, 31, 20, 0, 0]);
%! assert_equal (posixtime (r), [posixtime(ny), posixtime(lh)]);

## Test 'cat' -- the conversion applies along any dimension and to every operand
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! ut = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC');
%! assert_equal (cat (1, ny, ut).Hour, [12; 7]);
%! assert_equal (vertcat (ny, ut).Hour, [12; 7]);
%! assert_equal ([ny, ny, ut].Hour, [12, 12, 7]);
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! ut = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'UTC');
%! r = cat (3, ny, ut);
%! assert_equal (size (r), [1, 1, 2]);
%! assert_equal (r.TimeZone, 'America/New_York');

## Test 'cat' -- Format and TimeZone come from the first operand, and a NaT in
## another zone stays a NaT
%!test
%! ny = datetime (2024, 1, 1, 'Format', 'uuuu-MM-dd HH:mm:ss', ...
%!                'TimeZone', 'America/New_York');
%! ut = datetime (2024, 1, 1, 'Format', 'dd-MMM-uuuu', 'TimeZone', 'UTC');
%! assert_equal ([ny, ut].Format, 'uuuu-MM-dd HH:mm:ss');
%! assert_equal ([ut, ny].Format, 'dd-MMM-uuuu');
%!test
%! ny = datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York');
%! r = [ny, NaT('TimeZone', 'UTC')];
%! assert_equal (r.TimeZone, 'America/New_York');
%! assert_equal (isnat (r), logical ([0, 1]));

## Test 'cat' -- a zoned and an unzoned array cannot be concatenated, in either
## order, and a bare NaT or empty datetime is unzoned
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), datetime (2024, 1, 1))
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! horzcat (datetime (2024, 1, 1), datetime (2024, 1, 1, 'TimeZone', 'UTC'))
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! cat (1, datetime (2024, 1, 1, 'TimeZone', 'UTC'), datetime (2024, 1, 1))
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! vertcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), datetime (2024, 1, 1))
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), ...
%!          datetime (2024, 1, 1, 'TimeZone', 'America/New_York'), ...
%!          datetime (2024, 1, 1))
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), NaT)
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! horzcat (datetime (2024, 1, 1, 'TimeZone', 'UTC'), datetime.empty)
%!assert_equal (isempty ([datetime(2024, 1, 1), datetime(2024, 1, 2)].TimeZone), ...
%!              true)

## Test 'cat' -- the zone check is made before the leap-second one, as MATLAB
## does, and two leap-second arrays concatenate unchanged
%!error <datetime.cat: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! horzcat (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ...
%!          datetime (2017, 1, 1, 'TimeZone', 'UTC'))
%!error <datetime.cat: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! horzcat (datetime (2017, 1, 1, 'TimeZone', 'UTC'), ...
%!          datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'))
%!error <datetime.cat: cannot concatenate a datetime array that has a time zone with one that does not have a time zone.> ...
%! horzcat (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ...
%!          datetime (2017, 1, 1))
%!test
%! d = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! r = [d, d];
%! assert_equal (r.Second, [60, 60]);
%! assert_equal (r.TimeZone, 'UTCLeapSeconds');

## Test 'repmat' / 'repelem'
%!assert_equal (size (repmat (datetime (2020, 1, 1), 2, 3)), [2, 3])
%!assert_equal (day (repmat (datetime (2020, 1, 5), 1, 3)), [5, 5, 5])
%!assert_equal (day (repelem (datetime (2020, 1, [1, 2]), 1, 2)), [1, 1, 2, 2])

## Test 'reshape'
%!assert_equal (size (reshape (datetime (2020, 1, 1:6), 2, 3)), [2, 3])
%!assert_equal (size (reshape (datetime (2020, 1, 1:6), 3, [])), [3, 2])
%!assert_equal (day (reshape (datetime (2020, 1, 1:4), 2, 2)), [1, 3; 2, 4])

## Test 'circshift'
%!assert_equal (day (circshift (datetime (2020, 1, [1; 2; 3]), 1)), [3; 1; 2])

## Test 'permute' / 'ipermute'
%!assert_equal (size (permute (datetime (2020, 1, [1, 2, 3]), [2, 1])), [3, 1])
%!assert_equal (day (permute (datetime (2020, 1, [1, 2]), [2, 1])), [1; 2])
%!test
%! d = datetime (2020, ones (2, 3), 1);
%! assert_equal (size (ipermute (permute (d, [2, 1]), [2, 1])), [2, 3]);

## Test 'transpose' / 'ctranspose'
%!assert_equal (size (transpose (datetime (2020, 1, [1; 2; 3]))), [1, 3])
%!assert_equal (day (datetime (2020, 1, [1; 2; 3]).'), [1, 2, 3])
%!assert_equal (day (datetime (2020, 1, [1; 2; 3])'), [1, 2, 3])

################################################################################
##                  ** Reference and Assignment Operations **                 ##
################################################################################

## Test 'end' and subscripted reference
%!assert_equal (day (datetime (2020, 1, [1; 2; 3])(end)), 3)
%!assert_equal (day (datetime (2020, 1, [1; 2; 3])(2)), 2)
%!assert_equal (day (datetime (2020, 1, [1, 2, 3])(1:2)), [1, 2])
%!assert_equal (class (datetime (2020, 1, [1; 2; 3])(2)), 'datetime')

## Test property reference via subsref
%!assert_equal (datetime (2020, 6, 15).Year, 2020)
%!assert_equal (datetime (2020, 6, 15).Month, 6)
%!assert_equal (datetime (2020, 6, 15).Day, 15)
%!assert_equal (datetime (2020, 1, 1, 3, 4, 5).Hour, 3)
%!assert_equal (datetime (2020, 1, 1).Format, 'dd-MMM-uuuu')
%!error <datetime.subsref: unrecognized property: bogus> datetime (2020, 1, 1).bogus
%!error <datetime.subsref: '{}' invalid indexing for referencing values. Use '\(\)' instead.> ...
%! datetime (2020, 1, 1){1}

## Test subscripted assignment -- element assignment
%!test
%! d = datetime (2020, 1, [1; 2; 3]);
%! d(2) = datetime (2020, 1, 20);
%! assert_equal (day (d), [1; 20; 3]);
%!test
%! d = datetime (2020, 1, [1; 2; 3]);
%! d(2) = [];
%! assert_equal (day (d), [1; 3]);
%!error <datetime.subsasgn: cannot assign double values> ...
%! d = datetime (2020, 1, 1); d(1) = 5;

## Test subscripted assignment -- component setters re-canonicalise (rollover)
%!test  # d.Month = 13 rolls into the following year (MATLAB parity)
%! d = datetime (2020, 1, 15);
%! d.Month = 13;
%! assert_equal ([d.Year, d.Month, d.Day], [2021, 1, 15]);
%!test  # d.Day = 32 rolls into the next month
%! d = datetime (2020, 1, 15);
%! d.Day = 32;
%! assert_equal ([d.Year, d.Month, d.Day], [2020, 2, 1]);
%!test  # d.Hour = 25 rolls into the next day
%! d = datetime (2020, 1, 1);
%! d.Hour = 25;
%! assert_equal ([d.Day, d.Hour], [2, 1]);
%!test  # setting a component preserves sub-second precision
%! d = datetime (2020, 1, 1, 0, 0, 0.25);
%! d.Minute = 30;
%! assert_equal (second (d), 0.25);
%!test  # direct component assignment on a single element of a vector
%! d = datetime (2020, 1, [1; 2; 3]);
%! d.Year(2) = 1999;
%! assert_equal (year (d), [2020; 1999; 2020]);

## Test 'proxyArray' (regression: was undefined, breaking table interop)
%!assert_equal (datetime (2020, 6, 15).proxyArray, [2020, 6, 15, 0, 0, 0, 0])
%!assert_equal (size (datetime (2020, 1, [1; 2; 3]).proxyArray), [3, 7])
%!test
%! d = datetime (2020, 1, [1; 2]);
%! assert_equal (d.proxyArray, [2020, 1, 1, 0, 0, 0, 0; 2020, 1, 2, 0, 0, 0, 0]);
%!test  # NaT maps to NaN across its component columns
%! d = datetime (2020, 1, [1; 2]);
%! d(1) = NaT;
%! p = d.proxyArray;
%! assert_equal (p(1, :), [NaN, NaN, NaN, NaN, NaN, NaN, NaN]);

## The seventh column is the negated UTC offset, which keeps the two moments
## sharing a wall clock across a fall-back apart and in chronological order.
%!test
%! t = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! t.TimeZone = 'America/New_York';
%! u = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! u.TimeZone = 'America/New_York';
%! p = [t; u].proxyArray;
%! assert_equal (p(:, 1:6), [2024, 11, 3, 1, 30, 0; 2024, 11, 3, 1, 30, 0]);
%! assert_equal (p(:, 7), [14400; 18000]);

################################################################################
##                          ** TimeZone handling **                           ##
################################################################################

## Test attaching a zone to an unzoned array does NOT shift the wall clock
%!test
%! d = datetime (2020, 6, 1, 12, 0, 0);
%! d.TimeZone = 'America/New_York';
%! assert_equal ([d.Hour, d.Minute], [12, 0]);
%! assert_equal (d.TimeZone, 'America/New_York');

## Test converting between zones preserves the instant (wall clock shifts)
%!test  # UTC noon in January is 07:00 EST (UTC-5)
%! d = datetime (2020, 1, 1, 12, 0, 0, 'TimeZone', 'UTC');
%! d.TimeZone = 'America/New_York';
%! assert_equal ([d.Hour, d.Minute], [7, 0]);
%!test  # UTC noon in July is 08:00 EDT (UTC-4, daylight saving)
%! d = datetime (2020, 7, 1, 12, 0, 0, 'TimeZone', 'UTC');
%! d.TimeZone = 'America/New_York';
%! assert_equal ([d.Hour, d.Minute], [8, 0]);

## Test dropping the zone keeps the current wall-clock values
%!test
%! d = datetime (2020, 1, 1, 12, 0, 0, 'TimeZone', 'UTC');
%! d.TimeZone = '';
%! assert_equal ([d.Hour, d.Minute], [12, 0]);
%! assert_equal (d.TimeZone, '');

## Test invalid TimeZone assignment errors
%!error <datetime.subsasgn: 'TimeZone' must be a character vector.> ...
%! d = datetime (2020, 1, 1); d.TimeZone = 5;

## Test the two instants sharing a wall clock across a fall-back stay apart.
## On 3 November 2024 America/New_York puts its clock back at 02:00, so
## 01:30 comes round twice: once at UTC-4 and once at UTC-5.  Every value
## below was measured against MATLAB R2024a.
%!test  # the pair is built from UTC, which names each of them unambiguously
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! b2.TimeZone = 'America/New_York';
%! assert_equal (posixtime ([b1, b2]), [1730611800, 1730615400]);
%! assert_equal (hours (tzoffset ([b1, b2])), [-4, -5]);
%! assert_equal (cellstr ([b1, b2]), ...
%!               {'03-Nov-2024 01:30:00', '03-Nov-2024 01:30:00'});
%! assert_equal (isdst ([b1, b2]), [true, false]);
%!test  # concatenation carries each element's own offset, in either order
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! b2.TimeZone = 'America/New_York';
%! assert_equal (hours (tzoffset ([b2, b1])), [-5, -4]);
%! assert_equal (posixtime ([b2, b1]), [1730615400, 1730611800]);
%!test  # a round trip through another zone comes back to the same instant
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! d = b1; d.TimeZone = 'UTC'; d.TimeZone = 'America/New_York';
%! assert_equal (posixtime (d), 1730611800);
%! assert_equal (hours (tzoffset (d)), -4);
%! e = b1; e.TimeZone = 'Europe/Athens'; e.TimeZone = 'America/New_York';
%! assert_equal (posixtime (e), 1730611800);
%! assert_equal (hours (tzoffset (e)), -4);
%!test  # they are two values, not one, to every comparison and set operation
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! b2.TimeZone = 'America/New_York';
%! assert_equal (b1 == b2, false);
%! assert_equal (b1 < b2, true);
%! assert_equal (isequal (b1, b2), false);
%! assert_equal (numel (unique ([b1, b2])), 2);
%! assert_equal (ismember (b1, b2), false);
%! assert_equal (numel (intersect (b1, b2)), 0);
%! assert_equal (numel (union (b1, b2)), 2);
%! assert_equal (keyHash (b1) == keyHash (b2), false);
%!test  # sorting is by instant, so the pair comes back in the other order
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! b2.TimeZone = 'America/New_York';
%! [sv, si] = sort ([b2, b1]);
%! assert_equal (si, [2, 1]);
%! assert_equal (posixtime (sv), [1730611800, 1730615400]);
%! assert_equal (hours (diff ([b1, b2])), 1);
%!test  # the zone fields of a display format read the element's own offset
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! b2.TimeZone = 'America/New_York';
%! p = [b1, b2];
%! p.Format = 'yyyy-MM-dd HH:mm:ss z';
%! assert_equal (cellstr (p), {'2024-11-03 01:30:00 EDT', ...
%!                             '2024-11-03 01:30:00 EST'});
%! p.Format = 'yyyy-MM-dd HH:mm:ss Z';
%! assert_equal (cellstr (p), {'2024-11-03 01:30:00 -0400', ...
%!                             '2024-11-03 01:30:00 -0500'});
%!test  # the wall-clock numeric views stay deliberately non-injective
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! b2.TimeZone = 'America/New_York';
%! assert_equal (datenum (b1), datenum (b2));
%! assert_equal (exceltime (b1), exceltime (b2));
%! assert_equal (datevec (b1), datevec (b2));
%! assert (juliandate (b1) != juliandate (b2));
%!test  # a calendar difference of none still leaves the hour between them
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! b2.TimeZone = 'America/New_York';
%! assert_equal (char (between (b1, b2)), '1h 0m 0s');
%! assert_equal (char (between (b2, b1)), '-1h 0m 0s');
%! assert_equal (char (caldiff ([b1, b2])), '1h 0m 0s');
%!test  # a shift within the day stays in the element's own fold
%! b1 = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! b1.TimeZone = 'America/New_York';
%! b2 = datetime (2024, 11, 3, 6, 30, 0, 'TimeZone', 'UTC');
%! b2.TimeZone = 'America/New_York';
%! p = [b1, b2];
%! assert_equal (posixtime (dateshift (p, 'start', 'hour')), ...
%!               [1730610000, 1730613600]);
%! assert_equal (hours (tzoffset (dateshift (p, 'start', 'hour'))), [-4, -5]);
%! assert_equal (posixtime (dateshift (p, 'end', 'hour')), ...
%!               [1730613600, 1730617200]);
%! assert_equal (posixtime (dateshift (p, 'start', 'day')), ...
%!               [1730606400, 1730606400]);
%! assert_equal (posixtime (dateshift (p, 'end', 'day')), ...
%!               [1730696400, 1730696400]);
%!test  # a calendar step onto a repeated clock lands on the source's fold
%! z = 'America/New_York';
%! a = datetime (2024, 11, 2, 1, 30, 0, 'TimeZone', z);
%! r = a : caldays (1) : a + caldays (2);
%! assert_equal (posixtime (r), [1730525400, 1730611800, 1730701800]);
%! assert_equal (hours (tzoffset (r)), [-4, -4, -5]);
%!test  # stepping the instant crosses the fold rather than resolving to it
%! z = 'America/New_York';
%! a = datetime (2024, 11, 3, 0, 30, 0, 'TimeZone', z);
%! L = linspace (a, a + hours (3), 4);
%! assert_equal (posixtime (L), ...
%!               [1730608200, 1730611800, 1730615400, 1730619000]);
%! assert_equal (hours (tzoffset (L)), [-4, -4, -5, -5]);
%!test  # a POSIX instant names one of the pair, and says which
%! z = 'America/New_York';
%! p = datetime (1730611800, 'ConvertFrom', 'posixtime', 'TimeZone', z);
%! q = datetime (1730615400, 'ConvertFrom', 'posixtime', 'TimeZone', z);
%! assert_equal (posixtime ([p, q]), [1730611800, 1730615400]);
%! assert_equal (hours (tzoffset ([p, q])), [-4, -5]);
%! assert_equal (isdst ([p, q]), [true, false]);

## Test the same on a zone whose transition is half an hour, which is why the
## offset is stored in seconds rather than as a one-hour fold flag.  Australia
## /Lord_Howe puts its clock back 30 minutes on 7 April 2024; measured against
## MATLAB R2024a.
%!test
%! g1 = datetime (2024, 4, 6, 14, 45, 0, 'TimeZone', 'UTC');
%! g1.TimeZone = 'Australia/Lord_Howe';
%! g2 = datetime (2024, 4, 6, 15, 15, 0, 'TimeZone', 'UTC');
%! g2.TimeZone = 'Australia/Lord_Howe';
%! assert_equal (posixtime ([g1, g2]), [1712414700, 1712416500]);
%! assert_equal (hours (tzoffset ([g1, g2])), [11, 10.5]);
%! assert_equal (cellstr ([g1, g2]), ...
%!               {'07-Apr-2024 01:45:00', '07-Apr-2024 01:45:00'});
%! assert_equal (g1 == g2, false);
%! assert_equal (numel (unique ([g1, g2])), 2);

## Test the compiled regime cache never answers for a neighbouring regime.
## The tz database is consulted once per regime rather than once per element,
## so an array crossing a transition is where a stale answer would show: the
## same clocks are therefore read in one call, in the reverse order, and one at
## a time, and all three must agree.  Australia/Lord_Howe shifts by half an
## hour and Europe/Dublin's entries count winter as the saving period, so both
## the size of the step and the sense of the flag are covered.
%!test
%! for z = {'Australia/Lord_Howe', 'Europe/Dublin', 'America/New_York'}
%!   mi = 0:239;
%!   t = datetime (2024, 4, 7, 0, mi, 0, 'TimeZone', z{1});
%!   r = datetime (2024, 4, 7, 0, fliplr (mi), 0, 'TimeZone', z{1});
%!   assert_equal (posixtime (t), fliplr (posixtime (r)));
%!   assert_equal (hours (tzoffset (t)), fliplr (hours (tzoffset (r))));
%!   assert_equal (isdst (t), fliplr (isdst (r)));
%!   p = posixtime (t);
%!   o = hours (tzoffset (t));
%!   d = isdst (t);
%!   for i = 1:10:numel (mi)
%!     e = datetime (2024, 4, 7, 0, mi(i), 0, 'TimeZone', z{1});
%!     assert_equal (posixtime (e), p(i));
%!     assert_equal (hours (tzoffset (e)), o(i));
%!     assert_equal (isdst (e), d(i));
%!   endfor
%! endfor

## Test a wall clock the spring-forward gap skips still moves ahead by the gap
%!test
%! z = 'America/New_York';
%! assert_equal (posixtime (datetime (2024, 3, 10, 2, 30, 0, 'TimeZone', z)), ...
%!               1710055800);
%! assert_equal (hours (tzoffset (datetime (2024, 3, 10, 2, 30, 0, ...
%!                                          'TimeZone', z))), -4);

################################################################################
##                         ** Arithmetic Operations **                        ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'minus'                                                                    ##
##                                                                            ##
################################################################################

## Test 'minus' -- datetime minus datetime returns a duration
%!assert_equal (class (datetime (2021, 1, 2) - datetime (2021, 1, 1)), 'duration')
%!assert_equal (seconds (datetime (2021, 1, 2) - datetime (2021, 1, 1)), 86400)
%!assert_equal (seconds (datetime (2021, 1, 2, 6, 30, 15) ...
%!                       - datetime (2021, 1, 1, 0, 0, 0)), 109815)
%!assert_equal (days (datetime (2021, 1, 2) - datetime (2021, 1, 1)), 1)
%!assert_equal (seconds (datetime (2021, 1, 1) - datetime (2021, 1, 2)), -86400)

## Test 'minus' -- datetime difference is element-wise and size compatible
%!assert_equal (seconds (datetime (2021, 1, [2, 3, 4]) - datetime (2021, 1, 1)), ...
%!              [86400, 172800, 259200])
%!assert_equal (size (datetime (2021, 1, [1, 2; 3, 4]) - datetime (2021, 1, 1)), ...
%!              [2, 2])

## Test 'minus' -- zoned difference uses absolute instants across zones
%!assert_equal (hours (datetime (2021, 1, 1, 'TimeZone', 'Europe/London') ...
%!                     - datetime (2021, 1, 1, 'TimeZone', 'America/New_York')), -5)
%!assert_equal (seconds (datetime (2021, 7, 1, 12, 0, 0, 'TimeZone', 'UTC') ...
%!                       - datetime (2021, 7, 1, 12, 0, 0, 'TimeZone', 'UTC')), 0)

## Test 'minus' -- mixing a zoned and an unzoned datetime errors
%!error <datetime.minus: cannot subtract a datetime array with a time zone from one without a time zone.> ...
%! datetime (2021, 1, 1, 'TimeZone', 'UTC') - datetime (2021, 1, 1);
%!error <datetime.minus: cannot subtract a datetime array with a time zone from one without a time zone.> ...
%! datetime (2021, 1, 1) - datetime (2021, 1, 1, 'TimeZone', 'UTC');

## Test 'minus' -- datetime minus duration returns a shifted datetime
%!assert_equal (class (datetime (2021, 1, 1) - hours (30)), 'datetime')
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) - hours (30)), ...
%!              [2020, 12, 30, 18, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1) - days (1)), [2020, 12, 31, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) - minutes (90)), ...
%!              [2020, 12, 31, 22, 30, 0])

## Test 'minus' -- duration subtraction on a zoned array is instant-based (DST)
%!assert_equal (datevec (datetime (2021, 3, 14, 3, 0, 0, ...
%!                       'TimeZone', 'America/New_York') - hours (2)), ...
%!              [2021, 3, 14, 0, 0, 0])

## Test 'minus' -- numeric and logical operands are a number of 24-hour days
%!assert_equal (class (datetime (2021, 1, 1) - 5), 'datetime')
%!assert_equal (datevec (datetime (2021, 1, 1) - 5), [2020, 12, 27, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) - 1.5), ...
%!              [2020, 12, 30, 12, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1) - true), [2020, 12, 31, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1) - [1, 2, 3]), ...
%!              [2020, 12, 31, 0, 0, 0; 2020, 12, 30, 0, 0, 0; 2020, 12, 29, 0, 0, 0])

## Test 'minus' -- calendarDuration subtracts whole months with day clamping
%!assert_equal (class (datetime (2021, 1, 1) - calmonths (1)), 'datetime')
%!assert_equal (datevec (datetime (2021, 1, 1) - calmonths (1)), [2020, 12, 1, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 3, 31) - calmonths (1)), [2021, 2, 28, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 3, 30) - calmonths (1)), [2021, 2, 28, 0, 0, 0])
%!assert_equal (datevec (datetime (2020, 3, 31) - calmonths (13)), [2019, 2, 28, 0, 0, 0])
%!assert_equal (datevec (datetime (2020, 2, 29) - calyears (1)), [2019, 2, 28, 0, 0, 0])

## Test 'minus' -- calendarDuration applies months, then days, then time
%!assert_equal (datevec (datetime (2021, 1, 1) ...
%!                       - calendarDuration (0, 1, 15, 0, 0, 0)), [2020, 11, 16, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 31) - calmonths (1) - caldays (1)), ...
%!              [2020, 12, 30, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) ...
%!                       - calendarDuration (0, 0, 0, 6, 0, 0)), [2020, 12, 31, 18, 0, 0])

## Test 'minus' -- Not-A-Time and infinite elements propagate
%!assert_equal (isnan (seconds (NaT - datetime (2021, 1, 1))), true)
%!assert_equal (isnat (NaT - hours (1)), true)
%!assert_equal (isnat (datetime (2021, 1, 1) - days (NaN)), true)
%!assert_equal (isinf (year (datetime (2021, 1, 1) - days (Inf))), true)
%!assert_equal (sign (year (datetime (2021, 1, 1) - days (Inf))), -1)
%!assert_equal (sign (year (datetime (2021, 1, 1) - days (-Inf))), 1)

## Test 'minus' -- empty arrays return empty
%!assert_equal (isempty (datetime (2021, 1, []) - datetime (2021, 1, [])), true)
%!assert_equal (isempty (datetime (2021, 1, []) - 1), true)

## Test 'minus' -- integer, char, string, and cell operands error
%!error <datetime.minus: cannot subtract a 'int8' array from a datetime array; convert it to double or a duration first.> ...
%! datetime (2021, 1, 1) - int8 (5);
%!error <datetime.minus: cannot subtract a 'char' array from a datetime array.> ...
%! datetime (2021, 1, 1) - 'A';
%!error <datetime.minus: cannot subtract a 'string' array from a datetime array.> ...
%! datetime (2021, 1, 1) - string ("A");
%!error <datetime.minus: cannot subtract a 'cell' array from a datetime array.> ...
%! datetime (2021, 1, 1) - {1};

## Test 'minus' -- subtracting a datetime from a non-datetime left operand errors
%!error <datetime.minus: cannot subtract a datetime array from a 'double' array.> ...
%! 5 - datetime (2021, 1, 1);
%!error <datetime.minus: cannot subtract a datetime array from a 'int8' array.> ...
%! int8 (5) - datetime (2021, 1, 1);
%!error <datetime.minus: cannot subtract a datetime array from a 'logical' array.> ...
%! true - datetime (2021, 1, 1);
%!error <datetime.minus: cannot subtract a datetime array from a 'char' array.> ...
%! 'A' - datetime (2021, 1, 1);

## Test arithmetic -- fixed vs calendar shifts across a DST spring-forward
%!assert_equal (char (datetime (2024, 3, 10, 1, 30, 0, 'TimeZone', ...
%!              'America/New_York') + hours (1), 'yyyy-MM-dd HH:mm:ss'), ...
%!              '2024-03-10 03:30:00')
%!assert_equal (char (datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', ...
%!              'America/New_York') + days (1), 'yyyy-MM-dd HH:mm:ss'), ...
%!              '2024-03-10 13:00:00')
%!assert_equal (char (datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', ...
%!              'America/New_York') + caldays (1), 'yyyy-MM-dd HH:mm:ss'), ...
%!              '2024-03-10 12:00:00')

## Test arithmetic -- elapsed time spans 23 h / 25 h on DST transition days
%!assert_equal (hours (datetime (2024, 3, 11, 'TimeZone', 'America/New_York') ...
%!              - datetime (2024, 3, 10, 'TimeZone', 'America/New_York')), 23)
%!assert_equal (hours (datetime (2024, 11, 4, 'TimeZone', 'America/New_York') ...
%!              - datetime (2024, 11, 3, 'TimeZone', 'America/New_York')), 25)

## Test arithmetic -- calendar month/year addition clamps to month end
%!assert_equal (char (datetime (2024, 1, 31) + calmonths (1), 'yyyy-MM-dd'), ...
%!              '2024-02-29')
%!assert_equal (char (datetime (2023, 1, 31) + calmonths (1), 'yyyy-MM-dd'), ...
%!              '2023-02-28')
%!assert_equal (char (datetime (2024, 2, 29) + calyears (1), 'yyyy-MM-dd'), ...
%!              '2025-02-28')

## Test 'diff' -- successive differences return a duration in 'hh:mm:ss'
%!test
%! d = diff (datetime (2026, 1, [1 2 4 8]));
%! assert_equal (class (d), 'duration');
%! assert_equal (hours (d), [24 48 96]);
%! assert_equal (d.Format, 'hh:mm:ss');

## Test 'diff' -- N-th order difference is still a duration
%!assert_equal (hours (diff (datetime (2026, 1, [1 2 4 8]), 2)), [24 48])
%!assert_equal (hours (diff (datetime (2026, 1, [1 2 4 8]), 3)), 24)
%!assert_equal (class (diff (datetime (2026, 1, [1 2 4 8]), 2)), 'duration')

## Test 'diff' -- default operates along dim 1, DIM selects another
%!assert_equal (hours (diff (datetime (2026, 1, [1 2; 4 8]))), [72 144])
%!assert_equal (hours (diff (datetime (2026, 1, [1 2; 4 8]), 1, 2)), [24; 96])

## Test 'diff' -- a scalar yields a 0-by-0 empty duration
%!assert_equal (size (diff (datetime (2026, 1, 1))), [0 0])
%!assert_equal (class (diff (datetime (2026, 1, 1))), 'duration')

## Test 'diff' -- Not-A-Time propagates as a NaN duration
%!assert_equal (isnan (hours (diff ([datetime(2026,1,1), NaT, datetime(2026,1,5)]))), ...
%!              [true true])

## Test 'diff' -- differencing along a trailing singleton dimension is empty
%!assert_equal (size (diff (datetime (2026, 1, [1 2 4 8]), 1, 3)), [1 4 0])

## Test 'diff' -- zoned differences are daylight-saving aware (spring forward)
%!test
%! zs = datetime (2026, 3, 28, 12, 0, 0, 'TimeZone', 'Europe/Athens');
%! ze = datetime (2026, 3, 30, 12, 0, 0, 'TimeZone', 'Europe/Athens');
%! assert_equal (hours (diff ([zs ze])), 47);

## Test 'diff' -- N must be a positive integer scalar
%!error <datetime.diff: order N must be a positive integer scalar.> ...
%! diff (datetime (2026, 1, [1 2 3]), -1);
%!error <datetime.diff: order N must be a positive integer scalar.> ...
%! diff (datetime (2026, 1, [1 2 3]), 0);
%!error <datetime.diff: order N must be a positive integer scalar.> ...
%! diff (datetime (2026, 1, [1 2 3]), 1.5);
%!error <datetime.diff: DIM must be a positive integer scalar.> ...
%! diff (datetime (2026, 1, [1 2 3]), 1, 0);
%!error <datetime.diff: too many input arguments.> ...
%! diff (datetime (2026, 1, [1 2 3]), 1, 2, 3);

## Test 'caldiff' -- consecutive calendar differences as a calendarDuration
%!test
%! d = caldiff ([datetime(2026,1,31) datetime(2026,3,31) datetime(2027,5,1)]);
%! assert_equal (class (d), 'calendarDuration');
%! assert_equal (dispstrings (d), {'2mo', '1y 1mo 1d'});

## Test 'caldiff' -- a single requested component expresses the whole difference
%!test
%! tt = [datetime(2026,1,31) datetime(2026,3,31) datetime(2027,5,1)];
%! assert_equal (dispstrings (caldiff (tt, 'Time')), {'1416h 0m 0s', '9504h 0m 0s'});
%! assert_equal (dispstrings (caldiff (tt, 'Day')), {'59d', '396d'});
%! assert_equal (dispstrings (caldiff (tt, 'Month')), {'2mo', '13mo'});
%! assert_equal (dispstrings (caldiff (tt, 'q')), {'0q', '4q'});
%! assert_equal (calmonths (caldiff (tt, 'Year')), [0 12]);

## Test 'caldiff' -- an empty COMPONENTS string selects the default set
%!assert_equal (dispstrings (caldiff ([datetime(2026,1,31) datetime(2027,5,1)], '')), ...
%!              {'1y 3mo 1d'})

## Test 'caldiff' -- whole months honour month-end day clamping
%!assert_equal (dispstrings (caldiff ([datetime(2026,1,31) datetime(2026,2,28)])), {'1mo'})
%!assert_equal (dispstrings (caldiff ([datetime(2026,1,31), datetime(2026,2,28), ...
%!                                     datetime(2026,3,31)])), {'1mo', '1mo 3d'})

## Test 'caldiff' -- a time-of-day remainder appears as the Time component
%!assert_equal (dispstrings (caldiff ([datetime(2026,1,1,6,0,0), ...
%!                                     datetime(2026,3,15,18,30,0)])), {'2mo 14d 12h 30m 0s'})
%!assert_equal (dispstrings (caldiff ([datetime(2026,1,1,0,0,0.25), ...
%!                                     datetime(2026,1,1,0,0,1.75)])), {'0h 0m 1.5s'})

## Test 'caldiff' -- a scalar yields a 0-by-1 empty, Not-A-Time yields NaN
%!assert_equal (size (caldiff (datetime (2026, 1, 1))), [0 1])
%!assert_equal (class (caldiff (datetime (2026, 1, 1))), 'calendarDuration')
%!assert_equal (dispstrings (caldiff ([datetime(2026,1,1), NaT, datetime(2026,5,5)])), ...
%!              {'NaN', 'NaN'})

## Test 'caldiff' -- a matrix differences along dimension 1 by default
%!assert_equal (dispstrings (caldiff ([datetime(2026,1,31) datetime(2026,3,31); ...
%!              datetime(2026,5,1) datetime(2027,1,1)])), {'3mo 1d', '9mo 1d'})

## Test 'caldiff' -- an invalid COMPONENTS is rejected
%!error <datetime.caldiff: COMPONENTS must be 'Years', 'Quarters', 'Months', 'Weeks', 'Days', or 'Time', or a string array or cell array containing those components.> ...
%! caldiff ([datetime(2026,1,1) datetime(2026,2,1)], 'ym');

## Test 'between' -- signed calendar difference from A to B
%!test
%! d = between (datetime (2026, 1, 31), datetime (2026, 3, 30));
%! assert_equal (class (d), 'calendarDuration');
%! assert_equal (dispstrings (d), {'1mo 30d'});
%!assert_equal (dispstrings (between (datetime (2026, 3, 30), datetime (2026, 1, 31))), ...
%!              {'-1mo -28d'})
%!assert_equal (dispstrings (between (datetime (2026, 1, 31), datetime (2026, 1, 31))), {'0d'})

## Test 'between' -- month-end clamping and leap-day cases
%!assert_equal (dispstrings (between (datetime (2026, 1, 31), datetime (2026, 2, 28))), {'1mo'})
%!assert_equal (dispstrings (between (datetime (2026, 3, 31), datetime (2026, 2, 28))), {'-1mo'})
%!assert_equal (dispstrings (between (datetime (2026, 1, 31), datetime (2026, 3, 1))), {'1mo 1d'})
%!assert_equal (dispstrings (between (datetime (2024, 2, 29), datetime (2025, 2, 28))), {'1y'})

## Test 'between' -- the whole-unit borrow is gated by the time of day
%!assert_equal (dispstrings (between (datetime (2026, 1, 31, 10, 0, 0), ...
%!                                    datetime (2026, 2, 28, 8, 0, 0))), {'27d 22h 0m 0s'})
%!assert_equal (dispstrings (between (datetime (2024, 2, 29, 6, 0, 0), ...
%!                          datetime (2027, 5, 10, 20, 30, 15))), {'3y 2mo 12d 14h 30m 15s'})

## Test 'between' -- broadcasting a scalar against a vector
%!assert_equal (dispstrings (between ([datetime(2026,1,1) datetime(2026,6,1)], ...
%!                                    datetime (2026, 3, 15))), {'2mo 14d', '-2mo -17d'})

## Test 'between' -- Not-A-Time yields NaN
%!assert_equal (dispstrings (between (datetime (2026, 1, 1), ...
%!                                    [datetime(2026,3,15) NaT])), {'2mo 14d', 'NaN'})

## Test 'between' -- calendar differences ignore daylight saving; Time does not
%!test
%! zs = datetime (2026, 3, 28, 12, 0, 0, 'TimeZone', 'Europe/Athens');
%! ze = datetime (2026, 3, 30, 12, 0, 0, 'TimeZone', 'Europe/Athens');
%! assert_equal (dispstrings (between (zs, ze)), {'2d'});
%! assert_equal (dispstrings (between (zs, ze, 'Time')), {'47h 0m 0s'});

## Test 'between' -- invalid operands and options are rejected
%!error <datetime.between: A and B must be of common size or scalars.> ...
%! between (datetime (2026, 1, [1 2 3]), datetime (2026, 1, [1 2]));
%!error <datetime.between: cannot combine a datetime array with a time zone with one without a time zone.> ...
%! between (datetime (2026, 1, 1), datetime (2026, 1, 1, 'TimeZone', 'UTC'));
%!error <datetime.between: comparison is not defined between datetime and duration arrays.> ...
%! between (datetime (2026, 1, 31), days (1));
%!error <datetime.between: COMPONENTS must be 'Years', 'Quarters', 'Months', 'Weeks', 'Days', or 'Time', or a string array or cell array containing those components.> ...
%! between (datetime (2026, 1, 31), datetime (2026, 3, 30), 'bogus');

## Test 'mean' -- average of the instants as a datetime
%!test
%! m = mean (datetime (2026, 1, [1 2 4 8]));
%! assert_equal (class (m), 'datetime');
%! assert_equal (dispstrings (m), {'03-Jan-2026 18:00:00'});
%!assert_equal (dispstrings (mean (datetime (2026, 1, [1 2]))), {'01-Jan-2026 12:00:00'})
%!assert_equal (dispstrings (mean (datetime (2026, 1, 5))), {'05-Jan-2026'})

## Test 'mean' -- Not-A-Time is kept by default and dropped with 'omitnat'
%!test
%! rn = [datetime(2026,1,1) NaT datetime(2026,1,5)];
%! assert_equal (isnat (mean (rn)), true);
%! assert_equal (dispstrings (mean (rn, 'omitnat')), {'03-Jan-2026'});
%! assert_equal (dispstrings (mean (rn, 'omitmissing')), {'03-Jan-2026'});
%! assert_equal (isnat (mean (rn, 'includenat')), true);

## Test 'mean' -- dimension arguments and an empty array
%!test
%! rm = datetime (2026, 1, [1 2; 4 8]);
%! assert_equal (dispstrings (mean (rm)), ...
%!               {'02-Jan-2026 12:00:00', '05-Jan-2026 00:00:00'});
%! assert_equal (dispstrings (mean (rm, 2)), ...
%!               {'01-Jan-2026 12:00:00'; '06-Jan-2026 00:00:00'});
%! assert_equal (dispstrings (mean (rm, 'all')), {'03-Jan-2026 18:00:00'});
%!assert_equal (isnat (mean (datetime (2026, 1, []))), true)

## Test 'mean' -- the Format and TimeZone are inherited; zoned mean is DST-aware
%!test
%! rf = datetime (2026, 1, [1 2 4 8]);
%! rf.Format = 'dd/MM/uuuu HH:mm';
%! assert_equal (mean (rf).Format, 'dd/MM/uuuu HH:mm');
%!test
%! z = mean (datetime (2026, 1, [1 2 4], 'TimeZone', 'Europe/Athens'));
%! assert_equal (z.TimeZone, 'Europe/Athens');
%! assert_equal (dispstrings (z), {'02-Jan-2026 08:00:00'});

## Test 'mean' -- infinite instants propagate
%!assert_equal (isinf (mean ([datetime(2026,1,1), datetime(2026,1,1) + days(Inf)])), true)
%!assert_equal (isnat (mean ([datetime(2026,1,1) + days(Inf), ...
%!                            datetime(2026,1,1) - days(Inf)])), true)

## Test 'median' -- middle instant, averaging the two middle for an even count
%!assert_equal (dispstrings (median (datetime (2026, 1, [1 2 4 8]))), {'03-Jan-2026'})
%!assert_equal (dispstrings (median (datetime (2026, 1, [1 2 3 4]))), ...
%!              {'02-Jan-2026 12:00:00'})
%!assert_equal (dispstrings (median (datetime (2026, 1, [1 2 3]))), {'02-Jan-2026'})
%!assert_equal (class (median (datetime (2026, 1, [1 2 3]))), 'datetime')
%!test
%! rn = [datetime(2026,1,1) NaT datetime(2026,1,5)];
%! assert_equal (isnat (median (rn)), true);
%! assert_equal (dispstrings (median (rn, 'omitnat')), {'03-Jan-2026'});

## Test 'mode' -- smallest most-frequent value, ignoring Not-A-Time
%!assert_equal (dispstrings (mode (datetime (2026, 1, [1 2 4 8]))), {'01-Jan-2026'})
%!assert_equal (dispstrings (mode ([datetime(2026,1,1) NaT datetime(2026,1,5)])), ...
%!              {'01-Jan-2026'})
%!assert_equal (class (mode (datetime (2026, 1, [1 2 2]))), 'datetime')

## Test 'mode' -- the frequency and the list of all modal values
%!test
%! [m, f, c] = mode (datetime (2026, 1, [1 1 2 2 3]));
%! assert_equal (dispstrings (m), {'01-Jan-2026'});
%! assert_equal (f, 2);
%! assert_equal (dispstrings (c{1}), {'01-Jan-2026'; '02-Jan-2026'});

## Test 'std' -- standard deviation of the instants as a duration
%!test
%! s = std (datetime (2026, 1, [1 2 4 8]));
%! assert_equal (class (s), 'duration');
%! assert_equal (dispstrings (s), {'74:17:48'});

## Test 'std' -- the weight selects N-1 (default) or N normalisation
%!assert_equal (dispstrings (std (datetime (2026, 1, [1 2 4 8]), 1)), {'64:20:34'})
%!assert_equal (dispstrings (std (datetime (2026, 1, [1 2; 4 8]), 0, 2)), ...
%!              {'16:58:14'; '67:52:56'})

## Test 'std' -- 'omitnat' skips missing values and the mean is a second output
%!assert_equal (dispstrings (std ([datetime(2026,1,1) NaT datetime(2026,1,5)], ...
%!              'omitnat')), {'67:52:56'})
%!test
%! [s, m] = std (datetime (2026, 1, [1 2 4 8]));
%! assert_equal (class (s), 'duration');
%! assert_equal (class (m), 'datetime');
%! assert_equal (dispstrings (m), {'03-Jan-2026 18:00:00'});

## Test 'mean'/'median'/'mode' -- omit-flags, and mean along a chosen dimension
%!assert_equal (isnat (mean ([datetime(2024, 3, [1 9 20]), NaT])), true)
%!assert_equal (char (mean ([datetime(2024, 3, [1 9 20]), NaT], 'omitnat'), ...
%!              'yyyy-MM-dd'), '2024-03-10')
%!assert_equal (char (mean ([datetime(2024, 3, [1 9 20]), NaT], 'omitmissing'), ...
%!              'yyyy-MM-dd'), '2024-03-10')
%!assert_equal (char (median (datetime (2024, 3, [1 9 20])), 'yyyy-MM-dd'), ...
%!              '2024-03-09')
%!assert_equal (char (mode (datetime (2024, 1, [1 1 2 3])), 'yyyy-MM-dd'), ...
%!              '2024-01-01')
%!assert_equal (cellstr (mean (datetime (2024, 1, [10 20; 30 40]))), ...
%!              {'20-Jan-2024', '30-Jan-2024'})
%!assert_equal (cellstr (mean (datetime (2024, 1, [10 20; 30 40]), 2)), ...
%!              {'15-Jan-2024'; '04-Feb-2024'})

## Test 'dateshift' -- start of each calendar unit (a week starts on Sunday)
%!test
%! sh = datetime (2026, 7, 17, 13, 45, 30);
%! assert_equal (class (dateshift (sh, 'start', 'year')), 'datetime');
%! assert_equal (dispstrings (dateshift (sh, 'start', 'year')), {'01-Jan-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'quarter')), {'01-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'month')), {'01-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'week')), {'12-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'day')), {'17-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'hour')), {'17-Jul-2026 13:00:00'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'minute')), {'17-Jul-2026 13:45:00'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'second')), {'17-Jul-2026 13:45:30'});

## Test 'dateshift' -- end of each unit (next boundary below a day, last day above)
%!test
%! sh = datetime (2026, 7, 17, 13, 45, 30);
%! assert_equal (dispstrings (dateshift (sh, 'end', 'year')), {'31-Dec-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'end', 'quarter')), {'30-Sep-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'end', 'month')), {'31-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'end', 'week')), {'18-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'end', 'day')), {'18-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'end', 'hour')), {'17-Jul-2026 14:00:00'});
%! assert_equal (dispstrings (dateshift (sh, 'end', 'minute')), {'17-Jul-2026 13:46:00'});
%! assert_equal (dispstrings (dateshift (sh, 'end', 'second')), {'17-Jul-2026 13:45:31'});

## Test 'dateshift' -- end of a day is the following midnight
%!assert_equal (dispstrings (dateshift (datetime (2026, 1, 1), 'end', 'day')), {'02-Jan-2026'})
%!assert_equal (dispstrings (dateshift (datetime (2026, 1, 1), 'end', 'year')), {'31-Dec-2026'})

## Test 'dateshift' -- a rule shifts by whole units before truncating
%!test
%! sh = datetime (2026, 7, 17, 13, 45, 30);
%! assert_equal (dispstrings (dateshift (sh, 'start', 'week', 'next')), {'19-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'week', 'previous')), {'05-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'month', 'next')), {'01-Aug-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'end', 'month', 'next')), {'31-Aug-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'day', 2)), {'19-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'day', -3)), {'14-Jul-2026'});
%! assert_equal (dispstrings (dateshift (sh, 'start', 'month', 0)), {'01-Jul-2026'});

## Test 'dateshift' -- 'dayofweek' moves to a weekday, keeping the time of day
%!test
%! sh = datetime (2026, 7, 17, 13, 45, 30);
%! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 1)), {'19-Jul-2026 13:45:30'});
%! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 2)), {'20-Jul-2026 13:45:30'});
%! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 'Monday')), {'20-Jul-2026 13:45:30'});
%! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 2, 'previous')), {'13-Jul-2026 13:45:30'});
%! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 2, 'nearest')), {'20-Jul-2026 13:45:30'});
%! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 6, 'current')), {'17-Jul-2026 13:45:30'});
%! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 6, 2)), {'24-Jul-2026 13:45:30'});
%! assert_equal (dispstrings (dateshift (sh, 'dayofweek', 6, -1)), {'17-Jul-2026 13:45:30'});

## Test 'dateshift' -- across a week vector, and matrix / empty shapes
%!test
%! ww = datetime (2026, 7, [13 14 15 16 17 18 19]);
%! assert_equal (day (dateshift (ww, 'start', 'week')), [12 12 12 12 12 12 19]);
%! assert_equal (day (dateshift (ww, 'end', 'week')), [18 18 18 18 18 18 25]);
%!assert_equal (day (dateshift (datetime (2026, 1, [15 20; 25 30]), 'start', 'month')), ...
%!              [1 1; 1 1])
%!assert_equal (size (dateshift (datetime (2026, 1, []), 'start', 'month')), [0 0])

## Test 'dateshift' -- Not-A-Time and infinite elements pass through
%!assert_equal (isnat (dateshift (NaT, 'start', 'month')), true)
%!assert_equal (isinf (dateshift (datetime (2026, 1, 1) + days (Inf), 'start', 'month')), true)
%!test
%! v = dateshift ([datetime(2026,1,15) NaT], 'start', 'month');
%! assert_equal (day (v(1)), 1);
%! assert_equal (isnat (v(2)), true);

## Test 'dateshift' -- month-end respects leap years, and zoned start keeps the zone
%!assert_equal (dispstrings (dateshift (datetime (2024, 2, 10), 'end', 'month')), {'29-Feb-2024'})
%!assert_equal (dispstrings (dateshift (datetime (2026, 2, 10), 'end', 'month')), {'28-Feb-2026'})
%!test
%! z = dateshift (datetime (2026, 3, 29, 12, 0, 0, 'TimeZone', 'Europe/Athens'), ...
%!                'start', 'day');
%! assert_equal (z.TimeZone, 'Europe/Athens');
%! assert_equal (dispstrings (z), {'29-Mar-2026'});

## Test 'dateshift' -- invalid arguments are rejected
%!error <datetime.dateshift: second input must be 'start', 'end', or 'dayofweek'.> ...
%! dateshift (datetime (2026, 1, 1), 'bogus', 'month');
%!error <datetime.dateshift: unit must be 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', or 'year'.> ...
%! dateshift (datetime (2026, 1, 1), 'start', 'bogus');
%!error <datetime.dateshift: not enough input arguments.> ...
%! dateshift (datetime (2026, 1, 1), 'start');
%!error <datetime.dateshift: day of week must be a number from 1 to 7 or a day name.> ...
%! dateshift (datetime (2026, 1, 1), 'dayofweek', 8);
%!error <datetime.dateshift: day of week must be a number from 1 to 7 or a day name.> ...
%! dateshift (datetime (2026, 1, 1), 'dayofweek', 'Funday');
%!error <datetime.dateshift: rule must be an integer, 'next', 'previous', 'current', or 'nearest'.> ...
%! dateshift (datetime (2026, 1, 1), 'start', 'month', 'bogus');
%!error <datetime.dateshift: rule must be an integer, 'next', 'previous', 'current', or 'nearest'.> ...
%! dateshift (datetime (2026, 1, 1), 'start', 'week', 1.5);
%!error <datetime.dateshift: unit must be 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', or 'year'.> ...
%! dateshift (datetime (2026, 1, 1), 'end', 'dayofweek', 2);

## Test 'dateshift' -- start of every calendar/time unit
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'start', 'year'), 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'start', 'quarter'), 'yyyy-MM-dd HH:mm:ss'), '2024-01-01 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'start', 'month'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-01 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'start', 'week'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-10 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'start', 'hour'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-15 10:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'start', 'minute'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-15 10:30:00')

## Test 'dateshift' -- end of every calendar/time unit
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'end', 'year'), 'yyyy-MM-dd HH:mm:ss'), '2024-12-31 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'end', 'quarter'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-31 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'end', 'month'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-31 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'end', 'week'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-16 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'end', 'day'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-16 00:00:00')
%!assert_equal (char (dateshift (datetime (2024, 3, 15, 10, 30, 45), ...
%!              'end', 'hour'), 'yyyy-MM-dd HH:mm:ss'), '2024-03-15 11:00:00')

## Test 'between' -- month-end, negative, and leap-edge calendar differences
%!assert_equal (cellstr (between (datetime (2024, 1, 31), ...
%!              datetime (2024, 3, 31))), {'2mo'})
%!assert_equal (cellstr (between (datetime (2024, 1, 31), ...
%!              datetime (2024, 2, 29))), {'1mo'})
%!assert_equal (cellstr (between (datetime (2025, 3, 20), ...
%!              datetime (2024, 1, 15))), {'-1y -2mo -5d'})
%!assert_equal (cellstr (between (datetime (2020, 2, 29), ...
%!              datetime (2021, 2, 28))), {'1y'})
%!assert_equal (cellstr (between (datetime (2024, 1, 15), ...
%!              datetime (2025, 3, 20), 'days')), {'430d'})
%!assert_equal (cellstr (between (datetime (2024, 1, 15), ...
%!              datetime (2025, 3, 20), 'months')), {'14mo'})

## Test 'plus' -- datetime plus duration returns a shifted datetime
%!assert_equal (class (datetime (2021, 1, 1) + hours (30)), 'datetime')
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) + hours (30)), ...
%!              [2021, 1, 2, 6, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1) + days (1)), [2021, 1, 2, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) + minutes (90)), ...
%!              [2021, 1, 1, 1, 30, 0])

## Test 'plus' -- addition commutes with the datetime on either side
%!assert_equal (datevec (hours (5) + datetime (2021, 1, 1, 0, 0, 0)), ...
%!              [2021, 1, 1, 5, 0, 0])
%!assert_equal (datevec (calmonths (1) + datetime (2021, 1, 1)), [2021, 2, 1, 0, 0, 0])
%!assert_equal (datevec (caldays (3) + datetime (2021, 1, 1)), [2021, 1, 4, 0, 0, 0])
%!assert_equal (datevec (5 + datetime (2021, 1, 1)), [2021, 1, 6, 0, 0, 0])
%!assert_equal (datevec (5 + datetime (2021, 1, 1)), datevec (datetime (2021, 1, 1) + 5))

## Test 'plus' -- numeric and logical operands are a number of 24-hour days
%!assert_equal (datevec (datetime (2021, 1, 1) + 5), [2021, 1, 6, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) + 1.5), [2021, 1, 2, 12, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1) + true), [2021, 1, 2, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1) + [1, 2, 3]), ...
%!              [2021, 1, 2, 0, 0, 0; 2021, 1, 3, 0, 0, 0; 2021, 1, 4, 0, 0, 0])

## Test 'plus' -- calendarDuration adds whole months with day clamping
%!assert_equal (datevec (datetime (2021, 1, 1) + calmonths (1)), [2021, 2, 1, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1) + calmonths (13)), [2022, 2, 1, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 31) + calmonths (1)), [2021, 2, 28, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 3, 31) + calmonths (1)), [2021, 4, 30, 0, 0, 0])
%!assert_equal (datevec (datetime (2020, 2, 29) + calyears (1)), [2021, 2, 28, 0, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 1) + caldays (40)), [2021, 2, 10, 0, 0, 0])

## Test 'plus' -- calendarDuration applies months, then days, then time
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) ...
%!                       + calendarDuration (0, 1, 15, 6, 0, 0)), [2021, 2, 16, 6, 0, 0])
%!assert_equal (datevec (datetime (2021, 1, 31) + calmonths (1) + caldays (1)), ...
%!              [2021, 3, 1, 0, 0, 0])

## Test 'plus' -- duration addition on a zoned array is instant-based (DST)
%!assert_equal (datevec (datetime (2021, 3, 14, 0, 0, 0, ...
%!                       'TimeZone', 'America/New_York') + hours (2)), ...
%!              [2021, 3, 14, 3, 0, 0])
%!assert_equal (datevec (datetime (2021, 3, 14, 0, 0, 0, ...
%!                       'TimeZone', 'America/New_York') + caldays (1)), ...
%!              [2021, 3, 15, 0, 0, 0])

## Test 'plus' -- adding two datetime arrays is not defined
%!error <datetime.plus: addition is not defined between two datetime arrays.> ...
%! datetime (2021, 1, 1) + datetime (2021, 1, 2);

## Test 'plus' -- Not-A-Time and infinite elements propagate
%!assert_equal (isnat (NaT + hours (1)), true)
%!assert_equal (isnat (datetime (2021, 1, 1) + days (NaN)), true)
%!assert_equal (isnat (datetime (2021, 1, 1) + calmonths (NaN)), true)
%!assert_equal (isinf (year (datetime (2021, 1, 1) + days (Inf))), true)
%!assert_equal (sign (year (datetime (2021, 1, 1) + days (Inf))), 1)
%!assert_equal (sign (year (datetime (2021, 1, 1) + days (-Inf))), -1)

## Test 'plus' -- empty arrays return empty
%!assert_equal (isempty (datetime (2021, 1, []) + 1), true)
%!assert_equal (isempty (1 + datetime (2021, 1, [])), true)

## Test 'plus' -- integer, char, string, and cell operands error
%!error <datetime.plus: cannot add a 'int8' array to a datetime array; convert it to double or a duration first.> ...
%! datetime (2021, 1, 1) + int8 (5);
%!error <datetime.plus: cannot add a 'char' array to a datetime array.> ...
%! datetime (2021, 1, 1) + 'A';
%!error <datetime.plus: cannot add a 'string' array to a datetime array.> ...
%! datetime (2021, 1, 1) + string ("A");
%!error <datetime.plus: cannot add a 'cell' array to a datetime array.> ...
%! datetime (2021, 1, 1) + {1};
%!error <datetime.plus: cannot add a 'int8' array to a datetime array; convert it to double or a duration first.> ...
%! int8 (5) + datetime (2021, 1, 1);

################################################################################
##                        ** Relational Operations **                         ##
################################################################################
##                             Available Methods                              ##
##                                                                            ##
## 'lt'               'le'               'gt'               'ge'              ##
## 'eq'               'ne'                                                    ##
##                                                                            ##
################################################################################

## Test 'lt' -- earlier is less than later
%!assert_equal (datetime (2021, 1, 1) < datetime (2021, 6, 1), true)
%!assert_equal (datetime (2021, 6, 1) < datetime (2021, 1, 1), false)
%!assert_equal (datetime (2021, 1, 1) < datetime (2021, 1, 1), false)
%!assert_equal (datetime (2021, 1, 1, 0, 0, 0) < datetime (2021, 1, 1, 0, 0, 1), true)
%!assert_equal (class (datetime (2021, 1, 1) < datetime (2021, 6, 1)), 'logical')

## Test 'le' -- earlier or equal
%!assert_equal (datetime (2021, 1, 1) <= datetime (2021, 1, 1), true)
%!assert_equal (datetime (2021, 1, 1) <= datetime (2021, 6, 1), true)
%!assert_equal (datetime (2021, 6, 1) <= datetime (2021, 1, 1), false)

## Test 'gt' -- later is greater
%!assert_equal (datetime (2021, 6, 1) > datetime (2021, 1, 1), true)
%!assert_equal (datetime (2021, 1, 1) > datetime (2021, 6, 1), false)
%!assert_equal (datetime (2021, 1, 1) > datetime (2021, 1, 1), false)

## Test 'ge' -- later or equal
%!assert_equal (datetime (2021, 1, 1) >= datetime (2021, 1, 1), true)
%!assert_equal (datetime (2021, 6, 1) >= datetime (2021, 1, 1), true)
%!assert_equal (datetime (2021, 1, 1) >= datetime (2021, 6, 1), false)

## Test 'eq' and 'ne' -- equality of the same instant
%!assert_equal (datetime (2021, 1, 1) == datetime (2021, 1, 1), true)
%!assert_equal (datetime (2021, 1, 1) == datetime (2021, 6, 1), false)
%!assert_equal (datetime (2021, 1, 1) != datetime (2021, 6, 1), true)
%!assert_equal (datetime (2021, 1, 1) != datetime (2021, 1, 1), false)
%!assert_equal (datetime (2021, 1, 1, 12, 0, 0) == datetime (2021, 1, 1, 12, 0, 0), true)

## Test relational -- element-wise over arrays with broadcasting
%!assert_equal (datetime (2021, 1, [1, 2, 3]) < datetime (2021, 1, 2), ...
%!              [true, false, false])
%!assert_equal (datetime (2021, 1, [1, 2, 3]) == datetime (2021, 1, 2), ...
%!              [false, true, false])
%!assert_equal (datetime (2021, 1, [1, 2, 3]) >= datetime (2021, 1, 2), ...
%!              [false, true, true])
%!assert_equal (size (datetime (2021, 1, [1, 2; 3, 4]) < datetime (2021, 1, 3)), ...
%!              [2, 2])

## Test relational -- Not-A-Time never compares less/greater/equal
%!assert_equal (NaT < datetime (2021, 1, 1), false)
%!assert_equal (NaT > datetime (2021, 1, 1), false)
%!assert_equal (NaT <= datetime (2021, 1, 1), false)
%!assert_equal (NaT >= datetime (2021, 1, 1), false)
%!assert_equal (NaT == datetime (2021, 1, 1), false)
%!assert_equal (NaT == NaT, false)
%!assert_equal (NaT != NaT, true)
%!assert_equal (NaT != datetime (2021, 1, 1), true)
%!assert_equal ([datetime(2021,1,1), NaT] != NaT, [true, true])

## Test relational -- zoned arrays compare by absolute instant across zones
%!assert_equal (datetime (2021, 1, 1, 'TimeZone', 'America/New_York') ...
%!              == datetime (2021, 1, 1, 'TimeZone', 'Europe/London'), false)
%!assert_equal (datetime (2021, 1, 1, 'TimeZone', 'America/New_York') ...
%!              > datetime (2021, 1, 1, 'TimeZone', 'Europe/London'), true)
%!assert_equal (datetime (2021, 1, 1, 12, 0, 0, 'TimeZone', 'UTC') ...
%!              == datetime (2021, 1, 1, 7, 0, 0, 'TimeZone', 'America/New_York'), true)

## Test relational -- infinite datetimes order correctly
%!assert_equal (datetime (2021, 1, 1) - days (-Inf) > datetime (2021, 6, 1), true)
%!assert_equal (datetime (2021, 1, 1) - days (Inf) < datetime (2021, 1, 1), true)
%!assert_equal (datetime (2021, 1, 1) - days (Inf) == datetime (2021, 1, 1) - days (Inf), true)

## Test relational -- explicit cross-zone instant comparison and NaT inequality
%!assert_equal (datetime (2024, 1, 1, 12, 0, 0, 'TimeZone', 'America/New_York') ...
%!              == datetime (2024, 1, 1, 17, 0, 0, 'TimeZone', 'UTC'), true)
%!assert_equal (datetime (2024, 1, 1, 11, 0, 0, 'TimeZone', 'America/New_York') ...
%!              < datetime (2024, 1, 1, 17, 0, 0, 'TimeZone', 'UTC'), true)
%!assert_equal (NaT != NaT, true)
%!assert_equal (isbetween (datetime (2024, 1, 1, 14, 0, 0, 'TimeZone', ...
%!              'America/New_York'), datetime (2024, 1, 1, 18, 0, 0, 'TimeZone', ...
%!              'UTC'), datetime (2024, 1, 1, 20, 0, 0, 'TimeZone', 'UTC')), true)

## Test relational -- empty arrays return empty
%!assert_equal (isempty (datetime (2021, 1, []) < datetime (2021, 1, [])), true)

## Test relational -- comparing a zoned and an unzoned datetime errors
%!error <datetime.eq: cannot compare a datetime array with a time zone to one without a time zone.> ...
%! datetime (2021, 1, 1, 'TimeZone', 'UTC') == datetime (2021, 1, 1);
%!error <datetime.lt: cannot compare a datetime array with a time zone to one without a time zone.> ...
%! datetime (2021, 1, 1, 'TimeZone', 'UTC') < datetime (2021, 1, 1);

## Test relational -- a non-datetime operand errors
%!error <datetime.lt: both operands must be datetime arrays.> ...
%! datetime (2021, 1, 1) < 5;
%!error <datetime.gt: both operands must be datetime arrays.> ...
%! 5 > datetime (2021, 1, 1);
%!error <datetime.eq: both operands must be datetime arrays.> ...
%! datetime (2021, 1, 1) == 'x';

## Test relational -- broadcasting holds for all six operators
%!assert_equal (size (datetime (2021, 1, [1, 2, 3]) < datetime (2021, 1, [1; 2; 4])), [3, 3])
%!assert_equal (size (datetime (2021, 1, [1, 2, 3]) <= datetime (2021, 1, [1; 2; 4])), [3, 3])
%!assert_equal (size (datetime (2021, 1, [1, 2, 3]) > datetime (2021, 1, [1; 2; 4])), [3, 3])
%!assert_equal (size (datetime (2021, 1, [1, 2, 3]) >= datetime (2021, 1, [1; 2; 4])), [3, 3])
%!assert_equal (size (datetime (2021, 1, [1, 2, 3]) == datetime (2021, 1, [1; 2; 4])), [3, 3])
%!assert_equal (size (datetime (2021, 1, [1, 2, 3]) != datetime (2021, 1, [1; 2; 4])), [3, 3])
%!assert_equal (datetime (2021, 1, [1, 2, 3]) == datetime (2021, 1, [1; 2; 4]), ...
%!              logical ([1, 0, 0; 0, 1, 0; 0, 0, 0]))
%!assert_equal (datetime (2021, 1, [1, 2, 3]) < datetime (2021, 1, [1; 2; 4]), ...
%!              logical ([0, 0, 0; 1, 0, 0; 1, 1, 1]))
%!assert_equal (datetime (2021, 1, 2) <= datetime (2021, 1, [1, 2, 3]), ...
%!              [false, true, true])
%!assert_equal (datetime (2021, 1, [1, 2; 3, 4]) > datetime (2021, 1, 2), ...
%!              logical ([0, 0; 1, 1]))

## Test 'isequal' -- equal and unequal datetime arrays
%!assert_equal (isequal (datetime (2021, 1, 1), datetime (2021, 1, 1)), true)
%!assert_equal (isequal (datetime (2021, 1, 1), datetime (2021, 6, 1)), false)
%!assert_equal (isequal (datetime (2021, 1, 1, 12, 0, 0), ...
%!                       datetime (2021, 1, 1, 12, 0, 0)), true)
%!assert_equal (isequal (datetime (2021, 1, [1, 2, 3]), datetime (2021, 1, [1, 2, 3])), true)
%!assert_equal (class (isequal (datetime (2021, 1, 1), datetime (2021, 1, 1))), 'logical')
%!assert_equal (isscalar (isequal (datetime (2021, 1, [1, 2]), datetime (2021, 1, [1, 2]))), true)

## Test 'isequal' -- Not-A-Time is never equal (unlike isequaln)
%!assert_equal (isequal (NaT, NaT), false)
%!assert_equal (isequal (NaT, datetime (2021, 1, 1)), false)
%!assert_equal (isequal ([datetime(2021,1,1), NaT], [datetime(2021,1,1), NaT]), false)

## Test 'isequal' -- multiple arguments are all compared to the first
%!assert_equal (isequal (datetime (2021, 1, 1), datetime (2021, 1, 1), ...
%!                       datetime (2021, 1, 1)), true)
%!assert_equal (isequal (datetime (2021, 1, 1), datetime (2021, 1, 1), ...
%!                       datetime (2021, 6, 1)), false)

## Test 'isequal' -- size and orientation mismatches return false
%!assert_equal (isequal (datetime (2021, 1, [1, 2, 3]), datetime (2021, 1, [1, 2])), false)
%!assert_equal (isequal (datetime (2021, 1, [1, 2, 3]), datetime (2021, 1, [1; 2; 3])), false)

## Test 'isequal' -- empties of equal size are equal
%!assert_equal (isequal (datetime (2021, 1, []), datetime (2021, 1, [])), true)

## Test 'isequal' -- a non-datetime operand returns false, never errors
%!assert_equal (isequal (datetime (2021, 1, 1), 738522), false)
%!assert_equal (isequal (datetime (2021, 1, 1), 'x'), false)
%!assert_equal (isequal (datetime (2021, 1, 1), {1}), false)

## Test 'isequal' -- time zones compare by instant; a zone mismatch is false
%!assert_equal (isequal (datetime (2021, 1, 1, 'TimeZone', 'UTC'), ...
%!                       datetime (2021, 1, 1)), false)
%!assert_equal (isequal (datetime (2021, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'), ...
%!              datetime (2021, 1, 1, 7, 0, 0, 'TimeZone', 'America/New_York')), true)
%!assert_equal (isequal (datetime (2021, 1, 1, 'TimeZone', 'UTC'), ...
%!                       datetime (2021, 1, 1, 'TimeZone', 'America/New_York')), false)

## Test 'isequal' -- infinite elements compare equal by sign
%!assert_equal (isequal (datetime (2021, 1, 1) - days (Inf), ...
%!                       datetime (2021, 1, 1) - days (Inf)), true)
%!assert_equal (isequal (datetime (2021, 1, 1) - days (Inf), ...
%!                       datetime (2021, 1, 1) - days (-Inf)), false)

## Test 'isequaln' -- like isequal but Not-A-Time compares equal
%!assert_equal (isequaln (NaT, NaT), true)
%!assert_equal (isequaln (NaT, datetime (2021, 1, 1)), false)
%!assert_equal (isequaln ([datetime(2021,1,1), NaT], [datetime(2021,1,1), NaT]), true)
%!assert_equal (isequaln ([datetime(2021,1,1), NaT], [datetime(2021,1,1), datetime(2021,6,1)]), false)
%!assert_equal (isequaln (datetime (2021, 1, 1), datetime (2021, 1, 1)), true)
%!assert_equal (isequaln (datetime (2021, 1, 1), datetime (2021, 6, 1)), false)
%!assert_equal (class (isequaln (NaT, NaT)), 'logical')

## Test 'isequaln' -- multiple arguments are all compared to the first
%!assert_equal (isequaln (NaT, NaT, NaT), true)
%!assert_equal (isequaln (datetime (2021, 1, 1), NaT, datetime (2021, 1, 1)), false)

## Test 'isequaln' -- size, type, and time-zone mismatches return false
%!assert_equal (isequaln (datetime (2021, 1, [1, 2, 3]), datetime (2021, 1, [1, 2])), false)
%!assert_equal (isequaln (datetime (2021, 1, 1), 738522), false)
%!assert_equal (isequaln (datetime (2021, 1, 1, 'TimeZone', 'UTC'), datetime (2021, 1, 1)), false)
%!assert_equal (isequaln (datetime (2021, 1, 1, 12, 0, 0, 'TimeZone', 'UTC'), ...
%!              datetime (2021, 1, 1, 7, 0, 0, 'TimeZone', 'America/New_York')), true)

## Test 'isequaln' -- infinite elements are equal but distinct from Not-A-Time
%!assert_equal (isequaln (datetime (2021, 1, 1) - days (Inf), ...
%!                        datetime (2021, 1, 1) - days (Inf)), true)
%!assert_equal (isequaln (NaT, datetime (2021, 1, 1) - days (Inf)), false)

## Test 'colon' -- issue #2: ranges must not blow the stack
%!assert_equal (numel (datetime (2025, 5, 7) : datetime (2025, 5, 8)), 2)
%!assert_equal (class (datetime (2025, 5, 7) : datetime (2025, 5, 8)), 'datetime')
%!assert_equal (numel (datetime (2025, 5, 7, 0, 0, 0) : hours (1) ...
%!                     : datetime (2025, 5, 8, 0, 0, 0)), 25)

## Test 'colon' -- default step is one calendar day and yields a row vector
%!assert_equal (cellstr (datetime (2021, 1, 1) : datetime (2021, 1, 4)), ...
%!              {'01-Jan-2021', '02-Jan-2021', '03-Jan-2021', '04-Jan-2021'})
%!assert_equal (size (datetime (2021, 1, 1) : datetime (2021, 1, 5)), [1, 5])
%!assert_equal (isscalar (datetime (2021, 1, 1) : datetime (2021, 1, 1)), true)

## Test 'colon' -- duration and numeric steps advance by fixed elapsed time
%!assert_equal (numel (datetime (2021, 1, 1, 0, 0, 0) : hours (6) ...
%!                     : datetime (2021, 1, 2, 0, 0, 0)), 5)
%!assert_equal (cellstr (datetime (2021, 1, 1) : 2 : datetime (2021, 1, 5)), ...
%!              {'01-Jan-2021', '03-Jan-2021', '05-Jan-2021'})
%!assert_equal (datevec (datetime (2021, 1, 1, 0, 0, 0) : minutes (30) ...
%!                       : datetime (2021, 1, 1, 1, 0, 0)), ...
%!              [2021, 1, 1, 0, 0, 0; 2021, 1, 1, 0, 30, 0; 2021, 1, 1, 1, 0, 0])

## Test 'colon' -- calendarDuration steps clamp the day per element
%!assert_equal (cellstr (datetime (2021, 1, 31) : calmonths (1) : datetime (2021, 5, 31)), ...
%!              {'31-Jan-2021', '28-Feb-2021', '31-Mar-2021', '30-Apr-2021', '31-May-2021'})
%!assert_equal (cellstr (datetime (2021, 1, 31) : calmonths (2) : datetime (2021, 5, 31)), ...
%!              {'31-Jan-2021', '31-Mar-2021', '31-May-2021'})
%!assert_equal (numel (datetime (2021, 1, 1) : calyears (1) : datetime (2025, 1, 1)), 5)
%!assert_equal (cellstr (datetime (2021, 1, 1) : calquarters (1) : datetime (2021, 10, 1)), ...
%!              {'01-Jan-2021', '01-Apr-2021', '01-Jul-2021', '01-Oct-2021'})

## Test 'colon' -- default calendar-day step preserves the wall clock over DST
%!assert_equal ((datetime (2021, 3, 12, 'TimeZone', 'America/New_York') ...
%!               : datetime (2021, 3, 16, 'TimeZone', 'America/New_York')).Hour, ...
%!              [0, 0, 0, 0, 0])

## Test 'colon' -- a fixed 24-hour step drifts across a DST change
%!assert_equal ((datetime (2021, 3, 12, 'TimeZone', 'America/New_York') ...
%!               : days (1) : datetime (2021, 3, 16, 'TimeZone', 'America/New_York')).Hour, ...
%!              [0, 0, 0, 1])

## Test 'colon' -- reversed ranges need a negative step
%!assert_equal (isempty (datetime (2021, 1, 5) : datetime (2021, 1, 1)), true)
%!assert_equal (size (datetime (2021, 1, 5) : datetime (2021, 1, 1)), [1, 0])
%!assert_equal (isempty (datetime (2021, 1, 5) : caldays (1) : datetime (2021, 1, 1)), true)
%!assert_equal (cellstr (datetime (2021, 1, 5) : caldays (-1) : datetime (2021, 1, 3)), ...
%!              {'05-Jan-2021', '04-Jan-2021', '03-Jan-2021'})
%!assert_equal (numel (datetime (2021, 1, 1, 12, 0, 0) : hours (-6) ...
%!                     : datetime (2021, 1, 1, 0, 0, 0)), 3)

## Test 'colon' -- the range inherits the endpoints' time zone
%!assert_equal ((datetime (2021, 1, 1, 'TimeZone', 'UTC') ...
%!               : datetime (2021, 1, 3, 'TimeZone', 'UTC')).TimeZone, 'UTC')

## Test 'colon' -- error branches
%!error <datetime.colon: range endpoints must be datetime arrays.> ...
%! datetime (2021, 1, 1) : 5;
%!error <datetime.colon: range endpoints must be datetime scalars.> ...
%! datetime (2021, 1, [1, 2]) : datetime (2021, 1, 5);
%!error <datetime.colon: cannot create a range between a datetime with a time zone and one without a time zone.> ...
%! datetime (2021, 1, 1) : datetime (2021, 1, 5, 'TimeZone', 'UTC');
%!error <datetime.colon: range endpoints must be finite> ...
%! datetime (2021, 1, 1) : NaT;
%!error <datetime.colon: STEP must be nonzero.> ...
%! datetime (2021, 1, 1) : caldays (0) : datetime (2021, 1, 5);
%!error <datetime.colon: STEP must be a duration, calendarDuration, or numeric scalar.> ...
%! datetime (2021, 1, 1) : {1} : datetime (2021, 1, 5);
%!error <datetime.colon: STEP must be a scalar.> ...
%! datetime (2021, 1, 1) : caldays ([1, 2]) : datetime (2021, 1, 5);

## Test 'linspace' -- both endpoints are included and 100 values by default
%!test
%! r = linspace (datetime (2024, 1, 1), datetime (2024, 1, 11), 3);
%! assert_equal (size (r), [1, 3]);
%! assert_equal (r.Day, [1, 6, 11]);
%!test
%! a = datetime (2024, 1, 1);
%! b = datetime (2024, 1, 11);
%! assert_equal (size (linspace (a, b)), [1, 100]);
%! assert_equal (linspace (a, b, 2).Day, [1, 11]);

## Test 'linspace' -- N of one returns the end point, N below one is empty, and
## a non-integer N is floored
%!test
%! a = datetime (2024, 1, 1);
%! b = datetime (2024, 1, 11);
%! assert_equal (linspace (a, b, 1).Day, 11);
%! assert_equal (size (linspace (a, b, 0)), [1, 0]);
%! assert_equal (class (linspace (a, b, 0)), 'datetime');
%! assert_equal (size (linspace (a, b, -3)), [1, 0]);
%! assert_equal (linspace (a, b, 3.7).Day, [1, 6, 11]);
%! assert_equal (linspace (a, b, int32 (3)).Day, [1, 6, 11]);

## Test 'linspace' -- a reversed range descends, and equal endpoints repeat
%!test
%! a = datetime (2024, 1, 1);
%! assert_equal (linspace (datetime (2024, 1, 11), a, 3).Day, [11, 6, 1]);
%! assert_equal (linspace (a, a, 3).Day, [1, 1, 1]);

## Test 'linspace' -- the result carries the Format and TimeZone of the first
## endpoint
%!test
%! a = datetime (2024, 1, 1, 'Format', 'uuuu-MM-dd HH:mm:ss');
%! b = datetime (2024, 1, 11, 'Format', 'dd-MMM-uuuu');
%! assert_equal (linspace (a, b, 3).Format, 'uuuu-MM-dd HH:mm:ss');
%! assert_equal (linspace (b, a, 3).Format, 'dd-MMM-uuuu');
%!test
%! tz = 'America/New_York';
%! r = linspace (datetime (2024, 1, 1, 'TimeZone', tz), ...
%!               datetime (2024, 1, 11, 'TimeZone', tz), 3);
%! assert_equal (r.TimeZone, tz);

## Test 'linspace' -- spacing is by absolute instant, so it is daylight saving
## aware and the wall-clock readings are not equally spaced
%!test
%! a = datetime (2024, 3, 9, 0, 0, 0, 'TimeZone', 'America/New_York');
%! b = datetime (2024, 3, 11, 0, 0, 0, 'TimeZone', 'America/New_York');
%! r = linspace (a, b, 5);
%! assert_equal (r.Day, [9, 9, 9, 10, 11]);
%! assert_equal (r.Hour, [0, 11, 23, 12, 0]);
%! assert_equal (r.Minute, [0, 45, 30, 15, 0]);

## Test 'linspace' -- the two endpoints may sit in different time zones, the
## result being expressed in the zone of the first
%!test
%! a = datetime (2024, 1, 1, 0, 0, 0, 'TimeZone', 'America/New_York');
%! b = datetime (2024, 1, 11, 0, 0, 0, 'TimeZone', 'UTC');
%! r = linspace (a, b, 3);
%! assert_equal (r.TimeZone, 'America/New_York');
%! assert_equal (r.Day, [1, 5, 10]);
%! assert_equal (r.Hour, [0, 21, 19]);
%! assert_equal (r.Minute, [0, 30, 0]);

## Test 'linspace' -- sub-second spacing
%!test
%! r = linspace (datetime (2024, 1, 1, 0, 0, 0), ...
%!               datetime (2024, 1, 1, 0, 0, 1), 3);
%! assert_equal (r.Second, [0, 0.5, 1]);

## Test 'linspace' -- an inserted leap second is stepped through, the span from
## 23:59:59 to the next midnight being two SI seconds
%!test
%! tz = 'UTCLeapSeconds';
%! a = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', tz);
%! b = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', tz);
%! r = linspace (a, b, 3);
%! assert_equal (r.Second, [59, 60, 0]);
%! assert_equal (r.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'");

## Test 'linspace' -- a Not-A-Time endpoint carries through every value that
## depends on it, leaving only the opposite endpoint
%!test
%! r = linspace (datetime (2024, 1, 1), NaT, 3);
%! assert_equal (isnat (r), logical ([0, 1, 1]));
%! assert_equal (r.Day(1), 1);
%!test
%! r = linspace (NaT, datetime (2024, 1, 11), 3);
%! assert_equal (isnat (r), logical ([1, 1, 0]));
%! assert_equal (r.Day(3), 11);
%!assert_equal (isnat (linspace (NaT, NaT, 3)), logical ([1, 1, 1]))
%!assert_equal (isnat (linspace (datetime (2024, 1, 1), NaT, 1)), true)

## Test 'linspace' -- an infinite endpoint carries its infinity forward, while
## stepping back from an infinite start reaches nothing definite
%!test
%! a = datetime (2024, 1, 1);
%! pinf = datetime (Inf, 'ConvertFrom', 'datenum');
%! ninf = datetime (-Inf, 'ConvertFrom', 'datenum');
%! assert_equal (linspace (a, pinf, 3).Year, [2024, Inf, Inf]);
%! assert_equal (linspace (a, ninf, 3).Year, [2024, -Inf, -Inf]);
%! assert_equal (linspace (pinf, a, 3).Year, [Inf, NaN, 2024]);
%! assert_equal (linspace (ninf, a, 3).Year, [-Inf, NaN, 2024]);
%!test
%! pinf = datetime (Inf, 'ConvertFrom', 'datenum');
%! ninf = datetime (-Inf, 'ConvertFrom', 'datenum');
%! assert_equal (linspace (pinf, pinf, 3).Year, [Inf, NaN, Inf]);
%! assert_equal (linspace (ninf, pinf, 4).Year, [-Inf, NaN, NaN, Inf]);
%! assert_equal (linspace (pinf, ninf, 4).Year, [Inf, NaN, NaN, -Inf]);
%! assert_equal (isnat (linspace (pinf, ninf, 4)), logical ([0, 1, 1, 0]));

## Test 'linspace' -- error branches
%!error <datetime.linspace: too few input arguments.> ...
%! linspace (datetime (2024, 1, 1))
%!error <datetime.linspace: both endpoints must be datetime arrays.> ...
%! linspace (datetime (2024, 1, 1), 5, 3)
%!error <datetime.linspace: both endpoints must be datetime arrays.> ...
%! linspace (5, datetime (2024, 1, 11), 3)
%!error <datetime.linspace: both endpoints must be datetime arrays.> ...
%! linspace (datetime (2024, 1, 1), days (3), 3)
%!error <datetime.linspace: endpoints must be datetime scalars.> ...
%! linspace (datetime (2024, 1, [1, 2]), datetime (2024, 1, 11), 3)
%!error <datetime.linspace: endpoints must be datetime scalars.> ...
%! linspace (datetime (2024, 1, 1), datetime (2024, 1, [11, 12]), 3)
%!error <datetime.linspace: N must be a real numeric scalar.> ...
%! linspace (datetime (2024, 1, 1), datetime (2024, 1, 11), 'x')
%!error <datetime.linspace: cannot combine a datetime array with a time zone with one without a time zone.> ...
%! linspace (datetime (2024, 1, 1, 'TimeZone', 'UTC'), ...
%!           datetime (2024, 1, 11), 3)
%!error <datetime.linspace: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! linspace (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ...
%!           datetime (2017, 1, 2, 'TimeZone', 'UTC'), 3)

## Test 'sort'/'min'/'max' -- an array of more than two dimensions is reduced
## along the dimension asked for, checked against the numeric answer
%!test
%! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]);
%! a = datetime (2024, 1, x);
%! for d = 1:3
%!   [b, i] = sort (a, d);
%!   [w, wi] = sort (x, d);
%!   assert_equal (size (b), size (w));
%!   assert_equal (b.Day, w);
%!   assert_equal (i, wi);
%! endfor
%!test
%! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]);
%! a = datetime (2024, 1, x);
%! for d = 1:3
%!   [m, i] = min (a, [], d);
%!   [wm, wi] = min (x, [], d);
%!   assert_equal (size (m), size (wm));
%!   assert_equal (m.Day, wm);
%!   assert_equal (i, wi);
%!   [M, j] = max (a, [], d);
%!   [wM, wj] = max (x, [], d);
%!   assert_equal (M.Day, wM);
%!   assert_equal (j, wj);
%! endfor
%!test
%! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]);
%! a = datetime (2024, 1, x);
%! [b, i] = sort (a, 2, 'descend');
%! [w, wi] = sort (x, 2, 'descend');
%! assert_equal (b.Day, w);
%! assert_equal (i, wi);
%!test
%! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]);
%! z = datetime (2024, 1, x, 'TimeZone', 'America/New_York');
%! assert_equal (sort (z, 3).Day, sort (x, 3));
%! assert_equal (min (z, [], 3).Day, min (x, [], 3));
%! assert_equal (sort (z, 3).TimeZone, 'America/New_York');

## Test 'sort' -- ascending order is the default and ties are stable
%!test
%! [b, i] = sort (datetime (2021, 1, [3, 1, 2, 1]));
%! assert_equal (b.Day, [1, 1, 2, 3]);
%! assert_equal (i, [2, 4, 3, 1]);

## Test 'sort' -- descending order keeps the original order of equal elements
%!test
%! [b, i] = sort (datetime (2021, 1, [3, 1, 2, 1]), 'descend');
%! assert_equal (b.Day, [3, 2, 1, 1]);
%! assert_equal (i, [1, 3, 2, 4]);

## Test 'sort' -- Not-A-Time goes last for ascending, first for descending
%!test
%! n = datetime ([2021, 2020, NaN, 2022], 1, 1);
%! [b, i] = sort (n);
%! assert_equal (isnat (b), logical ([0, 0, 0, 1]));
%! assert_equal (b.Year(1:3), [2020, 2021, 2022]);
%! assert_equal (i, [2, 1, 4, 3]);
%! [b, i] = sort (n, 'descend');
%! assert_equal (isnat (b), logical ([1, 0, 0, 0]));
%! assert_equal (i, [3, 4, 1, 2]);

## Test 'sort' -- MissingPlacement overrides the direction default
%!test
%! n = datetime ([2021, 2020, NaN, 2022], 1, 1);
%! b = sort (n, 'MissingPlacement', 'first');
%! assert_equal (isnat (b), logical ([1, 0, 0, 0]));
%! assert_equal (b.Year(2:4), [2020, 2021, 2022]);
%! b = sort (n, 'MissingPlacement', 'last');
%! assert_equal (isnat (b), logical ([0, 0, 0, 1]));
%! b = sort (n, 'descend', 'MissingPlacement', 'first');
%! assert_equal (isnat (b), logical ([1, 0, 0, 0]));
%! assert_equal (b.Year(2:4), [2022, 2021, 2020]);

## Test 'sort' -- a matrix sorts down each column by default
%!test
%! [b, i] = sort (datetime (2021, 1, [3, 1; 2, 4]));
%! assert_equal (b.Day, [2, 1; 3, 4]);
%! assert_equal (i, [2, 1; 1, 2]);

## Test 'sort' -- dimension two sorts along each row
%!assert_equal (sort (datetime (2021, 1, [3, 1; 2, 4]), 2).Day, [1, 3; 2, 4])

## Test 'sort' -- descending sort along the first dimension of a matrix
%!test
%! [b, i] = sort (datetime (2021, 1, [3, 1; 2, 4]), 1, 'descend');
%! assert_equal (b.Day, [3, 4; 2, 1]);
%! assert_equal (i, [1, 2; 2, 1]);

## Test 'sort' -- an empty array is returned unchanged
%!assert_equal (size (sort (datetime (2021, 1, []))), [0, 0])

## Test 'sort' -- a zoned array sorts by absolute instant
%!test
%! [b, i] = sort (datetime (2021, 1, [3, 1, 2], ...
%!                          'TimeZone', 'America/New_York'), 'descend');
%! assert_equal (b.Day, [3, 2, 1]);
%! assert_equal (i, [1, 3, 2]);

## Test 'sort' -- error branches
%!error <datetime.sort: DIM must be a positive integer.> ...
%! sort (datetime (2021, 1, 1), 0);
%!error <datetime.sort: invalid DIRECTION 'up'.> ...
%! sort (datetime (2021, 1, 1), 'up');
%!error <datetime.sort: invalid 'MissingPlacement' value.> ...
%! sort (datetime (2021, 1, 1), 'MissingPlacement', 'middle');
%!error <datetime.sort: 'MissingPlacement' requires a value.> ...
%! sort (datetime (2021, 1, 1), 'MissingPlacement');
%!error <datetime.sort: invalid input argument.> ...
%! sort (datetime (2021, 1, 1), {1});

## Test 'maxk' -- the K latest elements come back in descending order
%!test
%! [b, i] = maxk (datetime (2024, 1, [3, 1, 2, 5]), 3);
%! assert_equal (b.Day, [5, 3, 2]);
%! assert_equal (i, [4, 1, 3]);

## Test 'maxk' -- ties keep their original order
%!test
%! [b, i] = maxk (datetime (2024, 1, [2, 1, 2, 1, 2]), 3);
%! assert_equal (b.Day, [2, 2, 2]);
%! assert_equal (i, [1, 3, 5]);

## Test 'mink' -- the K earliest elements come back in ascending order
%!test
%! [b, i] = mink (datetime (2024, 1, [3, 1, 2, 5]), 3);
%! assert_equal (b.Day, [1, 2, 3]);
%! assert_equal (i, [2, 3, 1]);

## Test 'mink' -- ties keep their original order
%!test
%! [b, i] = mink (datetime (2024, 1, [2, 1, 2, 1, 2]), 3);
%! assert_equal (b.Day, [1, 1, 2]);
%! assert_equal (i, [2, 4, 1]);

## Test 'maxk'/'mink' -- Not-A-Time is never ranked
%!test
%! t = datetime (2024, 1, [3, NaN, 1, NaN, 5]);
%! [b, i] = maxk (t, 3);
%! assert_equal (b.Day, [5, 3, 1]);
%! assert_equal (i, [5, 1, 3]);
%! [b, i] = mink (t, 3);
%! assert_equal (b.Day, [1, 3, 5]);
%! assert_equal (i, [3, 1, 5]);

## Test 'maxk'/'mink' -- Not-A-Time is appended last in either direction, and
## only once K passes the number of non-missing elements
%!test
%! t = datetime (2024, 1, [3, NaN, 1, NaN, 5]);
%! [b, i] = maxk (t, 10);
%! assert_equal (isnat (b), logical ([0, 0, 0, 1, 1]));
%! assert_equal (b.Day(1:3), [5, 3, 1]);
%! assert_equal (i, [5, 1, 3, 2, 4]);
%! [b, i] = mink (t, 10);
%! assert_equal (isnat (b), logical ([0, 0, 0, 1, 1]));
%! assert_equal (b.Day(1:3), [1, 3, 5]);
%! assert_equal (i, [3, 1, 5, 2, 4]);

## Test 'maxk'/'mink' -- an all-missing array returns missing elements
%!test
%! t = datetime ([NaN, NaN, NaN], 1, 1);
%! [b, i] = maxk (t, 2);
%! assert_equal (isnat (b), logical ([1, 1]));
%! assert_equal (i, [1, 2]);
%! [b, i] = mink (t, 2);
%! assert_equal (isnat (b), logical ([1, 1]));
%! assert_equal (i, [1, 2]);

## Test 'maxk'/'mink' -- a matrix ranks down each column by default
%!test
%! a = datetime (2024, 1, [1, 5; 4, 2; 3, 6]);
%! [b, i] = maxk (a, 2);
%! assert_equal (b.Day, [4, 6; 3, 5]);
%! assert_equal (i, [2, 3; 3, 1]);
%! [b, i] = mink (a, 2);
%! assert_equal (b.Day, [1, 2; 3, 5]);
%! assert_equal (i, [1, 2; 3, 1]);

## Test 'maxk' -- dimension two ranks along each row
%!test
%! a = datetime (2024, 1, [1, 5; 4, 2; 3, 6]);
%! [b, i] = maxk (a, 2, 2);
%! assert_equal (b.Day, [5, 1; 4, 2; 6, 3]);
%! assert_equal (i, [2, 1; 1, 2; 2, 1]);

## Test 'maxk' -- a dimension beyond the array leaves it unchanged
%!test
%! a = datetime (2024, 1, [1, 5; 4, 2; 3, 6]);
%! [b, i] = maxk (a, 2, 3);
%! assert_equal (b.Day, [1, 5; 4, 2; 3, 6]);
%! assert_equal (i, ones (3, 2));

## Test 'maxk' -- an array of more than two dimensions is ranked along the
## dimension asked for, checked against the numeric answer
%!test
%! x = cat (3, [5 1 9; 2 8 4], [7 3 6; 10 12 11]);
%! a = datetime (2024, 1, x);
%! w = sort (x, 1, 'descend');
%! assert_equal (maxk (a, 2, 1).Day, w(1:2,:,:));
%! w = sort (x, 2, 'descend');
%! assert_equal (maxk (a, 2, 2).Day, w(:,1:2,:));
%! w = sort (x, 3, 'descend');
%! assert_equal (maxk (a, 2, 3).Day, w(:,:,1:2));

## Test 'maxk'/'mink' -- K may be zero, and empties keep their orientation
%!assert_equal (size (maxk (datetime (2024, 1, [1, 2, 3]), 0)), [1, 0])
%!assert_equal (size (mink (datetime (2024, 1, [1, 2, 3]), 0)), [1, 0])
%!assert_equal (size (maxk (datetime (2024, 1, [1, 5; 4, 2; 3, 6]), 0)), [0, 2])
%!assert_equal (size (maxk (datetime.empty (0, 0), 2)), [0, 0])
%!assert_equal (class (maxk (datetime (2024, 1, 1), 1)), 'datetime')

## Test 'maxk' -- a zoned array ranks by absolute instant and keeps its zone
%!test
%! z = datetime (2024, 1, [3, 1, 5], 'TimeZone', 'America/New_York');
%! [b, i] = maxk (z, 2);
%! assert_equal (b.Day, [5, 3]);
%! assert_equal (i, [3, 1]);
%! assert_equal (b.TimeZone, 'America/New_York');

## Test 'maxk' -- the display format is carried through
%!test
%! t = datetime (2024, 1, [3, 1, 5]);
%! t.Format = 'yyyy-MM-dd';
%! assert_equal (maxk (t, 2).Format, 'yyyy-MM-dd');

## Test 'maxk' -- an inserted leap second ranks as the latest instant
%!test
%! L = datetime (2016, 12, 31, 23, 59, [58, 59, 60], ...
%!               'TimeZone', 'UTCLeapSeconds');
%! [b, i] = maxk (L, 2);
%! assert_equal (b.Second, [60, 59]);
%! assert_equal (i, [3, 2]);

## Test 'maxk'/'mink' -- infinite datetimes rank as ordinary values
%!test
%! pinf = datetime (Inf, 'ConvertFrom', 'datenum');
%! ninf = datetime (-Inf, 'ConvertFrom', 'datenum');
%! t = [pinf, ninf, datetime(2024, 1, 1), NaT];
%! [b, i] = maxk (t, 4);
%! assert_equal (i, [1, 3, 2, 4]);
%! assert_equal (isnat (b), logical ([0, 0, 0, 1]));
%! [b, i] = mink (t, 4);
%! assert_equal (i, [2, 3, 1, 4]);
%! assert_equal (isnat (b), logical ([0, 0, 0, 1]));

## Test 'maxk'/'mink' -- error branches
%!error <datetime.maxk: too few input arguments.> maxk (datetime (2024, 1, 1))
%!error <datetime.mink: too few input arguments.> mink (datetime (2024, 1, 1))
%!error <datetime.maxk: K must be a nonnegative integer scalar.> ...
%! maxk (datetime (2024, 1, [1, 2, 3]), 2.5)
%!error <datetime.maxk: K must be a nonnegative integer scalar.> ...
%! maxk (datetime (2024, 1, [1, 2, 3]), -1)
%!error <datetime.maxk: K must be a nonnegative integer scalar.> ...
%! maxk (datetime (2024, 1, [1, 2, 3]), [2, 3])
%!error <datetime.mink: K must be a nonnegative integer scalar.> ...
%! mink (datetime (2024, 1, [1, 2, 3]), Inf)
%!error <datetime.maxk: DIM must be a positive integer scalar.> ...
%! maxk (datetime (2024, 1, [1, 2, 3]), 2, 0)
%!error <datetime.mink: DIM must be a positive integer scalar.> ...
%! mink (datetime (2024, 1, [1, 2, 3]), 2, 1.5)
%!error <datetime.maxk: 'ComparisonMethod' is not a supported option.> ...
%! maxk (datetime (2024, 1, [1, 2, 3]), 2, 'ComparisonMethod', 'abs')
%!error <datetime.mink: 'MissingPlacement' is not a supported option.> ...
%! mink (datetime (2024, 1, [1, 2, 3]), 2, 'MissingPlacement', 'first')
%!error <datetime.maxk: too many input arguments.> ...
%! maxk (datetime (2024, 1, [1, 2, 3]), 2, 1, 2)

## Test 'issorted' -- ascending (non-decreasing) is the default
%!assert_equal (issorted (datetime (2021, 1, [1, 2, 3])), true)
%!assert_equal (issorted (datetime (2021, 1, [3, 2, 1])), false)
%!assert_equal (issorted (datetime (2021, 1, [1, 2, 2, 3])), true)

## Test 'issorted' -- descending order
%!assert_equal (issorted (datetime (2021, 1, [3, 2, 1]), 'descend'), true)

## Test 'issorted' -- strict variants reject equal neighbours
%!assert_equal (issorted (datetime (2021, 1, [1, 2, 2, 3]), 'strictascend'), false)
%!assert_equal (issorted (datetime (2021, 1, [1, 2, 3]), 'strictascend'), true)
%!assert_equal (issorted (datetime (2021, 1, [1, 2, 3]), 'strictmonotonic'), true)

## Test 'issorted' -- monotonic accepts either direction
%!assert_equal (issorted (datetime (2021, 1, [1, 2, 3]), 'monotonic'), true)
%!assert_equal (issorted (datetime (2021, 1, [3, 2, 1]), 'monotonic'), true)

## Test 'issorted' -- Not-A-Time counts as greater than any value
%!assert_equal (issorted (datetime ([2021, 2022, NaN], 1, 1)), true)
%!assert_equal (issorted (datetime ([NaN, 2021, 2022], 1, 1)), false)
%!assert_equal (issorted (datetime ([2021, 2022, NaN], 1, 1), 'descend'), false)

## Test 'issorted' -- every column (or row) of a matrix must be sorted
%!assert_equal (issorted (datetime (2021, 1, [3, 1; 2, 4])), false)
%!assert_equal (issorted (datetime (2021, 1, [3, 1; 2, 4]), 2), false)
%!assert_equal (issorted (datetime (2021, 1, [1, 3; 2, 4])), true)

## Test 'issorted' -- scalars and empties are trivially sorted
%!assert_equal (issorted (datetime (2021, 1, 1)), true)
%!assert_equal (issorted (datetime (2021, 1, [])), true)

## Test 'issorted' -- error branches
%!error <datetime.issorted: DIM must be a positive integer.> ...
%! issorted (datetime (2021, 1, 1), 1.5);
%!error <datetime.issorted: invalid DIRECTION 'up'.> ...
%! issorted (datetime (2021, 1, 1), 'up');
%!error <datetime.issorted: invalid input argument.> ...
%! issorted (datetime (2021, 1, 1), {1});

## Test 'sortrows' -- rows sort lexicographically in ascending order
%!test
%! [b, i] = sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]));
%! assert_equal (b.Day, [1 5; 3 1; 3 2]);
%! assert_equal (i, [3; 1; 2]);

## Test 'sortrows' -- a bare direction applies to every column
%!test
%! [b, i] = sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), 'descend');
%! assert_equal (b.Day, [3 2; 3 1; 1 5]);
%! assert_equal (i, [2; 1; 3]);

## Test 'sortrows' -- a column list selects and orders the sort keys
%!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), 2).Day, ...
%!              [3 1; 3 2; 1 5])
%!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), [2 1]).Day, ...
%!              [3 1; 3 2; 1 5])

## Test 'sortrows' -- a negative column sorts that key in descending order
%!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), [1 -2]).Day, ...
%!              [1 5; 3 2; 3 1])

## Test 'sortrows' -- a cell array gives one direction per sort column
%!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), ...
%!                        {'ascend', 'descend'}).Day, [1 5; 3 2; 3 1])
%!assert_equal (sortrows (datetime (2021, 1, [3 1; 3 2; 1 5]), [1 2], ...
%!                        {'ascend', 'descend'}).Day, [1 5; 3 2; 3 1])

## Test 'sortrows' -- Not-A-Time sorts as the largest value by default
%!test
%! N = datetime (2021, 1, [2 1; NaN 3; 2 5; 1 1; 2 1]);
%! [b, i] = sortrows (N);
%! assert_equal (i, [4; 1; 5; 3; 2]);
%! assert_equal (isnat (b)(:, 1), logical ([0; 0; 0; 0; 1]));
%! [b, i] = sortrows (N, 'descend');
%! assert_equal (i, [2; 3; 1; 5; 4]);

## Test 'sortrows' -- MissingPlacement overrides the NaT position
%!test
%! N = datetime (2021, 1, [2 1; NaN 3; 2 5; 1 1; 2 1]);
%! bf = sortrows (N, 'MissingPlacement', 'first');
%! assert_equal (isnat (bf)(1, 1), true);
%! bl = sortrows (N, 'MissingPlacement', 'last');
%! assert_equal (isnat (bl)(5, 1), true);

## Test 'sortrows' -- a single sort column keeps ties in original order
%!test
%! N = datetime (2021, 1, [2 1; NaN 3; 2 5; 1 1; 2 1]);
%! [b, i] = sortrows (N, 1);
%! assert_equal (i, [4; 1; 3; 5; 2]);

## Test 'sortrows' -- a column vector sorts as a single-column matrix
%!test
%! [b, i] = sortrows (datetime (2021, 1, [3; 1; 2]));
%! assert_equal (i, [2; 3; 1]);

## Test 'sortrows' -- error branches
%!error <datetime.sortrows: COLUMN out of range.> ...
%! sortrows (datetime (2021, 1, [1 2]), 3);
%!error <datetime.sortrows: invalid DIRECTION 'up'.> ...
%! sortrows (datetime (2021, 1, [1 2]), 'up');
%!error <datetime.sortrows: DIRECTION must have one entry per sort column.> ...
%! sortrows (datetime (2021, 1, [1 2]), [1 2], {'ascend'});
%!error <datetime.sortrows: COLUMN must precede DIRECTION.> ...
%! sortrows (datetime (2021, 1, [1 2]), 'ascend', 'descend');
%!error <datetime.sortrows: invalid 'MissingPlacement' value.> ...
%! sortrows (datetime (2021, 1, [1 2]), 'MissingPlacement', 'middle');

## Test 'issortedrows' -- ascending lexicographic order is the default
%!assert_equal (issortedrows (datetime (2021, 1, [3 1; 3 2; 1 5])), false)
%!assert_equal (issortedrows (datetime (2021, 1, [1 5; 3 1; 3 2])), true)

## Test 'issortedrows' -- ties are resolved by later columns
%!assert_equal (issortedrows (datetime (2021, 1, [1 1; 1 2; 2 0])), true)
%!assert_equal (issortedrows (datetime (2021, 1, [1 2; 1 1])), false)

## Test 'issortedrows' -- a column list selects and orients the sort keys
%!assert_equal (issortedrows (datetime (2021, 1, [3 1; 3 2; 1 5]), 2), true)
%!assert_equal (issortedrows (datetime (2021, 1, [3 1; 3 2; 1 5]), [1 -2]), false)

## Test 'issortedrows' -- descend and monotonic directions
%!assert_equal (issortedrows (datetime (2021, 1, [3 2; 3 1; 1 5]), 'descend'), true)
%!assert_equal (issortedrows (datetime (2021, 1, [3; 2; 1]), 'monotonic'), true)
%!assert_equal (issortedrows (datetime (2021, 1, [1; 2; 3]), 'monotonic'), true)

## Test 'issortedrows' -- strict checks only the first sort column
%!assert_equal (issortedrows (datetime (2021, 1, [1 5; 2 5; 3 5]), 'strictascend'), true)
%!assert_equal (issortedrows (datetime (2021, 1, [1 5; 2 5; 2 5]), 'strictascend'), false)
%!assert_equal (issortedrows (datetime (2021, 1, [1 1; 1 2; 2 0]), 'strictascend'), false)
%!assert_equal (issortedrows (datetime (2021, 1, [3 5; 2 5; 1 5]), 'strictdescend'), true)
%!assert_equal (issortedrows (datetime (2021, 1, [1; 2; 3]), 'strictmonotonic'), true)

## Test 'issortedrows' -- strict is false when the first column has NaT
%!assert_equal (issortedrows (datetime ([2021; 2022; NaN], 1, 1), 'strictascend'), false)
%!assert_equal (issortedrows (datetime ([2021; 2022; NaN], 1, 1), 'ascend'), true)

## Test 'issortedrows' -- a cell array gives one direction per column
%!assert_equal (issortedrows (datetime (2021, 1, [1 5; 3 2; 3 1]), ...
%!                            {'ascend', 'descend'}), true)

## Test 'issortedrows' -- error branches
%!error <datetime.issortedrows: COLUMN out of range.> ...
%! issortedrows (datetime (2021, 1, [1 2]), 3);
%!error <datetime.issortedrows: invalid DIRECTION 'up'.> ...
%! issortedrows (datetime (2021, 1, [1 2]), 'up');
%!error <datetime.issortedrows: DIRECTION must have one entry per sort column.> ...
%! issortedrows (datetime (2021, 1, [1 2]), {'ascend'});
%!error <datetime.issortedrows: COLUMN must precede DIRECTION.> ...
%! issortedrows (datetime (2021, 1, [1 2]), 'ascend', 'descend');

## Test 'topkrows' -- the top K rows come back in descending order, with
## missing elements ranked last within each sort column
%!test
%! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]);
%! [B, I] = topkrows (T, 5);
%! assert_equal (I, [1; 3; 2; 5; 4]);
%! assert_equal (B.Day, [3, 1; 3, NaN; 1, 2; 1, 1; NaN, 5]);

## Test 'topkrows' -- a column list and a direction
%!test
%! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]);
%! [B, I] = topkrows (T, 5, 1, 'ascend');
%! assert_equal (I, [2; 5; 1; 3; 4]);
%! [B, I] = topkrows (T, 5, 2);
%! assert_equal (I, [4; 2; 1; 5; 3]);

## Test 'topkrows' -- K past the number of rows, K zero, and empty input
%!test
%! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]);
%! assert_equal (topkrows (T, 10).Day, topkrows (T, 5).Day);
%! assert_equal (size (topkrows (T, 0)), [0, 2]);
%! assert_equal (size (topkrows (datetime.empty (0, 0), 2)), [0, 0]);

## Test 'topkrows' -- the display format and the time zone are carried through
%!test
%! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]);
%! assert_equal (topkrows (T, 2).Format, 'dd-MMM-uuuu');
%! z = datetime (2024, 1, [3; 1; 5], 'TimeZone', 'America/New_York');
%! [B, I] = topkrows (z, 2);
%! assert_equal (I, [3; 1]);
%! assert_equal (B.TimeZone, 'America/New_York');

## Test 'topkrows' -- 'MissingPlacement' is an Octave extension defaulting to
## 'last', which is what MATLAB always does
%!test
%! T = datetime (2024, 1, [3, 1; 1, 2; 3, NaN; NaN, 5; 1, 1]);
%! [~, I] = topkrows (T, 5, 'MissingPlacement', 'last');
%! assert_equal (I, [1; 3; 2; 5; 4]);
%! [~, I] = topkrows (T, 5, 'MissingPlacement', 'first');
%! assert_equal (I, [4; 3; 1; 2; 5]);

## Test 'topkrows' -- error branches
%!error <datetime.topkrows: too few input arguments.> ...
%! topkrows (datetime (2024, 1, 1))
%!error <datetime.topkrows: A must be a 2-D array.> ...
%! topkrows (datetime (2024, 1, reshape (1:8, 2, 2, 2)), 1)
%!error <datetime.topkrows: K must be a nonnegative integer scalar.> ...
%! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 2.5)
%!error <datetime.topkrows: K must be a nonnegative integer scalar.> ...
%! topkrows (datetime (2024, 1, [1, 2; 3, 4]), -1)
%!error <datetime.topkrows: COL must contain positive integers indexing existing columns in A.> ...
%! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 1, -1)
%!error <datetime.topkrows: COL must contain positive integers indexing existing columns in A.> ...
%! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 1, 5)
%!error <datetime.topkrows: DIRECTION must be 'ascend' or 'descend'.> ...
%! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 1, 1, 'bogus')
%!error <datetime.topkrows: invalid value for 'MissingPlacement'.> ...
%! topkrows (datetime (2024, 1, [1, 2; 3, 4]), 1, 'MissingPlacement', 'bogus')

## Test 'min'/'max' -- reduce along the first non-singleton dimension
%!test
%! [m, i] = min (datetime (2021, 1, [3 1 2]));
%! assert_equal (m.Day, 1);
%! assert_equal (i, 2);
%!test
%! [m, i] = max (datetime (2021, 1, [3 1 2]));
%! assert_equal (m.Day, 3);
%! assert_equal (i, 1);

## Test 'min' -- NaT is omitted by default; ties return the first index
%!test
%! [m, i] = min (datetime (2021, 1, [3 NaN 1 2]));
%! assert_equal (m.Day, 1);
%! assert_equal (i, 3);
%!test
%! [m, i] = min (datetime (2021, 1, [2 1 1 3]));
%! assert_equal (i, 2);

## Test 'min' -- 'includenan' propagates NaT
%!assert_equal (isnat (min (datetime (2021, 1, [3 NaN 1]), [], 'includenan')), true)
%!assert_equal (max (datetime (2021, 1, [3 NaN 1]), [], 'omitnan').Day, 3)

## Test 'min' -- a reduction over only NaT yields NaT at index 1
%!test
%! [m, i] = min (datetime ([NaN NaN], 1, 1));
%! assert_equal (isnat (m), true);
%! assert_equal (i, 1);

## Test 'min' -- matrix reduction along each dimension and over all elements
%!assert_equal (min (datetime (2021, 1, [3 1; 2 4])).Day, [2 1])
%!assert_equal (min (datetime (2021, 1, [3 1; 2 4]), [], 2).Day, [1; 2])
%!assert_equal (min (datetime (2021, 1, [3 1; 2 4]), [], 'all').Day, 1)
%!test
%! [m, i] = min (datetime (2021, 1, [3 1; 2 4]));
%! assert_equal (i, [2 1]);

## Test 'min'/'max' -- element-wise comparison of two arrays with broadcasting
%!assert_equal (min (datetime (2021, 1, [1 5 3]), datetime (2021, 1, [4 2 3])).Day, [1 2 3])
%!assert_equal (max (datetime (2021, 1, [1 5 3]), datetime (2021, 1, [4 2 3])).Day, [4 5 3])
%!assert_equal (min (datetime (2021, 1, [1 5 3]), datetime (2021, 1, 3)).Day, [1 3 3])

## Test 'min' -- element-wise NaT handling (omit versus include)
%!assert_equal (min (datetime (2021, 1, [1 5 3]), ...
%!                   datetime (2021, 1, [NaN 2 NaN])).Day, [1 2 3])
%!test
%! m = min (datetime (2021, 1, [1 5 3]), ...
%!          datetime (2021, 1, [NaN 2 NaN]), 'includenan');
%! assert_equal (isnat (m), logical ([1 0 1]));
%! assert_equal (m.Day(2), 2);

## Test 'min' -- reductions are by absolute instant and keep the time zone
%!test
%! z = datetime (2021, 1, [3 1 2], 'TimeZone', 'America/New_York');
%! [m, i] = min (z);
%! assert_equal (m.Day, 1);
%! assert_equal (i, 2);
%! assert_equal (m.TimeZone, 'America/New_York');

## Test 'min' -- an empty array reduces to an empty array
%!assert_equal (size (min (datetime (2021, 1, []))), [0 0])

## Test 'min' -- error branches
%!error <datetime.min: comparison of two arrays requires both to be datetime.> ...
%! min (datetime (2021, 1, 1), 5);
%!error <datetime.min: a second output is not supported when comparing two arrays.> ...
%! [m, i] = min (datetime (2021, 1, 1), datetime (2021, 1, 2));
%!error <datetime.min: DIM must be a positive integer.> ...
%! min (datetime (2021, 1, 1), [], 0);
%!error <datetime.min: invalid option 'foo'.> ...
%! min (datetime (2021, 1, 1), [], 'foo');
%!error <datetime.max: cannot compare a datetime with a time zone to one without a time zone.> ...
%! max (datetime (2021, 1, 1, 'TimeZone', 'UTC'), datetime (2021, 1, 2));

################################################################################
##                    ** Leap seconds ('UTCLeapSeconds') **                   ##
################################################################################

## Test 'UTCLeapSeconds' -- the 60th second of a minute is representable
%!test
%! a = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (datevec (a), [2016, 12, 31, 23, 59, 60]);
%! assert_equal (a.Second, 60);
%! assert_equal (a.Minute, 59);
%! assert_equal (a.Hour, 23);
%! assert_equal (a.TimeZone, 'UTCLeapSeconds');
%!assert_equal (datetime (2016, 12, 31, 23, 59, 60.25, ...
%!                        'TimeZone', 'UTCLeapSeconds').Second, 60.25)
%!assert_equal (datetime (1972, 6, 30, 23, 59, 60, ...
%!                        'TimeZone', 'UTCLeapSeconds').Second, 60)

## Test 'UTCLeapSeconds' -- every second of the shipped table is recognized
%!test
%! L = [1972 6 30; 1972 12 31; 1973 12 31; 1974 12 31; 1975 12 31; 1976 12 31;
%!      1977 12 31; 1978 12 31; 1979 12 31; 1981 6 30; 1982 6 30; 1983 6 30;
%!      1985 6 30; 1987 12 31; 1989 12 31; 1990 12 31; 1992 6 30; 1993 6 30;
%!      1994 6 30; 1995 12 31; 1997 6 30; 1998 12 31; 2005 12 31; 2008 12 31;
%!      2012 6 30; 2015 6 30; 2016 12 31];
%! V = [L, repmat([23 59 60], rows (L), 1)];
%! d = datetime (V, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (d.Second, repmat (60, rows (L), 1));

## Test 'UTCLeapSeconds' -- a 60th second that was never inserted rolls over
%!test
%! d = datetime ([2000 6 30 23 59 60; 2019 6 30 23 59 60; 2020 12 31 23 59 60], ...
%!               'TimeZone', 'UTCLeapSeconds');
%! assert_equal (d.Second, [0; 0; 0]);
%! assert_equal (d.Day, [1; 1; 1]);
%!assert_equal (datevec (datetime (2016, 12, 30, 23, 59, 60, ...
%!              'TimeZone', 'UTCLeapSeconds')), [2016 12 31 0 0 0])
%!assert_equal (datevec (datetime (2016, 12, 31, 23, 58, 60, ...
%!              'TimeZone', 'UTCLeapSeconds')), [2016 12 31 23 59 0])
%!assert_equal (datevec (datetime (1971, 12, 31, 23, 59, 60, ...
%!              'TimeZone', 'UTCLeapSeconds')), [1972 1 1 0 0 0])

## Test 'UTCLeapSeconds' -- the seconds field is counted on the leap-second
## timeline, while a minute or hour that overflows steps over the insertion
%!assert_equal (datevec (datetime (2016, 12, 31, 23, 59, 61, ...
%!              'TimeZone', 'UTCLeapSeconds')), [2017 1 1 0 0 0])
%!assert_equal (datevec (datetime (2016, 12, 31, 23, 59, 61.5, ...
%!              'TimeZone', 'UTCLeapSeconds')), [2017 1 1 0 0 0.5])
%!assert_equal (datevec (datetime (2016, 12, 31, 23, 59, 120, ...
%!              'TimeZone', 'UTCLeapSeconds')), [2017 1 1 0 0 59])
%!assert_equal (datevec (datetime (2016, 12, 31, 23, 60, 0, ...
%!              'TimeZone', 'UTCLeapSeconds')), [2017 1 1 0 0 0])
%!assert_equal (datevec (datetime (2017, 1, 1, 0, 0, -1, ...
%!              'TimeZone', 'UTCLeapSeconds')), [2016 12 31 23 59 60])
%!assert_equal (datevec (datetime (2017, 1, 1, 0, 0, -0.5, ...
%!              'TimeZone', 'UTCLeapSeconds')), [2016 12 31 23 59 60.5])

## Test 'UTCLeapSeconds' -- arithmetic counts the inserted second
%!test
%! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds');
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! a3 = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (seconds (a2 - a1), 1);
%! assert_equal (seconds (a3 - a2), 1);
%! assert_equal (seconds (a3 - a1), 2);
%! assert_equal (seconds (a3 - datetime (1970, 1, 1, 0, 0, 0, ...
%!                                       'TimeZone', 'UTCLeapSeconds')), ...
%!               1483228827);
%!assert_equal (seconds (datetime (1972, 1, 1, 0, 0, 0, ...
%!                                 'TimeZone', 'UTCLeapSeconds') ...
%!                     - datetime (1971, 12, 31, 23, 59, 59, ...
%!                                 'TimeZone', 'UTCLeapSeconds')), 1)

## Test 'UTCLeapSeconds' -- a fixed-length day lands on the inserted second,
## a calendar day steps past it
%!test
%! d = datetime (2016, 12, 31, 0, 0, 0, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (datevec (d + days (1)), [2016 12 31 23 59 60]);
%! assert_equal (datevec (d + caldays (1)), [2017 1 1 0 0 0]);

## Test 'UTCLeapSeconds' -- calendar arithmetic clamps a 60th second onto a
## minute that never had one, keeping any fractional part
%!test
%! q = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (datevec (q + caldays (1)), [2017 1 1 23 59 59.25]);
%! assert_equal (datevec (q + calmonths (1)), [2017 1 31 23 59 59.25]);
%! assert_equal (datevec (q + calyears (1)), [2017 12 31 23 59 59.25]);
%!assert_equal (datevec (datetime (2016, 12, 31, 23, 59, 60, ...
%!              'TimeZone', 'UTCLeapSeconds') - caldays (1)), ...
%!              [2016 12 30 23 59 59])
%!assert_equal (datevec (datetime (2015, 6, 30, 23, 59, 60, ...
%!              'TimeZone', 'UTCLeapSeconds') + calmonths (18)), ...
%!              [2016 12 30 23 59 59])

## Test 'UTCLeapSeconds' -- 'posixtime' folds forward onto the next second
%!test
%! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds');
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! a3 = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (posixtime (a1), 1483228799);
%! assert_equal (posixtime (a2), 1483228800);
%! assert_equal (posixtime (a3), 1483228800);
%!assert_equal (posixtime (datetime (2016, 12, 31, 23, 59, 60.25, ...
%!                                   'TimeZone', 'UTCLeapSeconds')), ...
%!              1483228800.25)
%!assert_equal (posixtime (datetime (1972, 6, 30, 23, 59, 60, ...
%!                                   'TimeZone', 'UTCLeapSeconds')), 78796800)

## Test 'UTCLeapSeconds' -- 'datenum', 'exceltime' and 'datestr' fold backward
%!test
%! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds');
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (datenum (a2), datenum (a1));
%! assert_equal (exceltime (a2), exceltime (a1));
%! assert_equal (exceltime (a2, '1904'), exceltime (a1, '1904'));
%! assert_equal (datestr (a2), '31-Dec-2016 23:59:59');
%!test
%! q = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', 'UTCLeapSeconds');
%! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal ((datenum (q) - datenum (a1)) * 86400, 0.25, 1e-4);

## Test 'UTCLeapSeconds' -- both Julian scales stretch the day that holds an
## inserted second over its true length of 86401 seconds
%!test
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (juliandate (a2), 2457754 + 43200 / 86401, 1e-9);
%! assert_equal (juliandate (a2, 'modifiedjuliandate'), 57753 + 86400 / 86401, ...
%!               1e-9);
%!test
%! d = datetime (2016, 12, 31, 6, 0, 0, 'TimeZone', 'UTCLeapSeconds');
%! gap = (juliandate (d, 'modifiedjuliandate') ...
%!        - (juliandate (d) - 2400000.5)) * 86400;
%! assert_equal (gap, -0.25, 1e-4);
%!test
%! d = datetime (2016, 12, 31, 0, 0, 0, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (juliandate (d, 'modifiedjuliandate'), ...
%!               juliandate (d) - 2400000.5, 1e-9);
%!assert_equal (juliandate (datetime (1960, 1, 1, 0, 0, 0, ...
%!                                    'TimeZone', 'UTCLeapSeconds')), 2436934.5)

## Test 'UTCLeapSeconds' -- the integer fixed-epoch conversions fold backward
%!test
%! q = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (convertTo (q, 'epochtime'), int64 (1483228799));
%! assert_equal (convertTo (q, '.net'), uint64 (636188255992500000));
%! assert_equal (convertTo (q, 'ntp'), uint64 (15857953838494384128));
%!assert_equal (convertTo (datetime (2016, 12, 31, 23, 59, 60, ...
%!              'TimeZone', 'UTCLeapSeconds'), 'epochtime', ...
%!              'TicksPerSecond', 1000), int64 (1483228799000))
%!assert_equal (convertTo (datetime (2017, 1, 1, ...
%!              'TimeZone', 'UTCLeapSeconds'), 'epochtime'), ...
%!              int64 (1483228800))

## Test 'UTCLeapSeconds' -- 'yyyymmdd' and the component readers are unaffected
%!test
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (yyyymmdd (a2), 20161231);
%! assert_equal ([hour(a2), minute(a2), second(a2)], [23, 59, 60]);
%! assert_equal (second (a2, 'secondofday'), 86400);
%! assert_equal (week (a2), 53);
%! assert_equal (week (a2, 'weekofmonth'), 5);
%! assert_equal (day (a2, 'dayofyear'), 366);
%! assert_equal (day (a2, 'name'), {'Saturday'});
%! assert_equal (char (timeofday (a2)), '24:00:00');
%! assert_equal (isdst (a2), false);
%!assert_equal (second (datetime (2016, 12, 31, 23, 59, 60.25, ...
%!              'TimeZone', 'UTCLeapSeconds'), 'secondofday'), 86400.25)

## Test 'convertTo' -- 'tt2000' is nanoseconds since the J2000 TT epoch
%!test
%! tz = 'UTCLeapSeconds';
%! assert_equal (convertTo (datetime (1972, 1, 1, 0, 0, 0, 'TimeZone', tz), ...
%!               'tt2000'), int64 (-883655957816000000));
%! assert_equal (convertTo (datetime (1980, 1, 6, 0, 0, 0, 'TimeZone', tz), ...
%!               'tt2000'), int64 (-630763148816000000));
%! assert_equal (convertTo (datetime (1999, 1, 1, 0, 0, 0, 'TimeZone', tz), ...
%!               'tt2000'), int64 (-31579135816000000));
%! assert_equal (convertTo (datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', tz), ...
%!               'tt2000'), int64 (536500869184000000));
%! assert_equal (convertTo (datetime (2030, 1, 1, 0, 0, 0, 'TimeZone', tz), ...
%!               'tt2000'), int64 (946728069184000000));
%!test
%! tz = 'UTCLeapSeconds';
%! assert_equal (convertTo (datetime (2016, 12, 31, 23, 59, 60, ...
%!               'TimeZone', tz), 'tt2000'), int64 (536500868184000000));
%! assert_equal (convertTo (datetime (2016, 12, 31, 23, 59, 60.123456, ...
%!               'TimeZone', tz), 'tt2000'), int64 (536500868307456000));
%! assert_equal (convertTo (datetime (1972, 6, 30, 23, 59, 60, ...
%!               'TimeZone', tz), 'tt2000'), int64 (-867931157816000000));
%!test
%! tz = 'UTCLeapSeconds';
%! assert_equal (convertTo (datetime (1900, 1, 1, 0, 0, 0, 'TimeZone', tz), ...
%!               'tt2000'), int64 (-3155716767816000000));
%! assert_equal (convertTo (datetime (1707, 9, 22, 12, 13, 15, ...
%!               'TimeZone', tz), 'tt2000'), int64 (-9223371972816000000));
%! assert_equal (convertTo (datetime (2292, 4, 11, 11, 46, 7, ...
%!               'TimeZone', tz), 'tt2000'), int64 (9223372036184000000));

## Test 'convertTo' -- the tt2000 epoch itself, and round trips through it
%!test
%! tz = 'UTCLeapSeconds';
%! assert_equal (datevec (datetime (int64 (0), 'ConvertFrom', 'tt2000', ...
%!               'TimeZone', tz)), [2000 1 1 11 58 55.816]);
%! assert_equal (datevec (datetime (int64 (536500868307456000), ...
%!               'ConvertFrom', 'tt2000', 'TimeZone', tz)), ...
%!               [2016 12 31 23 59 60.123456]);
%! assert_equal (datevec (datetime (int64 ([0; 1000000000]), ...
%!               'ConvertFrom', 'tt2000', 'TimeZone', tz)), ...
%!               [2000 1 1 11 58 55.816; 2000 1 1 11 58 56.816]);
%!test
%! tz = 'UTCLeapSeconds';
%! d = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', tz);
%! assert_equal (datevec (datetime (convertTo (d, 'tt2000'), ...
%!               'ConvertFrom', 'tt2000', 'TimeZone', tz)), datevec (d));
%!assert_equal (datetime (int64 (0), 'ConvertFrom', 'tt2000', ...
%!              'TimeZone', 'UTCLeapSeconds').TimeZone, 'UTCLeapSeconds')

## Test 'convertTo' -- 'tt2000' error branches
%!error <datetime.convertTo: to convert datetimes to tt2000 times, the 'TimeZone' property of the input must be 'UTCLeapSeconds'.> ...
%! convertTo (datetime (2016, 12, 31, 'TimeZone', 'UTC'), 'tt2000')
%!error <datetime.convertTo: 'tt2000' conversion is not supported for missing values, infinite datetimes, or datetimes outside the interval \[1707-09-22T12:12:10Z, 2292-04-11T11:46:08Z\).> ...
%! convertTo (NaT ('TimeZone', 'UTCLeapSeconds'), 'tt2000')
%!error <datetime.convertTo: 'tt2000' conversion is not supported for missing values, infinite datetimes, or datetimes outside the interval \[1707-09-22T12:12:10Z, 2292-04-11T11:46:08Z\).> ...
%! convertTo (datetime (1707, 9, 22, 12, 12, 10, 'TimeZone', 'UTCLeapSeconds'), 'tt2000')
%!error <datetime.convertTo: 'tt2000' conversion is not supported for missing values, infinite datetimes, or datetimes outside the interval \[1707-09-22T12:12:10Z, 2292-04-11T11:46:08Z\).> ...
%! convertTo (datetime (2292, 4, 11, 11, 46, 8, 'TimeZone', 'UTCLeapSeconds'), 'tt2000')
%!error <datetime: input for converting from tt2000 times must be int64.> ...
%! datetime (0, 'ConvertFrom', 'tt2000', 'TimeZone', 'UTCLeapSeconds')
%!error <datetime: to create datetimes from tt2000 times, the 'TimeZone' parameter must be 'UTCLeapSeconds'.> ...
%! datetime (int64 (0), 'ConvertFrom', 'tt2000', 'TimeZone', 'UTC')
%!error <datetime: to create datetimes from tt2000 times, the 'TimeZone' parameter must be 'UTCLeapSeconds'.> ...
%! datetime (int64 (0), 'ConvertFrom', 'tt2000')
%!error <datetime: int64 input values for tt2000 times must be larger than -9223372036854775805.> ...
%! datetime (int64 (-9223372036854775806), 'ConvertFrom', 'tt2000', 'TimeZone', 'UTCLeapSeconds')

## Test 'UTCLeapSeconds' -- the display format is locked to the ISO 8601 UTC
## pattern, with the fractional-second digits optional
%!test
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (a2.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'");
%! assert_equal (char (a2), '2016-12-31T23:59:60.000Z');
%! assert_equal (char (string (a2)), '2016-12-31T23:59:60.000Z');
%!test
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! a2.Format = "uuuu-MM-dd'T'HH:mm:ss'Z'";
%! assert_equal (char (a2), '2016-12-31T23:59:60Z');
%!test
%! q = datetime (2016, 12, 31, 23, 59, 60.25, 'TimeZone', 'UTCLeapSeconds');
%! q.Format = "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'";
%! assert_equal (char (q), '2016-12-31T23:59:60.250000000Z');
%!assert_equal (char (datetime (2016, 12, 31, 23, 59, 60, ...
%!              'TimeZone', 'UTCLeapSeconds', ...
%!              'Format', "uuuu-MM-dd'T'HH:mm:ss'Z'")), '2016-12-31T23:59:60Z')
%!assert_equal (NaT ('TimeZone', 'UTCLeapSeconds').Format, ...
%!              "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'")
%!assert_equal (size (NaT ('TimeZone', 'UTCLeapSeconds')), [1 1])

## Test 'UTCLeapSeconds' -- any other display format is rejected
%!error <datetime.subsasgn: the display format of a 'UTCLeapSeconds' datetime array must be "uuuu-MM-dd'T'HH:mm:ss'Z'", optionally with one to nine fractional second digits, as in "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'".> ...
%! d = datetime (2016, 12, 31, 'TimeZone', 'UTCLeapSeconds'); d.Format = 'uuuu-MM-dd HH:mm:ss';
%!error <datetime.subsasgn: the display format of a 'UTCLeapSeconds' datetime array must be "uuuu-MM-dd'T'HH:mm:ss'Z'", optionally with one to nine fractional second digits, as in "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'".> ...
%! d = datetime (2016, 12, 31, 'TimeZone', 'UTCLeapSeconds'); d.Format = "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSSS'Z'";
%!error <datetime.subsasgn: the display format of a 'UTCLeapSeconds' datetime array must be "uuuu-MM-dd'T'HH:mm:ss'Z'", optionally with one to nine fractional second digits, as in "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'".> ...
%! d = datetime (2016, 12, 31, 'TimeZone', 'UTCLeapSeconds'); d.Format = 'default';
%!error <datetime: the display format of a 'UTCLeapSeconds' datetime array must be "uuuu-MM-dd'T'HH:mm:ss'Z'", optionally with one to nine fractional second digits, as in "uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'".> ...
%! datetime (2016, 12, 31, 'TimeZone', 'UTCLeapSeconds', 'Format', 'dd-MMM-uuuu')

## Test 'UTCLeapSeconds' -- leaving the zone folds the inserted second back and
## restores the data-dependent display format
%!test
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! a2.TimeZone = 'UTC';
%! assert_equal (datevec (a2), [2016 12 31 23 59 59]);
%! assert_equal (a2.Format, 'dd-MMM-uuuu HH:mm:ss');
%!test
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! a2.TimeZone = '';
%! assert_equal (datevec (a2), [2016 12 31 23 59 59]);
%!test
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! a2.TimeZone = 'America/New_York';
%! assert_equal (datevec (a2), [2016 12 31 18 59 59]);

## Test 'UTCLeapSeconds' -- entering the zone keeps the instant and takes the
## locked display format, and re-assigning the same zone changes nothing
%!test
%! d = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTC');
%! d.TimeZone = 'UTCLeapSeconds';
%! assert_equal (datevec (d), [2016 12 31 23 59 59]);
%! assert_equal (d.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'");
%!test
%! d = datetime (2016, 12, 31, 23, 59, 59, 'Format', 'dd-MMM-uuuu HH:mm:ss');
%! d.TimeZone = 'UTCLeapSeconds';
%! assert_equal (datevec (d), [2016 12 31 23 59 59]);
%! assert_equal (d.Format, "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'");
%!test
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! a2.TimeZone = 'UTCLeapSeconds';
%! assert_equal (datevec (a2), [2016 12 31 23 59 60]);

## Test 'UTCLeapSeconds' -- assigning a component re-canonicalizes on the
## leap-second timeline
%!test
%! d = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds');
%! d.Second = 60;
%! assert_equal (datevec (d), [2016 12 31 23 59 60]);
%! d.Second = 60.5;
%! assert_equal (datevec (d), [2016 12 31 23 59 60.5]);
%!test
%! d = datetime (2016, 12, 30, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds');
%! d.Second = 60;
%! assert_equal (datevec (d), [2016 12 31 0 0 0]);
%!test
%! d = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', 'UTCLeapSeconds');
%! d.Minute = 58;
%! assert_equal (datevec (d), [2016 12 31 23 59 0]);

## Test 'UTCLeapSeconds' -- an array with leap seconds cannot be combined with
## one without them, a UTC array included
%!error <datetime.eq: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds') == datetime (2017, 1, 1, 'TimeZone', 'UTC')
%!error <datetime.lt: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds') < datetime (2017, 1, 1, 'TimeZone', 'UTC')
%!error <datetime.minus: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds') - datetime (2017, 1, 1, 'TimeZone', 'UTC')
%!error <datetime.ismember: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! ismember (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), datetime (2017, 1, 1, 'TimeZone', 'UTC'))
%!error <datetime.isbetween: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! isbetween (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), datetime (2016, 12, 31, 'TimeZone', 'UTC'), datetime (2017, 1, 2, 'TimeZone', 'UTC'))
%!error <datetime.colon: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'):seconds (1):datetime (2017, 1, 2, 'TimeZone', 'UTC')
%!error <datetime.cat: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! horzcat (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), datetime (2017, 1, 1, 'TimeZone', 'UTC'))
%!error <datetime.convertTo: cannot combine or compare a datetime array with leap seconds with one without leap seconds.> ...
%! convertTo (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), 'epochtime', 'Epoch', datetime (2017, 1, 1, 'TimeZone', 'UTC'))
%!assert_equal (isequal (datetime (2017, 1, 1, 'TimeZone', 'UTCLeapSeconds'), ...
%!                       datetime (2017, 1, 1, 'TimeZone', 'UTC')), false)
%!assert_equal (isequal (datetime (2016, 12, 31, 23, 59, 60, ...
%!                                 'TimeZone', 'UTCLeapSeconds'), ...
%!                       datetime (2016, 12, 31, 23, 59, 60, ...
%!                                 'TimeZone', 'UTCLeapSeconds')), true)

## Test 'UTCLeapSeconds' -- ordering, reductions and interpolation step through
## the inserted second
%!test
%! tz = 'UTCLeapSeconds';
%! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', tz);
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', tz);
%! a3 = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', tz);
%! assert_equal (datevec (sort ([a3; a2; a1])), datevec ([a1; a2; a3]));
%! assert_equal (datevec (max ([a1 a2 a3])), datevec (a3));
%! assert_equal (datevec (unique ([a2 a2 a1])), datevec ([a1; a2]));
%! assert_equal (isbetween (a2, a1, a3), true);
%! assert_equal (seconds (diff ([a1; a2; a3])), [1; 1]);
%! assert_equal (isregular ([a1; a2; a3]), true);
%! assert_equal (datevec (mean ([a1 a3])), datevec (a2));
%! assert_equal (datevec (a1:seconds (1):a3), datevec ([a1 a2 a3]));
%! assert_equal (interp1 ([a1 a3], [0 2], a2), 1);
%! assert_equal (char (std ([a1 a2 a3])), '00:00:01');

## Test 'UTCLeapSeconds' -- 'dateshift' treats the inserted second as the last
## second of its day
%!test
%! tz = 'UTCLeapSeconds';
%! a2 = datetime (2016, 12, 31, 23, 59, 60, 'TimeZone', tz);
%! assert_equal (datevec (dateshift (a2, 'start', 'minute')), ...
%!               [2016 12 31 23 59 0]);
%! assert_equal (datevec (dateshift (a2, 'start', 'day')), [2016 12 31 0 0 0]);
%! assert_equal (datevec (dateshift (a2, 'start', 'year')), [2016 1 1 0 0 0]);
%! assert_equal (datevec (dateshift (a2, 'end', 'day')), [2017 1 1 0 0 0]);
%! assert_equal (datevec (dateshift (a2, 'end', 'minute')), [2017 1 1 0 0 0]);
%! assert_equal (datevec (dateshift (a2, 'start', 'minute', 'nearest')), ...
%!               [2017 1 1 0 0 0]);
%! assert_equal (datevec (dateshift (a2, 'dayofweek', 'Monday')), ...
%!               [2017 1 3 0 0 0]);

## Test 'UTCLeapSeconds' -- 'caldiff' and 'between' across the inserted second
%!test
%! tz = 'UTCLeapSeconds';
%! a1 = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', tz);
%! a3 = datetime (2017, 1, 1, 0, 0, 0, 'TimeZone', tz);
%! assert_equal (char (caldiff ([a1 a3])), '0h 0m 2s');
%! assert_equal (char (between (a1, a3)), '0h 0m 2s');

## Test 'UTCLeapSeconds' -- text is read in the one shape the array also writes
%!test
%! tz = 'UTCLeapSeconds';
%! assert_equal (datevec (datetime ('2016-12-31T23:59:60Z', 'TimeZone', tz)), ...
%!               [2016 12 31 23 59 60]);
%! assert_equal (datevec (datetime ('2016-12-31T23:59:60.250Z', ...
%!               'TimeZone', tz)), [2016 12 31 23 59 60.25]);
%! assert_equal (datevec (datetime ('2016-12-31T23:59:59Z', 'TimeZone', tz)), ...
%!               [2016 12 31 23 59 59]);
%!error <datetime: text for a 'UTCLeapSeconds' datetime array must be an ISO 8601 UTC instant, as in '2016-12-31T23:59:60Z', optionally with fractional seconds.> ...
%! datetime ('31-Dec-2016 23:59:59', 'TimeZone', 'UTCLeapSeconds')
%!error <datetime: text for a 'UTCLeapSeconds' datetime array must be an ISO 8601 UTC instant, as in '2016-12-31T23:59:60Z', optionally with fractional seconds.> ...
%! datetime ('2016-12-31', 'TimeZone', 'UTCLeapSeconds')

## Test 'UTCLeapSeconds' -- an 'InputFormat' admits a 60th second only where one
## was really inserted
%!test
%! tz = 'UTCLeapSeconds';
%! F = "uuuu-MM-dd'T'HH:mm:ss'Z'";
%! assert_equal (datevec (datetime ('2016-12-31T23:59:60Z', 'InputFormat', F, ...
%!               'TimeZone', tz)), [2016 12 31 23 59 60]);
%! F3 = "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'";
%! assert_equal (datevec (datetime ('2016-12-31T23:59:60.250Z', ...
%!               'InputFormat', F3, 'TimeZone', tz)), ...
%!               [2016 12 31 23 59 60.25]);
%!test
%! F = "uuuu-MM-dd'T'HH:mm:ss'Z'";
%! d = datetime ({'2016-12-31T23:59:60Z'; '2016-12-30T23:59:60Z'}, ...
%!               'InputFormat', F, 'TimeZone', 'UTCLeapSeconds');
%! assert_equal (isnat (d), logical ([0; 1]));
%! assert_equal (d.Second(1), 60);
%!error <datetime: could not parse the date/time string '2016-12-30T23:59:60Z' with 'InputFormat' 'uuuu-MM-dd'T'HH:mm:ss'Z''.> ...
%! datetime ('2016-12-30T23:59:60Z', 'InputFormat', "uuuu-MM-dd'T'HH:mm:ss'Z'", 'TimeZone', 'UTCLeapSeconds')
%!error <datetime: could not parse the date/time string '2016-12-31T23:59:60Z' with 'InputFormat' 'uuuu-MM-dd'T'HH:mm:ss'Z''.> ...
%! datetime ('2016-12-31T23:59:60Z', 'InputFormat', "uuuu-MM-dd'T'HH:mm:ss'Z'")
%!error <datetime: could not parse the date/time string '2016-12-31T23:59:61Z' with 'InputFormat' 'uuuu-MM-dd'T'HH:mm:ss'Z''.> ...
%! datetime ('2016-12-31T23:59:61Z', 'InputFormat', "uuuu-MM-dd'T'HH:mm:ss'Z'", 'TimeZone', 'UTCLeapSeconds')

## Test 'UTCLeapSeconds' -- before 1972 no second had been inserted, so the zone
## reads exactly as UTC
%!test
%! tz = 'UTCLeapSeconds';
%! assert_equal (posixtime (datetime (1960, 1, 1, 0, 0, 0, 'TimeZone', tz)), ...
%!               posixtime (datetime (1960, 1, 1, 0, 0, 0, 'TimeZone', 'UTC')));
%! assert_equal (posixtime (datetime (1971, 12, 31, 23, 59, 59, ...
%!               'TimeZone', tz)), 63071999);
%! assert_equal (posixtime (datetime (1972, 1, 1, 0, 0, 0, 'TimeZone', tz)), ...
%!               63072000);

## Test 'setDefaultFormats' -- each default governs the arrays it applies to
%!test
%! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss');
%! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd');
%! assert_equal (char (datetime (2024, 1, 1)), '2024/01/01');
%! assert_equal (char (datetime (2024, 1, 1, 12, 30, 0)), '2024-01-01 12:30:00');
%! datetime.setDefaultFormats ('reset');

## Test 'setDefaultFormats' -- the values choose the default, not the
## constructor call, so components given as an explicit midnight still take the
## date-only default, while a mixed array takes the date-and-time one
%!test
%! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss');
%! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd');
%! assert_equal (char (datetime (2024, 1, 1, 0, 0, 0)), '2024/01/01');
%! assert_equal (datetime (2024, 1, 1, 0, 0, 0).Format, 'yyyy/MM/dd');
%! m = datetime (2024, 1, [1, 2], [0, 12], 0, 0);
%! assert_equal (m.Format, 'yyyy-MM-dd HH:mm:ss');
%! datetime.setDefaultFormats ('reset');

## Test 'setDefaultFormats' -- the choice is remade on every display, so an
## array moves between the two defaults when its components change
%!test
%! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss');
%! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd');
%! d = datetime (2024, 1, 1);
%! assert_equal (d.Format, 'yyyy/MM/dd');
%! d.Hour = 12;
%! assert_equal (d.Format, 'yyyy-MM-dd HH:mm:ss');
%! d.Hour = 0;
%! assert_equal (d.Format, 'yyyy/MM/dd');
%! datetime.setDefaultFormats ('reset');

## Test 'setDefaultFormats' -- an explicitly set Format is never touched
%!test
%! k = datetime (2024, 1, 1, 'Format', 'dd.MM.yyyy');
%! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss');
%! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd');
%! assert_equal (k.Format, 'dd.MM.yyyy');
%! assert_equal (char (k), '01.01.2024');
%! datetime.setDefaultFormats ('reset');

## Test 'setDefaultFormats' -- 'reset' restores both factory formats
%!test
%! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss');
%! datetime.setDefaultFormats ('defaultdate', 'yyyy/MM/dd');
%! datetime.setDefaultFormats ('reset');
%! assert_equal (char (datetime (2024, 1, 1)), '01-Jan-2024');
%! assert_equal (char (datetime (2024, 1, 1, 12, 30, 0)), '01-Jan-2024 12:30:00');

## Test 'setDefaultFormats' -- a set format is stored as a preference so that it
## survives the session, and a factory format is stored as no preference at all
%!test
%! datetime.setDefaultFormats ('reset');
%! assert_equal (ispref ('datatypes', 'datetime_DefaultFormat'), false);
%! assert_equal (ispref ('datatypes', 'datetime_DefaultDateFormat'), false);
%! datetime.setDefaultFormats ('default', 'yyyy-MM-dd HH:mm:ss');
%! assert_equal (ispref ('datatypes', 'datetime_DefaultFormat'), true);
%! assert_equal (getpref ('datatypes', 'datetime_DefaultFormat'), ...
%!               'yyyy-MM-dd HH:mm:ss');
%! assert_equal (ispref ('datatypes', 'datetime_DefaultDateFormat'), false);
%! datetime.setDefaultFormats ('reset');
%! assert_equal (ispref ('datatypes', 'datetime_DefaultFormat'), false);

## Test 'setDefaultFormats' -- a string scalar is accepted for both arguments
%!test
%! datetime.setDefaultFormats (string ('defaultdate'), string ('yyyy/MM/dd'));
%! assert_equal (char (datetime (2024, 1, 1)), '2024/01/01');
%! datetime.setDefaultFormats ('reset');

## Test 'setDefaultFormats' -- error branches
%!error <datetime.setDefaultFormats: too few input arguments.> ...
%! datetime.setDefaultFormats ()
%!error <datetime.setDefaultFormats: too few input arguments.> ...
%! datetime.setDefaultFormats ('default')
%!error <datetime.setDefaultFormats: the first input must be 'default', 'defaultdate', or 'reset'.> ...
%! datetime.setDefaultFormats ('bogus', 'yyyy')
%!error <datetime.setDefaultFormats: the first input must be 'default', 'defaultdate', or 'reset'.> ...
%! datetime.setDefaultFormats ('preserveinput', 'yyyy')
%!error <datetime.setDefaultFormats: the first input must be 'default', 'defaultdate', or 'reset'.> ...
%! datetime.setDefaultFormats (42, 'yyyy')
%!error <datetime.setDefaultFormats: FMT must be a character vector or a string scalar.> ...
%! datetime.setDefaultFormats ('default', 42)
%!error <datetime.setDefaultFormats: FMT must be a display format, not the name of a default.> ...
%! datetime.setDefaultFormats ('default', 'default')
%!error <datetime: the format 'yyyy-VV' contains an unsupported symbol: 'V'.> ...
%! datetime.setDefaultFormats ('default', 'yyyy-VV')

## Test 'setDefaultFormats' -- unlike MATLAB, which accepts anything in the
## second slot after 'reset' and silently resets both defaults regardless
%!error <datetime.setDefaultFormats: 'reset' takes no further arguments; it resets both defaults.> ...
%! datetime.setDefaultFormats ('reset', 'default')
%!error <datetime.setDefaultFormats: 'reset' takes no further arguments; it resets both defaults.> ...
%! datetime.setDefaultFormats ('reset', 42)

## Test 'discretize' -- explicit datetime edges, half open with the last bin
## closed at both ends
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (discretize (t, t([1 4 8 10])), [1 1 1 2 2 2 2 3 3 3]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [b, e] = discretize (t, t([1 5 10]));
%! assert_equal (class (b), 'double');
%! assert_equal (class (e), 'datetime');
%! assert_equal (posixtime (e), [1704067200 1704412800 1704844800]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (discretize (t, t([1 5 10]), 'IncludedEdge', 'right'), ...
%!               [1 1 1 1 1 2 2 2 2 2]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (discretize (t, t([1 5 10]), [10 20]), ...
%!               [10 10 10 10 20 20 20 20 20 20]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! c = discretize (t, t([1 5 10]), 'categorical');
%! assert_equal (categories (c), ...
%!               {'[01-Jan-2024, 05-Jan-2024)'; '[05-Jan-2024, 10-Jan-2024)'});
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! c = discretize (t, t([1 5 10]), 'categorical', {'early', 'late'});
%! assert_equal (categories (c), {'early'; 'late'});
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [b, e] = discretize (t, 3);
%! assert_equal (b, [1 1 1 2 2 2 2 3 3 3]);
%! assert_equal (posixtime (e), ...
%!               [1703980800 1704326400 1704672000 1705017600]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = discretize (t, 1);
%! assert_equal (posixtime (e), [1704067200 1704931200]);
%!test
%! u = datetime (2024, 1, 1) + hours (0:47);
%! [~, e] = discretize (u, 7);
%! assert_equal (posixtime (e), 1704067200 + (-1 + (0:7) * 7) * 3600);
%!test
%! t = reshape (datetime (2024, 1, 1) + days (0:11), 2, 3, 2);
%! b = discretize (t, 3);
%! assert_equal (size (b), [2 3 2]);
%!test
%! t = datetime (2024, 1, 1) + days ([0 0]);
%! [~, e] = discretize (t, 3);
%! ref = 1704067200 + [-0.5 -1/6 1/6 0.5];
%! assert_equal (max (abs (posixtime (e) - ref)) < 1e-6, true);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! t(6) = NaT;
%! b = discretize (t, 3);
%! assert_equal (isnan (b(6)), true);
%! assert_equal (isnan (b([1 10])), [false false]);

## Test 'discretize' -- a duration or calendarDuration bin width
%!test
%! u = datetime (2024, 1, 1) + hours (0:47);
%! [~, e] = discretize (u, days (1));
%! assert_equal (posixtime (e), [1704067200 1704153600 1704240000]);
%!test
%! u = datetime (2024, 1, 1) + hours (0:47);
%! [~, e1] = discretize (u, days (1));
%! [~, e2] = discretize (u, 'day');
%! assert_equal (posixtime (e1), posixtime (e2));
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (discretize (t, calmonths (1)), ones (1, 10));
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = discretize (t, caldays (7));
%! assert_equal (posixtime (e), [1704067200 1704672000 1705276800]);

## Test 'discretize' -- named units on real calendar boundaries
%!test
%! u = datetime (2024, 1, 1) + hours (0:47);
%! [~, e] = discretize (u, 'hour');
%! assert_equal (numel (e), 49);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = discretize (t, 'week');
%! assert_equal (posixtime (e), [1703980800 1704585600 1705190400]);
%! assert_equal (weekday (datenum (2023, 12, 31)), 1);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = discretize (t, 'month');
%! assert_equal (posixtime (e), [1704067200 1706745600]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = discretize (t, 'quarter');
%! assert_equal (posixtime (e), [1704067200 1711929600]);
%!test
%! t = datetime (2024, 5, 17) + days (0:9);
%! [~, e] = discretize (t, 'quarter');
%! [Y, M, D] = ymd (e);
%! assert_equal ([Y(:), M(:), D(:)], [2024 4 1; 2024 7 1]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = discretize (t, 'year');
%! assert_equal (posixtime (e), [1704067200 1735689600]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = discretize (t, 'decade');
%! assert_equal (posixtime (e), [1577836800 1893456000]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = discretize (t, 'century');
%! [Y, M, D] = ymd (e);
%! assert_equal ([Y(:), M(:), D(:)], [2000 1 1; 2100 1 1]);
%!test
%! t = datetime (2024, 1, 1) + minutes (0:59);
%! [~, e] = discretize (t, 'minute');
%! assert_equal (numel (e), 61);

## Test 'discretize' -- zoned data bins on local midnight, so a bin holding a
## daylight-saving transition really is 23 or 25 hours long
%!test
%! t0 = datetime (2024, 3, 5, 'TimeZone', 'America/New_York');
%! [~, e] = discretize (t0 + days ([0 5]), 3);
%! [H, MI, S] = hms (e);
%! assert_equal ([H(:).', MI(:).', S(:).'], zeros (1, 12));
%! assert_equal (hours (diff (e)), [48 48 47]);
%!test
%! t0 = datetime (2024, 10, 30, 'TimeZone', 'America/New_York');
%! [~, e] = discretize (t0 + days ([0 9]), 3);
%! assert_equal (hours (diff (e)), [72 73 72]);
%!test
%! t0 = datetime (2024, 3, 5);
%! [~, e] = discretize (t0 + days ([0 5]), 3);
%! assert_equal (hours (diff (e)), [48 48 48]);
%!test
%! t0 = datetime (2024, 3, 9, 12, 0, 0, 'TimeZone', 'America/New_York');
%! [~, e] = discretize (t0 + days ([0 1]), 3);
%! assert_equal (hours (diff (e)), [9 9 9]);
%! H = hms (e);
%! assert_equal (H(:).', [11 20 6 15]);

## Test 'histcounts' -- automatic binning, counts, and the third output
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [n, e] = histcounts (t);
%! assert_equal (n, [5 5]);
%! assert_equal (posixtime (e), [1704067200 1704499200 1704931200]);
%!test
%! u = datetime (2024, 1, 1) + hours (0:47);
%! [n, e] = histcounts (u);
%! assert_equal (n, [12 12 12 12]);
%! assert_equal (posixtime (e), 1704067200 + (0:4) * 43200);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 3);
%! assert_equal (posixtime (e), ...
%!               [1703980800 1704326400 1704672000 1705017600]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [n, e, b] = histcounts (t, t([1 5 10]));
%! assert_equal (n, [4 6]);
%! assert_equal (posixtime (e), [1704067200 1704412800 1704844800]);
%! assert_equal (b, [1 1 1 1 2 2 2 2 2 2]);
%!test
%! t = reshape (datetime (2024, 1, 1) + days (0:11), 2, 3, 2);
%! [n, ~, b] = histcounts (t, 3);
%! assert_equal (size (b), [2 3 2]);
%! assert_equal (isrow (n), true);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (class (histcounts (t, 3)), 'double');

## Test 'histcounts' -- the Name/Value options
%!test
%! u = datetime (2024, 1, 1) + hours (0:47);
%! [~, e] = histcounts (u, 'BinWidth', days (1));
%! assert_equal (posixtime (e), [1704067200 1704153600 1704240000]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 'BinEdges', t([1 5 10]));
%! assert_equal (posixtime (e), [1704067200 1704412800 1704844800]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 'NumBins', 3);
%! assert_equal (posixtime (e), ...
%!               [1703980800 1704326400 1704672000 1705017600]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 'BinMethod', 'day');
%! assert_equal (numel (e), 11);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 'BinMethod', 'month');
%! assert_equal (posixtime (e), [1704067200 1706745600]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (histcounts (t, 'BinMethod', 'decade'), 10);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (histcounts (t, 'BinMethod', 'scott'), [5 5]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 'BinMethod', 'sturges');
%! assert_equal (posixtime (e), 1704067200 + (0:5) * 172800);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 'BinMethod', 'sqrt');
%! assert_equal (posixtime (e), ...
%!               [1703980800 1704240000 1704499200 1704758400 1705017600]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 'BinMethod', 'fd');
%! assert_equal (posixtime (e), [1704067200 1704499200 1704931200]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! [~, e] = histcounts (t, 'BinLimits', t([3 7]));
%! assert_equal (posixtime (e), [1704240000 1704412800 1704585600]);

## Test 'histcounts' -- the accepted normalizations
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (histcounts (t, 3, 'Normalization', 'count'), [3 4 3]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (histcounts (t, 3, 'Normalization', 'probability'), ...
%!               [0.3 0.4 0.3]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (histcounts (t, 3, 'Normalization', 'percentage'), [30 40 30]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (histcounts (t, 3, 'Normalization', 'cdf'), [0.3 0.7 1]);
%!test
%! t = datetime (2024, 1, 1) + days (0:9);
%! assert_equal (histcounts (t, 'Normalization', 'cumcount'), [5 10]);

## Test 'discretize' and 'histcounts' -- error branches
%!error <datetime.discretize: not enough input arguments.> ...
%! discretize (datetime (2024, 1, 1))
%!error <datetime.discretize: N must be a real positive integer.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), 0)
%!error <datetime.discretize: N must be a real positive integer.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), 2.5)
%!error <datetime.discretize: N must be a real positive integer.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), Inf)
%!error <datetime.discretize: numeric bin edges are not accepted for a datetime; give a datetime vector.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), [1 2 3])
%!error <datetime.discretize: the second argument must be a datetime, a bin count, a duration, a calendarDuration, or a unit name.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), {1})
%!error <datetime.discretize: UNIT must be one of 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year', 'decade', or 'century'.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), 'fortnight')
%!error <datetime.discretize: a bin width must be positive and finite.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), days (0))
%!error <datetime.discretize: a bin width must be positive and finite.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), caldays (0))
%!error <datetime.discretize: a bin width must be positive and finite.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), caldays (-1))
%!error <datetime.discretize: a bin width must be positive and finite.> ...
%! discretize (datetime (2024, 1, 1) + days (0:9), caldays ([1, 2]))
%!error <datetime.histcounts: a bin width must be a scalar duration or calendarDuration, or a unit name, and must be positive and finite.> ...
%! histcounts (datetime (2024, 1, 1) + days (0:9), 'BinWidth', 3)
%!error <datetime.histcounts: 'integers' is not a valid 'BinMethod' for a datetime.> ...
%! histcounts (datetime (2024, 1, 1) + days (0:9), 'BinMethod', 'integers')
%!error <datetime.histcounts: 'countdensity' is not a valid 'Normalization' for a datetime; a density per unit time has no meaning.> ...
%! histcounts (datetime (2024, 1, 1) + days (0:9), 3, 'Normalization', 'countdensity')
%!error <datetime.histcounts: 'pdf' is not a valid 'Normalization' for a datetime; a density per unit time has no meaning.> ...
%! histcounts (datetime (2024, 1, 1) + days (0:9), 3, 'Normalization', 'pdf')
%!error <datetime.histcounts: 'BinEdges' must be a datetime.> ...
%! histcounts (datetime (2024, 1, 1) + days (0:9), 'BinEdges', [1 2 3])
%!error <datetime.histcounts: 'BinLimits' must be a datetime.> ...
%! histcounts (datetime (2024, 1, 1) + days (0:9), 'BinLimits', [1 2])
%!error <datetime.histcounts: 'NumBins' must be a real, finite, positive, integer value.> ...
%! histcounts (datetime (2024, 1, 1) + days (0:9), 'NumBins', 0)

## Test 'discretize' -- the fixed units are anchored on LOCAL midnight and then
## stepped by a fixed span of seconds.  Flooring the instant itself would anchor
## on absolute time, which lands the edges on the half hour in a zone offset by
## one.  Values captured from R2024a.
%!test
%! t = datetime (2024, 1, 1, 'TimeZone', 'Asia/Kolkata') + hours (0:24);
%! [~, e] = discretize (t, 3);
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal ([Y(:), M(:), D(:), H(:), MI(:), S(:)], ...
%!               [2023 12 31 23 0 0; 2024 1 1 8 0 0; ...
%!                2024 1 1 17 0 0; 2024 1 2 2 0 0]);
%!test
%! t = datetime (2024, 1, 1, 'TimeZone', 'Asia/Kolkata') + hours (0:5);
%! [~, e] = discretize (t, 'hour');
%! [H, MI, S] = hms (e);
%! assert_equal (H(:).', 0:6);
%! assert_equal ([MI(:).', S(:).'], zeros (1, 14));

## Test 'discretize' -- across a daylight-saving transition a named 'day' keeps
## every edge on local midnight, while a fixed width of days (1) is anchored on
## local midnight and then outruns the clock, its later edges reading 01:00.
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + days ([0 5]);
%! [~, e] = discretize (t, 'day');
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal ([D(:).'], 8:14);
%! assert_equal ([H(:).'], zeros (1, 7));
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + days ([0 5]);
%! [~, e] = discretize (t, days (1));
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal ([Y(1), M(1), D(1), H(1), MI(1), S(1)], [2024 3 8 0 0 0]);
%! assert_equal (H(1:3)(:).', [0 0 0]);
%! assert_equal (H(4:6)(:).', [1 1 1]);

## Test 'histcounts' -- the same daylight-saving contract on the Name/Value
## side, and it is the KIND of width that decides it, not the length: days (1),
## hours (24), minutes (1440) and seconds (86400) all bin alike as a fixed
## 24-hour span whose edges outrun the clock, while a named unit follows the
## calendar and yields a 23-hour day.  Values captured from R2024a.  The last
## test covers the Octave extension of taking a unit name through 'BinWidth',
## which MATLAB refuses; it agrees with 'BinMethod' by construction.
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5);
%! [n, e] = histcounts (t, 'BinWidth', days (1));
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal (n, [1, 1, 2, 1, 1]);
%! assert_equal (D(:).', 8:13);
%! assert_equal (H(:).', [0, 0, 0, 1, 1, 1]);
%! assert_equal (hours (diff (e)), [24, 24, 24, 24, 24]);
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5);
%! [~, e1] = histcounts (t, 'BinWidth', days (1));
%! [~, e2] = histcounts (t, 'BinWidth', hours (24));
%! [~, e3] = histcounts (t, 'BinWidth', minutes (1440));
%! [~, e4] = histcounts (t, 'BinWidth', seconds (86400));
%! assert_equal (posixtime (e2), posixtime (e1));
%! assert_equal (posixtime (e3), posixtime (e1));
%! assert_equal (posixtime (e4), posixtime (e1));
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5);
%! [n, e] = histcounts (t, 'BinMethod', 'day');
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal (n, ones (1, 6));
%! assert_equal (D(:).', 8:14);
%! assert_equal (H(:).', zeros (1, 7));
%! assert_equal (hours (diff (e)), [24, 24, 23, 24, 24, 24]);
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5);
%! [n, e] = histcounts (t, 'BinMethod', 'week');
%! assert_equal (n, [2, 4]);
%! assert_equal (hours (diff (e)), [168, 167]);
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5);
%! [n, e] = histcounts (t, 'BinMethod', 'month');
%! assert_equal (n, 6);
%! assert_equal (hours (diff (e)), 743);
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5);
%! [n, e] = histcounts (t, 'BinMethod', 'auto');
%! [Y, M, D] = ymd (e);
%! assert_equal (n, [3, 3]);
%! assert_equal (D(:).', [8, 11, 14]);
%! assert_equal (hours (diff (e)), [71, 72]);
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5);
%! [n1, e1] = histcounts (t, 'BinWidth', 'day');
%! [n2, e2] = histcounts (t, 'BinMethod', 'day');
%! assert_equal (n1, n2);
%! assert_equal (posixtime (e1), posixtime (e2));

## Test 'discretize' / 'histcounts' -- a MIXED calendarDuration bin width is
## accepted, as MATLAB accepts it.  The grid is anchored on 1 January of the
## smallest element's year when the width carries months and on the 1st of its
## month when it does not, and steps by the whole width, time of day included.
## Values captured from R2024a.
%!test
%! t = datetime (2024, 3, 1) + hours (0:6:120);
%! [b, e] = discretize (t, caldays (1) + hours (2));
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal (D(:).', [1, 2, 3, 4, 5, 6]);
%! assert_equal (H(:).', [0, 2, 4, 6, 8, 10]);
%! assert_equal (b([1, 6, 10, 21]), [1, 2, 3, 5]);
%!test
%! t = datetime (2024, 5, 10) + caldays (0:60);
%! [~, e] = discretize (t, calmonths (1) + caldays (1));
%! [Y, M, D] = ymd (e);
%! assert_equal (M(:).', [5, 6, 7, 8]);
%! assert_equal (D(:).', [5, 6, 7, 8]);
%!test
%! t = datetime (2024, 12, 10) + caldays (0:60);
%! [~, e] = discretize (t, calmonths (1) + caldays (1));
%! [Y, M, D] = ymd (e);
%! assert_equal (Y(:).', [2024, 2024, 2025, 2025]);
%! assert_equal (M(:).', [11, 12, 1, 2]);
%! assert_equal (D(:).', [11, 12, 13, 14]);
%!test
%! t = datetime (2024, 5, 10) + caldays (0:90);
%! [~, e] = discretize (t, calmonths (1) + caldays (3));
%! [Y, M, D] = ymd (e);
%! assert_equal (M(:).', [4, 5, 6, 7, 8]);
%! assert_equal (D(:).', [10, 13, 16, 19, 22]);
%!test
%! t = datetime (2024, 5, 10, 0, 0, 0) + hours (0:6:96);
%! [~, e] = discretize (t, caldays (1) + hours (2));
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal (D(:).', [9, 10, 11, 12, 14]);
%! assert_equal (H(:).', [16, 18, 20, 22, 0]);
%!test
%! t = datetime (2024, 3, 1) + hours (0:6:120);
%! [~, e] = discretize (t, calyears (1) + calmonths (1));
%! [Y, M, D] = ymd (e);
%! assert_equal (Y(:).', [2024, 2025]);
%! assert_equal (M(:).', [1, 2]);
%!test
%! t = datetime (2024, 3, 1) + hours (0:6:120);
%! [~, e] = discretize (t, calendarDuration (0, 0, 0, 6, 0, 0));
%! [H, MI, S] = hms (e);
%! assert_equal (H(1:5), [0, 6, 12, 18, 0]);

## Test 'discretize' / 'histcounts' -- a duration bin width is anchored at
## local midnight on the 1st of the month holding the smallest element and
## stepped from there in elapsed time, not from that element's own midnight.
## The two agree unless a daylight-saving transition falls between the 1st and
## the data: in Pacific/Chatham (+12:45, and a 25-hour 7 April 2024) seven
## 24-hour steps from the 1st land on the 7th at 23:00.  In a sub-hour-offset
## zone without a transition in the way, every edge is still on the hour.
## Values captured from R2024a.
%!test
%! t = datetime (2024, 4, 8, 'TimeZone', 'Pacific/Chatham') + hours (0:23);
%! [n, e] = histcounts (t, 'BinWidth', days (1));
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal (n, 24);
%! assert_equal (D(:).', [7, 8]);
%! assert_equal (H(:).', [23, 23]);
%! assert_equal (hours (diff (e)), 24);
%!test
%! t = datetime (2024, 6, 10, 'TimeZone', 'Asia/Kolkata') + hours (0:23);
%! [~, e] = histcounts (t, 'BinWidth', hours (6));
%! [H, MI, S] = hms (e);
%! assert_equal (H(:).', [0, 6, 12, 18, 0]);
%! assert_equal (MI(:).', zeros (1, 5));
%!test
%! t = datetime (2024, 6, 10, 'TimeZone', 'Asia/Kathmandu') + hours (0:23);
%! [~, e] = histcounts (t, 'BinMethod', 'hour');
%! [H, MI, S] = hms (e);
%! assert_equal (MI, zeros (size (MI)));
%! assert_equal (H(1), 0);
%!test
%! t = datetime (2024, 6, 10, 'TimeZone', 'Australia/Eucla') + hours (0:23);
%! [~, e] = histcounts (t, 'BinWidth', hours (12));
%! [H, MI, S] = hms (e);
%! assert_equal (H(:).', [0, 12, 0]);
%! assert_equal (MI(:).', zeros (1, 3));

## Test 'discretize' / 'histcounts' -- a bin width of several days lands on a
## grid anchored at the 1st of the month holding the smallest element, not on
## that element's own date.  caldays (2) opens on an odd day of the month,
## caldays (3) on the 1st, 4th, 7th ..., and calweeks (1) seven days from the
## 1st, which is NOT the Sunday the named 'week' unit uses.  A duration width
## of whole days shares the same grid.  The time of day of the data does not
## move the anchor.  Values captured from R2024a.
%!test
%! t = datetime (2024, 6, 10) + caldays (0:5);
%! [n, e] = histcounts (t, 'BinWidth', caldays (2));
%! [Y, M, D] = ymd (e);
%! assert_equal (n, [1, 2, 3]);
%! assert_equal (D(:).', [9, 11, 13, 15]);
%! assert_equal (M(:).', 6 * ones (1, 4));
%!test
%! t = datetime (2024, 6, 10, 17, 0, 0) + caldays (0:5);
%! [~, e] = histcounts (t, 'BinWidth', caldays (2));
%! [Y, M, D] = ymd (e);
%! assert_equal (D(1), 9);
%!test
%! t = datetime (2024, 6, 10) + caldays (0:5);
%! [n, e] = histcounts (t, 'BinWidth', caldays (3));
%! [Y, M, D] = ymd (e);
%! assert_equal (n, [3, 3]);
%! assert_equal (D(:).', [10, 13, 16]);
%!test
%! t = datetime (2024, 6, 10) + caldays (0:5);
%! [n, e] = histcounts (t, 'BinWidth', days (2));
%! [Y, M, D] = ymd (e);
%! assert_equal (n, [1, 2, 3]);
%! assert_equal (D(:).', [9, 11, 13, 15]);
%!test
%! t = datetime (2024, 6, 10) + caldays ([0 7]);
%! [n, e] = histcounts (t, 'BinWidth', calweeks (1));
%! [Y, M, D] = ymd (e);
%! assert_equal (n, [1, 1]);
%! assert_equal (D(:).', [8, 15, 22]);
%!test
%! t = datetime (2024, 1, 20) + calmonths ([0 1]);
%! [n, e] = histcounts (t, 'BinWidth', calmonths (2));
%! [Y, M, D] = ymd (e);
%! assert_equal (n, 2);
%! assert_equal (M(:).', [1, 3]);
%! assert_equal (D(:).', [1, 1]);
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + caldays (0:5);
%! [n, e] = histcounts (t, 'BinWidth', caldays (2));
%! [Y, M, D] = ymd (e);
%! assert_equal (n, [1, 2, 3]);
%! assert_equal (D(:).', [7, 9, 11, 13]);
%! assert_equal (hours (diff (e)), [48, 47, 48]);

## Test 'discretize' / 'histcounts' -- a calendarDuration width covers the
## largest element even when it does not sit on an edge.  The bin count was
## taken from the gap between the enclosing boundaries (midnight to midnight,
## or month index to month index), which discards the largest element's time of
## day, so a width dividing that gap exactly closed on an edge the data had
## already passed and the last element fell outside every bin.  Zone-independent.
## The first test's values are captured from R2024a.
%!test
%! t = datetime (2024, 3, 8, 7, 0, 0, 'TimeZone', 'America/New_York') ...
%!     + caldays (0:5);
%! [n, e, b] = histcounts (t, 'BinWidth', caldays (1));
%! [Y, M, D] = ymd (e);
%! [H, MI, S] = hms (e);
%! assert_equal (n, ones (1, 6));
%! assert_equal (D(:).', 8:14);
%! assert_equal (H(:).', zeros (1, 7));
%! assert_equal (hours (diff (e)), [24, 24, 23, 24, 24, 24]);
%! assert_equal (b, 1:6);
%!test
%! t = datetime (2024, 3, 8, 7, 0, 0) + caldays (0:5);
%! [n, ~, b] = histcounts (t, 'BinWidth', caldays (1));
%! assert_equal (sum (n), 6);
%! assert_equal (any (b == 0), false);
%! assert_equal (discretize (t, caldays (1)), 1:6);
%!test
%! t = datetime (2024, 3, 8, 7, 0, 0) + caldays (0:13);
%! [n, ~, b] = histcounts (t, 'BinWidth', calweeks (1));
%! assert_equal (sum (n), 14);
%! assert_equal (any (b == 0), false);
%!test
%! t = datetime (2024, 3, 8) + caldays ([0 36]);
%! [n, e, b] = histcounts (t, 'BinWidth', calmonths (1));
%! [Y, M, D] = ymd (e);
%! assert_equal (M(:).', [3, 4, 5]);
%! assert_equal (sum (n), 2);
%! assert_equal (b, [1, 2]);
%!test
%! t = datetime (2024, 1, 15) + caldays ([0 200]);
%! [n, ~, b] = histcounts (t, 'BinWidth', calquarters (1));
%! assert_equal (sum (n), 2);
%! assert_equal (any (b == 0), false);
%!test
%! t = datetime (2024, 3, 8) + caldays (0:5);
%! [~, ew] = histcounts (t, 'BinWidth', caldays (1));
%! [~, en] = histcounts (t, 'BinMethod', 'day');
%! assert_equal (numel (ew), 6);
%! assert_equal (numel (en), 7);

## Test 'discretize' -- a unit given by NAME opens one more bin than the same
## unit given as a WIDTH, but only when the data end exactly on an edge.  The
## two agree otherwise, which is why the difference went unseen.  A duration
## array makes no such distinction; this is a datetime behaviour.  Values
## captured from R2024a.
%!test
%! t = datetime (2024, 1, 1) + hours ([0 5]);
%! [~, ew] = discretize (t, hours (1));
%! [~, en] = discretize (t, 'hour');
%! assert_equal (numel (ew), 6);
%! assert_equal (numel (en), 7);
%!test
%! t = datetime (2024, 1, 1) + days ([0 3]);
%! [~, ew] = discretize (t, days (1));
%! [~, en] = discretize (t, 'day');
%! assert_equal (numel (ew), 4);
%! assert_equal (numel (en), 5);
%!test
%! t = datetime (2024, 1, 1) + days ([0 2.5]);
%! [~, ew] = discretize (t, days (1));
%! [~, en] = discretize (t, 'day');
%! assert_equal (numel (ew), 4);
%! assert_equal (numel (en), 4);
%!test
%! t = datetime (2024, 3, 8, 'TimeZone', 'America/New_York') + days ([0 5]);
%! [~, ew] = discretize (t, days (1));
%! [~, en] = discretize (t, 'day');
%! assert_equal (numel (ew), 6);
%! assert_equal (numel (en), 7);
%!test
%! d = [seconds(0), seconds(5 * 3600)];
%! [~, ew] = discretize (d, hours (1));
%! [~, en] = discretize (d, 'hour');
%! assert_equal (numel (ew), 6);
%! assert_equal (numel (en), 6);

## Test 'discretize' / 'histcounts' -- the extra bin a NAMED unit opens is
## conditioned on the largest element sitting on a unit boundary of the WALL
## CLOCK, not on the elapsed span dividing exactly.  The bins step in elapsed
## time, so a zone shifting by less than one unit -- Australia/Lord_Howe moves
## its clock half an hour against an hour bin -- carries the grid off the wall
## clock past the transition and the element then lands mid-unit.  A whole-hour
## zone such as America/New_York keeps the grid aligned and keeps its extra
## bin, and so does a day with no transition, which is why only a sub-unit
## shift tells the two readings apart.  Values captured from R2024a.
%!test
%! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [n, e] = histcounts (t + hours (0:5), 'BinMethod', 'hour');
%! [H, MI] = hms (e);
%! assert_equal (n, [1, 1, 1, 1, 1, 1]);
%! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 2]);
%! assert_equal (MI(:).', [0, 0, 0, 0, 0, 0, 30]);
%!test
%! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [n, e] = histcounts (t + hours (0:6), 'BinMethod', 'hour');
%! [H, MI] = hms (e);
%! assert_equal (n, [1, 1, 1, 1, 1, 2]);
%! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 2]);
%! assert_equal (MI(:).', [0, 0, 0, 0, 0, 0, 30]);
%!test
%! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour');
%! [H, MI] = hms (e);
%! assert_equal (n, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]);
%! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 2, 3, 4, 5, 6, 7, 8]);
%! assert_equal (MI(:).', [0, 0, 0, 0, 0, 0, 30, 30, 30, 30, 30, 30, 30]);
%!test
%! t = datetime (2024, 4, 6, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour');
%! [H, MI] = hms (e);
%! assert_equal (n, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]);
%! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 1, 2, 3, 4, 5, 6, 7]);
%! assert_equal (MI(:).', [0, 0, 0, 0, 0, 0, 30, 30, 30, 30, 30, 30, 30]);
%!test
%! t = datetime (2024, 11, 2, 20, 0, 0, 'TimeZone', 'America/New_York');
%! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour');
%! [H, MI] = hms (e);
%! assert_equal (n, ones (1, 13));
%! assert_equal (H(:).', [20, 21, 22, 23, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8]);
%! assert_equal (MI(:).', zeros (1, 14));
%!test
%! t = datetime (2024, 10, 5, 20, 30, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour');
%! [H, MI] = hms (e);
%! assert_equal (n, ones (1, 13));
%! assert_equal (H(end), 9);
%! assert_equal (MI(end), 30);
%!test
%! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [b, e] = discretize (t + hours (0:6), 'hour');
%! assert_equal (b, [1, 2, 3, 4, 5, 6, 6]);
%! assert_equal (numel (e), 7);

## Test 'histcounts' -- DELIBERATE DEVIATION.  Where a zone shifts by less than
## one bin unit and the largest element lands mid-unit past the transition,
## MATLAB R2024a closes its last edge short of that element and drops it: 13
## points over Australia/Lord_Howe from 20:01 return 12 bins summing to 12.
## Every element of a finite input must fall in a bin when no 'BinLimits' was
## given, so we open the bin that covers it and count all 13.  See
## PENDING_NEWS.md.
%!test
%! t = datetime (2024, 10, 5, 20, 1, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [n, e] = histcounts (t + hours (0:12), 'BinMethod', 'hour');
%! [H, MI] = hms (e);
%! assert_equal (numel (n), 13);
%! assert_equal (sum (n), 13);
%! assert_equal (H(end), 9);
%! assert_equal (MI(end), 30);

## Test 'histcounts' / 'discretize' with a BIN COUNT -- the grid of a fixed
## sub-day unit is anchored on the WALL CLOCK, and only the step is elapsed.
## Anchoring on the seconds elapsed since local midnight put every edge half an
## hour out whenever a transition fell between that midnight and the data:
## noon on the day Australia/Lord_Howe moves its clock forward half an hour is
## 11.5 elapsed hours after midnight.  Note there is no transition inside the
## data here -- it lies behind them.  The span, by contrast, stays elapsed: a
## fall-back makes it an hour longer than the wall span, which is what the
## America/New_York row pins down.  Values captured from R2024a.
%!test
%! t = datetime (2024, 10, 6, 12, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours (0:7), 3);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [11, 14, 17, 20]);
%! assert_equal (MI(:).', zeros (1, 4));
%! assert_equal (hours (diff (e)), [3, 3, 3]);
%!test
%! t = datetime (2024, 11, 2, 20, 0, 0, 'TimeZone', 'America/New_York');
%! [~, e] = histcounts (t + hours (0:9), 2);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [20, 1, 5]);
%! assert_equal (MI(:).', zeros (1, 3));
%! assert_equal (hours (diff (e)), [5, 5]);
%!test
%! t = datetime (2024, 6, 10, 20, 0, 0, 'TimeZone', 'Asia/Kolkata');
%! [~, e] = histcounts (t + hours (0:7), 3);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [19, 22, 1, 4]);
%! assert_equal (MI(:).', zeros (1, 4));
%!test
%! t = datetime (2024, 9, 28, 20, 0, 0, 'TimeZone', 'Pacific/Chatham');
%! [~, e] = histcounts (t + hours (0:7), 3);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [19, 22, 1, 5]);
%! assert_equal (MI(:).', zeros (1, 4));

## Test 'histcounts' with a BIN COUNT -- where the zone's shift is not a whole
## number of grid units, the span is read off the WALL clock rather than the
## elapsed one.  Australia/Lord_Howe moved its clock a whole hour until 1985
## and half an hour since, so the same zone over the same dates and spans takes
## the elapsed reading in 1983 and the wall reading today.  Values captured
## from R2024a.
%!test
%! t = datetime (1983, 10, 29, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours (0:7), 2);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [20, 0, 5]);
%! assert_equal (MI(:).', zeros (1, 3));
%! assert_equal (hours (diff (e)), [4, 4]);
%!test
%! t = datetime (2024, 10, 5, 20, 30, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours (0:7), 2);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [19, 0, 5]);
%! assert_equal (MI(:).', [0, 0, 30]);
%! assert_equal (hours (diff (e)), [5, 5]);
%!test
%! t = datetime (2024, 3, 24, 0, 0, 0, 'TimeZone', 'Antarctica/Troll');
%! [~, e] = histcounts (t + hours (0:7), 2);
%! [H, MI] = hms (e);
%! assert_equal (MI(:).', zeros (1, 3));
%! assert_equal (hours (diff (e)), [4, 4]);

## Test 'histcounts' with a BIN COUNT -- where the grid point that anchors the
## bins names a wall time inside the gap a spring-forward opens, it steps BACK
## out of the gap rather than forward, and the remaining edges are counted from
## there in elapsed time.  Data opening at 02:30 on Australia/Lord_Howe's
## spring-forward day anchor on 02:00, which does not exist; that resolves to
## 01:30, and the edge one unit earlier is 00:30, not the 01:00 its own grid
## index would name.  Values captured from R2024a.
%!test
%! t = datetime (2024, 10, 6, 2, 30, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours (0:7), 2);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [1, 6, 10]);
%! assert_equal (MI(:).', [30, 0, 0]);
%! assert_equal (hours (diff (e)), [4, 4]);
%!test
%! t = datetime (2024, 10, 6, 2, 30, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours (0:7), 3);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [0, 4, 7, 10]);
%! assert_equal (MI(:).', [30, 0, 0, 0]);
%! assert_equal (hours (diff (e)), [3, 3, 3]);

## Test 'histcounts' with a BIN COUNT -- a transition INSIDE the data whose
## shift is not a whole number of grid units moves the whole grid one unit
## earlier.  A whole-unit shift does not, which is why the Chatham and
## New_York rows above keep their place.  The move is conditioned on the parity
## of the span and of the grid: the s = 7 row takes it and the s = 10 row does
## not.  This is MATLAB's placement, established empirically by sweeping the
## data window across a spring-forward and a fall-back alike; see STATUS.
## Values captured from R2024a.
%!test
%! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours (0:7), 3);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [18, 21, 0, 3]);
%! assert_equal (MI(:).', [0, 0, 0, 30]);
%! assert_equal (hours (diff (e)), [3, 3, 3]);
%!test
%! t = datetime (2024, 10, 5, 20, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours (0:10), 2);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [19, 1, 7]);
%! assert_equal (MI(:).', [0, 0, 30]);
%! assert_equal (hours (diff (e)), [6, 6]);

## Test 'ConvertFrom' -- the six conversions that 'convertTo' performs and the
## constructor could not invert.  Tick zero of each scale, captured from R2024a.
%!test
%! [Y, M, D] = ymd (datetime (0, 'ConvertFrom', 'excel1904'));
%! assert_equal ([Y, M, D], [1904, 1, 1]);
%!test
%! [Y, M, D] = ymd (datetime (0, 'ConvertFrom', 'juliandate'));
%! [h, m, s] = hms (datetime (0, 'ConvertFrom', 'juliandate'));
%! assert_equal ([Y, M, D, h, m, s], [-4713, 11, 24, 12, 0, 0]);
%!test
%! [Y, M, D] = ymd (datetime (0, 'ConvertFrom', 'modifiedjuliandate'));
%! assert_equal ([Y, M, D], [1858, 11, 17]);

## Test 'ConvertFrom' -- each new conversion inverts 'convertTo' of the same
## name.  Reference values captured from R2024a for 2024-03-15T12:34:56.789Z.
%!test
%! t = datetime (2024, 3, 15, 12, 34, 56, 'TimeZone', 'UTC') + milliseconds (789);
%! assert_equal (convertTo (t, 'excel1904'), 43904.52426839120017, 1e-9);
%! assert_equal (convertTo (t, 'juliandate'), 2460385.024268391076, 1e-9);
%! assert_equal (convertTo (t, 'ntfs'), uint64 (133549796967890000));
%! for f = {'excel1904', 'juliandate', 'modifiedjuliandate', 'ntp', 'ntfs', '.net'}
%!   r = datetime (convertTo (t, f{1}), 'ConvertFrom', f{1}, 'TimeZone', 'UTC');
%!   assert_equal (posixtime (r), posixtime (t), 1e-4);
%! endfor

## Test 'ConvertFrom' -- the fixed-epoch counts require uint64, as MATLAB
## requires, rather than rounding a double: an NTP stamp runs past the 2^53 a
## double holds exactly.
%!error <datetime: input for converting from NTP times must be uint64.> ...
%! datetime (0, 'ConvertFrom', 'ntp')
%!error <datetime: input for converting from NTFS times must be uint64.> ...
%! datetime (0, 'ConvertFrom', 'ntfs')
%!error <datetime: input for converting from .NET times must be uint64.> ...
%! datetime (0, 'ConvertFrom', '.net')

## Test 'ConvertFrom' -- a conversion that folded an inserted leap second away
## cannot recover it and returns the preceding second, while the Julian scales
## measure against the day's own length and do return 23:59:60.  Values
## captured from R2024a, except the Julian scales on the leap day, where
## MATLAB does not invert its own convertTo -- see the deviation note in the
## datetime docstring.
%!test
%! t = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds') ...
%!     + seconds ([0 1 2]);
%! for f = {'ntp', 'ntfs', '.net', 'excel1904'}
%!   r = datetime (convertTo (t, f{1}), 'ConvertFrom', f{1}, ...
%!                 'TimeZone', 'UTCLeapSeconds');
%!   [~, ~, ~, ~, ~, sec] = datevec (r);
%!   assert_equal (round (sec(:).'), [59, 59, 0]);
%! endfor
%!test
%! t = datetime (2016, 12, 31, 23, 59, 59, 'TimeZone', 'UTCLeapSeconds') ...
%!     + seconds ([0 1 2]);
%! r = datetime (convertTo (t, 'modifiedjuliandate'), 'ConvertFrom', ...
%!               'modifiedjuliandate', 'TimeZone', 'UTCLeapSeconds');
%! [~, ~, ~, ~, ~, sec] = datevec (r);
%! assert_equal (sec(1:2).', [59, 60], 1e-4);

## Test 'ConvertFrom' -- the Modified Julian scale is counted from its own
## epoch, not by subtracting 2400000.5 from the Julian one, which would lose
## about 14 microseconds to cancellation.  The round-trip is exact.
%!test
%! t = datetime (2024, 11, 3, 5, 30, 0, 'TimeZone', 'UTC');
%! assert_equal (convertTo (t, 'modifiedjuliandate'), 40587 + 1730611800 / 86400, 0);
%! r = datetime (convertTo (t, 'modifiedjuliandate'), 'ConvertFrom', ...
%!               'modifiedjuliandate', 'TimeZone', 'UTC');
%! assert_equal (posixtime (r), posixtime (t), 0);

## Test 'discretize' / 'histcounts' -- a fixed bin width is anchored on the
## start of the largest clock unit that does not exceed it: a whole number of
## days on the 1st of the month, an hour or more on local midnight, a minute or
## more on the hour, and anything shorter on the minute.  A width that divides
## its unit lands on the same edges either way, so only one that does not --
## minutes (50) or minutes (25) against an hour -- shows the rung.  Values
## captured from R2024a.
%!test
%! t = datetime (2024, 6, 15, 7, 0, 0) + hours (0:5);
%! [~, e] = histcounts (t, 'BinWidth', minutes (50));
%! [H, MI] = hms (e);
%! assert_equal (numel (e), 7);
%! assert_equal ([H(1), MI(1)], [7, 0]);
%!test
%! t = datetime (2024, 6, 15, 7, 37, 0) + minutes (0:10);
%! [~, e] = histcounts (t, 'BinWidth', minutes (25));
%! [H, MI] = hms (e);
%! assert_equal ([H(1), MI(1)], [7, 25]);
%!test
%! t = datetime (2024, 6, 15, 23, 40, 0) + minutes (0:10);
%! [~, e] = histcounts (t, 'BinWidth', minutes (40));
%! [H, MI] = hms (e);
%! assert_equal ([H(1), MI(1)], [23, 40]);
%!test
%! t = datetime (2024, 6, 15, 7, 23, 17) + minutes (0:10);
%! [~, e] = histcounts (t, 'BinWidth', seconds (7));
%! [H, MI, S] = hms (e);
%! assert_equal ([H(1), MI(1), S(1)], [7, 23, 14]);
%!test
%! t = datetime (2024, 6, 15, 7, 0, 0) + hours (0:5);
%! [~, e] = histcounts (t, 'BinWidth', minutes (90));
%! [H, MI] = hms (e);
%! assert_equal ([H(1), MI(1)], [6, 0]);
%!test
%! t = datetime (2024, 6, 15, 7, 0, 0) + hours (0:5);
%! [~, eh] = histcounts (t, 'BinWidth', hours (5));
%! [~, ed] = histcounts (datetime (2024, 6, 2, 7, 0, 0) + hours (0:5), ...
%!                       'BinWidth', hours (5));
%! H1 = hms (eh);
%! H2 = hms (ed);
%! assert_equal ([H1(1), H2(1)], [5, 5]);

## Test 'histcounts' with a BIN COUNT of one -- both edges of a single bin are
## grid points of the WALL clock, so the bin spans a whole number of units of
## the local clock and its elapsed length absorbs whatever the zone did in
## between.  Two wall hours across Australia/Lord_Howe's spring forward are 90
## elapsed minutes; seven wall hours across its fall-back are 450.  From two
## bins up the interior edges step in elapsed time instead and leave the wall
## clock, which the rows above pin.  Values captured from R2024a.
%!test
%! t = datetime (2024, 10, 6, 1, 40, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + minutes ([0 20]), 1);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [1, 3]);
%! assert_equal (MI(:).', [0, 0]);
%! assert_equal (minutes (diff (e)), 90);
%!test
%! t = datetime (2024, 4, 7, 0, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours ([0 7]), 1);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [0, 7]);
%! assert_equal (MI(:).', [0, 0]);
%! assert_equal (minutes (diff (e)), 450);
%!test
%! t = datetime (2024, 10, 6, 0, 0, 0, 'TimeZone', 'Australia/Lord_Howe');
%! [~, e] = histcounts (t + hours ([0 7]), 1);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [0, 8]);
%! assert_equal (MI(:).', [0, 0]);
%! assert_equal (minutes (diff (e)), 450);

## Test 'histcounts' with a BIN COUNT -- the empirical placement correction is
## applied only where a zone's shift is exactly HALF the bin unit, the domain it
## was measured on (Australia/Lord_Howe's half hour against an hour bin).  At
## any other fraction the plain rule stands.  Asia/Singapore moved +07:00 to
## +07:20 on 1933-01-01, a third of an hour, and firing the correction there is
## measurably wrong -- confining it recovers 60 rows of the probe corpus.  These
## two agree with R2024a; others across that transition still do not, which is
## a documented deviation and STATUS item 0h.
%!test
%! t = datetime (1932, 12, 31, 20, 0, 0, 'TimeZone', 'Asia/Singapore');
%! [~, e] = histcounts (t + hours ([0 7]), 2);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [20, 0, 4]);
%! assert_equal (MI(:).', [0, 20, 20]);
%! assert_equal (minutes (diff (e)), [240, 240]);
%!test
%! t = datetime (1932, 12, 31, 20, 0, 0, 'TimeZone', 'Asia/Singapore');
%! [~, e] = histcounts (t + hours ([0 7]), 3);
%! [H, MI] = hms (e);
%! assert_equal (H(:).', [19, 22, 1, 4]);
%! assert_equal (MI(:).', [0, 0, 20, 20]);
%! assert_equal (minutes (diff (e)), [180, 180, 180]);

## Test 'ConvertFrom' 'epochtime' -- 'Epoch' and 'TicksPerSecond'.  Whether the
## count measures elapsed or wall-clock time follows from the epoch: both sides
## zoned makes it elapsed from a known instant, so a UTC epoch agrees with
## 'posixtime'; otherwise the epoch is a wall clock, its zone dropped, which is
## why the default (an unzoned 1970-01-01) does not agree.  Values captured
## from R2024a.
%!test
%! p = 1730611800;
%! r = datetime (p, 'ConvertFrom', 'epochtime', 'Epoch', ...
%!               datetime (1970, 1, 1, 'TimeZone', 'UTC'), ...
%!               'TimeZone', 'America/New_York');
%! assert_equal (posixtime (r), p);
%! [H, MI] = hms (r);
%! assert_equal ([H, MI], [1, 30]);
%!test
%! p = 1730611800;
%! r = datetime (p, 'ConvertFrom', 'epochtime', 'Epoch', ...
%!               datetime (1970, 1, 1), 'TimeZone', 'America/New_York');
%! assert_equal (posixtime (r), p + 18000);
%! [H, MI] = hms (r);
%! assert_equal ([H, MI], [5, 30]);
%!test
%! r = datetime (1730611800, 'ConvertFrom', 'epochtime', 'Epoch', ...
%!               datetime (2000, 1, 1));
%! [Y, M, D] = ymd (r);
%! assert_equal ([Y, M, D], [2054, 11, 3]);
%!test
%! r = datetime (1730611800123, 'ConvertFrom', 'epochtime', ...
%!               'TicksPerSecond', 1000);
%! assert_equal (posixtime (r), 1730611800.123, 1e-6);
%!test
%! r = datetime (1730611800, 'ConvertFrom', 'epochtime', 'Epoch', ...
%!               datetime (1970, 1, 1, 'TimeZone', 'America/New_York'));
%! assert_equal (posixtime (r), 1730611800);
%! assert_equal (isempty (r.TimeZone), true);

## Test 'ConvertFrom' -- 'Epoch' and 'TicksPerSecond' bear on 'epochtime' alone
## and are ignored by every other conversion, as MATLAB ignores them.
%!test
%! r = datetime (739559, 'ConvertFrom', 'datenum', 'Epoch', datetime (1970, 1, 1));
%! [Y, M, D] = ymd (r);
%! assert_equal ([Y, M, D], [2024, 11, 3]);
%!test
%! r = datetime (1730611800, 'ConvertFrom', 'posixtime', 'TicksPerSecond', 1000);
%! assert_equal (posixtime (r), 1730611800);

## Test 'ConvertFrom' 'epochtime' -- DELIBERATE DEVIATION on a leap-second
## array.  Both operands are counted on the same timeline here; MATLAB folds the
## array onto the POSIX one and the epoch onto the leap-second one, so its
## answer runs long by the seconds inserted before that epoch -- it returns
## 2017-01-01T00:00:25 where both consistent readings give 23:59:59.  The
## mirror of the deviation already documented for convertTo.
%!test
%! E = datetime (2016, 12, 31, 'TimeZone', 'UTCLeapSeconds');
%! r = datetime (int64 (86399), 'ConvertFrom', 'epochtime', 'Epoch', E, ...
%!               'TimeZone', 'UTCLeapSeconds');
%! [Y, M, D] = ymd (r);
%! [H, MI, S] = hms (r);
%! assert_equal ([Y, M, D, H, MI, S], [2016, 12, 31, 23, 59, 59]);

%!error <datetime: 'Epoch' must be a scalar datetime.> ...
%! datetime (0, 'ConvertFrom', 'epochtime', 'Epoch', 5)
%!error <datetime: 'TicksPerSecond' must be a positive finite scalar.> ...
%! datetime (0, 'ConvertFrom', 'epochtime', 'TicksPerSecond', -1)

## Restore whatever default formats the user had before this file ran.
%!test
%! datetime.setDefaultFormats ('reset');
%! if (! isempty (savedDefaultFormat))
%!   datetime.setDefaultFormats ('default', savedDefaultFormat);
%! endif
%! if (! isempty (savedDefaultDateFormat))
%!   datetime.setDefaultFormats ('defaultdate', savedDefaultDateFormat);
%! endif
