function [G,GR,GC] = groupsummary(T,groupVars,varargin)
%GROUPSUMMARY Summary computations by group.
%   G = GROUPSUMMARY(T,GROUPVARS,METHOD) or
%   G = GROUPSUMMARY(X,GROUPVARS,METHOD) groups all the variables in table
%   or timetable T, or all the columns in a matrix or cell of matrices X
%   according to the unique combinations of grouping variables in GROUPVARS
%   and applies the METHOD to each group.
%
%   For table T, G is a table containing the unique groups, group counts
%   and the results of applying the methods to the groups. GROUPVARS must 
%   be a table variable name, a cell array of table variable names, a 
%   vector of table variable indices, a logical vector, a function handle 
%   that returns a logical scalar (such as @isnumeric), or a table vartype 
%   subscript.
%
%   For matrix or cell X, G is a matrix containing the concatenated results
%   of applying the methods to the groups. GROUPVARS must have the same
%   number of rows as X or as cell elements of X and must be a column
%   vector, a group of column vectors stored as a matrix or a group of
%   column vectors stored in a cell array. GROUPVARS can also be [] to
%   indicate no grouping.
%
%   METHOD can be a function handle or name, or a cell array containing 
%   multiple function handles or names. Names can be any of the following:
%
%      "mean"          - mean
%      "sum"           - sum
%      "min"           - minimum
%      "max"           - maximum
%      "range"         - maximum - minimum
%      "median"        - median
%      "mode"          - mode
%      "var"           - variance
%      "std"           - standard deviation
%      "nummissing"    - number of missing elements 
%      "nnz"           - number of non-zero and non-NaN elements
%      "all"           - all methods above
%
%   When METHOD is a function handle with more than one input, use syntax
%   GROUPSUMMARY(T,___,METHOD,DATAVARS) below for table or timetable T.
%
%   When METHOD is a function handle with more than one input, X must be a
%   cell array of matrices, where each matrix contains the data for one of
%   the inputs to the METHOD. In each METHOD call, the input arguments are
%   corresponding columns of the matrices. For example, if X is a cell with
%   two matrices each having two columns X = {[a1,a2], [b1,b2]} and METHOD
%   is @(x,y)myfun, then the data passed to myfun is (a1,b1) and (a2,b2).
%
%   G = GROUPSUMMARY(T,GROUPVARS,GROUPBINS,METHOD) or
%   G = GROUPSUMMARY(X,GROUPVARS,GROUPBINS,METHOD) additionally specifies
%   the GROUPBINS to be applied to GROUPVARS prior to grouping. If 
%   GROUPBINS is one of the options from the list below, then that 
%   discretization is applied to every grouping variable. 
%   Otherwise, GROUPBINS must be a cell array with one element for each 
%   grouping variable. Elements of GROUPBINS can be one of the following:
% 
%      - (default) "none" indicating no discretization
%      - a list of bin edges specified as a numeric vector
%      - number of bins specified as an integer scalar
%      - time duration specified as a scalar of type duration or 
%        calendarDuration indicating bin widths for datetime or duration 
%        grouping variables
%      - time bins for datetime grouping variables specified as one of the 
%        following: "second", "minute", "hour", "day", "week", "month", 
%        "quarter", "year", "decade", "century", "secondofminute", 
%        "minuteofhour", "hourofday", "dayname", "dayofweek", "dayofmonth", 
%        "dayofyear", "weekofmonth", "weekofyear", "monthofyear", 
%        "monthname", or "quarterofyear"
%      - time bins for duration grouping variables specified as one of the 
%        following: "second", "minute", "hour", "day", or "year"
%
%   G = GROUPSUMMARY(T,GROUPVARS) for a table or timetable T, returns a 
%   table containing the number of elements in each group created by the 
%   unique combinations of grouping variables in GROUPVARS. 
%
%   G = GROUPSUMMARY(T,GROUPVARS,GROUPBINS) for a table or timetable T, 
%   returns a table containing the number of elements in each group created 
%   by the unique combinations of grouping variables in GROUPVARS binned 
%   according to GROUPBINS. 
% 
%   G = GROUPSUMMARY(T,___,METHOD,DATAVARS) applies the methods only to the 
%   data in the table variables specified by DATAVARS. The default is all 
%   non-grouping variables in T. DATAVARS must be a table variable name, a
%   cell array of table variable names, a vector of table variable indices,
%   a logical vector, a function handle that returns a logical scalar (such
%   as @isnumeric), or a table vartype subscript.
%
%   When METHOD is a function handle with more than one input, DATAVARS
%   must be a cell array where each element is any of the previous options
%   and specifies the table variables for each input to the METHOD. In each
%   METHOD call, the input arguments are corresponding table variables of
%   the cell elements. For example, if DATAVARS is a cell of table
%   variable names {["Var1","Var2"],["Var3","Var4"]} and METHOD is
%   @(x,y)myfun, then the data passed to myfun is (T.Var1,T.Var3) and
%   (T.Var2,T.Var4).
%
%   G = GROUPSUMMARY(___,"IncludeMissingGroups",TF) specifies whether 
%   groups of missing data in the grouping variables are included and given  
%   their own category. TF must be one of the following:
%      true     - (default) include missing groups 
%      false    - exclude missing groups
%
%   G = GROUPSUMMARY(___,"IncludeEmptyGroups",TF) specifies whether groups  
%   with 0 elements are included in the output. TF must be one of the 
%   following:
%      true     - include empty groups 
%      false    - (default) exclude empty groups
%
%   G = GROUPSUMMARY(___,"IncludedEdge",LR) specifies which edge is  
%   included for each bin in the discretization. This N-V pair can only be  
%   used when GROUPBINS is specified. LR must be one of the following:
%      "left"     - (default) all bins include the left bin edge, except  
%                 for the last bin which includes both edges.
%      "right"    - all bins include the right bin edge, except for the  
%                 first bin which includes both edges.
%
%   [G,GR] = GROUPSUMMARY(X,___) also returns the groups GR for any of the
%   previous matrix or cell syntaxes.
%
%   [G,GR,GC] = GROUPSUMMARY(X,___) also returns the group counts GC for
%   any of the previous matrix or cell syntaxes.
%
%   Examples:
%
%      % Load data and create table
%      load patients;
%      T = table(Age,Diastolic,Gender,Height,Smoker,Systolic,Weight);
%
%      % Compute the mean height by gender
%      G = groupsummary(T,"Gender","mean","Height")
%
%      % Compute the range for height and weight grouped by gender and age,
%      % and discretize age into 10 bins
%      G = groupsummary(T,["Gender","Age"],{"none",10},"range")
%
%      % Compute the variances of the differences between systolic and 
%      % diastolic blood-pressure readings for smokers and nonsmokers
%      G = groupsummary(T,"Smoker",@(x,y)var(x-y),{"Systolic","Diastolic"})
%
%      % Compute correlation between height and weight and systolic and 
%      % diastolic by gender
%      G = groupsummary(T,"Gender",@(x,y)xcov(x,y,0,'coeff'),...
%           {["Height","Systolic"],["Weight","Diastolic"]})
%
%      % Compute average weight by age groups
%      [G, GR, GC] = groupsummary(Weight,Age,[20 30 40 50 Inf],"mean")
%
%      % Compute average BMI by gender
%      [G, GR, GC] = groupsummary({Height,Weight},Gender,...
%           @(h,w)mean((w ./ (h.^2)) * 703))
%
%      % Compute correlation between age and weight and systolic and 
%      % height by gender
%      [G, GR, GC] = groupsummary({[Age,Systolic],[Weight,Height]},...
%           Gender,@(x,y)xcov(x,y,0,'coeff'))
%
%   See also GROUPTRANSFORM, GROUPFILTER, GROUPCOUNTS, FINDGROUPS, SPLITAPPLY, DISCRETIZE.

%   Copyright 2017-2020 The MathWorks, Inc.

narginchk(2,Inf);

% Compute matrix/table switch flag
tableFlag = isa(T,'tabular');

if tableFlag
    % Error if asking for more than 1 output for table
    nargoutchk(0,1);
end

% Parse grouping variables
[groupingData,groupVars] = matlab.internal.math.parseGroupVars(groupVars,tableFlag,'groupsummary',T);

if tableFlag
    % Create labels for groupVars in output table
    gvLabels = groupVars;

    % Create label for the group count in output table
    gcLabel = {'GroupCount'};
    
    % Set default values for data variables
    varNamesT = T.Properties.VariableNames;
    dataVars = varNamesT;
else
    % To avoid errors create numbered labels
    tmp = num2cell(1:numel(groupingData));
    gvLabels = cellfun(@num2str,tmp,'UniformOutput',false);
    dataVars = T;
end

% Set rest of default values
inclEmpty = false;
inclNan = true;
inclEdge = 'left';
numMethods = 0;
numMethodInput = 1;
if iscell(T) && ~iscellstr(T) %#ok<ISCLSTR>
    numMethodInput = numel(T);
end

% Parse remaining inputs
gbProvided = false;
if nargin > 2
    indStart = 1;
    % Parse group bins
    dvNotProvided = true;
    if matlab.internal.math.isgroupbins(varargin{indStart},'groupsummary')
        [groupBins,scalarExpandBins,flag] = matlab.internal.math.parsegroupbins(varargin{indStart},numel(gvLabels),'groupsummary');
        if flag
            indStart = indStart + 1;
            gbProvided = true;
        end
    end
    if indStart < nargin-1
        % Parse method
        if matlab.internal.math.isgroupmethod(varargin{indStart})
            [methods,methodPrefix,numMethods] = parseMethods(varargin{indStart});
            indStart = indStart + 1;
            if indStart < nargin-1
                if tableFlag
                    % Parse data variables
                    if iscell(varargin{indStart}) && ~iscellstr(varargin{indStart})
                        % This is the case where we have a cell where each
                        % input tells you the values for one input to the
                        % function handle method
                        dataVars = varargin{indStart};
                        numMethodInput = numel(dataVars);
                        % dataVars will be concatenated names of input sets
                        % to be passed to function handle
                        % dvSets is a numeric matrix where the columns
                        % represent the indices of input data sets
                        [dataVars,dvSets] = matlab.internal.math.checkMultDataVariables(T,dataVars,varNamesT,numMethodInput);
                        dvNotProvided = false;
                        indStart = indStart + 1;
                    elseif (isnumeric(varargin{indStart}) || islogical(varargin{indStart}) || ...
                            ((ischar(varargin{indStart}) || isstring(varargin{indStart})) && ...
                            ~any(matlab.internal.math.checkInputName(varargin{indStart},{'IncludeEmptyGroups','IncludedEdge','IncludeMissingGroups'},8))) || ...
                            isa(varargin{indStart},'function_handle') || iscellstr(varargin{indStart}) || ...
                            rem(nargin-(indStart),2) == 0)
                        % This is the case where we have 1 input to the method
                        dataVars = matlab.internal.math.checkDataVariables(T, varargin{indStart}, 'groupsummary', 'Data');
                        if ~iscell(dataVars)
                            dataVars = varNamesT(dataVars);
                        end
                        dataVars = unique(dataVars,'stable');
                        dvNotProvided = false;
                        indStart = indStart + 1;
                    end
                end
            end
        end
    end
    
    % Parse name-value pairs
    if rem(nargin-(1+indStart),2) == 0
        for j = indStart:2:length(varargin)
            name = varargin{j};
            if (~(ischar(name) && isrow(name)) && ~(isstring(name) && isscalar(name))) ...
                || (isstring(name) && strlength(name) == 0)
                error(message('MATLAB:groupsummary:ParseFlags'));
            elseif matlab.internal.math.checkInputName(name,{'IncludeEmptyGroups'},8)
                inclEmpty = varargin{j+1};
                matlab.internal.datatypes.validateLogical(inclEmpty,'IncludeEmptyGroups');
            elseif matlab.internal.math.checkInputName(name,{'IncludeMissingGroups'},8)
                inclNan = varargin{j+1};
                matlab.internal.datatypes.validateLogical(inclNan,'IncludeMissingGroups');
            elseif matlab.internal.math.checkInputName(name,{'IncludedEdge'},8)
                if ~gbProvided
                    error(message('MATLAB:groupsummary:IncludedEdgeNoGroupBins'));
                end
                inclEdge = varargin{j+1};
            else
                error(message('MATLAB:groupsummary:ParseFlags'));
            end
        end
    elseif (nargin < 4) || (gbProvided && nargin < 5)
        error(message('MATLAB:groupsummary:InvalidMethodOption'));
    else
        error(message('MATLAB:groupsummary:KeyWithoutValue'));
    end
end

if gbProvided
    % Discretize grouping variables and remove repeated pairs of grouping
    % variables and group bins
    [groupingData,groupVars,gvLabels] = matlab.internal.math.discgroupvar(groupingData,...
        groupVars,gvLabels,groupBins,inclEdge,scalarExpandBins,'groupsummary',tableFlag);
elseif tableFlag
    % Remove repeated grouping variables
    [groupVars,ridx] = unique(groupVars,'stable');
    groupingData = groupingData(ridx);
    gvLabels = gvLabels(ridx);
end

% Compute final number of grouping variables from labels
numGroupVars = numel(gvLabels);

% Compute grouping index and data
numrows = size(T,1);
if iscell(T) && ~iscellstr(T) && ~isempty(T) %#ok<ISCLSTR>
    numrows = size(T{1},1);
end
if tableFlag || (nargout == 3)
    [gvIdx,numGroups,gData,gCount] = matlab.internal.math.mgrp2idx(groupingData,numrows,inclNan,inclEmpty);
elseif nargout == 2
    [gvIdx,numGroups,gData] = matlab.internal.math.mgrp2idx(groupingData,numrows,inclNan,inclEmpty);
else
    [gvIdx,numGroups] = matlab.internal.math.mgrp2idx(groupingData,numrows,inclNan,inclEmpty);
end

% Set group count correctly in case of 0xN input
if ~tableFlag && numrows == 0
    numGroups = 0;
    if (nargout == 3)
        gCount = zeros(0,1);
    end
end

% Extract data variables
if numMethods == 0
    if tableFlag
        % Create output table
        if isempty(groupVars)
            G = table.init({gCount},numel(gCount),{},1,gcLabel);
        else
            % Make sure labels are unique
            uniqueLabels = matlab.lang.makeUniqueStrings([gvLabels,gcLabel],["Row", "Variables"],namelengthmax);
            vars = [gData,{gCount}];
            G = table.init(vars,numel(gCount),{},numel(vars),uniqueLabels);
        end
        return;
    else
        % Error if no methods are specified in matrix case
        error(message('MATLAB:groupsummary:MustSpecifyMethod'));
    end
else
    if numMethodInput > 1
        % Each column of dvData will have the data input sets
        if tableFlag
            numDataVars = numel(dataVars);
            dataLabels = dataVars;
            dvData = cell(size(dvSets));
            for k=1:numel(dvSets)
                dvData{k} = T{:,dvSets(k)};
            end
        else
            numDVSets = unique(cellfun(@(x)size(x,2),T));
            if numel(numDVSets) > 2 || (numel(numDVSets) == 2 && numDVSets(1) ~= 1)
                error(message('MATLAB:groupsummary:FirstInputSizeCell'));
            end
            numDataVars = numDVSets(end);
            
            % To avoid errors create numbered labels
            dtmp = num2cell(1:numDataVars);
            dataLabels = cellfun(@num2str,dtmp,'UniformOutput',false);
            
            % Copy columns over to cell array
            if numDataVars ~= 0
                dvData = cell(numMethodInput,numDataVars);
                for k=1:numel(T)
                    if size(T{k},2) == 1
                        for jj = 1:numDataVars
                            dvData{k,jj} = T{k}(:,1);
                        end
                    else
                        for jj = 1:size(T{k},2)
                            dvData{k,jj} = T{k}(:,jj);
                        end
                    end
                end
            else
                % Special case where cell contains nx0 matrices
                dvData = T(:);
                numDataVars = 1;
                dataLabels={'1'};
            end
        end
    else
        [dvData,dataLabels,numDataVars] = matlab.internal.math.extractDataVars(T,groupVars,dataVars,tableFlag,dvNotProvided,true);
    end
end

% Compute group summary computations
gStats = cell(1,numMethods*numDataVars);
gStatsLabel = cell(1,numMethods*numDataVars);
% Remove NaNs from gvIdx, which represent missing groups when we don't include them
idx = ~isnan(gvIdx);
gvIdx = gvIdx(idx);
for ii = 1:numDataVars
    for jj = 1:numMethods
        % Error for median of char
        if strcmp(methodPrefix{jj},'median') && ischar(dvData{ii})
            if tableFlag
                error(message('MATLAB:groupsummary:ApplyDataVarsError',methodPrefix{jj},dataLabels{ii}));
            else
                error(message('MATLAB:groupsummary:ApplyDataVecsError',methodPrefix{jj},dataLabels{ii}));
            end
        end
        
        try
            % Extract set of data to be passed to method. x will be a cell,
            % where each cell is the data for each input to the method
            x = dvData(:,ii);
           
            f = methods{jj};

            if numMethodInput == 1
                % Special case one input for performance
                x = x{1};
                ind = repmat({':'},ndims(x)-1,1); % Need this because table variables can be ND
                if ~isempty(x)
                    x = x(idx,ind{:});
                end
                g = @(y) {f(x(y,ind{:}))};
                x = {x};
            else
                % When not including missing groups, need to remove data associated with those groups
                ndimsData = zeros(numMethodInput,1);
                for k = 1:numMethodInput
                    ndimsData(k) = ndims(x{k});
                    ind = repmat({':'},ndimsData(k)-1,1); % Need this because table variables can be ND
                    if ~isempty(x{k})
                        x{k} = x{k}(idx,ind{:});
                    end
                end
                % Build function handle to pass to accumarray, the values
                % passed to accumarray will index into the data
                ind = repmat({':'},max(ndimsData)-1,1);
                f2 = @(y) cellfun(@(z) z(y,ind{:}),x,'Uniform',0); % extract data for the group from each cell, once again data could be ND
                f3 = @(w)f(w{:}); % pass data to method by extracting it from cell array
                g = @(y) {f3(f2(y))};
            end
            
            % If not grouping by anything then apply method directly
            if isempty(gvIdx)
                d = {methods{jj}(x{:})};
                % If we need to build empty for x, build it out of the class of d
                if any(cellfun(@isempty,x))
                    c = str2func([class(d{1}) '.empty']);
                    if (numGroups == 0)
                        if issparse(d{1})
                            d{1} = sparse(c(size(x{1},1),size(x{1},2)));
                        else
                            d{1} = c(size(x{1},1),size(x{1},2));
                        end
                    elseif inclEmpty && isempty(vertcat(d{:,1}))
                        if issparse(d{1})
                            d{1} = sparse(c(1,0));
                        else
                            d{1} = c(1,0);
                        end
                    end
                end
            else
                % Otherwise group with accumarray
                if inclEmpty
                    fillval = matlab.internal.math.groupGetFillVal(x,methods{jj},methodPrefix{jj});
                    d = accumarray(gvIdx,(1:numel(gvIdx)).',[numGroups 1],g,{fillval});
                else
                    d = accumarray(gvIdx,(1:numel(gvIdx)).',[numGroups 1],g);
                end
            end
            
            gStats{(ii-1)*numMethods + jj} = vertcat(d{:,1});
            gStatsLabel{(ii-1)*numMethods + jj} = [methodPrefix{jj} '_' dataLabels{ii}];
        catch ME
            % Return error message with added information
            if tableFlag 
                mid = 'MATLAB:groupsummary:ApplyDataVarsError';
            else
                mid = 'MATLAB:groupsummary:ApplyDataVecsError';
            end
            m = message(mid,methodPrefix{jj},dataLabels{ii});
            throw(addCause(MException(m.Identifier,'%s',getString(m)),ME));
        end
        
        % Check to make sure method returned one element per group
        if tableFlag
            if any(cellfun(@(x)size(x,1),d) > 1)
                error(message('MATLAB:groupsummary:ApplyDataVarsError',methodPrefix{jj},dataLabels{ii}));
            end
        else
            if any(cellfun(@numel,d) > 1)
                error(message('MATLAB:groupsummary:ApplyDataVecsError',methodPrefix{jj},dataLabels{ii}));
            end
        end
        
        % Check to make sure vertcat output the correct number of elements
        if ~isequal(size(gStats{(ii-1)*numMethods + jj},1),numGroups)
            if tableFlag
                error(message('MATLAB:groupsummary:ApplyDataVarsError',methodPrefix{jj},dataLabels{ii}));
            else
                error(message('MATLAB:groupsummary:ApplyDataVecsError',methodPrefix{jj},dataLabels{ii}));
            end
        end
    end
end


if tableFlag
    % Make sure all labels are unique
    uniqueLabels = matlab.lang.makeUniqueStrings([gvLabels,gcLabel,gStatsLabel],["Row", "Variables"],namelengthmax);
    uniqueLabels = string(uniqueLabels);
    gvLabels = {uniqueLabels{1:numGroupVars}};
    gcLabel = {uniqueLabels{numGroupVars+1}};
    gStatsLabel = {uniqueLabels{numGroupVars+2:end}};
    
    % Assemble results
    if numGroupVars == 0
        vars = [{gCount},gStats];
        G = table.init(vars,numel(gCount),{},numel(vars),[gcLabel; gStatsLabel(:)]);
    else
        vars = [gData,{gCount},gStats];
        G = table.init(vars,numel(gCount),{},numel(vars),[gvLabels(:); gcLabel; gStatsLabel(:)]);
    end

else       
    % Try to extract G from data
    try
        G = [gStats{:}];
    catch ME
        % Error if data cannot be concatenated
        error(message('MATLAB:groupsummary:StatsCattable'));
    end
    
    % If requested also return groups
    if nargout > 1
        % Set groups
        if numGroupVars == 1
            GR = gData{1};
        elseif numGroupVars ==0
            emptyfun = str2func([class(groupVars) '.empty']);
            if size(T,1) == 0 || (iscell(T) && ~isempty(T) && size(T{1},1) == 0)
                % groupVars must be 0x0 here because for 0xn numGroupVars is n
                GR = emptyfun(0,0);
            else
                if size(groupVars,1) == 0
                    GR = emptyfun(1,0);
                else
                    GR = groupVars(1,:);
                end
            end
            if issparse(groupVars)
                GR = sparse(GR);
            end
        else
            GR = gData;
        end
    end
    
    % If requested return group counts also
    if nargout > 2
        % Set group counts
        GC = gCount;
    end
end

end

%--------------------------------------------------------------------------
function [methods,methodPrefix,numMethods] = parseMethods(methods)
%PARSEMETHODS Assembles methods into a cell array of function handles
%   This function checks and replaces all with list of methods then changes
%   values into function handles and computes correct prefixes for the
%   methods given

if isstring(methods)
    methods = cellstr(methods);
elseif ~iscell(methods)
    methods = {methods};
end
numMethods = numel(methods);

% Check for all option
isall = false(1,numMethods);
for jj = numMethods:-1:1
    if isstring(methods{jj})
        methods{jj} = char(methods{jj});
    end
    if ischar(methods{jj}) && strncmpi(methods{jj},'all',1)
        isall(jj) = true;
        firstall = jj;
    end
end
if any(isall)
    methods(isall) = [];
    allmethods = {'mean', 'sum', 'min', 'max', 'range', 'median', 'mode', 'var', 'std', 'nummissing', 'nnz'};
    methods = {methods{1:firstall-1} allmethods{:} methods{firstall:end}}; %#ok
    numMethods = numel(methods);
end

% Change each option to a function handle and set names appropriately
methodPrefix = cell(1,numMethods);
numfun = 1;
for jj = 1:numMethods
    if ischar(methods{jj})
        if strncmpi(methods{jj},'nummissing',2)
            methods{jj} = @(x) sum(ismissing(x),1);
            methodPrefix{jj} = 'nummissing';
        elseif strncmpi(methods{jj},'nnz',2)
            methods{jj} = @(x) nnz(x(~ismissing(x)));
            methodPrefix{jj} = 'nnz';
        elseif strncmpi(methods{jj},'mean',3)
            methods{jj} = @(x) mean(x,1,'omitnan');
            methodPrefix{jj} = 'mean';
        elseif strncmpi(methods{jj},'median',3)
            methods{jj} = @(x) median(x,1,'omitnan');
            methodPrefix{jj} = 'median';
        elseif strncmpi(methods{jj},'mode',3)
            methods{jj} = @(x) mode(x,1);
            methodPrefix{jj} = 'mode';
        elseif strncmpi(methods{jj},'var',1)
            methods{jj} = @(x) var(x,0,1,'omitnan');
            methodPrefix{jj} = 'var';
        elseif strncmpi(methods{jj},'std',2)
            methods{jj} = @(x) std(x,0,1,'omitnan');
            methodPrefix{jj} = 'std';
        elseif strncmpi(methods{jj},'min',3)
            methods{jj} = @(x) min(x,[],1,'omitnan');
            methodPrefix{jj} = 'min';
        elseif strncmpi(methods{jj},'max',2)
            methods{jj} = @(x) max(x,[],1,'omitnan');
            methodPrefix{jj} = 'max';
        elseif strncmpi(methods{jj},'range',1)
            methods{jj} = @(x) unsignedRange(x);
            methodPrefix{jj} = 'range';
        elseif strncmpi(methods{jj},'sum',2)
            methods{jj} = @(x) sum(x,1,'omitnan');
            methodPrefix{jj} = 'sum';
        else
            error(message('MATLAB:groupsummary:InvalidMethodOption'));
        end
    else
        if ~isa(methods{jj},'function_handle')
            error(message('MATLAB:groupsummary:InvalidMethodOption'));
        end
        methodPrefix{jj} = ['fun' num2str(numfun)];
        numfun = numfun +1;
    end
end

% Remove repeated method names
[methodPrefix,idx] = unique(methodPrefix,'stable');
methods = methods(idx);
numMethods = numel(methods);

end

%--------------------------------------------------------------------------
function R = unsignedRange(x)
% Compute the range and if the input is a signed integer then return range 
% as an unsigned integer value.
a = max(x,[],1,'omitnan');
b = min(x,[],1,'omitnan');
xClass = class(x);
if isa(x, 'integer') && strncmp('i',xClass,1)
    if sign(a) == sign(b)
        R = cast(a - b, ['u', xClass]);
    else % a is always greater than b, so a > 0, b < 0
        R = cast(a, ['u', xClass]) + cast(-b, ['u', xClass]);
        if b == intmin(xClass)
            R = R + 1;
        end
    end
else
    R = a - b;
end

end
