# HG changeset patch # User Carnë Draug # Date 1408490643 -3600 # Node ID ff820f92cbb5703e114d082fca05cd0c8507d138 # Parent f707835af86783242964216c2ad65d0a8237952b inputParser: classdef port of @inputParser from Octave Forge general pkg. * scripts/general/classdef.m: an almost Matlab compatible version of this class was part of the Octave Forge general package since 2011, and made use of @class syntax. With classdef now implemented in Octave, this is a port of that function with the incompatibilities fixed. The help text needs to be adapted after a new format is decided for this files. * scripts/general/module.mk: add new file to the build system. * NEWS: reference new class. * doc/interpreter/func.texi: add DOCSTRING on the manual. diff -r f707835af867 -r ff820f92cbb5 NEWS --- a/NEWS Wed Aug 20 10:19:09 2014 -0700 +++ b/NEWS Wed Aug 20 00:24:03 2014 +0100 @@ -13,6 +13,10 @@ methods endmethods properties endproperties + ** New classes in Octave 4.2: + + inputParser + ** Interpolation function changes for Matlab compatibility The interpolation method 'cubic' is now equivalent to 'pchip' diff -r f707835af867 -r ff820f92cbb5 doc/interpreter/func.txi --- a/doc/interpreter/func.txi Wed Aug 20 10:19:09 2014 -0700 +++ b/doc/interpreter/func.txi Wed Aug 20 00:24:03 2014 +0100 @@ -404,6 +404,11 @@ @DOCSTRING(nargoutchk) +There is also the class @code{inputParser} which can perform extremely +complex input checking for functions. + +@DOCSTRING(inputParser) + @anchor{XREFvarargin} @anchor{XREFvarargout} @node Variable-length Argument Lists @section Variable-length Argument Lists diff -r f707835af867 -r ff820f92cbb5 scripts/general/inputParser.m --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/general/inputParser.m Wed Aug 20 00:24:03 2014 +0100 @@ -0,0 +1,573 @@ +## Copyright (C) 2011-2014 Carnë Draug +## +## This file is part of Octave. +## +# Octave is free software; you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or (at +## your option) any later version. +## +## Octave is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Octave; see the file COPYING. If not, see +## . + +## -*- texinfo -*- +## @deftypefn {Function File} {} inputParser () +## Create object @var{parser} of the inputParser class. +## +## This class is designed to allow easy parsing of function arguments. This +## class supports four types of arguments: +## +## @enumerate +## @item mandatory (see @command{addRequired}); +## @item optional (see @command{addOptional}); +## @item named (see @command{addParamValue}); +## @item switch (see @command{addSwitch}). +## @end enumerate +## +## After defining the function API with this methods, the supplied arguments +## can be parsed with the @command{parse} method and the parsing results +## accessed with the @command{Results} accessor. +## +## @deftypefnx {Accessor method} parser.Parameters +## Return list of parameters name already defined. +## +## @deftypefnx {Accessor method} parser.Results +## Return structure with argument names as fieldnames and corresponding values. +## +## @deftypefnx {Accessor method} parser.Unmatched +## Return structure similar to @command{Results} for unmatched parameters. See +## the @command{KeepUnmatched} property. +## +## @deftypefnx {Accessor method} parser.UsingDefaults +## Return cell array with the names of arguments that are using default values. +## +## @deftypefnx {Class property} parser.CaseSensitive = @var{boolean} +## Set whether matching of argument names should be case sensitive. Defaults to false. +## +## @deftypefnx {Class property} parser.FunctionName = @var{name} +## Set function name to be used on error messages. Defauls to empty string. +## +## @deftypefnx {Class property} parser.KeepUnmatched = @var{boolean} +## Set whether an error should be given for non-defined arguments. Defaults to +## false. If set to true, the extra arguments can be accessed through +## @code{Unmatched} after the @code{parse} method. Note that since @command{Switch} +## and @command{ParamValue} arguments can be mixed, it is not possible to know +## the unmatched type. If argument is found unmatched it is assumed to be of the +## @command{ParamValue} type and it is expected to be followed by a value. +## +## @deftypefnx {Class property} parser.StructExpand = @var{boolean} +## Set whether a structure can be passed to the function instead of parameter +## value pairs. Defaults to true. Not implemented yet. +## +## The following example shows how to use this class: +## +## @example +## @group +## function check (varargin) +## p = inputParser (); # create object +## p.FunctionName = "check"; # set function name +## p.addRequired ("pack", @@ischar); # create mandatory argument +## +## p.addOptional ("path", pwd(), @@ischar); # create optional argument +## +## ## one can create a function handle to anonymous functions for validators +## val_mat = @@(x) isvector (x) && all (x <= 1) && all (x >= 0); +## p.addOptional ("mat", [0 0], val_mat); +## +## ## create two ParamValue type of arguments +## val_type = @@(x) any (strcmp (x, @{"linear", "quadratic"@})); +## p.addParamValue ("type", "linear", val_type); +## val_verb = @@(x) any (strcmp (x, @{"low", "medium", "high"@})); +## p.addParamValue ("tolerance", "low", val_verb); +## +## ## create a switch type of argument +## p.addSwitch ("verbose"); +## +## p.parse (varargin@{:@}); +## +## ## the rest of the function can access the input by accessing p.Results +## ## for example, to access the value of tolerance, use p.Results.tolerance +## endfunction +## +## check ("mech"); # valid, will use defaults for other arguments +## check (); # error since at least one argument is mandatory +## check (1); # error since !ischar +## check ("mech", "~/dev"); # valid, will use defaults for other arguments +## +## check ("mech", "~/dev", [0 1 0 0], "type", "linear"); # valid +## +## ## the following is also valid. Note how the Switch type of argument can be +## ## mixed into or before the ParamValue (but still after Optional) +## check ("mech", "~/dev", [0 1 0 0], "verbose", "tolerance", "high"); +## +## ## the following returns an error since not all optional arguments, `path' and +## ## `mat', were given before the named argument `type'. +## check ("mech", "~/dev", "type", "linear"); +## @end group +## @end example +## +## @emph{Note 1}: a function can have any mixture of the four API types but they +## must appear in a specific order. @command{Required} arguments must be the very +## first which can be followed by @command{Optional} arguments. Only the +## @command{ParamValue} and @command{Switch} arguments can be mixed together but +## must be at the end. +## +## @emph{Note 2}: if both @command{Optional} and @command{ParamValue} arguments +## are mixed in a function API, once a string Optional argument fails to validate +## against, it will be considered the end of @command{Optional} arguments and the +## first key for a @command{ParamValue} and @command{Switch} arguments. +## +## @seealso{nargin, validateattributes, validatestring, varargin} +## @end deftypefn + +## -*- texinfo -*- +## @deftypefnx {Function File} {} addOptional (@var{argname}, @var{default}) +## @deftypefnx {Function File} {} addOptional (@var{argname}, @var{default}, @var{validator}) +## Add new optional argument to the object @var{parser} of the class inputParser +## to implement an ordered arguments type of API +## +## @var{argname} must be a string with the name of the new argument. The order +## in which new arguments are added with @command{addOptional}, represents the +## expected order of arguments. +## +## @var{default} will be the value used when the argument is not specified. +## +## @var{validator} is an optional anonymous function to validate the given values +## for the argument with name @var{argname}. Alternatively, a function name +## can be used. +## +## See @command{help inputParser} for examples. +## +## @emph{Note}: if a string argument does not validate, it will be considered a +## ParamValue key. If an optional argument is not given a validator, anything +## will be valid, and so any string will be considered will be the value of the +## optional argument (in @sc{matlab}, if no validator is given and argument is +## a string it will also be considered a ParamValue key). +## +## @end deftypefn + +## -*- texinfo -*- +## @deftypefn {Function File} {} addParamValue (@var{argname}, @var{default}) +## @deftypefnx {Function File} {} addParamValue (@var{argname}, @var{default}, @var{validator}) +## Add new parameter to the object @var{parser} of the class inputParser to implement +## a name/value pair type of API. +## +## @var{argname} must be a string with the name of the new parameter. +## +## @var{default} will be the value used when the parameter is not specified. +## +## @var{validator} is an optional function handle to validate the given values +## for the parameter with name @var{argname}. Alternatively, a function name +## can be used. +## +## See @command{help inputParser} for examples. +## +## @end deftypefn + +## -*- texinfo -*- +## @deftypefn {Function File} {} addRequired (@var{argname}) +## @deftypefnx {Function File} {} addRequired (@var{argname}, @var{validator}) +## Add new mandatory argument to the object @var{parser} of inputParser class. +## +## This method belongs to the inputParser class and implements an ordered +## arguments type of API. +## +## @var{argname} must be a string with the name of the new argument. The order +## in which new arguments are added with @command{addrequired}, represents the +## expected order of arguments. +## +## @var{validator} is an optional function handle to validate the given values +## for the argument with name @var{argname}. Alternatively, a function name +## can be used. +## +## See @command{help inputParser} for examples. +## +## @emph{Note}: this can be used together with the other type of arguments but +## it must be the first (see @command{@@inputParser}). +## +## @end deftypefn + +## -*- texinfo -*- +## @deftypefn {Function File} {} addSwitch (@var{argname}) +## Add new switch type of argument to the object @var{parser} of inputParser class. +## +## This method belongs to the inputParser class and implements a switch +## arguments type of API. +## +## @var{argname} must be a string with the name of the new argument. Arguments +## of this type can be specified at the end, after @code{Required} and @code{Optional}, +## and mixed between the @code{ParamValue}. They default to false. If one of the +## arguments supplied is a string like @var{argname}, then after parsing the value +## of @var{parse}.Results.@var{argname} will be true. +## +## See @command{help inputParser} for examples. +## +## @end deftypefn + +## -*- texinfo -*- +## @deftypefn {Function File} {} parse (@var{varargin}) +## Parses and validates list of arguments according to object @var{parser} of the +## class inputParser. +## +## After parsing, the results can be accessed with the @command{Results} +## accessor. See @command{help inputParser} for a more complete description. +## +## @end deftypefn + +## Author: Carnë Draug + +classdef inputParser < handle + properties + ## TODO set input checking for this properties + CaseSensitive = false; + FunctionName = ""; + KeepUnmatched = false; +# PartialMatching = true; # TODO unimplemented +# StructExpand = true; # TODO unimplemented + endproperties + + properties (SetAccess = protected) + Parameters = cell (); + Results = struct (); + Unmatched = struct (); + UsingDefaults = cell (); + endproperties + + properties (Access = protected) + ## Since Required and Optional are ordered, they get a cell array of + ## structs with the fields "name", "def" (default), and "val" (validator). + Required = cell (); + Optional = cell (); + ## ParamValue and Swicth are unordered so we have a struct whose fieldnames + ## are the argname, and values are a struct with fields "def" and "val" + ParamValue = struct (); + Switch = struct (); + + ## List of ParamValues and Switch names to ease searches + ParamValueNames = cell (); + SwitchNames = cell (); + + ## When checking for fieldnames in a Case Insensitive way, this variable + ## holds the correct identifier for the last searched named using the + ## is_argname method. + last_name = ""; + endproperties + + properties (Access = protected, Constant = true) + ## Default validator, always returns scalar true. + def_val = @() true; + endproperties + + methods + function addRequired (this, name, val = inputParser.def_val) + if (nargin < 2 || nargin > 3) + print_usage (); + elseif (numel (this.Optional) || numel (fieldnames (this.ParamValue)) + || numel (fieldnames (this.Switch))) + error (["inputParser.addRequired: can't have a Required argument " ... + "after Optional, ParamValue, or Switch"]); + endif + this.validate_name ("Required", name); + this.Required{end+1} = struct ("name", name, "val", val); + endfunction + + function addOptional (this, name, def, val = inputParser.def_val) + if (nargin < 3 || nargin > 4) + print_usage (); + elseif (numel (fieldnames (this.ParamValue)) + || numel (fieldnames (this.Switch))) + error (["inputParser.Optional: can't have Optional arguments " ... + "after ParamValue or Switch"]); + endif + this.validate_name ("Optional", name); + this.validate_default ("Optional", name, def, val); + this.Optional{end+1} = struct ("name", name, "def", def, "val", val); + endfunction + + function addParamValue (this, name, def, val = inputParser.def_val) + if (nargin < 3 || nargin > 4) + print_usage (); + endif + this.validate_name ("ParamValue", name); + this.validate_default ("ParamValue", name, def, val); + this.ParamValue.(name).def = def; + this.ParamValue.(name).val = val; + endfunction + + function addSwitch (this, name) + if (nargin != 2) + print_usage (); + endif + this.validate_name ("Switch", name); + this.Switch.(name).def = false; + endfunction + + function parse (this, varargin) + if (numel (varargin) < numel (this.Required)) + if (this.FunctionName) + print_usage (this.FunctionName); + else + this.error ("not enough input arguments"); + endif + endif + pnargin = numel (varargin); + + this.ParamValueNames = fieldnames (this.ParamValue); + this.SwitchNames = fieldnames (this.Switch); + + ## Evaluate the Required arguments first + nReq = numel (this.Required); + for idx = 1:nReq + req = this.Required{idx}; + this.validate_arg (req.name, req.val, varargin{idx}); + endfor + + vidx = nReq; # current index in varargin + + ## Search for a list of Optional arguments + idx = 0; # current index on the array of Optional + nOpt = numel (this.Optional); + while (vidx < pnargin && idx < nOpt) + opt = this.Optional{++idx}; + in = varargin{++vidx}; + if (! opt.val (in)) + ## If it does not match there's two options: + ## 1) input is actually wrong and we should error; + ## 2) it's a ParamValue or Switch name and we should use the + ## the default for the rest. + if (ischar (in)) + idx--; + vidx--; + break + else + this.error (sprintf ("failed validation of %s", + toupper (opt.name))); + endif + endif + this.Results.(opt.name) = in; + endwhile + + ## Fill in with defaults of missing Optional + while (idx++ < nOpt) + opt = this.Optional{idx}; + this.UsingDefaults{end+1} = opt.name; + this.Results.(opt.name) = opt.def; + endwhile + + ## Search unordered Options (Switch and ParamValue) + while (vidx++ < pnargin) + name = varargin{vidx}; + if (this.is_argname ("ParamValue", name)) + if (vidx++ > pnargin) + this.error (sprintf ("no matching value for option '%s'", + toupper (name))); + endif + this.validate_arg (this.last_name, this.ParamValue.(this.last_name).val, + varargin{vidx}); + elseif (this.is_argname ("Switch", name)) + this.Results.(this.last_name) = true; + else + if (vidx++ < pnargin && this.KeepUnmatched) + this.Unmatched.(name) = varargin{vidx}; + else + this.error (sprintf ("argument '%s' is not a valid parameter", + toupper (name))); + endif + endif + endwhile + ## Add them to the UsingDeafults list + this.add_missing ("ParamValue"); + this.add_missing ("Switch"); + + endfunction + + function display (this) + if (nargin > 1) + print_usage (); + endif + printf ("inputParser object with properties:\n\n"); + b2s = @(x) ifelse (any (x), "true", "false"); + printf ([" CaseSensitive : %s\n FunctionName : %s\n" ... + " KeepUnmatched : %s\n PartialMatching : %s\n" ... + " StructExpand : %s\n\n"], + b2s (this.CaseSensitive), b2s (this.FunctionName), + b2s (this.KeepUnmatched), b2s (this.PartialMatching), + b2s (this.StructExpand)); + printf ("Defined parameters:\n\n {%s}\n", + strjoin (this.Parameters, ", ")); + endfunction + endmethods + + methods (Access = private) + function validate_name (this, type, name) + if (! isvarname (name)) + error ("inputParser.add%s: NAME is an invalid identifier", method); + elseif (any (strcmpi (this.Parameters, name))) + ## Even if CaseSensitive is "on", we still shouldn't allow + ## two args with the same name. + error ("inputParser.add%s: argname '%s' has already been specified", + type, name); + endif + this.Parameters{end+1} = name; + endfunction + + function validate_default (this, type, name, def, val) + if (! feval (val, def)) + error ("inputParser.add%s: failed validation for '%s' default value", + type, name); + endif + endfunction + + function validate_arg (this, name, val, in) + if (! val (in)) + this.error (sprintf ("failed validation of %s", toupper (name))); + endif + this.Results.(name) = in; + endfunction + + function r = is_argname (this, type, name) + if (this.CaseSensitive) + r = isfield (this.(type), name); + this.last_name = name; + else + fnames = this.([type "Names"]); + l = strcmpi (name, fnames); + r = any (l(:)); + if (r) + this.last_name = fnames{l}; + endif + endif + endfunction + + function add_missing (this, type) + unmatched = setdiff (fieldnames (this.(type)), fieldnames (this.Results)); + for namec = unmatched(:)' + name = namec{1}; + this.UsingDefaults{end+1} = name; + this.Results.(name) = this.(type).(name).def; + endfor + endfunction + + function error (this, msg) + where = ""; + if (this.FunctionName) + where = [this.FunctionName ": "]; + endif + error ("%s%s", where, msg); + endfunction + endmethods + +endclassdef + +%!function p = create_p () +%! p = inputParser (); +%! p.CaseSensitive = true; +%! p.addRequired ("req1", @(x) ischar (x)); +%! p.addOptional ("op1", "val", @(x) any (strcmp (x, {"val", "foo"}))); +%! p.addOptional ("op2", 78, @(x) x > 50); +%! p.addSwitch ("verbose"); +%! p.addParamValue ("line", "tree", @(x) any (strcmp (x, {"tree", "circle"}))); +%!endfunction + +## check normal use, only required are given +%!test +%! p = create_p (); +%! p.parse ("file"); +%! r = p.Results; +%! assert (r.req1, "file"); +%! assert (sort (p.UsingDefaults), sort ({"op1", "op2", "verbose", "line"})); +%! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, +%! {"file", "val", 78, false, "tree"}); + +## check normal use, but give values different than defaults +%!test +%! p = create_p (); +%! p.parse ("file", "foo", 80, "line", "circle", "verbose"); +%! r = p.Results; +%! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, +%! {"file", "foo", 80, true, "circle"}); + +## check optional is skipped and considered ParamValue if unvalidated string +%!test +%! p = create_p (); +%! p.parse ("file", "line", "circle"); +%! r = p.Results; +%! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, +%! {"file", "val", 78, false, "circle"}); + +## check case insensitivity +%!test +%! p = create_p (); +%! p.CaseSensitive = false; +%! p.parse ("file", "foo", 80, "LiNE", "circle", "vERbOSe"); +%! r = p.Results; +%! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, +%! {"file", "foo", 80, true, "circle"}); + +## check KeepUnmatched +%!test +%! p = create_p (); +%! p.KeepUnmatched = true; +%! p.parse ("file", "foo", 80, "line", "circle", "verbose", "extra", 50); +%! assert (p.Unmatched.extra, 50) + +## check error when missing required +%!error +%! p = create_p (); +%! p.parse (); + +## check error when given required does not validate +%!error +%! p = create_p (); +%! p.parse (50); + +## check error when given optional does not validate +%!error +%! p = create_p (); +%! p.parse ("file", "no-val"); + +## check error when given ParamValue does not validate +%!error +%! p = create_p (); +%! p.parse ("file", "foo", 51, "line", "round"); + +## check alternative method (obj, ...) API +%!function p2 = create_p2 (); +%! p2 = inputParser; +%! addRequired (p2, "req1", @(x) ischar (x)); +%! addOptional (p2, "op1", "val", @(x) any (strcmp (x, {"val", "foo"}))); +%! addOptional (p2, "op2", 78, @(x) x > 50); +%! addSwitch (p2, "verbose"); +%! addParamValue (p2, "line", "tree", @(x) any (strcmp (x, {"tree", "circle"}))); +%!endfunction + +## check normal use, only required are given +%!test +%! p2 = create_p2 (); +%! parse (p2, "file"); +%! r = p2.Results; +%! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, +%! {"file", "val", 78, false, "tree"}); +%! assert (sort (p2.UsingDefaults), sort ({"op1", "op2", "verbose", "line"})); + +## check normal use, but give values different than defaults +%!test +%! p2 = create_p2 (); +%! parse (p2, "file", "foo", 80, "line", "circle", "verbose"); +%! r = p2.Results; +%! assert ({r.req1, r.op1, r.op2, r.verbose, r.line}, +%! {"file", "foo", 80, true, "circle"}); + +## FIXME: This somehow works in Matlab +#%!test +#%! p = inputParser; +#%! p.addOptional ("op1", "val"); +#%! p.addParamValue ("line", "tree"); +#%! p.parse ("line", "circle"); +#%! assert (p.Results, struct ("op1", "val", "line", "circle")); diff -r f707835af867 -r ff820f92cbb5 scripts/general/module.mk --- a/scripts/general/module.mk Wed Aug 20 10:19:09 2014 -0700 +++ b/scripts/general/module.mk Wed Aug 20 00:24:03 2014 +0100 @@ -33,6 +33,7 @@ general/flipud.m \ general/gradient.m \ general/idivide.m \ + general/inputParser.m \ general/int2str.m \ general/interp1.m \ general/interp2.m \