function TT = addBusinessCalendar(TT, varargin)
%ADDBUSINESSCALENDAR Add business calendar awareness to timetables
%
% Syntax:
%
%   TT = addBusinessCalendar(TT)
%   TT = addBusinessCalendar(TT,name1,value1...)
%
% Description:
%
%   Given a timetable TT, business calendar awareness is added to TT by adding 
%   a custom property. 
%
% Input Arguments:
%
%   TT - Input timetable to update with business calendar awareness.
%
% Optional Input Parameter Name/Value Pairs:
%
% 'Holidays'  Vector of datetimes of alternate holidays and market closure 
%     dates. The dates in Holidays must be whole dates, without any HH:MM:SS 
%     components. No business is conducted on the dates in Holidays. The 
%     default is New York Stock Exchange (NYSE) holidays and market closures 
%     (see "holidays" and "isbusday" for details).
%
% 'Weekends'  Vector of alternate weekend days on which no business is 
%     conducted. Weekends is a logical vector with 7 elements in which true
%     (1) indicates a weekend day and false (0) indicates a weekday; elements 
%     of Weekends are encoded as Sunday, Monday, Tuesday, Wednesday, Thursday, 
%     Friday, and Saturday. Weekends may also be specified as a string vector 
%     whose elements explicitly list the weekend days. The default is 
%     [1 0 0 0 0 0 1] which is equivalent to ["Sunday" "Saturday"]. 
%
% Output:
%
%   TT - Updated timetable with business calendar awareness added to TT by 
%     a custom property, "BusinessCalendar". The custom property contains a 
%     data structure which contains a field, "IsBusinessDay", that stores a 
%     callable function (F). The function F accepts a datetime matrix (D) 
%     and returns a logical indicator matrix (I) of the same size: I = F(D). 
%     True elements of I indicate that the corresponding element of D occurs 
%     on a business day; false (0) indicates otherwise. 
%
%     The callable function is accessed as follows:
%
%     F = TT.Properties.CustomProperties.BusinessCalendar.IsBusinessDay;
%
% Notes:
%
% o By default, business calendar information, including holidays, market 
%   closures and weekends, is captured by "isbusday".
%
% o If business is conducted 7 days per week, then specify Weekends as
%   [0 0 0 0 0 0 0].
%
% o Alternate holiday schedules can be generated by the "createholidays"
%   function. To do so, contact FinancialCalendar.com and perform the following
%   steps:
%   - Generate a new "holidays" function by calling "createholidays"
%   - Call the new "holidays" function to get the list of holidays
%   - Pass the alternate holidays to "addBusinessCalendar"
%
% See also HOLIDAYS, ISBUSDAY.

% Copyright 2020 The MathWorks, Inc.

%
% Perform error-checking & input validation.
%

parser = inputParser;
parser.addRequired ('TT'      ,                  @(x) validateattributes(x, {'timetable'}                       , {}        , '', 'TT'));
parser.addParameter('Holidays', holidays       , @(x) validateattributes(x, {'datetime' 'double'}               , {'vector'}, '', 'Holidays'));
parser.addParameter('Weekends', [1 0 0 0 0 0 1], @(x) validateattributes(x, {'cell' 'string' 'logical' 'double'}, {'vector'}, '', 'Weekends'));

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

TT       = parser.Results.TT;
Holidays = parser.Results.Holidays;
W        = parser.Results.Weekends;

%
% Ensure user-specified holidays are whole dates, without any HH:MM:SS components.
%

if isa(Holidays, 'double')
   Holidays = datetime(Holidays, 'ConvertFrom', 'datenum');
end

if ~any(strcmpi('Holidays', parser.UsingDefaults)) && (sum(timeofday(Holidays)) ~= 0)
   error(message('financelib:addBusinessCalendar:HolidaysMustBeWholeDates'))
end

Holidays = sort(Holidays);

%
% Convert string holidays to 7-element numeric vector and validate.
%

if any(strcmpi('Weekends', parser.UsingDefaults))
   Weekends = double(W(:));     % The user did NOT specify their own weekends
else                            % The user specified their own weekends
   if isstring(W) || iscell(W)
      daysOfWeek = ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"];
      Weekends   = zeros(7,1);  % Pre-allocate doubles as expected by ISBUSDAY
      W          = string(W);   % Ensure a string
      for i = 1:numel(W)
          W(i) = validatestring(W(i), daysOfWeek, '', 'weekend days');
          Weekends(strcmp(W(i),daysOfWeek)) = 1;
      end
   else
      validateattributes(W, {'logical' 'double'}, {'integer' 'numel' 7}, '', 'Weekends')
      if any(W < 0) || any(W > 1)
         error(message('financelib:addBusinessCalendar:WeekendsMustBeLogicalOrStringVector'))
      end
      Weekends = double(W(:));
   end
end

%
% Add or update business calendar awareness to TT as a custom property that 
% contains an anonymous function. The following logic allows an existing
% business-calendar-aware TT to be updated (any existing custom 'BusinessCalendar'
% property is over-written).
%

if ~any(strcmp('BusinessCalendar', properties(TT.Properties.CustomProperties)))
   TT = addprop(TT, 'BusinessCalendar', 'table');
end

TT.Properties.CustomProperties.BusinessCalendar.IsBusinessDay = @(D) isbusday(D,Holidays,Weekends);

end

%
% * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
%

function I = isbusday(D,H,weekends)
%ISBUSDAY Determine business days
%
% Syntax:
%   I = ISBUSDAY(Dates, Holidays, Weekends)
%
% Description:
%   Determine if business is conducted on a given date.
%
% Inputs:
%
%   Dates - Datetime vector of dates to test for business activity.
%
%   Holidays - Datetime vector of holidays.
%
%   Weekends - A 7-element integer double vector of 0s and 1s in which 1 indicates 
%     weekend days, or a 7-element logical vector. Element 1 corresponds to Sunday 
%     and 7 to Saturday. For example, to specify Saturday and Sunday as weekend 
%     days set Weekends = [1 0 0 0 0 0 1].
%
% Outputs:
%
%   I - Array of logical indicators the same size as Dates: TRUE = 1 if the 
%     corresponding date in Date is a business day and FALSE = 0 otherwise.
%

DoW      = day(D,'dayofweek');          % Find the day of the week
D        = dateshift(D,'start','day');  % Shift to start of day to remove HH:MM:SS time-of-day components
H        = H(:);                           
weekends = find(weekends(:));

if numel(D) == 1
   I = all(DoW ~= weekends) && all(D ~= H);
else
   I = false(numel(D),1);               % Pre-allocate output matrix
   for i = 1:numel(D)
       I(i) = all(DoW(i) ~= weekends) && all(D(i) ~= H);
   end
   I = reshape(I,size(D));              % Reshape to size consistent with input Dates.
end

end

%
% * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
%

function h = holidays
%
% Load New York Stock Exchange holidays and non-trading days (1885 to 2070).
%

persistent NYSE_Holidays

if isempty(NYSE_Holidays)
   load('Data_NYSE_Closures.mat','NYSE')  % Closures (excludes Sundays)
   NYSE_Holidays = NYSE(day(NYSE.Date,'dayofweek') ~= 7,:);  % Exclude Saturdays
   NYSE_Holidays = NYSE_Holidays.Date;
end

h = NYSE_Holidays;

end