function TT = convert2daily(TT, varargin)
%Aggregate timetable data to daily periodicity
%
% Syntax:
%
%   TT2 = convert2daily(TT1)
%   TT2 = convert2daily(TT1,name1,value1,name2,value2,...)
%
% Description:
%
%   Aggregate data (e.g., high-frequency, intra-day) to daily periodicity. 
%
% Input Arguments:
%
%   TT1 - Timetable of data to aggregate to daily periodicity. NaNs indicate 
%     missing values and date/time stamps must be in ascending or descending order.
%
% Optional Input Parameter Name/Value Pairs: 
%
% 'Aggregation'  Method by which data in TT1 that occurs on the same day is 
%     aggregated to daily periodicity (intra-day aggregation). Aggregation is a 
%     character vector, string scalar, or function handle applied to all time 
%     series in TT1, or a cell vector of character vectors, string vector, or cell 
%     vector of function handles the same length as number of variables in TT1. 
%     The default is 'lastvalue' and returns the most recent data observed on a 
%     given date for all variables in TT1. See notes below for additional details.
%
% Output:
%
%   TT2 - Timetable of daily data. NaNs indicate missing values. If TT1 is in 
%     ascending order, so too is TT2; if TT1 is in descending order, so too is TT2.
%
% Notes:
%
% o To include business-calendar-awareness and account for non-business days 
%   (weekends, holidays, market closures), first call "addBusinessCalendar". For 
%   example, the following command adds business calendar logic to include only
%   NYSE business days:
%
%   TT = addBusinessCalendar(TT); 
%
%   See "addBusinessCalendar" for details. By default, all days are business days.
%
% o The first date in TT2 is the first business date on or after the first date 
%   in TT1. The last date in TT2 is the last business date on or before the last 
%   date in TT1.
%
% o NaNs are returned for variables in TT2 for business days in which no data is 
%   recorded for those variables in TT1.
%
% o Available daily aggregation methods are as follows:
%
%   - 'sum'        Sum the values in each day.
%   - 'mean'       Calculate the mean of the values in each day.
%   - 'prod'       Calculate the product of the values in each day.
%   - 'min'        Calculate the minimum of the values in each day.
%   - 'max'        Calculate the maximum of the values in each day.
%   - 'firstvalue' Use the first value in each day.
%   - 'lastvalue'  Use the last value in each day.
%
%   All methods listed above omit missing data (NaNs) in direct aggregation
%   calculations. However, in situations in which missing values appear in
%   the first row of TT1, it is possible that missing values may also appear
%   in aggregated results TT2.
%
%   Additionally, aggregation methods may be specified as functions (function 
%   handles). To include missing data, specify functions as function handles
%   that include them when aggregating data. Aggregation functions must accept 
%   the underlying data stored in TT1 and return an output that is a scalar or 
%   a row vector, and must accept empty inputs. Each aggregation function is 
%   applied to the corresponding variable and called one at a time. See 
%   "timetable/retime" for details. 
%
% o Each variable must contain either a single numeric vector or numeric matrix.
%   For example, consider a timetable with 3 variables:
%
%             Time             AAA       BBB            CCC       
%     ____________________    ______    ______    ________________
%     01-Jan-2018 09:45:47    100.00    200.00    300.00    400.00
%     01-Jan-2018 12:48:09    100.03    200.06    300.09    400.12
%     02-Jan-2018 10:27:32    100.07    200.14    300.21    400.28
%     02-Jan-2018 12:46:09    100.08    200.16    300.24    400.32
%     02-Jan-2018 14:14:13    100.25    200.50    300.75    401.00
%     02-Jan-2018 15:52:31    100.19    200.38    300.57    400.76
%     03-Jan-2018 09:47:11    100.54    201.08    301.62    402.16
%     03-Jan-2018 11:24:23    100.59    201.18    301.77    402.36
%     03-Jan-2018 14:41:17    101.40    202.80    304.20    405.60
%     03-Jan-2018 16:00:00    101.94    203.88    305.82    407.76
%     04-Jan-2018 09:55:51    102.53    205.06    307.59    410.12
%     04-Jan-2018 10:07:12    103.35    206.70    310.05    413.40
%     04-Jan-2018 14:26:23    103.40    206.80    310.20    413.60
%     05-Jan-2018 13:13:12    103.91    207.82    311.73    415.64
%     05-Jan-2018 14:57:53    103.89    207.78    311.67    415.56
% 
%   The corresponding default daily results (the last values reported on each
%   day in which all days are business days) are:
%
%        Time         AAA       BBB            CCC       
%     ___________    ______    ______    ________________
%     01-Jan-2018    100.03    200.06    300.09    400.12
%     02-Jan-2018    100.19    200.38    300.57    400.76
%     03-Jan-2018    101.94    203.88    305.82    407.76
%     04-Jan-2018    103.40    206.80    310.20    413.60
%     05-Jan-2018    103.89    207.78    311.67    415.56
%
% See also CONVERT2WEEKLY, CONVERT2MONTHLY, CONVERT2QUARTERLY, CONVERT2SEMIANNUAL,
% CONVERT2ANNUAL.

% Copyright 2021 The MathWorks, Inc.

%
% Perform error-checking & input validation.
%

parser = inputParser;
parser.addRequired ('TT1'        ,              @(x) validateattributes(x, {'timetable'}                             , {}        , '', 'TT1'));
parser.addParameter('Aggregation', 'lastvalue', @(x) validateattributes(x, {'function_handle' 'char' 'cell' 'string'}, {'vector'}, '', 'aggregation function'));

try
  parser.parse(TT, varargin{:});
catch exception
  exception.throwAsCaller();
end

TT          = parser.Results.TT1;
aggregation = parser.Results.Aggregation;

nVariables  = size(TT,2);
aggregation = TimeTablePeriodicityConversionUtilities.validateAggregationMethods(aggregation, nVariables, "daily");

%
% Exit if no data exists and TT is empty.
%

if isempty(TT)
   return
end

%
% Ensure TT contains datetimes in ascending or descending order, and sort in ascending
% order for aggregation.
%

[TT,inDescendingOrder] = TimeTablePeriodicityConversionUtilities.sortTimeTableInAscendingOrder(TT);

%
% Get the dimension names so TT referencing is agnostic to user preference.
%

Date      = TT.Properties.DimensionNames{1};  % TT default is 'Time'
Variables = TT.Properties.DimensionNames{2};  % TT default is 'Variables'

%
% Account for business days.
%

if any(strcmp('BusinessCalendar',fieldnames(TT.Properties.CustomProperties)))
   isBusinessDay = TT.Properties.CustomProperties.BusinessCalendar.IsBusinessDay;
else
   isBusinessDay = @(D) true(size(D));   % Ensure it exists
end

%
% Determine if all elements of daily Aggregation are the same such that "timetable/retime" 
% may be called once for all variables rather than separately for each variable in TT, and
% set a performance flag accordingly.
%
% Important Usability Note:
%
%   Daily Aggregation method is a cell vector ONLY if it contains function handles; otherwise, 
%   it is a string vector. This feature is relied upon in the following code segment 
%   (see "TimeTablePeriodicityConversionUtilities.validateAggregationMethods").
%

if iscell(aggregation) || (numel(unique(aggregation)) > 1)
   isAggregationUnique = true;         % All variables have a unique daily aggregation
else
   aggregation         = aggregation(1);
   isAggregationUnique = false;
end

%
% Initialize parameters and compute "whole" dates (i.e., dates referenced at 
% midnight with time stamps such that HH:MM:SS = 00:00:00).
%

tDelta = caldays(1);
period = caldays(1);                                % Daily conversion
tStart = dateshift(TT.(Date)(1)  ,'start','day');   % First date in TT
tStop  = dateshift(TT.(Date)(end),'start','day');   % Last  date in TT
D      = (tStart:period:tStop)';                    % Initial sequence of daily dates

%
% Aggregate data in timetables to daily periodicity. 
%

for t = 1:numel(D)
    if isBusinessDay(D(t))            % Is the current date a business day?
%
%      The current date is a business day, so find all observations (rows) 
%      that occur on this date (e.g., trades that occur during this day) 
%      and perform daily aggregation.
%
       TT1 = TT(timerange(D(t), D(t) + tDelta, 'openright'),:); 

       if isempty(TT1) || all(all(isnan(TT1.(Variables)))) 
          TT(D(t),:) = {NaN};         % No data found for the entire day (see notes above)
       else
%
%         TT contains data recorded on this date, so aggregate each variable.
%
          if isAggregationUnique
             for j = 1:nVariables     % Apply a specific aggregation to each variable with data
%
%                Determine if the j-th variable has at least one non-missing (i.e., 
%                non-NaN) value and aggregate its data. If it has no observed data then
%                it already contains NaNs and no assignment is needed.
%
                 if ~all(isnan(TT1(:,j).(Variables)),'all')
                    TT1(end,j) = retime(TT1(:,j),'Daily',aggregation{j});
                 end
             end
          else                        % Apply the same aggregation to all variables with data
%
%            Determine the variables with at least one non-missing value (i.e., those
%            with some non-NaN data found on the current day) and aggregate only those. 
%            Variables without any observed data are already NaNs and need no assignment.
%
             iNotAllMissing = true(1,nVariables);       % Pre-allocate
             for j = 1:nVariables
                 iNotAllMissing(j) = ~all(isnan(TT1(:,j).(Variables)),'all');
             end
             TT1(end,iNotAllMissing) = retime(TT1(:,iNotAllMissing),'Daily',aggregation);
          end
%          
%         Ensure that the whole date (i.e., with no HH:MM:SS component) exists in
%         TT to ensure successful subset indexing at the very end of this function.
%      
          if ~any(ismember(TT.(Date), D(t)))
              TT.(Date)(ismember(TT.(Date), TT1.(Date)(end))) = D(t);
          end
          TT(D(t),:) = TT1(end,:);    % Copy the aggregated results
       end
    end
end

%
% Retain only whole (i.e., with no HH:MM:SS component) business dates. Since 
% dates (D) are sorted, the following statement will also sort TT. Also, return 
% the output TT in descending order if necessary.
%

TT = TT(D,:);                       % Assign & sort
if isempty(TT)
   return                           % Simply return if no data remains
end
TT = TT(isBusinessDay(TT.Time),:);  % Retain business dates only

if inDescendingOrder
   TT = TT(end:-1:1,:);             % Descending order
end

end