######################################################################## ## ## Copyright (C) 2022 The Octave Project Developers ## ## See the file COPYRIGHT.md in the top-level directory of this ## distribution or . ## ## 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 ## . ## ######################################################################## classdef dictionary ## -*- texinfo -*- ## @deftypefn {} {@var{d} =} dictionary () ## @deftypefnx {} {@var{d} =} dictionary (@var{keys}, @var{vals}) ## Construct a new dictionary object. ## ## @seealso{struct, containers.Map} ## @end deftypefn properties (private) map = struct (); all_keys = {}; key_type = ""; value_type = ""; endproperties methods (Access = public) function this = dictionary (varargin) if (nargin == 0) ## Unconfigured dictionary with no keys or values elseif (numel (varargin) == 1 && isa (varargin{1}, "dictionary")) ## Copy constructor this = varargin{1}; elseif (mod (numel (varargin), 2) == 0) for p = 1:2:numel (varargin) k = varargin{p}; v = varargin{p+1}; ## Promote char arrays to string if implemented if (exist ("string", "class")) if (ischar (k)) k = string (k); endif if (ischar (v)) v = string (v); endif endif if (numel (k) != numel (v) && numel (v) != 1) error ("dictionary: number of keys and values are not compatible"); endif ## Configure data type of keys and values if (isempty (this.key_type)) this.key_type = class (k); this.value_type = class (v); endif ## Insert key/value pairs after conversion for i = 1:numel (k) key = k(i); if (!isa (key, this.key_type)) key = encode_data (this.key_type, k(i)); endif h = num2hex (keyHash (key)); if (! isfield (this.map, h)) this.all_keys{end+1} = key; endif if (numel (v) == 1) if (isa (v, this.value_type)) this.map.(h) = v; else this.map.(h) = encode_data (this.value_type, v); endif else if (isa (v, this.value_type)) this.map.(h) = v(i); else this.map.(h) = encode_data (this.value_type, v(i)); endif endif endfor endfor else error ("dictionary: a value is expected after a key"); endif endfunction function e = entries (this, fmt) ## -*- texinfo -*- ## @deftypefn {} {} dictionary.entries (@var{fmt}) ## @end deftypefn if (nargin < 2) fmt = "struct"; # default should be "table" else if (! ischar (fmt) && ! isstring (fmt)) error ("dictionary: 'format' option has to be a string"); endif endif k = keys (this, "cell"); v = values (this, "cell"); if (startsWith ("cell", fmt, "IgnoreCase", true)) e = [k, v]; elseif (startsWith ("struct", fmt, "IgnoreCase", true)) e = struct ("Key", k, "Value", v); elseif (startsWith ("table", fmt, "IgnoreCase", true)) error ("dictionary: 'table' format option is not implemented"); else error ("dictionary: 'format' option can only be 'struct', 'cell' or 'table'"); endif endfunction function k = keys (this, fmt) ## -*- texinfo -*- ## @deftypefn {} {} dictionary.keys (@var{fmt}) ## @end deftypefn if (! isConfigured (this)) error ("dictionary: dictionary is not configured"); endif if (nargin < 2) fmt = "uniform"; else if (! ischar (fmt) && ! isstring (fmt)) error ("dictionary: 'format' option has to be a string"); endif endif if (startsWith ("cell", fmt, "IgnoreCase", true)) k = this.all_keys(:); elseif (startsWith ("uniform", fmt, "IgnoreCase", true)) try k = [this.all_keys{:}].'; catch error ("dictionary: cannot concatenate keys. Use 'cell' option."); end_try_catch else error ("dictionary: 'format' option can only be 'cell'"); endif endfunction function v = values (this, fmt) ## -*- texinfo -*- ## @deftypefn {} {} dictionary.values (@var{fmt}) ## @end deftypefn if (! isConfigured (this)) error ("dictionary: dictionary is not configured"); endif if (nargin < 2) fmt = "uniform"; else if (! ischar (fmt) && ! isstring (fmt)) error ("dictionary: 'format' option has to be a string"); endif endif if (startsWith ("cell", fmt, "IgnoreCase", true)) v = struct2cell (this.map); elseif (startsWith ("uniform", fmt, "IgnoreCase", true)) v = struct2cell (this.map); try v = [v{:}].'; catch error ("dictionary: cannot concatenate values. Use 'cell' option."); end_try_catch else error ("dictionary: 'format' option can only be 'cell'"); endif endfunction function tf = isKey (this, key) ## -*- texinfo -*- ## @deftypefn {} {} dictionary.isKey (@var{key}) ## @end deftypefn if (! isConfigured (this)) error ("dictionary: dictionary is not configured"); endif ## Promote char arrays to string if implemented if (ischar (key) && exist ("string", "class")) key = string (key); endif tf = false (size (key)); for k = 1:numel (key) cur_key = key(k); if (!isa (cur_key, this.key_type)) cur_key = encode_data (this.key_type, cur_key); endif h = num2hex (keyHash (cur_key)); tf(k) = isfield (this.map, h); ## try ## this.map.(h); ## tf = true; ## catch ## tf = false; ## end_try_catch endfor endfunction function tf = isConfigured (this) ## -*- texinfo -*- ## @deftypefn {} {} dictionary.isConfigured () ## @end deftypefn tf = ! isempty (this.key_type); endfunction function [keyType, valueType] = types (this) ## -*- texinfo -*- ## @deftypefn {} {} dictionary.types () ## @end deftypefn if (! isConfigured (this)) keyType = valueType = "missing"; # replace with string scalar else keyType = this.key_type; valueType = this.value_type; endif endfunction function count = numEntries (this) ## -*- texinfo -*- ## @deftypefn {} {} dictionary.numEntries () ## @end deftypefn count = numel (this.all_keys); endfunction function sref = subsref (this, s) switch (s(1).type) case "." extra = {}; if (numel (s) > 1 && strcmp (s(2).type, "()")) if (! isempty (s(2).subs)) extra = s(2).subs; endif s(2) = []; endif switch (s(1).subs) case "keys" sref = keys (this, extra{:}); case "values" sref = values (this, extra{:}); case "types" sref = types (this, extra{:}); case "numEntries" sref = numEntries (this, extra{:}); case "isConfigured" sref = isConfigured (this, extra{:}); case "isKey" sref = isKey (this, extra{:}); otherwise error ("dictionary: unknown property '%s'", s(1).subs); endswitch case "()" if (isempty (s(1).subs)) error ("dictionary: no key specified"); endif if (! isConfigured (this)) error ("dictionary: dictionary not configured"); endif key = s(1).subs{1}; ## Promote char arrays to string if implemented if (ischar (key) && exist ("string", "class")) key = string (key); endif sref = cell (size (key)); for k = 1:numel (key) cur_key = key(k); if (!isa (cur_key, this.key_type)) cur_key = encode_data (this.key_type, cur_key); endif h = num2hex (keyHash (cur_key)); if (isfield (this.map, h)) sref{k} = this.map.(h); else error ("dictionary: unknown key"); endif ## try ## sref = this.map.(h); ## catch ## error ("dictionary: unknown key"); ## end_try_catch endfor try sref = [sref{:}]; catch error ("dictionary: cannot concatenate values"); end_try_catch sref = reshape (sref, size (key)); otherwise error ("dictionary: '{}' indexing is not 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 ("dictionary: only one level of indexing is supported"); endif switch (s(1).type) case "()" key = s(1).subs{1}; ## Promote char arrays to string if implemented if (exist ("string", "class")) if (ischar (key)) key = string (key); endif if (ischar (val)) val = string (val); endif endif if (! isConfigured (this)) this.key_type = class (key); this.value_type = class (val); endif if (numel (key) != numel (val) && numel (val) != 1 && numel (val) != 0) error ("dictionary: number of keys and values are not compatible"); endif for k = 1:numel (key) cur_key = key(k); if (!isa (cur_key, this.key_type)) cur_key = encode_data (this.key_type, cur_key); endif h = num2hex (keyHash (cur_key)); if (isa (val, "double") && isequal (size (val), [0 0])) if (isfield (this.map, h)) this.map = rmfield (this.map, h); ## finding index of key to delete is O(N): another hashmap for indices? this.all_keys(cellfun (@(x) isequal (x, cur_key), this.all_keys)) = []; else ## non existing key - ignore endif continue; endif if (! isfield (this.map, h)) this.all_keys{end+1} = cur_key; endif ## try ## this.map.(h); ## catch ## this.all_keys{end+1} = cur_key; ## end_try_catch if (numel (val) == 1) if (isa (val, this.value_type)) this.map.(h) = val; else this.map.(h) = encode_data (this.value_type, val); endif else if (isa (val, this.value_type)) this.map.(h) = val(k); else this.map.(h) = encode_data (this.value_type, val(k)); endif endif endfor otherwise error ("dictionary: only '()' indexing is supported for assigning values"); endswitch endfunction function s = saveobj (this) s = struct ("entries", {entries(this, "cell")}); endfunction function newobj = horzcat (varargin) error ("dictionary: horizontal concatenation is not allowed"); endfunction function newobj = vertcat (varargin) error ("dictionary: vertical concatenation is not allowed"); endfunction function disp (this) if (! isConfigured (this)) printf ("dictionary object not configured.\n"); elseif (numEntries (this) == 0) printf ("dictionary object (%s --> %s) with no entries.\n", this.key_type, this.value_type); else entry_str = ifelse (numEntries (this) == 1, "entry", "entries"); printf ("dictionary object (%s --> %s) with %d %s:\n\n", this.key_type, this.value_type, numEntries (this), entry_str); for i = 1:numEntries (this) key = this.all_keys{i}; h = num2hex (keyHash (key)); if (! ischar (key)), key = sprintf ("<%s>", class (key)); end val = this.map.(h); if (! ischar (val)), val = sprintf ("<%s>", class (val)); end printf (" %s --> %s\n", key, val); endfor endif endfunction endmethods methods (Static = true) function this = loadobj (s) if (isstruct (s)) this = dictionary (s.entries'{:}); else this = s; end endfunction endmethods endclassdef function ret = encode_data (t, d) if (isa (d, t)) return; endif switch (t) case {"cell", "struct"} error ("dictionary: invalid conversion from '%s' to '%s'", ... class (d), t); case {"char"} if (isnumeric (d)) ret = num2str (d); else ret = feval (t, d); endif otherwise ret = feval (t, d); endswitch endfunction ## Test unconfigured dictionary %!test %! d = dictionary (); %! assert (isConfigured (d), false); %! assert (d.isConfigured, false); %! assert (length (d), 1); %! assert (numel (d), 1); %! assert (ndims (d), 2); %! assert (size (d), [1 1]); %! assert (numEntries (d), 0); %! assert (d.numEntries, 0); %! assert (types (d), "missing"); %! assert (d.types, "missing"); %! [key_type, val_type] = types (d); %! assert (key_type, "missing"); %! assert (val_type, "missing"); ## Test constructor %!test %! d = dictionary (dictionary); %! assert (class (d), "dictionary"); %!test %! d = dictionary ("a", 1); %! assert (isConfigured (d), true); %! assert (numEntries (d), 1); %! assert (types (d), "char"); %! [key_type, val_type] = types (d); %! assert (key_type, "char"); %! assert (val_type, "double"); %!test %! d = dictionary ("a", 1, "b", 2, "c", 3); %! assert (isConfigured (d), true); %! assert (numEntries (d), 3); %! assert (types (d), "char"); %! [key_type, val_type] = types (d); %! assert (key_type, "char"); %! assert (val_type, "double"); %! assert (keys (d), strvcat ("a", "b", "c")); %! assert (keys (d, "cell"), {"a"; "b"; "c"}); %! assert (values (d), [1 2 3]'); %! assert (values (d, "cell"), {1; 2; 3}); %! assert (entries (d), struct ("Key", {"a"; "b"; "c"}, "Value", {1; 2; 3})); %! assert (entries (d, "struct"), struct ("Key", {"a"; "b"; "c"}, "Value", {1; 2; 3})); %! assert (entries (d, "cell"), {"a", 1; "b", 2; "c", 3}); %! assert (isKey (d, "a"), true); %! assert (isKey (d, "d"), false); ## Test configuration %!test %! d = dictionary; %! assert (isConfigured (d), false); %! d(1) = "a"; %! assert (isConfigured (d), true); %! [key_type, val_type] = types (d); %! assert (key_type, "double"); %! assert (val_type, "char"); %!test %! d = dictionary ([], ''); %! assert (isConfigured (d), true); %! [key_type, val_type] = types (d); %! assert (key_type, "double"); %! assert (val_type, "char"); ## Test isKey/numEntries/keys/values methods %!test %! d = dictionary ("a", 1, "b", 2); %! assert (numEntries (d), 2); %! assert (d("a"), 1); %! assert (isKey (d, "c"), false); %! d("c") = 3; %! assert (numEntries (d), 3); %! assert (d("c"), 3); %! assert (isKey (d, "c"), true); %! d("a") = pi; %! assert (numEntries (d), 3); %! assert (d("a"), pi); %! assert (values (d), [pi 2 3]'); %! assert (keys (d, "cell"), {"a"; "b"; "c"}); %! d("b") = []; %! assert (numEntries (d), 2); %! assert (keys (d, "cell"), {"a"; "c"}); %! assert (values (d), [pi; 3]); ## Test cell keys %!test %! d = dictionary ({"a"}, 1, {"b"}, 2); %! d({"c"}) = 3; %! [key_type, val_type] = types (d); %! assert (key_type, "cell"); %! assert (val_type, "double"); %! assert (d({"c"}), 3); ## Test struct keys %! d = dictionary (struct ("a", 1), {1}, struct ("a", 2), {2}); %! d(struct ("b", 1)) = {3}; %! [key_type, val_type] = types (d); %! assert (key_type, "struct"); %! assert (val_type, "cell"); %! assert (d(struct ("a", 2)), {2}); ## Test vectorized susbref %! d = dictionary (1, 1, 2, 2, 3, 3, 4, 4); %! assert (d([1 2;3 4]), [1 2;3 4]); ## Test input validation %!error dictionary (1); %!error dictionary ("a"); %!error dictionary (1, "a", 2); %!error keys (dictionary); %!error values (dictionary); %!error keys (dictionary ("a", 1), {"cell"}); %!error keys (dictionary ("a", 1), "other"); %!error values (dictionary ("a", 1), {"cell"}); %!error values (dictionary ("a", 1), "other"); %!error entries (dictionary ("a", 1), struct); %!error entries (dictionary ("a", 1), "other"); %!error isKey (dictionary, "a"); %!error %! d = dictionary ("a", 1); %! d("a") = ''; %!error %! d = dictionary ({"a"}, 1); %! d(2) = 3; %!error %! d = dictionary ({"a"}, {1}); %! d({2}) = 3; %!error %! d = dictionary (struct ("a", 1,'b',2), {1}, struct ("a", 2), {2}); %! keys(d) ## Test concatenation %!error %! d = dictionary; %! [d, d]; %!error %! d = dictionary; %! [d; d]; ## Test vectorized subsref %!error %! d = dictionary (1, struct ("a", 1), 2, struct ("a", 1, "b", 2)); %! d([1 2]);