Coding challenge on input parsing – Result
This week, Jiro is highlighting a code snippet submitted by Mikko Leppänen.
In my previous post, I posed a coding challenge to come up with an input validation code snippet to work with boundedline by Kelly. The function is a well-written piece of code with flexible input arguments. Here are the different ways you could call the function:
[hl, hp] = boundedline(x, y, b) [hl, hp] = boundedline(x, y, b, linespec) [hl, hp] = boundedline(x1, y1, b1, linespec1, x2, y2, b2, linespec2) [hl, hp] = boundedline(. . ., 'alpha') [hl, hp] = boundedline(. . ., ax) [hl, hp] = boundedline(. . ., 'transparency', trans) [hl, hp] = boundedline(. . ., 'orientation', orient) [hl, hp] = boundedline(. . ., 'cmap', cmap)
Here were the ground rules for the challenge:
- The function should be able to handle all, except the 3rd, calling syntax above. Any number of the optional arguments can be passed in.
- The input parameters should meet the respective criteria. (see help boundedline)
- All optional inputs should have default values. (see help boundedline)
- You can assume that the parameter-value pair inputs will be called after the other optional inputs.
- After parsing, the parameters should be put into a structure with fields "x", "y", "b", "linespec", "usealpha", "hax", "trans", "orient", "cmap".
- (Extra credit) Allow for the 3rd calling syntax.
Mikko was the only person who rose to the challenge, and it was great! His solution follows the ground rules, and he even did the extra credit! I wrote my own as well, but after seeing Mikko's solution, I was able to improve mine. Today I am going to dissect both of our codes and explain in detail.
Contents
Mikko's Solution
Rather than creating a single inputParser, Mikko makes use of try/catch statements to deal with multiple scenarios. Let's take a look. Note: I made some minor modifications to his code for this post.
function res = validateInputs(varargin)
1. First, an inputParser object is created. Since there are 3 required inputs x, y, and b, they are added to the inputParser object using the addRequired method. Note that validateattributes makes sure that the variables are numeric, finite, and 2d.
ipObj1 = inputParser; ipObj1.addRequired('x', @(x)validateattributes(x, {'numeric'}, {'finite', '2d'})); ipObj1.addRequired('y', @(x)validateattributes(x, {'numeric'}, {'finite', '2d'})); ipObj1.addRequired('b', @(x)validateattributes(x, {'numeric'}, {'finite'}));
2. Define optional parameters that could be passed.
keys = {'transparency', 'orientation', 'cmap'};
3. First try: consider a case where optional parameters linespec, useAlpha, and hax have been passed. Note that the validation for useAlpha and hax is checking for either the string "alpha" or a handle. This allows the two optional values to be passed in any order. See step 6, where it puts the parameters in the correct order. Following the optional parameters, the three parameter-value pairs are defined.
try
ipObj2 = ipObj1.createCopy; ipObj2.addOptional('linespec', '', @(x)ischar(x) && ~any(strcmp(x, ... [{'alpha'}, keys]))); ipObj2.addOptional('useAlpha', '', @(x)strcmp(x, 'alpha') || ishandle(x)); ipObj2.addOptional('hax', gca, @(x)strcmp(x, 'alpha') || ishandle(x)); ipObj2.addParamValue('orientation', 'horiz', ... @(x)any(validatestring(x, {'horiz', 'vert'}))); ipObj2.addParamValue('transparency', 0.2, @(x)validateattributes(x, ... {'numeric'}, {'scalar', 'positive'})); ipObj2.addParamValue('cmap', [], @(x)validateattributes(x, {'numeric'}, ... {'2d', 'finite', 'size', [size(x, 1), 3]})); ipObj2.parse(varargin{:}); res = ipObj2.Results;
4. If the above parsing fails, try a second parser. In this scenario, it is checking to see if the user supplied a second set of data for plotting - x2, y2, b2. Note that this is the extra credit!
catch me try
ipObj3 = ipObj1.createCopy; ipObj3.addOptional('linespec', '', @(x)ischar(x) && ~any(strcmp(x, ... [{'alpha'}, keys]))); ipObj3.addOptional('x2', [], @(x)validateattributes(x, {'numeric'}, ... {'finite', '2d'})); ipObj3.addOptional('y2', [], @(x)validateattributes(x, {'numeric'}, ... {'finite', '2d'})); ipObj3.addOptional('b2', [], @(x)validateattributes(x, {'numeric'}, ... {'finite'})); ipObj3.addOptional('linespec2', '', @(x)ischar(x) && ~any(strcmp(x, ... [{'alpha'}, keys]))); ipObj3.addOptional('useAlpha', '', @(x)strcmp(x, 'alpha') || ishandle(x)); ipObj3.addOptional('hax', gca, @(x)strcmp(x, 'alpha') || ishandle(x)); ipObj3.addParamValue('orientation', 'horiz', @(x)any(validatestring(x, ... {'horiz', 'vert'}))); ipObj3.addParamValue('transparency', 0.2, @(x)validateattributes(x, ... {'numeric'}, {'scalar', 'positive'})); ipObj3.addParamValue('cmap', [], @(x)validateattributes(x, {'numeric'}, ... {'2d', 'finite', 'size', [size(x, 1), 3]})); ipObj3.parse(varargin{:}); res = ipObj3.Results;
5. If the above parsing fails, try a third parser. In this scenario, it is checking to see if the user omitted the linespec parameter.
catch me2 ipObj4 = ipObj1.createCopy; ipObj4.addOptional('useAlpha', '', @(x)strcmp(x, 'alpha') || ishandle(x)); ipObj4.addOptional('hax', gca, @(x)strcmp(x, 'alpha') || ishandle(x)); ipObj4.addParamValue('orientation', 'horiz', @(x)any(validatestring(x, ... {'horiz', 'vert'}))); ipObj4.addParamValue('transparency', 0.2, @(x)validateattributes(x, ... {'numeric'}, {'scalar', 'positive'})); ipObj4.addParamValue('cmap', [], @(x)validateattributes(x, {'numeric'}, ... {'2d', 'finite', 'size', [size(x, 1), 3]})); ipObj4.addOptional('linespec', '', @(x)ischar(x) && ~any(strcmp(x, ... [{'alpha'}, keys]))); ipObj4.parse(varargin{:}); res = ipObj4.Results; end end
6. If it passed all of the parsing above, res has the necessary parameters. However, since useAlpha and hax could have been passed in either order, Mikko checks to see if useAlpha is a handle or not. If it is, he swaps useAlpha and hax.
if ishandle(res.useAlpha) tmp1 = res.useAlpha; tmp2 = res.hax; res.hax = tmp1; res.useAlpha = tmp2; end
7. Finally, since useAlpha needs to be a logical (true or false), it is set to true if it has the string "alpha". If not, it is set to false.
if isequal(res.useAlpha, 'alpha') res.useAlpha = true; else res.useAlpha = false; end
My Solution
I also have a solution that I was able to improve based on a technique I saw in Mikko's solution. I like the flexibility he introduced in the optional arguments "usealpha" and "hax", where the user could provide those inputs in either order. I've extended it to work for all 3 optional arguments "linespec", "usealpha" and "hax". Here's what I have. For this solution, I'm ignoring the "extra credit" part of the challenge.
I have a custom validation function validateOptionalArgs for the 3 optional arguments "linespec", "usealpha", and "hax".
p = inputParser; p.addRequired('x', @(x)validateattributes(x, {'numeric'}, {'finite', '2d'})); p.addRequired('y', @(x)validateattributes(x, {'numeric'}, {'finite', '2d'})); p.addRequired('b', @(x)validateattributes(x, {'numeric'}, {'finite'})); p.addOptional('linespec', [], @(x) validateOptionalArgs(x) > 0); p.addOptional('usealpha', [], @(x) validateOptionalArgs(x) > 0); p.addOptional('hax' , [], @(x) validateOptionalArgs(x) > 0); p.addParamValue('orientation', 'horiz', @(x) ismember(x, {'vert', 'horiz'})); p.addParamValue('transparency', 0.2, @(x) validateattributes(x, ... {'numeric'}, {'scalar', '>=', 0, '<=', 1})); p.addParamValue('cmap', colormap(gca), @(x) validateattributes(x, ... {'double'}, {'size', [NaN, 3], '>=', 0, '<=', 1})); p.parse(varargin{:}); res = p.Results;
Since "linespec", "usealpha", and "hax" could be passed in any order, map them back to the true parameters.
% Default values linespec = ''; usealpha = false; hax = gca; for iF = {'linespec', 'usealpha', 'hax'} field = iF{1}; switch validateOptionalArgs(res.(field)) case 1 linespec = res.(field); case 2 usealpha = res.(field); case 3 hax = res.(field); case 0 % skip end end % Map it back [res.linespec, res.usealpha, res.hax] = deal(linespec, usealpha, hax);
Finally, I set "usealpha" to TRUE if it has the string "alpha".
if isequal(res.usealpha, 'alpha') res.usealpha = true; end
Here's my validateOptionalArgs function which checks to see if the argument matches one of the 3 optional inputs. It returns which option it matched. If it did not match any, it returns 0.
function s = validateOptionalArgs(x)
% s = validateOptionalArgs(x) % % Returns 0, 1, 2, 3 % 0 - did not match any % 1 - matched linespec % 2 - matched 'alpha' % 3 - matched hax tf = false; % First, check to see if it's a valid LineSpec if ischar(x) % check for color x2 = regexprep(x, '(r|g|b|c|m|y|k|w)', '', 'once'); % check for marker x2 = regexprep(x2, '(+|o|*|[^-]\.|x|s|d|\^|v|>|<|p|h)', '', 'once'); % check if the remainder is a valid line style (or empty) tf = ismember(x2, {'', '-', '--', ':', '-.'}); end if tf s = 1; else if isequal(x, false) || strcmp(x, 'alpha') % Next, check to see if it's "alpha" s = 2; elseif isscalar(x) && ishandle(x) && strcmpi(get(x, 'Type'), 'axes') % Finally, check to see if it's a valid axes handle s = 3; else % Otherwise, return 0 s = 0; end end
Comments
Let us know what you think here.
- Category:
- Picks
Comments
To leave a comment, please click here to sign in to your MathWorks Account or create a new one.