function TT = convert2semiannual(TT, varargin)
%Aggregate timetable data to semiannual (half year) periodicity
%
% Syntax:
%
%   TT2 = convert2semiannual(TT1)
%   TT2 = convert2semiannual(TT1,name1,value1,name2,value2,...)
%
% Description:
%
%   Aggregate data (e.g., recorded daily or weekly) to semiannual periodicity. 
%
% Input Arguments:
%
%   TT1 - Timetable of data to aggregate to semiannual 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 semiannual period to semiannual 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 semiannual period 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.
%
% Output:
%
%   TT2 - Timetable of semiannual 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 Semiannual aggregation results are reported on the last business day of June
%   and December.
%
% 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 semiannual period in 
%   which the first date in TT1 occurs, provided TT1 has business dates in that 
%   period; otherwise the first date in TT2 is the next end-of-semiannual-period 
%   business date.
%
%   The last date in TT2 is the last business date of the semiannual period in 
%   which the last date in TT1 occurs, provided TT1 has business dates in that 
%   period; otherwise the last date in TT2 is the previous end-of-semiannual-period 
%   business date.
%
% o NaNs are returned for variables in TT2 for semiannual 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 semiannual period or day.
%   - 'mean'       Calculate the mean of the values in each semiannual period or day.
%   - 'prod'       Calculate the product of the values in each semiannual period or day.
%   - 'min'        Calculate the minimum of the values in each semiannual period or day.
%   - 'max'        Calculate the maximum of the values in each semiannual period or day.
%   - 'firstvalue' Use the first value in each semiannual period or day.
%   - 'lastvalue'  Use the last value in each semiannual period 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.02    200.04    300.06     400.08
%     03-Jan-2018     99.96    199.92    299.88     399.84
%         .             .         .         .          .
%         .             .         .         .          .
%         .             .         .         .          .
%     28-Jun-2018     69.63    139.26    208.89     278.52
%     29-Jun-2018     70.15     140.3    210.45     280.60
%     30-Jun-2018     75.77    151.54    227.31     303.08
%     01-Jul-2018     75.68    151.36    227.04     302.72
%     02-Jul-2018     71.34    142.68    214.02     285.36
%     03-Jul-2018     69.25    138.50    207.75     277.00
%         .             .         .         .          .
%         .             .         .         .          .
%         .             .         .         .          .
%     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 semiannual results (the values reported on the last
%   day of each semiannual period in which all days are business days) are:
%
%        Time         AAA       BBB            CCC       
%     ___________    ______    ______    ________________
%     30-Jun-2018     75.77    151.54    227.31    303.08
%     31-Dec-2018    256.75    513.50    770.25   1027.00
%
% See also CONVERT2DAILY, CONVERT2WEEKLY, CONVERT2MONTHLY, CONVERT2QUARTERLY,
% 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'}, '', 'semiannual aggregation function'));
parser.addParameter('Daily'      , ''         , @(x) validateattributes(x, {'function_handle' 'char' 'cell' 'string'} , {'vector'}, '', 'daily aggregation function'));

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

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

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

if ~any(strcmpi('Daily', parser.UsingDefaults))           % Did the user specify a daily aggregation method?
   daily = TimeTablePeriodicityConversionUtilities.validateAggregationMethods(daily, nVariables, "daily");
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 = calmonths(6);                              % Semi-annual 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,'start','year');          % Previous start-of-year date on or before the first 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 semi-annual aggregation interval such that all data within the closed
% interval [D0 D1] are aggregated together. 
%

D0 = (tStart:period:tStop)';               % Initial sequence of start-of-period dates
D1 = D0 + calquarters(2) - caldays(1);     % Initial sequence of end-of-period dates

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