classdef (Sealed) timerange < matlab.internal.tabular.private.subscripter
%TIMERANGE Timetable row subscripting by time range.
%   S = TIMERANGE(STARTTIME,ENDTIME) creates a subscript to select rows of a
%   timetable within a range of times. S selects all rows whose times are in the
%   time interval specified by STARTTIME and ENDTIME, including STARTTIME but
%   not ENDTIME. The time interval is a half-open interval. STARTTIME and
%   ENDTIME are either datetime or duration scalars, depending on the timetable
%   that S will be used to subscript into. STARTTIME and ENDTIME may also be
%   date/time text as accepted by DATETIME or DURATION.
%
%   S = TIMERANGE(STARTTIME,ENDTIME,INTERVALTYPE) creates a subscript over the
%   type of interval specified by INTERVALTYPE. INTERVALTYPE is one of the following:
% 
%      'open'        - select rows where STARTTIME <  ROWTIMES and ROWTIMES <  ENDTIME
%      'closed'      - select rows where STARTTIME <= ROWTIMES and ROWTIMES <= ENDTIME
%      'openleft'    - select rows where STARTTIME <  ROWTIMES and ROWTIMES <= ENDTIME
%      'openright'   - select rows where STARTTIME <= ROWTIMES and ROWTIMES <  ENDTIME
%      'closedright' - same as 'openleft'
%      'closedleft'  - same as 'openright'
%
%   S = TIMERANGE(STARTPERIOD,ENDPERIOD,UNITOFTIME) creates a subscript over the time
%   period between STARTPERIOD and ENDPERIOD, inclusive, using the date/time component
%   specified by UNITOFTIME. UNITOFTIME is one of the following:
% 
%      'years'    - select rows where    YEAR(ROWTIMES) is between    YEAR(STARTPERIOD) and    YEAR(ENDPERIOD)
%      'quarters' - select rows where QUARTER(ROWTIMES) is between QUARTER(STARTPERIOD) and QUARTER(ENDPERIOD)
%      'months'   - select rows where   MONTH(ROWTIMES) is between   MONTH(STARTPERIOD) and   MONTH(ENDPERIOD)
%      'weeks'    - select rows where    WEEK(ROWTIMES) is between    WEEK(STARTPERIOD) and    WEEK(ENDPERIOD)
%      'days'     - select rows where     DAY(ROWTIMES) is between     DAY(STARTPERIOD) and     DAY(ENDPERIOD)
%      'hours'    - select rows where    HOUR(ROWTIMES) is between    HOUR(STARTPERIOD) and    HOUR(ENDPERIOD)
%      'minutes'  - select rows where  MINUTE(ROWTIMES) is between  MINUTE(STARTPERIOD) and  MINUTE(ENDPERIOD)
%      'seconds'  - select rows where  SECOND(ROWTIMES) is between  SECOND(STARTPERIOD) and  SECOND(ENDPERIOD)
%
%   S = TIMERANGE(TIMEPERIOD,UNITOFTIME) creates a subscript spanning the beginning and 
%   end of the date/time component of TIMEPERIOD specified by UNITOFTIME. TIMEPERIOD is 
%   either a datetime array or date/time text as accepted in DATETIME by default. 
%
%
%
%   Examples:
%
%   % Select rows of a timetable by date, from April 3rd to 12th, inclusive.
%   tt = array2timetable(randn(10,3),'RowTimes',datetime(2016,4,randi(15,10,1)))
%   tr = timerange(datetime(2016,4,3),datetime(2016,4,12),'closed')
%   tt4to12 = tt(tr,:)
%
%   % Select rows from April 20th to the end of the month. There are no rows in
%   % the timetable that fall in that range.
%   tr = timerange(datetime(2016,4,20),datetime(2016,5,1))
%   tt20to30 = tt(tr,:)
%
%   % Select rows of a timetable whose times are between 15 and 30 minutes, not
%   % including the right endpoint.
%   tt = array2timetable(randn(10,3),'RowTimes',hours(rand(10,1)))
%   tr = timerange(minutes(15),minutes(30))
%   tt0to1 = tt(tr,:)
%
%   % Select rows of a timetable whose times fall within the fourth quarter of 2017.
%   tt = timetable(datetime(2017,1,1:365)',randn(365,1))
%   tr = timerange(datetime(2017,9,15),datetime(2017,12,15),'months')
%   tt_Q4_2017 = tt(tr,:)
%
%   % Select rows of a timetable whose times fall within the fourth quarter of 2017.
%   tr = timerange('Q4-2017','quarters')
%   tt_Q4_2017 = tt(tr,:)
%
%   See also WITHTOL, VARTYPE.

%   Copyright 2016-2020 The MathWorks, Inc.

    properties(Transient, GetAccess = {?timetable, ?matlab.internal.coder.timerange}, ...
            SetAccess='protected')
        % left & right edge of range: default to NAT to not match anything
        first = NaT;
        last  = NaT;
        
        % Range type: 
        %   'openright' (same as 'closedleft') {default}
        %   'openleft' (same as 'closedright')
        %   'open'
        %   'closed'
        type = 'openright';
    end
    
    properties(Transient, Access='private')
        first_matchTimeZone = false;
        last_matchTimeZone  = false;
        unitOfTime          = '';
    end

    methods
        % Constructor adds an extra, unused input so error handling catches
        % the common mistake of passing in a timetable as a leading input.
        % Otherwise, front-end would throw "Too many input arguments".
        function obj = timerange(first,last_or_unitOfTime,intervalType_or_unitOfTime,~)                           
            try
                narginchk(2,3);
                
                % If third argument is not specified, the second argument
                % can be either the LAST endpoint or a unitOfTime.
                % Treat this as the same value was passed as the third
                % argument for unified parsing in rectifyArg()
                if nargin == 2, intervalType_or_unitOfTime = last_or_unitOfTime; end
                
                % Rectify semantics of second and/or third parameters between possible syntaxes:
                %     timerange(startTime, endTime)
                %     timerange(time, unitOfTime)
                %     timerange(startTime, endTime, intervalType)
                %     timerange(startPeriod, endPeriod, unitOfTime)
                % Object properties are initialized accordingly
                obj = obj.rectifyArgs(first, last_or_unitOfTime, intervalType_or_unitOfTime, (nargin==3));
                
                % The obj.first and obj.last endpoints, properly assigned from user input above, can still be a TimeText
                % at this point. Parse and convert valid TimeText into appropriate TimeType (duration/datetime).
                obj = obj.endpoints2Timetype();
                
                % When the range is defined in terms of time periods rather than instants, shift the first and last 
                % endpoints to the beginning and end of the specified unitOfTime in which they fall.
                if ~isempty(obj.unitOfTime)
                    obj = obj.snapEndpointsToUnitOfTime();
                end
            catch ME % Limit error stack to this constructor scope
                if nargin == 0
                    return % No inputs: return default constructed timerange
                elseif isa(first,'tabular') % common error: timerange(tt,startTime,endTime,...)
                    error(message('MATLAB:timerange:TabularInput',upper(class(first))));
                else
                    throw(ME);
                end
            end
        end
             
        function tf = isequal(varargin)
            % ISEQUAL returns true when all timerange instances exhibit the
            % same subscripting behavior. Subscripting behaviors are identical 
            % when all properties other than UNITOFTIME are equal (NaT/NaT and 
            % NaN/NaN are equal w.r.t. the resulting subscripting operation)
            narginchk(2,Inf);
            tf = isequalUtil(varargin);
        end

        function tf = isequaln(varargin)
            % ISEQUALN returns true when all timerange instances exhibit the
            % same subscripting behavior. Subscripting behaviors are identifical 
            % when all properties other than UNITOFTIME are equal (NaT/NaT and 
            % NaN/NaN are equal w.r.t. the resulting subscripting operation).
            % This is the same behavior as ISEQUAL.
            narginchk(2,Inf);
            tf = isequalUtil(varargin);
        end
    end
    
    methods(Access=private)
        function obj = rectifyArgs(obj, first, last, arg, argCanBeIntervalType)
            % RECTIFYARGUMENTS processes ARG into IntervalType or UnitOfTime, and assign over default
            % values of the corresponding property, TYPE and UNITOFTIME, with the result
            isUnitOfTime = false;
            if matlab.internal.datatypes.isScalarText(arg)
                if argCanBeIntervalType && matches(arg,["openright" "closedleft" "openleft" "closedright" "open" "closed"],"IgnoreCase",true)
                    % timerange(first,last,IntervalType)
                    obj.type = convertStringsToChars(lower(arg));
                else % timerange(first,last,PeriodUnit), timerange(first,PeriodUnit) OR timerange(first,last) where LAST maybe a TimeText
                    validPeriods = {'years', 'quarters', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'};
                    idx = startsWith(validPeriods, arg, 'IgnoreCase', true);
                    isUnitOfTime = (nnz(idx) == 1); % ARG unambiguously matched a valid period
                    if isUnitOfTime
                        obj.unitOfTime = validPeriods{idx};
                    elseif (nnz(idx) > 1) && (strlength(arg) ~= 0) % ARG is a non-empty text and matches more than one PeriodUnit
                        ambiguousMatches = validPeriods(find(idx,2)); % only two values, MONTHS & MINUTES, can result in ambiguous match
                        error(message('MATLAB:timerange:AmbiguousUnitOfTime',ambiguousMatches{:}));
                    elseif argCanBeIntervalType % timerange(first,last,IntervalType) OR timerange(first,last,PeriodUnit)
                        error(message('MATLAB:timerange:InvalidIntervalType')); % Prefer InvalidIntervalType error
                    else
                        % Either timerange(first,last) where LAST maybe a TimeText OR empty text (''/"")
                        % defer parsing and error handling to text2timetype
                    end
                end
            elseif argCanBeIntervalType % timerange(first,last,IntervalType) OR timerange(first,last,unitOfTime): 3rd argument must be text
                error(message('MATLAB:timerange:InvalidIntervalType'));
            else % timerange(first,last) where LAST is a TimeType
                % nothing to do - no parsing needed
            end
            
            obj.first = first;
            if isUnitOfTime && ~argCanBeIntervalType
                % Treat timerange(first,unitOfTime) as timerange(first,first,unitOfTime)                
                obj.last = first;
            else
                obj.last = last;
            end
        end
        
        function obj = endpoints2Timetype(obj)
            firstTraits = endpointTraits(obj.first);
            lastTraits  = endpointTraits(obj.last);
            
            % Check upfront for error cases between two endpoints
            if firstTraits.isInvalid || lastTraits.isInvalid || (firstTraits.isNonFiniteNum && lastTraits.isNonFiniteNum)
                % Error if either type is invalid, or both are non-finite numeric
                error(message('MATLAB:timerange:InvalidTimes'));
            elseif (firstTraits.isDuration && lastTraits.isDatetime) || (lastTraits.isDuration && firstTraits.isDatetime)
                % Duration cannot mix with non-duration
                error(message('MATLAB:timerange:InputTypesMismatch'));
            elseif firstTraits.isDatetime && lastTraits.isDatetime && (isempty(obj.first.TimeZone) ~= isempty(obj.last.TimeZone))
                % If both are datetimes, they must be zoned or unzoned at the same time
                error(message('MATLAB:timerange:TimeZonesMismatch'));
            elseif ~firstTraits.isScalar || ~lastTraits.isScalar
                % Both endpoints must be scalars
                error(message('MATLAB:timerange:NonScalarInput'));
            end
            
            % If the first endpoint is text, convert to a time type. If the last endpoint is not also text,
            % convert to its class, otherwise infer from the text.
            if firstTraits.isText
                obj.first = text2Timetype_helper(obj.first, obj.last, obj.unitOfTime);
                [obj.first, obj.first_matchTimeZone] = handleTimeZone(obj.first, obj.last, lastTraits);
            end
            
            % If the last endpoint is text, convert it to the same time type as the first endpoint, which has already
            % been converted if it was also text.
            if lastTraits.isText
                obj.last = text2Timetype_helper(obj.last, obj.first, obj.unitOfTime);
                [obj.last, obj.last_matchTimeZone] = handleTimeZone(obj.last, obj.first, firstTraits);
            end
            
            % Convert non-finite numeric endpoints into TimeType
            % with respect to the template other endpoint's type.
            % The case where both are simultaneously non-finite numeric is
            % already ruled out in verifyEndpoints()
            if firstTraits.isNonFiniteNum && isnumeric(obj.first)
                obj.first = convertNonFinite(obj.first,obj.last);
            elseif lastTraits.isNonFiniteNum && isnumeric(obj.last)
                obj.last  = convertNonFinite(obj.last,obj.first);
            end
            
            % make sure after text conversion that the types are the same
            if ~matches(class(obj.first),class(obj.last))
                error(message('MATLAB:timerange:InputTypesMismatch'));
            end
        end
        
        function obj = snapEndpointsToUnitOfTime(obj)
            try % DATESHIFT only works if both FIRST and LAST are datetime.
                obj.first = dateshift(obj.first, 'start', obj.unitOfTime);
                obj.last  = dateshift(obj.last,  'start', obj.unitOfTime, 'next');
            catch ME
                if isa(obj.first,'duration') || isa(obj.last,'duration') % Duration endpoint(s) not compatible with UNITOFTIME semantics
                    error(message('MATLAB:timerange:UnitOfTimeTypesMismatch'));
                else
                    rethrow(ME)
                end
            end
        end
    end
    
    methods(Access={?withtol, ?timerange, ?vartype, ?matlab.internal.tabular.private.tabularDimension, ?tabular})
        % The getSubscripts method is called by table subscripting to find the indices
        % of the times (if any) along that dimension that fall between the specified
        % left and right time.
        function subs = getSubscripts(obj,dimObj)
            % A timerange is only supported on a rowTimesDim
            if ~isa(dimObj,'matlab.internal.tabular.private.rowTimesDim')
                throwAsCaller(MException(message('MATLAB:timerange:InvalidSubscripter')));
            end
            
            try
                % If the timerange was constructed with text datetime timestamps,
                % interpret them in the same time zone (if any) as the timetable's
                % row times. If they were duration timestamps, matchTimeZone is
                % false.
                if obj.first_matchTimeZone || obj.last_matchTimeZone
                    % Get an example of the timetable's row times, but avoid
                    % expanding out an implicit row times vector
                    rowTimes = dimObj.rowTimesTemplate();
                    rowTimesTZ = rowTimes.TimeZone; % errors if timetable's row times are durations
                    if  ~isempty(rowTimesTZ)
                        if obj.first_matchTimeZone, obj.first.TimeZone = rowTimesTZ; end
                        if obj.last_matchTimeZone, obj.last.TimeZone = rowTimesTZ; end
                    end
                end
                
                % Let the dimension object decide what's in the specified range,
                % without expanding an implicit row times vector
                subs = dimObj.timerange2subs(obj.first,obj.last,obj.type);
            catch ME
                rowTimes = dimObj.rowTimesTemplate(); % may not have gotten it above, make sure
                if ~isequal(class(rowTimes),class(obj.first))
                    % The timetable's row times and the timerange's first/last
                    % times were a mix of datetime and duration.
                    throwAsCaller(MException(message('MATLAB:timerange:MismatchRowTimesType',class(rowTimes),class(obj.first))));
                else
                    rethrow(ME);
                end
            end
        end
    end
    methods(Hidden)        
        function disp(obj)
            % Take care of formatSpacing
            import matlab.internal.display.lineSpacingCharacter
            tab = sprintf('\t');
            
            % Determine what string to display depending on the interval type
            is_NaN_NaT_endpoints = (~isfinite(obj.first) && ~isinf(obj.first)) || (~isfinite(obj.last) && ~isinf(obj.last));
            isNullRange = (obj.first > obj.last) || ((obj.type ~= "closed") && (obj.first == obj.last));
            if is_NaN_NaT_endpoints
                msgid = 'MATLAB:timerange:UIStringDispNaTNaNEndpoint';
            elseif isNullRange
                msgid = 'MATLAB:timerange:UIStringDispNullRange';
            elseif ~isempty(obj.unitOfTime)
                msgid = 'MATLAB:timerange:UIStringDispUnitOfTime';
            elseif matches(char(obj.type),["openright" "closedleft"]) % char is needed around obj.type to support disp of empty timerange
                msgid = 'MATLAB:timerange:UIStringDispRightOpen'; 
            elseif matches(char(obj.type),["openleft" "closedright"])
                msgid = 'MATLAB:timerange:UIStringDispLeftOpen';
            elseif char(obj.type) == "open"
                msgid = 'MATLAB:timerange:UIStringDispOpen';
            else % closed
                msgid = 'MATLAB:timerange:UIStringDispClosed';
            end
            
            % Plug in the time range end points
            if is_NaN_NaT_endpoints
                dispMsg = getString(message(msgid, 'NaN/NaT'));
            elseif isNullRange
                dispMsg = getString(message(msgid));
            elseif isa(obj.first,'datetime')
                % Use the default date and time format for datetime. The time portion must be included to make the range
                % as explicit as possible.
                s = settings;
                datetimeSettings = s.matlab.datetime;
                datetimeformat = datetimeSettings.DefaultFormat.ActiveValue;
                if isempty(obj.unitOfTime)
                    dispMsg = getString(message(msgid, char(obj.first, datetimeformat), char(obj.last, datetimeformat)));
                else
                    dispMsg = getString(message(msgid, upper(obj.unitOfTime), char(obj.first, datetimeformat), char(obj.last, datetimeformat)));
                end
            else % duration timerange, don't need a format specified
                dispMsg = getString(message(msgid, char(obj.first), char(obj.last)));
            end
            
            disp([tab getString(message('MATLAB:timerange:UIStringDispHeader')) lineSpacingCharacter]);
            disp([tab tab dispMsg lineSpacingCharacter]);
            disp([tab getString(message('MATLAB:timerange:UIStringDispFooter')) lineSpacingCharacter]);
        end
    end

    %%%% PERSISTENCE BLOCK ensures correct save/load across releases %%%%%%
    %%%% Properties and methods in this block maintain the exact class %%%%
    %%%% schema required for TIMERANGE to persist through MATLAB releases %
    properties(Constant, Access='protected')
        % current running version. This is used only for managing forward
        % compatibility. Value is not saved when an instance is serialized
        %
        %   1.0 : 16b. first shipping version
        %   1.1 : 18a. added 'first_matchTimeZone' & 'last_matchTimeZone'
        %              properties to support timezone inference on match
        %   1.2 : 18a. added serialized field 'incompatibilityMsg' to support
        %              customizable 'kill-switch' warning message. The field
        %              is only consumed in loadobj() and does not translate
        %              into any table property
        %   1.3 : 18b. added serialized field 'unitOfTime' for different
        %              display of timerange created with UNITOFTIME syntax.
        version = 1.3;
    end
    
    methods(Hidden)
        function s = saveobj(obj)
            s = struct;
            s = obj.setCompatibleVersionLimit(s, 1.0); % limit minimum version compatible with a serialized instance
            
            s.first                = obj.first; % scalar datetime or duration. left limit of the range.
            s.first_matchTimeZone  = obj.first_matchTimeZone;
            s.last                 = obj.last;  % scalar datetime or duration. right limit of the range.
            s.last_matchTimeZone   = obj.last_matchTimeZone;            
            s.type                 = obj.type;  % row character vector. One of {'openright' 'closedleft' 'openleft' 'closedright' 'open' 'closed'}
            s.unitOfTime           = obj.unitOfTime; % row character vector. '' or one of {'years' 'quarters' 'months' 'weeks' 'days' 'hours' 'minutes' 'seconds'}
        end
    end
    
    methods(Hidden, Static)
        function obj = loadobj(s)
            % Always default construct an empty instance, and recreate a
            % proper timerange in the current schema using attributes
            % loaded from the serialized struct
            obj = timerange();
            
            % Pre-18a (i.e. v1.0) saveobj did not save the versionSavedFrom
            % field. A missing field would indicate it is serialized in
            % version 1.0 format. Append the field if it is not present.
            if ~isfield(s,'versionSavedFrom')
                s.versionSavedFrom = 1.0;
            end            
            
            % Return the empty instance if current version is below the
            % minimum compatible version of the serialized object
            if obj.isIncompatible(s, 'MATLAB:timerange:IncompatibleLoad')
                return;
            end
            
            % Restore serialized data
            % ASSUMPTION: 1. type and semantics of the serialized struct
            %                fields are consistent as stated in saveobj above.
            %             2. as a result of #1, the values stored in the
            %                serialized struct fields are valid in this
            %                version of timerange, and can be assigned into
            %                the reconstructed object without any check
            obj.first               = s.first;
            obj.last                = s.last;
            obj.type                = s.type;            
            obj.first_matchTimeZone = (s.versionSavedFrom >= 1.1) && s.first_matchTimeZone;
            obj.last_matchTimeZone  = (s.versionSavedFrom >= 1.1) && s.last_matchTimeZone;                        
            if s.versionSavedFrom >= 1.3
                obj.unitOfTime = s.unitOfTime;
            end
        end
        
        function name = matlabCodegenRedirect(~)
            % Use the implementation in the class below when generating
            % code.
            name = 'matlab.internal.coder.timerange';
        end
    end
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function val = text2Timetype_helper(val, template, unitOfTime)
% TEXT2TIMETYPE_HELPER parse the text in VAL to a timetype
% (datetime/duration) by using TEMPLATE as the reference type 
import matlab.internal.datetime.text2timetype
import matlab.internal.datatypes.isScalarText

try
    if isa(template,'datetime') || isa(template,'duration')
        % Parse VAL into a timetype using TEMPLATE as reference type
        val = text2timetype(val,'MATLAB:datetime:InvalidTextInput',template);
    else
        timerFmtPattern = '^\d+:\d+:\d+:\d+$|^\d+:\d+:\d+$';
        if isScalarText(val) && ((strlength(val) == 0) || isscalar(regexp(val,timerFmtPattern)))
            if isempty(unitOfTime)
                val = duration(val);
            else % Unit-of-Time semantic: parse text to datetime
                val = datetime(val);
            end
        elseif isScalarText(template) && ((strlength(template) == 0) || isscalar(regexp(template,timerFmtPattern)))
            if isempty(unitOfTime)
                val = text2timetype(val,'MATLAB:datetime:InvalidTextInput',duration);
            else % Unit-of-Time semantic: parse text to datetime
                val = datetime(val);
            end
        else
            val = text2timetype(val,'MATLAB:datetime:InvalidTextInput');
        end
    end
catch ME
    if isempty(unitOfTime) % Not a construction with the UNITOFTIME syntax
        rethrow(ME);  % preserve stack
    else % Unit-of-Time semantic: try additional format
        timeUnitFmts.years    = "uuuu"; % Year
        timeUnitFmts.quarters = ["QQQ-uuuu","uuuu-QQQ","QQQ/uuuu","uuuu/QQQ","QQQ.uuuu","uuuu.QQQ","uuuuQQQ"];
        timeUnitFmts.months   = ["MMM-uuuu","uuuu-MMM","MMM/uuuu","uuuu/MMM","MMM.uuuu","uuuu.MMM"];
        timeUnitFmts.weeks    = []; % No anchored format to try
        timeUnitFmts.days     = ["MMM-dd-uuuu","uuuu-dd-MMM","uuuu-MMM-dd","dd/MMM/uuuu","uuuu/dd/MMM","uuuu/MMM/dd","dd.MMM.uuuu","MMM.dd.uuuu","uuuu.dd.MMM","uuuu.MMM.dd"];
        timeUnitFmts.hours    = timeUnitFmts.days(:) + " " + ["HH:mm" "hh:mm aa"];
        timeUnitFmts.minutes  = timeUnitFmts.hours;
        timeUnitFmts.seconds  = timeUnitFmts.days(:) + " " + ["HH:mm:ss" "hh:mm:ss aa"];
        
        % UNITOFTIME was passed in from the object (a non-empty value _must_ be one of the valid values)
        % Get the list of formats to try, for this particular UNITOFTIME, on parsing the text into datetime.
        % Note there is no need to try parsing the text into duration -- the UNITOFTIME syntax only support 
        % datetime/datetime-text endpoints.
        fmts = timeUnitFmts.(unitOfTime);
        numFmts = numel(fmts);
        trialCount = 0;
        while ~isa(val,'datetime')
            trialCount = trialCount + 1;
            try
                val = datetime(val,'InputFormat',fmts(trialCount));
            catch
                if (trialCount < numFmts)
                    continue; % try the next format
                else % no more to try
                    rethrow(ME);
                end
            end
        end
    end
end
end

function [this, matchTimeZoneOnSubscript] = handleTimeZone(this, other, otherTraits)
% If the other edge is a datetime, match its TimeZone;
% If both are text, match TimeZone of the subscripting context
matchTimeZoneOnSubscript = false;
if isa(this,'datetime')
    if otherTraits.isDatetime
        this.TimeZone = other.TimeZone;
    elseif otherTraits.isText
        matchTimeZoneOnSubscript = true;
    end
end
end

function val = convertNonFinite(val,template)
if isa(template,'datetime')
    val = datetime(val,0,0);
elseif isa(template,'duration')
    val = duration(val,0,0);
end
end

function traits = endpointTraits(in)
    traits.isText     = matlab.internal.datatypes.isScalarText(in);
    traits.isScalar   = traits.isText || isscalar(in);
    traits.isDatetime = ~traits.isText && isa(in,'datetime'); % isText<=>not-a-datetime: avoid isa() - it's slow to return FALSE.
    traits.isDuration = ~traits.isText && ~traits.isDatetime && isa(in,'duration'); % similarly, avoid isa() if established FALSE otherwise.
    traits.isNonFiniteNum = (traits.isScalar && isnumeric(in) && ~isfinite(in))...
        || traits.isText && matches(in,["inf","+inf","-inf"],"IgnoreCase",true); % +/- Inf or NaN
    traits.isInvalid = ~(traits.isText || traits.isDatetime || traits.isDuration || traits.isNonFiniteNum);
end

function [tf, firsts, lasts] = isequalUtil(instances)
numInstances = numel(instances);
firstMatchTZs = false(1, numInstances);
lastMatchTZs  = false(1, numInstances);
firsts = cell(1, numInstances);
lasts  = cell(1, numInstances);
types  = cell(1, numInstances);
for i = 1:numInstances
    instance = instances{i};
    firstMatchTZs(i) = instance.first_matchTimeZone;
    lastMatchTZs(i)  = instance.last_matchTimeZone;
    firsts{i}        = instance.first;
    lasts{i}         = instance.last;
    types{i}         = instance.type;
end

tf = (all(firstMatchTZs) || all(~firstMatchTZs)) && (all(lastMatchTZs) || all(~lastMatchTZs)) && ...
      isequal(types{:});
  
if tf
    nonFiniteFirst = false(1, numInstances);
    nonFiniteLast = false(1, numInstances);
    for i=1:numInstances
        nonFiniteFirst(i) = ~isfinite(firsts{i});
        nonFiniteLast(i) = ~isfinite(lasts{i});
    end
    
    tf = all(nonFiniteFirst | nonFiniteLast) || ( isequaln(firsts{:}) && isequaln(lasts{:}) );
end
end
