File Exchange Pick of the Week

Our best user submissions

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:

  1. The function should be able to handle all, except the 3rd, calling syntax above. Any number of the optional arguments can be passed in.
  2. The input parameters should meet the respective criteria. (see help boundedline)
  3. All optional inputs should have default values. (see help boundedline)
  4. You can assume that the parameter-value pair inputs will be called after the other optional inputs.
  5. After parsing, the parameters should be put into a structure with fields "x", "y", "b", "linespec", "usealpha", "hax", "trans", "orient", "cmap".
  6. (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.




Published with MATLAB® R2012b

|

Comments

To leave a comment, please click here to sign in to your MathWorks Account or create a new one.