########################################################################
##
## 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};
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
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};
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};
if (! isConfigured (this))
this.key_type = class (key);
this.value_type = class (val);
endif
## TODO ## key and val can be an arrays
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 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
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]);