changeset 23348:5ab7192f91d8

containers.Map: New key/value storage container (bug #49559). * scripts/+containers/Map.m: New function * scripts/+containers/module.mk: Add new function directory to build system. * scripts/module.mk: Add new module.mk file to build system. * NEWS: Announce new function. * container.txi: Add DOCSTRING to manual. * octave.texi: Update @detailmenu with new menu item.
author Rik <rik@octave.org>
date Wed, 05 Apr 2017 10:55:35 -0700
parents ed25d1f8163c
children 4f07b4770eec
files NEWS doc/interpreter/container.txi doc/interpreter/octave.texi scripts/+containers/Map.m scripts/+containers/module.mk scripts/module.mk
diffstat 6 files changed, 710 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Apr 05 09:23:20 2017 -0400
+++ b/NEWS	Wed Apr 05 10:55:35 2017 -0700
@@ -14,6 +14,11 @@
 
       glpk  Qhull
 
+ ** A new container data type--containers.Map--is available.  Map is a
+    key/value storage container (a.k.a, a hash) that efficiently allows
+    storing and retrieving values by name, rather than by position which
+    is how arrays work.
+
  ** The "names" option used in regular expressions now returns a struct
     array, rather than a struct with a cell array for each field.  This
     change was made for Matlab compatibility.
--- a/doc/interpreter/container.txi	Wed Apr 05 09:23:20 2017 -0400
+++ b/doc/interpreter/container.txi	Wed Apr 05 10:55:35 2017 -0700
@@ -20,15 +20,16 @@
 @chapter Data Containers
 @cindex containers
 
-Octave includes support for two different mechanisms to contain
-arbitrary data types in the same variable.  Structures, which are C-like,
-and are indexed with named fields, and cell arrays, where each element
-of the array can have a different data type and or shape.  Multiple
-input arguments and return values of functions are organized as
-another data container, the comma separated list.
+Octave includes support for three different mechanisms to contain arbitrary
+data types in the same variable: Structures, which are C-like, and are indexed
+with named fields; containers.Map objects, which store data in key/value pairs;
+and cell arrays, where each element of the array can have a different data type
+and or shape.  Multiple input arguments and return values of functions are
+organized as another data container, the comma separated list.
 
 @menu
 * Structures::
+* containers.Map::
 * Cell Arrays::
 * Comma Separated Lists::
 @end menu
@@ -537,6 +538,18 @@
 
 @DOCSTRING(struct2cell)
 
+@node containers.Map
+@section containers.Map
+@cindex Map
+@cindex key/value store
+@cindex hash table
+
+@c FIXME: Need to fill in documentation on what a Map is, when to use it over
+@c        other container types, how to perform basic operations with a Map. 
+
+@c FIXME: Currently have trouble getting documentation for classdef functions.
+@DOCSTRING(containers.Map)
+
 @node Cell Arrays
 @section Cell Arrays
 @cindex cell arrays
--- a/doc/interpreter/octave.texi	Wed Apr 05 09:23:20 2017 -0400
+++ b/doc/interpreter/octave.texi	Wed Apr 05 10:55:35 2017 -0700
@@ -309,6 +309,7 @@
 Data Containers
 
 * Structures::
+* containers.Map::
 * Cell Arrays::
 * Comma Separated Lists::
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/+containers/Map.m	Wed Apr 05 10:55:35 2017 -0700
@@ -0,0 +1,670 @@
+## Copyright (C) 2017 Guillaume Flandin
+##
+## 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
+## <http://www.gnu.org/licenses/>.
+
+## -*- texinfo -*-
+## @deftypefn  {} {@var{m} =} containers.Map ()
+## @deftypefnx {} {@var{m} =} containers.Map (@var{keys}, @var{vals})
+## @deftypefnx {} {@var{m} =} containers.Map (@var{keys}, @var{vals}, "UniformValues", @var{is_uniform})
+## @deftypefnx {} {@var{m} =} containers.Map ("KeyType", @var{kt}, "ValueType", @var{vt}))
+##
+## Create an object of the containers.Map class that stores a list of key/value
+## pairs.
+##
+## @var{keys} is an array of @emph{unique} keys for the map.  The keys can be
+## numeric scalars or strings.  The type for numeric keys may be one of
+## @qcode{"double"}, @qcode{"single"}, @qcode{"int32"}, @qcode{"uint32"},
+## @qcode{"int64"}, or @qcode{"uint64"}.  Other numeric or logical keys will
+## be converted to @qcode{"double"}.  A single string key may be entered as is.
+## Multiple string keys are entered as a cell array of strings.
+##
+## @var{vals} is an array of values for the map with the @emph{same} number
+## of elements as @var{keys}.
+##
+## When called with no input arguments a default map is created with strings
+## as the key type and "any" as the value type.
+##
+## The @qcode{"UniformValues"} option specifies specifies whether the values of
+## the map must be strictly of the same type.  If @var{is_uniform} is true, any
+## values which would be added to the map are first validated to ensure they
+## are of the correct type.
+##
+## When called with @qcode{"KeyType"} and @qcode{"ValueType"} arguments, create
+## an empty map with the specified types.  The inputs @var{kt} and @var{vt} are
+## the types for the keys and values of the map respectively.  Allowed values
+## for @var{kt} are @qcode{"char"}, @qcode{"double"}, @qcode{"single"},
+## @qcode{"int32"}, @qcode{"uint32"}, @qcode{"int64"}, @qcode{"uint64"}.
+## Allowed values for @var{vt} are @qcode{"any"}, @qcode{"char"},
+## @qcode{"double"}, @qcode{"single"}, @qcode{"int32"}, @qcode{"uint32"},
+## @qcode{"int64"}, @qcode{"uint64"}, @qcode{"logical"}.
+##
+## The return value @var{m} is an object of the containers.Map class.
+## @seealso{struct}
+## @end deftypefn
+
+## -*- texinfo -*-
+## @deftypefn {} {} Map.Count ()
+## Return the number of key/value pairs in the map, as a uint64.
+## @end deftypefn
+##
+## @deftypefn {} {} Map.KeyType ()
+## Return the key type.
+##
+## Possible values are listed above when describing input variable @var{kt}.
+## @end deftypefn
+##
+## @deftypefn {} {} Map.ValueType ()
+## Return the value type.
+##
+## Possible values are listed above when describing input variable @var{vt}.
+## @end deftypefn
+
+## -*- texinfo -*-
+## @deftypefn {} {@var{} =} Map.isKey (@var{keySet})
+## Return a logical array which is true where the elements of @var{keySet} are
+## keys of the map and false otherwise.
+##
+## @var{keySet} is a cell array of keys.  If a single key is being checked, it
+## can be entered directly as a scalar value or a char vector.
+## @end deftypefn
+##
+## @deftypefn {} {@var{key} =} Map.keys ()
+## Return the sorted list of all keys of the map as a cell vector.
+## @end deftypefn
+##
+## @deftypefn {} {@var{l} =} Map.length ()
+## Return the number of key/value pairs in the map.
+## @end deftypefn
+##
+## @deftypefn {} {} Map.remove (@var{keySet})
+## Remove the list of key/value pairs specified by a cell array of keys
+## @var{keySet} from the map.
+##
+## @var{keySet}) can also be a scalar value or a string when specifying a
+## single key.
+## @end deftypefn
+##
+## @deftypefn  {} {@var{l} =} Map.size (@var{n})
+## @deftypefnx {} {@var{sz} =} Map.size ()
+## @deftypefnx {} {@var{dim_1}, @dots{}, @var{dim_n} =} Map.size ()
+## If @var{n} is 1, return the number of key/value pairs in the map, otherwise
+## return 1.
+## Without input arguments, return vector @code{[@var{l}, 1]} where @var{l} is
+## the number of key/value pairs in the map.
+## With multiple outputs, return @code{[@var{l}, @dots{}, 1]}.
+## @end deftypefn
+##
+## @deftypefn  {} {@var{val} =} Map.values ()
+## @deftypefnx {} {@var{val} =} Map.values (@var{keySet})
+## Return the list of all the values stored in the map as a cell vector.
+##
+## If @var{keySet}, a cell array of keys is provided, the corresponding
+## values will be returned.
+## @end deftypefn
+
+classdef Map < handle
+
+  properties (GetAccess = public, SetAccess = private)
+    KeyType   = "char";
+    ValueType = "any";
+  endproperties
+
+  properties (Dependent, SetAccess = protected)
+    Count = 0;
+  endproperties
+
+  properties (private)
+    map = struct ();
+
+    numeric_keys = false;
+  endproperties
+
+  methods (Access = public)
+
+    function this = Map (varargin)
+      if (nargin == 0)
+        ## Empty object with "char" key type and "any" value type.
+      elseif (nargin == 2 ||
+              (nargin == 4 && strcmpi (varargin{3}, "UniformValues")))
+        ## Get Map keys
+        keys = varargin{1};
+        if (! iscell (keys))
+          if (isnumeric (keys) || islogical (keys))
+            keys = num2cell (keys);
+          else
+            keys = { keys };
+          endif
+        endif
+        if (isempty (keys))
+          error ("containers.Map: empty keys are not allowed");
+        endif
+        keys = keys(:);        # Use Nx1 column vector for implementation
+
+        ## Get Map values
+        vals = varargin{2};
+        if (! iscell (vals))
+          if ((isnumeric (vals) || islogical (vals)) && ! isscalar (keys))
+            vals = num2cell (vals);
+          else
+            vals = { vals };
+          endif
+        endif
+        vals = vals(:);
+        if (numel (keys) != numel (vals))
+          error ("containers.Map: the number of keys and values must match");
+        endif
+
+        ## Determine KeyType
+        kt = unique (cellfun (@class, keys, "UniformOutput", false));
+        if (numel (kt) == 1)
+          ## Single key type--most common case
+          if (strcmp (kt{1}, "char"))
+            ## String is most common key type
+          else
+            if (! all (cellfun ("isreal", keys)
+                       & (cellfun ("numel", keys) == 1)))
+              error ("containers.Map: keys must be real scalar numeric values or char vectors");
+            endif
+            if (any (strcmp (kt{1},
+                             {"logical", "int8", "uint8", "int16", "uint16"})))
+              kt = { "double" };
+            endif
+          endif
+          this.KeyType = char (kt);
+        else
+          ## Multiple key types
+          if (all (ismember (kt, {"double", "single","int8", "uint8", ...
+                                  "int16", "uint16", "int32", "uint32", ...
+                                  "int64", "uint64", "logical"})))
+            warning ("containers.Map: all keys will be converted to double");
+            this.KeyType = "double";
+          else
+            error ("containers.Map: all keys must be the same data type");
+          endif
+        endif
+
+        ## Determine ValueType
+        vt = unique (cellfun (@class, vals, "UniformOutput", false));
+        if (numel (vt) == 1
+            && any (strcmp (vt{1}, {"char", "logical", "double", "single", ...
+                                    "int32", "uint32", "int64", "uint64"})))
+          this.ValueType = vt{1};
+        else
+          this.ValueType = "any";
+        endif
+
+        ## Process UniformValues option
+        if (nargin == 4)
+          UniformValues = varargin{4};
+          if (! isscalar (UniformValues)
+              || ! (islogical (UniformValues) || isnumeric (UniformValues)))
+            error ("containers.Map: 'UniformValues' must be a logical scalar");
+          endif
+
+          if (UniformValues)
+            if (! strcmp (this.ValueType, "char")
+                && (! isscalar (vt) || ! all (cellfun (@numel, vals) == 1)))
+              error ("containers.Map: when 'UniformValues' is true, all values must be scalars of the same data type");
+            endif
+          else
+            this.ValueType = "any";
+          endif
+        endif
+
+        ## Check type of keys and values, and define numeric_keys
+        check_types (this);
+
+        ## Sort keys (faster than call to sort_keys once encoded)
+        if (this.numeric_keys)
+          [~, I] = sort (cell2mat (keys));
+        else
+          [~, I] = sort (keys);
+        endif
+        keys = keys(I);
+        vals = vals(I);
+        ## Convert keys to char vectors
+        keys = encode_keys (this, keys);
+        ## Fill in the Map
+        this.map = cell2struct (vals, keys);
+      elseif (nargin == 4)
+        for i = [1, 3]
+          switch (lower (varargin{i}))
+            case "keytype"
+              this.KeyType = varargin{i+1};
+            case "valuetype"
+              this.ValueType = varargin{i+1};
+            otherwise
+              error ("containers.Map: missing parameter name 'KeyType' or 'ValueType'");
+          endswitch
+        endfor
+        check_types (this);
+      else
+        error ("containers.Map: incorrect number of inputs specified");
+      endif
+    endfunction
+
+    function keySet = keys (this)
+      keySet = fieldnames (this.map).';
+      keySet = decode_keys (this, keySet);
+    endfunction
+
+    function valueSet = values (this, keySet)
+      if (nargin == 1)
+        valueSet = struct2cell (this.map).';
+      else
+        if (! iscell (keySet))
+          error ("containers.Map: input argument 'keySet' must be a cell");
+        endif
+        keySet = encode_keys (this, keySet);
+        valueSet = cell (size (keySet));
+        for i = 1:numel (valueSet)
+          if (! isfield (this.map, keySet{i}))
+            error ("containers.Map: key <%s> does not exist", keySet{i});
+          endif
+          valueSet{i} = this.map.(keySet{i});
+        endfor
+      endif
+    endfunction
+
+    function tf = isKey (this, keySet)
+      if (! iscell (keySet))
+        if (isnumeric (keySet) || islogical (keySet))
+          keySet = num2cell (keySet);
+        else
+          keySet = { keySet };
+        endif
+      endif
+      tf = false (size (keySet));
+      in = cellfun ("isnumeric", keySet) | cellfun ("islogical", keySet);
+      if (! this.numeric_keys)
+        in = !in;
+      endif
+      keySet = encode_keys (this, keySet(in));
+      tf(in) = isfield (this.map, keySet);
+    endfunction
+
+    function this = remove (this, keySet)
+      if (! iscell (keySet))
+        if (isnumeric (keySet) || islogical (keySet))
+          keySet = num2cell (keySet);
+        else
+          keySet = { keySet };
+        endif
+      endif
+      in = cellfun ("isnumeric", keySet) | cellfun ("islogical", keySet);
+      if (! this.numeric_keys)
+        in = !in;
+      endif
+      keySet = encode_keys (this, keySet(in));
+      in = isfield (this.map, keySet);
+      this.map = rmfield (this.map, keySet(in));
+    endfunction
+
+    function varargout = size (this, n)
+      c = length (this);
+      if (nargin == 1)
+        if (nargout <= 1)
+          varargout = { [c 1] };
+        else
+          varargout{1} = c;
+          [varargout{2:nargout}] = deal (1);
+        endif
+      else
+        if (n == 1)
+          varargout = { c };
+        else
+          varargout = { 1 };
+        endif
+      endif
+    endfunction
+
+    function len = length (this)
+      len = double (this.Count);
+    endfunction
+
+    function tf = isempty (this)
+      tf = (this.Count == 0);
+    endfunction
+
+    function count = get.Count (this)
+      count = uint64 (numfields (this.map));
+    endfunction
+
+    function sref = subsref (this, s)
+      switch (s(1).type)
+        case "."
+          switch (s(1).subs)
+            case "keys"
+              sref = keys (this);
+            case "values"
+              sref = values (this);
+            case "size"
+              sref = size (this);
+            case "length"
+              sref = length (this);
+            case "isempty"
+              sref = isempty (this);
+            case "Count"
+              sref = this.Count;
+            case "KeyType"
+              sref = this.KeyType;
+            case "ValueType"
+              sref = this.ValueType;
+            case {"isKey", "remove"}
+              if (numel (s) < 2 || numel (s(2).subs) != 1)
+                error ("containers.Map: input argument 'KeySet' is missing");
+              endif
+              sref = feval (s(1).subs, this, s(2).subs{1});
+              s = s(3:end);
+            otherwise
+              error ("containters.Map: unknown property '%s'", s(1).subs);
+          endswitch
+        case "()"
+          key = s(1).subs{1};
+          if ((! this.numeric_keys && ! ischar (key))
+              || (this.numeric_keys && (! (isnumeric (key) || islogical (key))
+                                        || ! isscalar (key))))
+            error ("containers.Map: specified key type does not match the type of this container");
+          endif
+          key = encode_keys (this, key);
+          if (! isfield (this.map, key))
+            error ("containers.Map: specified key <%s> does not exist", key);
+          endif
+          sref = this.map.(key);
+        otherwise
+          error ("containers.Map: only '()' indexing is supported");
+      endswitch
+      if (numel (s) > 1)
+        sref = subsref (sref, s(2:end));
+      endif
+    endfunction
+
+    function this = subsasgn (this, s, val)
+      if (numel (s) > 1)
+        error ("containers.Map: only one level of indexing is supported");
+      endif
+      switch (s(1).type)
+        case "."
+          error ("containers.Map: properties are read-only");
+        case "()"
+          key = s(1).subs{1};
+          if ((! this.numeric_keys && ! ischar (key))
+              || (this.numeric_keys && (! (isnumeric (key) || islogical (key))
+                                        || ! isscalar (key))))
+            error ("containers.Map: specified key type does not match the type of this container");
+          endif
+          if (! strcmp (this.ValueType, "any"))
+            if ((strcmp (this.ValueType, "char") && ! ischar (val))
+                || (! strcmp (this.ValueType, "char")
+                    && (! (isnumeric (val) || islogical (val))
+                        || ! isscalar (val))))
+              error ("containers.Map: specified value type does not match the type of this container");
+            endif
+            val = feval (this.ValueType, val);
+          endif
+          key = encode_keys (this, key);
+          if (isfield (this.map, key))
+            this.map.(key) = val;
+          else
+            this.map.(key) = val;
+            this = sort_keys (this);
+          endif
+        case "{}"
+          error ("containers.Map: only '()' indexing is supported for assigning values");
+      endswitch
+    endfunction
+
+    ## FIXME: Why not implement this?  Octave is a superset of Matlab and
+    ## just because they failed to implement this doesn't mean we need to.
+    function newobj = horzcat (varargin)
+      error ("containers.Map: horizontal concatenation is not supported");
+    endfunction
+
+    function newobj = vertcat (varargin)
+      ## When concatenating maps, the data type of all values must be
+      ## consistent with the ValueType of the leftmost map.
+      keySet = cell (1, 0);
+      for i = 1:numel (varargin)
+        keySet = [keySet, keys(varargin{i})];
+      endfor
+      valueSet = cell (1, 0);
+      for i = 1:numel (varargin)
+        valueSet = [valueSet, values(varargin{i})];
+      endfor
+      newobj = containers.Map (keySet, valueSet);
+    endfunction
+
+    function disp (this)
+      printf ("  containers.Map object with properties:\n\n");
+      printf (["    Count     : %d\n" ...
+               "    KeyType   : %s\n" ...
+               "    ValueType : %s\n\n"],
+               this.Count, this.KeyType, this.ValueType);
+    endfunction
+
+  endmethods
+
+  methods (Access = private)
+
+    ## All keys are encoded as strings.
+    ## For numeric keys, this requires conversion.
+    function keys = encode_keys (this, keys)
+      if (iscellstr (keys) || ischar (keys))
+        return;
+      endif
+      cell_input = iscell (keys);
+      if (! cell_input)
+        keys = { keys };
+      endif
+      if (! all (cellfun ("isclass", keys, this.KeyType)))
+        ## Convert input set to KeyType.  This is rarely necessary.
+        keys = cellfun (@(x) feval (this.KeyType, x), keys,
+                        "UniformOutput", false);
+      endif
+      ## FIXME: Replace with csprintf when it becomes available.
+      keys = ostrsplit (sprintf ("%.16g\n", keys{:}), "\n");
+      keys(end) = [];
+      if (! cell_input)
+        keys = char (keys);
+      endif
+
+    endfunction
+
+    function keys = decode_keys (this, keys)
+      if (this.numeric_keys)
+        keys = str2double (char (keys));
+        if (! strcmp (this.KeyType, "double"))
+          keys = feval (this.KeyType, keys);
+        endif
+        keys = mat2cell (keys, ones (numel (keys), 1), 1);
+      endif
+    endfunction
+
+    function this = sort_keys (this)
+      this.map = orderfields (this.map);
+    endfunction
+
+    function check_types (this)
+      switch (this.KeyType)
+        case {"char"}
+          this.numeric_keys = false;
+        case {"single", "double", "int32", "uint32", "int64", "uint64"}
+          this.numeric_keys = true;
+        otherwise
+          error ("containers.Map: unsupported KeyType");
+      endswitch
+      if (! any (strcmp (this.ValueType, {"char","double", "single", ...
+                                          "int32", "uint32", "int64", ...
+                                          "uint64", "logical", "any"})))
+        error ("containers.Map: unsupported ValueType");
+      endif
+    endfunction
+
+  endmethods
+
+endclassdef
+
+
+%!test
+%! m = containers.Map ();
+%! assert (m.Count, uint64 (0));
+%! assert (length (m), 0);
+%! assert (size (m, 1), 0);
+%! assert (size (m, 2), 1);
+%! assert (isempty (m));
+%! assert (isempty (keys (m)));
+%! assert (isempty (values (m)));
+%! assert (isKey (m, "Octave"), false);
+%! assert (isKey (m, 42), false);
+
+%!test
+%! key = {"One", "Two", "Three", "Four"};
+%! val = [1, 2, 3, 4];
+%! m = containers.Map (key, val);
+%! assert (m.KeyType, "char");
+%! assert (m.ValueType, "double");
+%! assert (m.Count, uint64 (4));
+%! assert (m("Two"), 2);
+%! m("Five") = 5;
+%! key2 = {"Six", "Seven", "Eight"};
+%! val2 = [6, 7, 8];
+%! m2 = containers.Map (key2, val2);
+%! m = [m; m2];
+%! assert (m.Count, uint64 (8));
+%! k = keys (m);
+%! assert (isempty (setdiff (k, [key, "Five", key2])));
+%! v = values (m, {"Three", "Four", "Five"});
+%! assert (v, {3, 4, 5});
+%! remove (m, {"Three", "Four"});
+%! k = keys (m);
+%! assert (numel (k), 6);
+
+%!test
+%! key = [1, 2, 3, 4];
+%! val = {"One", "Two", "Three", "Four"};
+%! m = containers.Map (key, val);
+%! assert (m.KeyType, "double");
+%! assert (m.ValueType, "char");
+
+%!test
+%! key = [2, 3, 4];
+%! val = {eye(2), eye(3), eye(4)};
+%! m = containers.Map (key, val);
+%! assert (m(3), eye(3));
+%! assert (m(2)(2,2), 1);
+
+%!test
+%! m = containers.Map ("KeyType","char", "ValueType","int32");
+%! assert (m.KeyType, "char");
+%! assert (m.ValueType, "int32");
+%! assert (m.Count, uint64 (0));
+%! assert (isempty (m));
+
+%!test
+%! key = {"one", "two", "three"};
+%! val = {1, 2, 3};
+%! m = containers.Map (key, val, "UniformValues",false);
+%! m("four") = "GNU";
+%! assert (values (m), {"GNU", 1, 3, 2});
+
+%!test
+%! key = [2, 3, 4];
+%! val = {2, 3, 4};
+%! types = {"int32", "uint32", "int64", "uint64", "single", "double"};
+%! for i = 1:numel (types)
+%!   k = feval (types{i}, key);
+%!   m = containers.Map (k, val);
+%!   assert (m.KeyType, types{i});
+%!   assert (isa (keys(m){1}, types{i}));
+%! endfor
+%! assert ( all (isKey (m, keys (m))));
+
+%!test
+%! key = [0, 1];
+%! val = {1, 2};
+%! types = {"logical", "int8", "uint8", "int16", "uint16"};
+%! for i = 1:numel (types)
+%!   k = feval (types{i}, key);
+%!   m = containers.Map (k, val);
+%!   assert (m.KeyType, "double");
+%!   assert (isa (keys(m){1}, "double"));
+%! endfor
+%! assert ( all (isKey (m, keys (m))));
+
+%!test
+%! key = {"a", "b"};
+%! val = {struct(), struct()};
+%! m = containers.Map (key, val);
+%! assert (m.ValueType, "any");
+%! m = containers.Map (key, val, "UniformValues", true);
+%! assert (m.ValueType, "any");
+
+%!test
+%! m = containers.Map ({"a","b","c"}, {1,2,3});
+%! assert (m.isKey("a"), true);
+%! assert (m.isKey({"a","d"}), [true, false]);
+%! m.remove("a");
+%! m.remove({"b","c"});
+%! assert (isempty (m));
+
+## Test input validation
+%!error containers.Map (1,2,3)
+%!error containers.Map (1,2,3,4,5)
+%!error <empty keys are not allowed> containers.Map ([], 1)
+%!error <number of keys and values must match> containers.Map (1, {2, 3})
+%!error <keys must be real .* values> containers.Map ({{1}}, 2)
+%!error <keys must be .* scalar .* values> containers.Map ({magic(3)}, 2)
+%!warning <keys .* converted to double>
+%! containers.Map ({1,int8(2)}, {3,4});
+%!error <keys must be the same data type> containers.Map ({1, {2}}, {3,4})
+%!error <'UniformValues' must be a logical scalar>
+%! containers.Map (1,2, 'UniformValues', ones(2,2))
+%!error <'UniformValues' must be a logical scalar>
+%! containers.Map (1,2, 'UniformValues', {true})
+%!error <all values must be scalars of the same data type>
+%! containers.Map ({1,2}, {3, uint32(4)}, "UniformValues", true)
+%!error <missing parameter name 'KeyType'>
+%! containers.Map ("keytype", "char", "vtype", "any")
+%!error <'keySet' must be a cell>
+%! m = containers.Map ();
+%! values (m, 1);
+%#!error <key .foobar. does not exist>
+%! m = containers.Map ();
+%! values (m, "foobar");
+%!error <input argument 'KeySet' is missing>
+%! m = containers.Map ();
+%! m.isKey (1,2);
+%!error <unknown property 'foobar'>
+%! m = containers.Map ();
+%! m.foobar;
+%!error <key type does not match the type of this container>
+%! m = containers.Map ("a", 1);
+%! m(1);
+%!error <specified key .b. does not exist>
+%! m = containers.Map ("a", 1);
+%! m("b");
+%!error <only '\(\)' indexing is supported>
+%! m = containers.Map ("a", 1);
+%! m{1};
+%!error <horizontal concatenation is not supported>
+%! m1 = containers.Map ("a", 1);
+%! m2 = containers.Map ("b", 2);
+%! m3 = horzcat (m1, m2);
+%!error <unsupported KeyType>
+%! m1 = containers.Map ("KeyType", "cell", "ValueType", "any");
+%!error <unsupported ValueType>
+%! m1 = containers.Map ("KeyType", "char", "ValueType", "cell");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/+containers/module.mk	Wed Apr 05 10:55:35 2017 -0700
@@ -0,0 +1,14 @@
+FCN_FILE_DIRS += scripts/+containers
+
+scripts_containers_FCN_FILES = \
+  scripts/+containers/Map.m
+
+scripts_containersdir = $(fcnfiledir)/strings
+
+scripts_containers_DATA = $(scripts_containers_FCN_FILES)
+
+FCN_FILES += $(scripts_containers_FCN_FILES)
+
+PKG_ADD_FILES += scripts/+containers/PKG_ADD
+
+DIRSTAMP_FILES += scripts/+containers/$(octave_dirstamp)
--- a/scripts/module.mk	Wed Apr 05 09:23:20 2017 -0400
+++ b/scripts/module.mk	Wed Apr 05 10:55:35 2017 -0700
@@ -4,6 +4,7 @@
 scripts_DISTCLEANFILES =
 scripts_MAINTAINERCLEANFILES =
 
+include scripts/+containers/module.mk
 include scripts/audio/module.mk
 include scripts/deprecated/module.mk
 include scripts/elfun/module.mk