Skip to content

Commit

Permalink
Merge pull request #1 from bahanonu/bahanonu/fxnUpdates
Browse files Browse the repository at this point in the history
Upload `getOptions` to GitHub.
  • Loading branch information
bahanonu authored May 11, 2020
2 parents 4acef38 + 8778c40 commit a8ddd94
Show file tree
Hide file tree
Showing 4 changed files with 449 additions and 1 deletion.
82 changes: 81 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,82 @@
# getOptions
getOptions: clean handling of variable arguments (varargin) into MATLAB functions.

`getOptions` is a simple, user friendly way to handle variable argument inputs (varargin) into MATLAB functions.

Please see my 2014 write-up __[dealing with variable options (varargin) in matlab](https://bahanonu.com/getOptions)__ ([https://bahanonu.com/getOptions](https://bahanonu.com/getOptions)) for details and reasons behind implementing and using this function. Installation and usage instructions below.

Contact: __Biafra Ahanonu, PhD (bahanonu [at] alum [dot] mit [dot] edu)__.

Made in USA.<br>
<img src="https://user-images.githubusercontent.com/5241605/71493809-322a5400-27ff-11ea-9b2d-52ff20b5f332.png" align="center" title="USA" alt="USA" width="auto" height="50">

## Installation

Download or clone the `getOptions` repository and place the root folder in the MATLAB path. Follow instructions for use below.

Test by running `unitTestGetOptions` unit test. Will print out the modified structures and give out several warnings to show examples of what happens if user inputs Name-Value pairs that are not associated with any options.

## Instructions on use
There are two ways to have parent functions pass Name-Value pairs to child functions that `getOptions` will parse to update default options in child function.

### Method #1
Use the `'options', options` [Name-Value](https://www.mathworks.com/help/matlab/ref/varargin.html) pair to input an `options` structure that will overwrite default options in a function.
- The name of the `opts` variable does not matter, but have to have the `options` Name input as one of the variable arguments for `getOptions` to recognize using Method #1.
- If the user inputs a Name-Value pair that is not associated with an option for that function, `getOptions` gives out a warning rather than an error.

```MATLAB
mutationList = [1 2 3];
opts.Stargazer = 1;
opts.SHH = 0;
exampleFxn(mutationList,'options',opts);
```

### Method #2
Alternatively use Name-Value pairs for all variable input arguments. Will produce the same result as above.

```MATLAB
mutationList = [1 2 3];
exampleFxn(mutationList,'Stargazer',1,'SHH',0);
```

### `getOptions` in user functions
To adapt `getOptions` to your own functions, add the section marked `FUNCTION OPTIONS` at the beginning and add all your options.

```MATLAB
function [out1] = exampleFxn(in1,varargin)
% ========================
% FUNCTION OPTIONS
% Description option #1
opts.Stargazer = '';
% Description option #2
opts.SHH = 1;
% get options
opts = getOptions(opts,varargin);
% disp(opts)
% unpack options into current workspace (not recommended)
% fn=fieldnames(opts);
% for i=1:length(fn)
% eval([fn{i} '=opts.' fn{i} ';']);
% end
% ========================
try
% Do something.
catch err
disp(repmat('@',1,7))
disp(getReport(err,'extended','hyperlinks','on'));
disp(repmat('@',1,7))
end
end
```

## Notes
- If `getOptions` masks an existing function due to naming conflicts, just rename it (e.g. `getOptionsCustom`) and it will function the same.
- `getOptions.m` can be placed in a package (e.g. `myPkg` within `+myPkg` folder) to allow it to be called as a function of that package (e.g. `myPkg.getOptions`) to reduce masking or naming conflicts if that is a concern.

## Using `getSettings`
To provide a central location for all options or allow users a central place to edit settings, can add each function's options to `getSettings` switch statement matching the parent functions name. See `getSettings.m` and call `getOptions` using the `getFunctionDefaults` Name-Value input as `opts = getOptions(opts,varargin,'getFunctionDefaults',1);`.

## License

See `LICENSE`. MIT @ Biafra Ahanonu
227 changes: 227 additions & 0 deletions getOptions.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
function [options] = getOptions(options,inputArgs,varargin)
% Gets default options for a function and replaces them with inputArgs inputs if they are present in Name-Value pair input (e.g. varargin).
% Biafra Ahanonu
% Started: 2013.11.04.
%
% inputs
% options - structure with options.
% inputArgs - an even numbered cell array, with {'option','value'} as the ordering. Normally pass varargin.
% Outputs
% options - options structure with inputs to function added.
% NOTE
% use the 'options' name-value pair to input an options structure that will overwrite default options in a function, example below.
% options.Stargazer = 1;
% options.SHH = 0;
% getMutations(mutationList,'options',options);

% This is in contrast to using name-value pairs, both will produce the same result.
% getMutations(mutationList,'Stargazer',1,'SHH',0);
%
% USAGE
% function [input1,input2] = exampleFxn(input1,input2,varargin)
% %========================
% % DESCRIPTION
% options.Stargazer = '';
% options.SHH = '';
% % get options
% options = getOptions(options,varargin);
% % display(options)
% % unpack options into current workspace
% % fn=fieldnames(options);
% % for i=1:length(fn)
% % eval([fn{i} '=options.' fn{i} ';']);
% % end
% %========================
% try
% % Do something.
% catch err
% disp(repmat('@',1,7))
% disp(getReport(err,'extended','hyperlinks','on'));
% disp(repmat('@',1,7))
% end
% end

% changelog
% 2014.02.12 [11:56:00] - added feature to allow input of an options structure that contains the options instead of having to input multiple name-value pairs. - Biafra
% 2014.07.10 [05:19:00] - added displayed warning if an option is input that was not present (this usually indicates typo). - Lacey (merged)
% 2014.12.10 [19:32:54] - now gets calling function and uses that to get default options - Biafra
% 2015.08.24 [23:31:36] - updated comments. - Biafra
% 2015.12.03 [13:52:15] - Added recursive aspect to mirrorRightStruct and added support for handling struct name-value inputs. mirrorRightStruct checks that struct options input by the user are struct in the input options. - Biafra
% 2016.xx.xx - warnings now show both calling function and it's parent function, improve debug for warnings. Slight refactoring of code to make easier to follow. - Biafra

% TODO
% allow input of an option structure - DONE!
% call settings function to have defaults for all functions in a single place - DONE!
% allow recursive overwriting of options structure - DONE!
% Type checking of all field names input by the user?

%========================
% Options for getOptions. Avoid recursion here, hence don't use getOptions for getOptions's options.
% Binary: 1 = whether getOptions should use recursive structures or crawl through a structure's field names or just replace the entire structure. For example, if "1" then options that themselves are a structure or contain sub-structures, the fields will be replaced rather than the entire strucutre.
goptions.recursiveStructs = 1;
% Binary: 1 = show warning if user inputs Name-Value pair option input that is not in original structure.
goptions.showWarnings = 1;
% Int: number of parent stacks to show during warning.
goptions.nParentStacks = 1;
% Binary: 1 = get defaults for a function from getSettings.
goptions.getFunctionDefaults = 0;
% Filter through options
try
for i = 1:2:length(varargin)
inputField = varargin{i};
if isfield(goptions, inputField)
inputValue = varargin{i+1};
goptions.(inputField) = inputValue;
end
end
catch err
localShowErrorReport(err);
display(['Incorrect options given to <a href="matlab: opentoline(''getOptions.m'')">getOptions</a>"'])
end
% Don't do this! Recursion with no base case waiting to happen...
% goptions = getOptions(goptions,varargin);
%========================

% Get default options for a function
if goptions.getFunctionDefaults==1
[ST,I] = dbstack;
% fieldnames(ST)
parentFunctionName = {ST.name};
parentFunctionName = parentFunctionName{2};
[optionsTmp] = getSettings(parentFunctionName);
if isempty(optionsTmp)
% Do nothing, don't use defaults if not present
else
options = optionsTmp;
% options = mirrorRightStruct(inputOptions,options,goptions,val);
end
end

% Get list of available options
validOptions = fieldnames(options);

% Loop over all input arguments, overwrite default/input options
for i = 1:2:length(inputArgs)
% inputArgs = inputArgs{1};
val = inputArgs{i};
if ischar(val)
%display([inputArgs{i} ': ' num2str(inputArgs{i+1})]);
if strcmp('options',val)
% Special options struct, only add field names defined by the user. Keep all original field names that are not input by the user.
inputOptions = inputArgs{i+1};
options = mirrorRightStruct(inputOptions,options,goptions,val);
elseif sum(strcmp(val,validOptions))>0&isstruct(options.(val))&goptions.recursiveStructs==1
% If struct name-value, add users field name changes only, keep all original field names in the struct intact, struct-recursion ON
inputOptions = inputArgs{i+1};
options.(val) = mirrorRightStruct(inputOptions,options.(val),goptions,val);
elseif sum(strcmp(val,validOptions))>0
% Non-options, non-struct value, struct-recursion OFF
% elseif ~isempty(strcmp(val,validOptions))
% Way more elegant, directly overwrite option
options.(val) = inputArgs{i+1};
% eval(['options.' val '=' num2str(inputArgs{i+1}) ';']);
else
if goptions.showWarnings==1
localShowWarnings(2,'name-value','','',val,goptions.nParentStacks);
end
end
else
if goptions.showWarnings==1
localShowWarnings(2,'name-value incorrect','','',val,goptions.nParentStacks);
end
continue;
end
end
%display(options);
end
function [toStruct] = mirrorRightStruct(fromStruct,toStruct,goptions,toStructName)
% Overwrites fields in toStruct with those in fromStruct, other toStruct fields remain intact.
% More generally, copies fields in fromStruct into toStruct, if there is an overlap in field names, fromStruct overwrites.
% Fields present in toStruct but not fromStruct are kept in toStruct output.
fromNames = fieldnames(fromStruct);
for name = 1:length(fromNames)
fromField = fromNames{name};
% if a field name is a struct, recursively grab user options from it
if isfield(toStruct, fromField)|isprop(toStruct, fromField)
if isstruct(fromStruct.(fromField))&goptions.recursiveStructs==1
% safety check: field exist in toStruct and is also a structure
if isstruct(toStruct.(fromField))
toStruct.(fromField) = mirrorRightStruct(fromStruct.(fromField),toStruct.(fromField),goptions,[toStructName '.' fromField]);
else
localShowWarnings(3,'notstruct',toStructName,fromField,'',goptions.nParentStacks);
end
else
toStruct.(fromField) = fromStruct.(fromField);
end
else
if goptions.showWarnings==1
localShowWarnings(3,'struct',toStructName,fromField,'',goptions.nParentStacks);
end
end
end
end
function localShowErrorReport(err)
% Displays an error report.
display(repmat('@',1,7))
disp(getReport(err,'extended','hyperlinks','on'));
display(repmat('@',1,7))
end
function localShowWarnings(stackLevel,displayType,toStructName,fromField,val,nParentStacks)
% Sub-function to centralize displaying of warnings within the function
try
% Calling localShowWarnings adds to the stack, adjust accordingly.
stackLevel = stackLevel+1;

% Get the entire function-call stack.
[ST,~] = dbstack;
callingFxn = ST(stackLevel).name;
callingFxnPath=which(ST(stackLevel).file);
callingFxnLine = num2str(ST(stackLevel).line);

% Add info about parent function of function that called getOptions.
callingFxnParentStr = '';
% nParentStacks = 2;
stackLevelTwo = stackLevel+1;
for stackNo = 1:nParentStacks
if length(ST)>=(stackLevelTwo)
callingFxnParent = ST(stackLevelTwo).name;
callingFxnParentPath = which(ST(stackLevelTwo).file);
callingFxnParentLine = num2str(ST(stackLevelTwo).line);
callingFxnParentStr = [callingFxnParentStr ' | <a href="matlab: opentoline(''' callingFxnParentPath ''',' callingFxnParentLine ')">' callingFxnParent '</a> line ' callingFxnParentLine];
else
callingFxnParentStr = '';
end
stackLevelTwo = stackLevelTwo+1;
end

% Display different information based on what type of warning occurred.
switch displayType
case 'struct'
warning(['<strong>WARNING</strong>: <a href="">' toStructName '.' fromField '</a> is not a valid option for <a href="matlab: opentoline(''' callingFxnPath ''',' callingFxnLine ')">' callingFxn '</a> on line ' callingFxnLine callingFxnParentStr])
case 'notstruct'
warning(['<strong>WARNING</strong>: <a href="">' toStructName '.' fromField '</a> is not originally a STRUCT, ignoring. <a href="matlab: opentoline(''' callingFxnPath ''',' callingFxnLine ')">' callingFxn '</a> on line ' callingFxnLine callingFxnParentStr])
case 'name-value incorrect'
warning(['<strong>WARNING</strong>: enter the parameter name before its associated value in <a href="matlab: opentoline(''' callingFxnPath ''',' callingFxnLine ')">' callingFxn '</a> on line ' callingFxnLine callingFxnParentStr])
case 'name-value'
warning(['<strong>WARNING</strong>: <a href="">' val '</a> is not a valid option for <a href="matlab: opentoline(''' callingFxnPath ''',' callingFxnLine ')">' callingFxn '</a> on line ' callingFxnLine callingFxnParentStr])
otherwise
% do nothing
end
catch err
localShowErrorReport(err);
callingFxn = 'UNKNOWN FUNCTION';
% Display different information based on what type of warning occurred.
switch displayType
case 'struct'
warning(['<strong>WARNING</strong>: <a href="">' toStructName '.' fromField '</a> is not a valid option for "' callingFxn '"'])
case 'notstruct'
warning('Unknown error.')
case 'name-value incorrect'
warning(['<strong>WARNING</strong>: enter the parameter name before its associated value in "' callingFxn '"'])
case 'name-value'
warning(['<strong>WARNING</strong>: <a href="">' val '</a> is not a valid option for "' callingFxn '"'])
otherwise
% do nothing
end
end
end
43 changes: 43 additions & 0 deletions getSettings.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
function [options] = getSettings(functionName)
% Send back default options to getOptions, users can modify settings here.
% Biafra Ahanonu
% started: 2014.12.10
%
% Inputs
% functionName - name of function whose option should be loaded
% Note
% Don't let this function call getOptions! Else you'll potentially get into an infinite loop.

% changelog
%

try
switch functionName
case 'exampleFxn'
options.example1 = '';
options.example1 = 0;
case 'exampleFxn2'
% Str: DESCRIPTION.
options.example1 = '';
% Binary: DESCRIPTION.
options.example1 = 0;
case 'unitTestGetOptions'
% Str: DESCRIPTION.
options.example1 = '';
% Binary: DESCRIPTION.
options.example2 = 0;
case 'unit_getOptions_testFunction'
% Str: DESCRIPTION.
options.example1 = '';
% Binary: DESCRIPTION.
options.example2 = 0;
otherwise
options.error = 1;
end
catch err
display(repmat('@',1,7))
disp(getReport(err,'extended','hyperlinks','on'));
display(repmat('@',1,7))
options = [];
end
end
Loading

0 comments on commit a8ddd94

Please sign in to comment.