function TT = convert2annual(TT, varargin)
%Aggregate timetable data to annual (yearly) periodicity
%
% Syntax:
%
%   TT2 = convert2annual(TT1)
%   TT2 = convert2annual(TT1,name1,value1,name2,value2,...)
%
% Description:
%
%   Aggregate data (e.g., recorded daily or weekly) to annual periodicity. 
%
% Input Arguments:
%
%   TT1 - Timetable of data to aggregate to annual 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 year to annual periodicity (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 year 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.
%
% 'EndOfYearMonth'  The month that ends annual periods (years), expressed as 
%     a string or character vector ('January', 'February', 'March', 'April', 'May', 
%     'June', 'July', 'August', 'September', 'October', 'November', 'December'), 
%     or an integer scalar [1, 2, ..., 12]. The default is 'December' = 12.
%
% Output:
%
%   TT2 - Timetable of annual 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 annual period (year) in 
%   which the first date in TT1 occurs, provided TT1 has business dates in that 
%   annual period (year); otherwise the first date in TT2 is the next end-of-year 
%   period business date.
%
%   The last date in TT2 is the last business date of the annual period (year) in 
%   which the last date in TT1 occurs, provided TT1 has business dates in that 
%   annual period (year); otherwise the last date in TT2 is the previous end-of-year
%   period business date.
%
% o NaNs are returned for variables in TT2 for annual periods 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 year or day.
%   - 'mean'       Calculate the mean of the values in each year or day.
%   - 'prod'       Calculate the product of the values in each year or day.
%   - 'min'        Calculate the minimum of the values in each year or day.
%   - 'max'        Calculate the maximum of the values in each year or day.
%   - 'firstvalue' Use the first value in each year or day.
%   - 'lastvalue'  Use the last value in each year 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
%         .             .         .         .         .
%         .             .         .         .         .
%         .             .         .         .         .
%     29-Dec-2018    249.16    498.32    747.48    996.64
%     30-Dec-2018    250.21    500.42    750.63   1000.84
%     31-Dec-2018    256.75    513.50    770.25   1027.00
%
%   The corresponding default annual results (the values reported on the last
%   day of each year in which all days are business days) are:
%
%        Time         AAA       BBB            CCC       
%     ___________    ______    ______    ________________
%     31-Dec-2018    256.75    513.50    770.25   1027.00
%
% See also CONVERT2DAILY, CONVERT2WEEKLY, CONVERT2MONTHLY, CONVERT2QUARTERLY, 
% CONVERT2SEMIANNUAL.

% 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'}, '', 'annual aggregation function'));
parser.addParameter('Daily'         , ''         , @(x) validateattributes(x, {'function_handle' 'char' 'cell' 'string'} , {'vector'}, '', 'daily aggregation function'));
parser.addParameter('EndOfYearMonth', 12         , @(x) validateattributes(x, {'char' 'string' 'numeric'}                , {}        , '', 'end-of-year month'));

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

TT          = parser.Results.TT1;
EoY         = parser.Results.EndOfYearMonth;
aggregation = parser.Results.Aggregation;
daily       = parser.Results.Daily;

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

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

if ~any(strcmpi('EndOfYearMonth', parser.UsingDefaults))
   if isnumeric(EoY)
      validateattributes(EoY, {'numeric'}, {'integer' '>=' 1 '<=' 12 'scalar'}, '', 'end-of-year month');
      EoY = double(EoY);
   else
      validateattributes(EoY, {'char' 'string'}, {'scalartext'}, '', 'end-of-year month');
      EoY = validatestring(EoY, {'January' 'February' 'March' 'April' 'May' 'June' 'July' 'August' 'September' 'October' 'November' 'December'}, '', 'end-of-year month');
      EoY = find(strcmp(EoY, {'January' 'February' 'March' 'April' 'May' 'June' 'July' 'August' 'September' 'October' 'November' 'December'}));
   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-period & end-of-period dates (i.e.,
% dates referenced at midnight with time stamps such that HH:MM:SS = 00:00:00).
%

period = calyears(1);                               % Yearly 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,'end','year');            % First end-of-year date in TT
tStop  = dateshift(tStop ,'end','year');            % Next  end-of-year 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 annual aggregation interval such that all data within the closed
% interval [D0 D1] are aggregated together. 
%
% By default, years start on 01-Jan-YYYY and end on 31-Dec-YYYY, but what follows
% also adjusts to allow other end-of-year months.
%

D1 = dateshift((tStart:period:tStop)','end','year'); % Initial sequence of default   end-of-year dates
D0 = dateshift(D1,'start','year');                   % Initial sequence of default start-of-year dates
if EoY ~= 12
   D0 = [(D0 - calmonths(12 - EoY)) ; (D0(end) + calmonths(EoY))];
   D1 = [(D1 - calmonths(12 - EoY)) ; (D1(end) + calmonths(EoY))];
end

%
% If TT has no business dates in the first/last calendar year spanned by the TT, 
% then remove (i.e., trim) those years (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 annual (yearly) 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