diff --git a/README.md b/README.md index 6f06afe..7fbbf0a 100644 --- a/README.md +++ b/README.md @@ -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.
+USA + +## 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 \ No newline at end of file diff --git a/getOptions.m b/getOptions.m new file mode 100644 index 0000000..c655b2a --- /dev/null +++ b/getOptions.m @@ -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 getOptions"']) + 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 ' | ' callingFxnParent ' line ' callingFxnParentLine]; + else + callingFxnParentStr = ''; + end + stackLevelTwo = stackLevelTwo+1; + end + + % Display different information based on what type of warning occurred. + switch displayType + case 'struct' + warning(['WARNING: ' toStructName '.' fromField ' is not a valid option for ' callingFxn ' on line ' callingFxnLine callingFxnParentStr]) + case 'notstruct' + warning(['WARNING: ' toStructName '.' fromField ' is not originally a STRUCT, ignoring. ' callingFxn ' on line ' callingFxnLine callingFxnParentStr]) + case 'name-value incorrect' + warning(['WARNING: enter the parameter name before its associated value in ' callingFxn ' on line ' callingFxnLine callingFxnParentStr]) + case 'name-value' + warning(['WARNING: ' val ' is not a valid option for ' callingFxn ' 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(['WARNING: ' toStructName '.' fromField ' is not a valid option for "' callingFxn '"']) + case 'notstruct' + warning('Unknown error.') + case 'name-value incorrect' + warning(['WARNING: enter the parameter name before its associated value in "' callingFxn '"']) + case 'name-value' + warning(['WARNING: ' val ' is not a valid option for "' callingFxn '"']) + otherwise + % do nothing + end + end +end \ No newline at end of file diff --git a/getSettings.m b/getSettings.m new file mode 100644 index 0000000..1ede874 --- /dev/null +++ b/getSettings.m @@ -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 \ No newline at end of file diff --git a/unitTestGetOptions.m b/unitTestGetOptions.m new file mode 100644 index 0000000..f8c842c --- /dev/null +++ b/unitTestGetOptions.m @@ -0,0 +1,98 @@ +function optionsCheck = unitTestGetOptions(varargin) + % Unit test for getOptions. + % Will perform 4 tests. Using Name-Value vs. options input and with or without structure crawling (recursion) enabled. + % Biafra Ahanonu + % started: 2015.12.03 + % inputs + % + % outputs + % + + % changelog + % + % TODO + % + clc + + + disp(repmat('=',1,7)) + disp('Default options') + opts = unit_getOptions_testFunction(1,0); + dispStruct(opts,'options',1); + + recursionStr = {'OFF','ON'}; + for recursionState = [0 1] + disp(repmat('=',1,7)) + disp(['options test | recursion ' recursionStr{recursionState+1}]) + % Show that inputing an options stuct with sub-struct returns entire + % sub-structs if user doesn't provide all sub-struct field names + optsCheck.check1 = 20; + optsCheck.check3 = 20; + optsCheck.secondaryOpts.check1 = 'USA USA USA'; + optsCheck.secondaryOpts.check3 = 'AMERICA'; + optsCheck.secondaryOpts.check2.go1.h1 = 'AMERICA'; + optsCheck.secondaryOpts.check2.go1.h2 = 1776; + optsCheck.secondaryOpts.check2.go2.h1 = 'AMERICA'; + optsCheck.secondaryOpts.check2.go2.h2 = 1776; + optsCheck.secondaryOpts.tertiaryOptions.check1 = 'The rest of the world'; + opts = unit_getOptions_testFunction(recursionState,0,'options',optsCheck,1); + dispStruct(opts,'options',1); + end + + for recursionState = [0 1] + disp(repmat('=',1,7)) + disp(['name-value struct test | recursion ' recursionStr{recursionState+1}]) + % Show that inputing struct as a name-value argument returns all that + % sub-structs values + secondaryOpts.check3 = 'America'; + secondaryOpts.tertiaryOpts.check1 = 'USA USA USA'; + secondaryOpts.tertiaryOpts.check3 = 'America'; + secondaryOpts.tertiaryOpts.moreOpts.check1 = 'USA USA USA'; + secondaryOpts.tertiaryOpts.moreOpts.check3 = 'America'; + opts = unit_getOptions_testFunction(recursionState,0,'secondaryOptions',secondaryOpts); + dispStruct(opts,'options',1); + end + + for recursionState = [0 1] + disp(repmat('=',1,7)) + disp(['Default options using getSettings, should see warnings | name-value struct test | recursion ' recursionStr{recursionState+1}]) + % Show that inputing struct as a name-value argument returns all that + % sub-structs values + secondaryOpts.check3 = 'America'; + secondaryOpts.tertiaryOpts.check1 = 'USA USA USA'; + secondaryOpts.tertiaryOpts.check3 = 'America'; + secondaryOpts.tertiaryOpts.moreOpts.check1 = 'USA USA USA'; + secondaryOpts.tertiaryOpts.moreOpts.check3 = 'America'; + opts = unit_getOptions_testFunction(recursionState,1,'options',optsCheck); + dispStruct(opts,'options',1); + end +end +function opts = unit_getOptions_testFunction(arg1,arg2,varargin) + % default options + opts.check1 = 1; + opts.check2 = 1; + opts.secondaryOpts.check1 = 1; + opts.secondaryOpts.check2.go1 = struct; + opts.secondaryOpts.check2.go2 = 'USA USA USA'; + opts.secondaryOpts.quadOpts.check1 = 1; + opts.secondaryOpts.quadOpts.check2 = {1,2}; + opts.secondaryOpts.tertiaryOpts.check1 = 1; + opts.secondaryOpts.tertiaryOpts.check2 = 1; + opts.secondaryOpts.tertiaryOpts.moreOpts.check1 = 1; + opts.secondaryOpts.tertiaryOpts.moreOpts.check2 = 1; + opts = getOptions(opts,varargin,'recursiveStructs',arg1,'getFunctionDefaults',arg2); +end +function dispStruct(iStruct,iField,lvlNum) + if isstruct(iStruct) + disp(['' iField '']) + disp(iStruct) + fprintf('\b') + nameList = fieldnames(iStruct); + for i = 1:length(nameList) + if isstruct(iStruct.(nameList{i})) + dispStruct(iStruct.(nameList{i}),nameList{i},lvlNum+1); + end + end + else + end +end \ No newline at end of file