function TT = convert2weekly(TT, varargin)
%Aggregate timetable data to weekly periodicity
%
% Syntax:
%
%   TT2 = convert2weekly(TT1)
%   TT2 = convert2weekly(TT1,name1,value1,name2,value2,...)
%
% Description:
%
%   Aggregate data (e.g., recorded daily) to weekly periodicity. 
%
% Input Arguments:
%
%   TT1 - Timetable of data to aggregate to weekly 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 is aggregated over business days 
%     in a week to weekly periodicity (intra-week or inter-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
%     the number of variables in TT1. The default is 'lastvalue' and returns 
%     the most recent data observed in a given week for all variables in TT1. 
%     See notes below for additional details.
%
% 'Daily'  Method by which data in TT1 that occurs on the same day is aggregated
%     to daily periodicity (intra-day aggregation). Daily 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 the number of variables in TT1. The default 
%     performs no daily aggregation. See notes below for additional details.
%
% 'EndOfWeekDay'  Day of the week that ends business weeks, expressed as a string
%     or character vector ('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
%     'Friday', 'Saturday'), or an integer scalar (1, 2, 3, 4, 5, 6, 7). The 
%     default is 'Friday' = 6 (weeks end on Friday). If a specified end-of-week 
%     day in a given week is a non-business day, then the preceding business day 
%     ends the week. 
%
% Output:
%
%   TT2 - Timetable of weekly 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 last business date of the week in which the first
%   date in TT1 occurs, provided TT1 has business dates in that week; otherwise 
%   the first date in TT2 is the next end-of-week business date.
%
%   The last date in TT2 is the last business date of the week in which the last 
%   date in TT1 occurs, provided TT1 has business dates in that week; otherwise 
%   the last date in TT2 is the previous end-of-week business date.
%
% o NaNs are returned for variables in TT2 for weeks in which no data is 
%   recorded on any business days for those variables in TT1.
%
% o Available aggregation methods are as follows:
%
%   - 'sum'        Sum the values in each week or day.
%   - 'mean'       Calculate the mean of the values in each week or day.
%   - 'prod'       Calculate the product of the values in each week or day.
%   - 'min'        Calculate the minimum of the values in each week or day.
%   - 'max'        Calculate the maximum of the values in each week or day.
%   - 'firstvalue' Use the first value in each week or day.
%   - 'lastvalue'  Use the last value in each week or 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 daily timetable with 3 variables:
%
%        Time         AAA       BBB            CCC       
%     ___________    ______    ______    ________________
%     01-Jan-2018    100.00    200.00    300.00    400.00
%     02-Jan-2018    100.03    200.06    300.09    400.12
%     03-Jan-2018    100.07    200.14    300.21    400.28
%     04-Jan-2018    100.08    200.16    300.24    400.32
%     05-Jan-2018    100.25    200.50    300.75    401.00
%     06-Jan-2018    100.19    200.38    300.57    400.76
%     07-Jan-2018    100.54    201.08    301.62    402.16
%     08-Jan-2018    100.59    201.18    301.77    402.36
%     09-Jan-2018    101.40    202.80    304.20    405.60
%     10-Jan-2018    101.94    203.88    305.82    407.76
%     11-Jan-2018    102.53    205.06    307.59    410.12
%     12-Jan-2018    103.35    206.70    310.05    413.40
%     13-Jan-2018    103.40    206.80    310.20    413.60
%     14-Jan-2018    103.91    207.82    311.73    415.64
%     15-Jan-2018    103.89    207.78    311.67    415.56
%     16-Jan-2018    104.44    208.88    313.32    417.76
%     17-Jan-2018    104.44    208.88    313.32    417.76
%     18-Jan-2018    104.04    208.08    312.12    416.16
%     19-Jan-2018    104.94    209.88    314.82    419.76
%
%   The corresponding default weekly results (the last values reported on 
%   Fridays in which all days are business days) are:
%
%        Time         AAA       BBB            CCC       
%     ___________    ______    ______    ________________
%     05-Jan-2018    100.25    200.50    300.75    401.00
%     12-Jan-2018    103.35    206.70    310.05    413.40
%     19-Jan-2018    104.94    209.88    314.82    419.76
%
% See also CONVERT2DAILY, 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'}, '', 'weekly aggregation function'));
parser.addParameter('Daily'       , ''         , @(x) validateattributes(x, {'function_handle' 'char' 'cell' 'string'} , {'vector'}, '', 'daily aggregation function'));
parser.addParameter('EndOfWeekDay', 6          , @(x) validateattributes(x, {'char' 'string' 'numeric'}                , {}        , '', 'end-of-week day'));

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

TT          = parser.Results.TT1;
EoW         = parser.Results.EndOfWeekDay;
aggregation = parser.Results.Aggregation;
daily       = parser.Results.Daily;

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

if ~any(strcmpi('Daily', parser.UsingDefaults))           % Did the user specify a daily aggregation method?
   daily = TimeTablePeriodicityConversionUtilities.validateAggregationMethods(daily, nVariables, "daily");
end

if ~any(strcmpi('EndOfWeekDay', parser.UsingDefaults))
   if isnumeric(EoW)
      validateattributes(EoW, {'numeric'}, {'integer' '>=' 1 '<=' 7 'scalar'}, '', 'end-of-week day');
      EoW = double(EoW);
   else
      validateattributes(EoW, {'char' 'string'}, {'scalartext'}, '', 'end-of-week day');
      EoW = validatestring(EoW, {'Sunday' 'Monday' 'Tuesday' 'Wednesday' 'Thursday' 'Friday' 'Saturday'}, '', 'end-of-week day');
      EoW = find(strcmp(EoW, {'Sunday' 'Monday' 'Tuesday' 'Wednesday' 'Thursday' 'Friday' 'Saturday'}));
   end
end

%
% 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 date dimension name so TT referencing is agnostic to user preference.
%

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

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

tDelta = caldays(1);
period = calweeks(1);                               % Weekly conversion
tStart = dateshift(TT.(Date)(1)  ,'start','day');   % Initial date in TT
tStop  = dateshift(TT.(Date)(end),'start','day');   % Last    date in TT
tStart = dateshift(tStart,'dayofweek',EoW,'next');  % First end-of-week date in TT
tStop  = dateshift(tStop ,'dayofweek',EoW,'next');  % Next end-of-week date on or after the last date in TT

%
% Compute the sequence of date pairs that represent the first (D0) and last (D1) 
% dates of each weekly aggregation interval such that all data within the closed
% interval [D0 D1] are aggregated together. 
%

D1 = (tStart:period:tStop)';                        % Initial sequence of   end-of-week dates
D0 = D1 - period + tDelta;                          % Initial sequence of start-of-week dates

%   
% If TT has no business dates in the first/last week spanned by the TT, 
% then remove (i.e., trim) those weeks (see notes in help header above).
%

[D0,D1] = TimeTablePeriodicityConversionUtilities.trimStartStopDates(TT,D0,D1);

if isempty(D1)
   TT = TT(D1,:);
   return                  % Exit if there are no business dates left.
end

%
% Aggregate data in timetables to weekly periodicity. 
%

TT = TimeTablePeriodicityConversionUtilities.aggregate(TT,D0,D1,aggregation,daily);

% 
% Return the output TT in descending order if necessary.
%

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

end