diff --git a/docs/sphinx/source/matlab-source/matpower/mp_table_subclass.m b/docs/sphinx/source/matlab-source/matpower/mp_table_subclass.m new file mode 120000 index 00000000..1057472d --- /dev/null +++ b/docs/sphinx/source/matlab-source/matpower/mp_table_subclass.m @@ -0,0 +1 @@ +../../../../../lib/mp_table_subclass.m \ No newline at end of file diff --git a/docs/sphinx/source/ref-manual/classes/index.rst b/docs/sphinx/source/ref-manual/classes/index.rst index 237644cb..71f3054a 100644 --- a/docs/sphinx/source/ref-manual/classes/index.rst +++ b/docs/sphinx/source/ref-manual/classes/index.rst @@ -237,6 +237,7 @@ Miscellaneous Classes :name: sec_misc_classes mp_table + mp_table_subclass mp/element_container mp/mapped_array mp/NODE_TYPE diff --git a/docs/sphinx/source/ref-manual/classes/mp_table_subclass.rst b/docs/sphinx/source/ref-manual/classes/mp_table_subclass.rst new file mode 100644 index 00000000..2c5c9529 --- /dev/null +++ b/docs/sphinx/source/ref-manual/classes/mp_table_subclass.rst @@ -0,0 +1,10 @@ +.. automodule:: matpower + +:raw-html:`
` + +mp_table_subclass +----------------- + +.. autoclass:: mp_table_subclass + :show-inheritance: + :members: diff --git a/lib/mp_table.m b/lib/mp_table.m index 6ec2b58f..1c0b9b84 100644 --- a/lib/mp_table.m +++ b/lib/mp_table.m @@ -53,15 +53,15 @@ % :: % % T = mp_table(var1, var2, ...); - % T = mp_table(..., 'VariableNames', {name1, name2, ...}}); - % T = mp_table(..., 'RowNames', {name1, name2, ...}}); - % T = mp_table(..., 'DimensionNames', {name1, name2, ...}}); + % T = mp_table(..., 'VariableNames', {name1, name2, ...}); + % T = mp_table(..., 'RowNames', {name1, name2, ...}); + % T = mp_table(..., 'DimensionNames', {name1, name2, ...}); args = varargin; if nargin %% extract named arguments [var_names, row_names, dim_names, args] = ... - extract_named_args(obj, args); + mp_table.extract_named_args(args); %% set default variable names nv = length(args); %% number of variables @@ -623,10 +623,19 @@ function display(obj) end end %% methods (public) - methods (Access=protected) + methods (Static) function [var_names, row_names, dim_names, args] = ... - extract_named_args(obj, args) - % used to extract named arguments pass to constructor + extract_named_args(args) + % Extracts special named constructor arguments. + % :: + % + % [var_names, row_names, dim_names, args] = extract_named_args(var1, var2, ...); + % [...] = extract_named_args(..., 'VariableNames', {name1, name2, ...}); + % [...] = extract_named_args(..., 'RowNames', {name1, name2, ...}); + % [...] = extract_named_args(..., 'DimensionNames', {name1, name2, ...}); + % + % Used to extract named arguments, ``'VariableNames'``, + % ``'RowNames'``, and ``'DimensionNames'``, to pass to constructor. var_names = {}; row_names = {}; dim_names = {}; diff --git a/lib/mp_table_subclass.m b/lib/mp_table_subclass.m new file mode 100644 index 00000000..5d5de306 --- /dev/null +++ b/lib/mp_table_subclass.m @@ -0,0 +1,187 @@ +classdef mp_table_subclass +% mp_table_subclass - Class that acts like a table but isn't one. +% +% Addresses two issues with inheriting from **table** classes (:class:`table`) +% or mp_table). +% +% 1. In MATLAB, :class:`table` is a sealed class, so you cannot inherit +% from it. You can, however, use a subclass of mp_table, but that can +% result in the next issue under Octave. +% 2. While nesting of tables works just fine in general, when using mp_table +% in Octave (at least up through 8.4.0), you cannot nest a subclass of +% mp_table inside another mp_table object because of this bug: +% https://savannah.gnu.org/bugs/index.php?65037. +% +% To work around these issues, your "table subclass" can inherit from **this** +% class. An object of this class **isn't** a :class:`table` or mp_table object, +% but rather it **contains** one and attempts to act like one. That is, it +% delegates method calls (currently only those available in mp_table, listed +% below) to the contained table object. +% +% The class of the contained table object is either :class:`table` or mp_table +% and is determined by mp_table_class. +% +% .. admonition:: Limitation +% +% In MATLAB, when nesting an mp_table_subclass object within another +% mp_table_subclass object, one cannot use multi-level indexing directly. +% E.g. If ``T2`` is a variable in ``T1`` and ``x`` is a variable in ``T2``, +% attempting ``x = T1.T2.x`` will result in an error. The indexing must +% be done in multiple steps ``T2 = T1.T2; x = T2.x``. Note: This only +% applies to MATLAB, where the contained table is a :class:`table`. It works +% just fine in Octave, where the contained table is an :class:`mp_table`. +% +% .. important:: +% +% Since the dot syntax ``T.`` is used to access table variables, +% you must use a functional syntax ``(T,...)``, as opposed to +% the object-oriented ``T.(...)``, to call methods of this class +% or subclasses, as with mp_table. +% +% mp.mp_table_subclass Properties: +% * tab - *(table or mp_table)* contained table object this class emulates +% +% mp.cost_table Methods: +% * mp_table_subclass - construct object +% * get_table - return the table stored in :attr:`tab` +% * set_table - assign a table to :attr:`tab` +% * istable - true for mp_table objects +% * size - dimensions of table +% * isempty - true if table has no columns or no rows +% * end - used to index last row or variable/column +% * subsref - indexing a table to retrieve data +% * subsasgn - indexing a table to assign data +% * horzcat - concatenate tables horizontally +% * vertcat - concatenate tables vertically +% * display - display table contents +% +% See also mp_table, mp_table_class. + +% MATPOWER +% Copyright (c) 2023, Power Systems Engineering Research Center (PSERC) +% by Ray Zimmerman, PSERC Cornell +% +% This file is part of MATPOWER. +% Covered by the 3-clause BSD License (see LICENSE file for details). +% See https://matpower.org for more info. + + properties + tab + end %% properties + + %% delegate all mp_table methods to obj.tab + methods + function obj = mp_table_subclass(varargin) + + args = varargin; + if nargin + %% extract named arguments + [var_names, row_names, dim_names, args] = ... + mp_table.extract_named_args(args); + + %% set default variable names + nv = length(args); %% number of variables + if length(var_names) < nv + for k = nv:-1:length(var_names)+1 + var_names{k} = inputname(k); + if isempty(var_names{k}) + var_names{k} = sprintf('Var%d', k); + end + end + end + args(end+1:end+2) = {'VariableNames', var_names}; + if ~isempty(row_names) + args(end+1:end+2) = {'RowNames', row_names}; + end + if ~isempty(dim_names) + args(end+1:end+2) = {'DimensionNames', dim_names}; + end + end + + table_class = mp_table_class(); + obj.tab = table_class(args{:}); + end + + function tab = get_table(obj) + % + % :: + % + % T = get_table(obj) + tab = obj.tab; + end + + function obj = set_table(obj, T) + % + % :: + % + % set_table(obj, T) + obj.tab = T; + end + + function TorF = istable(obj) + TorF = istable(obj.tab); + end + + function varargout = size(obj, varargin) + [varargout{1:nargout}] = size(obj.tab, varargin{:}); + end + + function TorF = isempty(obj) + TorF = isempty(obj.tab); + end + + function N = end(obj, k, n) + N = size(obj.tab, k); + end + + function n = numArgumentsFromSubscript(obj, varargin) + n = numArgumentsFromSubscript(obj.tab, varargin{:}); + end + + function n = numel(obj, varargin) + n = numel(obj.tab, varargin{:}); + end + + function b = subsref(obj, varargin) + b = subsref(obj.tab, varargin{:}); + if varargin{1}(1).type(1) == '(' && ... + (isa(b, 'table') || isa(b, 'mp_table')) + o = feval(class(obj)); + b = set_table(o, b); + end + end + + function obj = subsasgn(obj, varargin) + for k = 2:length(varargin) + if isa(varargin{k}, 'mp_table_subclass') + varargin{k} = get_table(varargin{k}); + end + end + obj.tab = subsasgn(obj.tab, varargin{:}); + end + + function obj = horzcat(obj, varargin) + args = varargin; + for k = 1:length(args) + if isa(args{k}, 'mp_table_subclass') + args{k} = args{k}.tab; + end + end + obj.tab = horzcat(obj.tab, args{:}); + end + + function obj = vertcat(obj, varargin) + args = varargin; + for k = 1:length(args) + if isa(args{k}, 'mp_table_subclass') + args{k} = args{k}.tab; + end + end + obj.tab = vertcat(obj.tab, args{:}); + end + + function display(obj) + obj.tab + end + end %% methods +end %% classdef diff --git a/lib/t/t_mp_table.m b/lib/t/t_mp_table.m index 758aa7b8..b146dcb8 100644 --- a/lib/t/t_mp_table.m +++ b/lib/t/t_mp_table.m @@ -20,15 +20,15 @@ verbose = 1; end -nt = 169; skip_tests_for_tablicious = 1; -table_classes = {@mp_table}; -class_names = {'mp_table'}; +table_classes = {@mp_table, @mp_table_subclass}; +class_names = {'mp_table', 'mp_table_subclass'}; if have_feature('table') table_classes = {@table, table_classes{:}}; class_names = {'table', class_names{:}}; end nc = length(table_classes); +nt = 161 + 8*nc; t_begin(nc*nt, quiet); @@ -53,8 +53,12 @@ T = table_class(); skip_oct_tab = isa(T, 'table') && have_feature('octave') && ... skip_tests_for_tablicious; - skip_ml_tab = isa(T, 'table') && have_feature('matlab') && ... - have_feature('matlab', 'vnum') < 9.010; + skip_oct_tab2 = isa(T, 'mp_table_subclass') && have_feature('table') && ... + have_feature('octave') && skip_tests_for_tablicious; + skip_ml_tab = (isa(T, 'table') || isa(T, 'mp_table_subclass')) && ... + have_feature('matlab') && have_feature('matlab', 'vnum') < 9.010; + skip_ml_tab2 = (isa(T, 'table') || isa(T, 'mp_table_subclass')) && ... + have_feature('matlab') && have_feature('matlab', 'vnum') < 9.012; t_ok(isa(T, class_names{k}), [t 'class']); t_ok(isempty(T), [t 'isempty']); @@ -95,7 +99,7 @@ t_ok(isempty(T1), [t 'true']); t = sprintf('%s : constructor - ind vars, w/names : ', cls); - if skip_oct_tab || skip_ml_tab + if skip_oct_tab || skip_ml_tab || skip_oct_tab2 T = table_class(var1, var2, var3, var4, var5, var6, ... 'VariableNames', var_names, 'RowNames', row_names); else @@ -112,7 +116,7 @@ t_is([nr, nz], [6 6], 12, [t '[nr, nz] = size(T)']); t_ok(isequal(T.Properties.VariableNames, var_names), [t 'VariableNames'] ); t_ok(isequal(T.Properties.RowNames, row_names), [t 'RowNames'] ); - if skip_oct_tab || skip_ml_tab + if skip_oct_tab || skip_ml_tab || skip_oct_tab2 t_skip(1, [t 'DimensionNames not yet supported'] ); else t_ok(isequal(T.Properties.DimensionNames, dim_names), [t 'DimensionNames'] ); @@ -183,7 +187,7 @@ %% {} indexing t = sprintf('%s : subsref {} : ', cls); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(6, [t 'T{:, j} syntax not yet supported']) else t_ok(isequal(T{:, 1}, v1), [t 'T{:, 1} == v1']); @@ -195,7 +199,7 @@ end t_ok(isequal(T{2, 1}, v1(2)), [t 'T{i, j} == v(i)']); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(1, [t 'T{i, j} syntax not yet supported for matrices']) else t_ok(isequal(T{4, 6}, v6(4, :)), [t 'T{i, j} == v(i)']); @@ -203,12 +207,12 @@ %% remove skip_oct_tab when %% https://github.com/apjanke/octave-tablicious/pull/89 is merged - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(1, [t 'T{end, end} syntax not yet supported']) else t_ok(isequal(T{end, end}, v6(end,:)), [t 'T{end, end} == v(end,:)']); end - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(8, [t 'T{ii, j} syntax not yet supported']) else t_ok(isequal(T{1:3, 2}, v2(1:3)), [t 'T{i1:iN, j} == v(i1:iN)']); @@ -232,7 +236,7 @@ t_is(T.dbl, var4, 12, [t 'T{:, 4} = var4']); T{:, 5} = var5; t_is(T.boo, var5, 12, [t 'T{:, 5} = var5']); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(1, 'T{:, j} = M syntax not yet supported for matrices'); else T{:, 6} = var6; @@ -241,7 +245,7 @@ T{2, 1} = v1(2); t_ok(isequal(T{2, 1}, v1(2)), [t 'T{i, j} = v(i)']); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(1, 'T{i, j} = M syntax not yet supported for matrices'); else T{4, 6} = v6(4, :); @@ -249,7 +253,7 @@ end T{1:3, 2} = v2(1:3); t_ok(isequal(T.flt(1:3), v2(1:3)), [t 'T{i1:iN, j} = v(i1:iN)']); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(2, 'T{i1:iN, j} = M syntax not yet supported for matrices'); else T{1:3, 6} = v6(1:3, :); @@ -259,7 +263,7 @@ end T{[6;3], 5} = v5([6;3]); t_ok(isequal(T.boo([6;3]), v5([6;3])), [t 'T{ii, j} = v(ii)']); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(2, 'T{ii, j} = M syntax not yet supported for matrices'); else T{[6;3], 6} = v6([6;3], :); @@ -267,7 +271,7 @@ T{[6;3], 6} = v6([6;3], [2;1]); t_ok(isequal(T.mat([6;3], :), v6([6;3], [2;1])), [t 'T{ii, j} = v(ii, jj)']); end - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(2, [t 'T{ii, jj} syntax not yet supported']) else T{6:-1:3, [2;4;6;5]} = [v2(6:-1:3) v4(6:-1:3) v6(6:-1:3, :) v5(6:-1:3)]; @@ -280,7 +284,7 @@ T{:, 3} = v3; T{:, 4} = v4; T{:, 5} = v5; - if ~skip_oct_tab + if ~skip_oct_tab && ~skip_oct_tab2 T{:, 6} = v6; end @@ -293,7 +297,7 @@ t_ok(isequal(T2.Properties.VariableNames, var_names(jj)), [t 'VariableNames']); t_ok(isequal(table_values(T2), {v1(ii), v3(ii), v4(ii), v6(ii, :)}), [t 'VariableValues']); t_ok(isequal(T2.Properties.RowNames, row_names(ii)), [t 'RowNames']); - if skip_oct_tab || skip_ml_tab + if skip_oct_tab || skip_ml_tab || skip_oct_tab2 t_skip(1, [t 'DimensionNames not yet supported']) else t_ok(isequal(T2.Properties.DimensionNames, dim_names), [t 'DimensionNames']); @@ -307,7 +311,7 @@ t_ok(isequal(T2.Properties.VariableNames, var_names(jj)), [t 'VariableNames']); t_ok(isequal(table_values(T2), {v2, v4, v6}), [t 'VariableValues']); t_ok(isequal(T2.Properties.RowNames, row_names), [t 'RowNames']); - if skip_oct_tab || skip_ml_tab + if skip_oct_tab || skip_ml_tab || skip_oct_tab2 t_skip(1, [t 'DimensionNames not yet supported']) else t_ok(isequal(T2.Properties.DimensionNames, dim_names), [t 'DimensionNames']); @@ -321,7 +325,7 @@ t_ok(isequal(T2.Properties.VariableNames, var_names), [t 'VariableNames']); t_ok(isequal(table_values(T2), {v1(ii), v2(ii), v3(ii), v4(ii), v5(ii), v6(ii, :)}), [t 'VariableValues']); t_ok(isequal(T2.Properties.RowNames, row_names(ii)), [t 'RowNames']); - if skip_oct_tab || skip_ml_tab + if skip_oct_tab || skip_ml_tab || skip_oct_tab2 t_skip(1, [t 'DimensionNames not yet supported']) else t_ok(isequal(T2.Properties.DimensionNames, dim_names), [t 'DimensionNames']); @@ -334,7 +338,7 @@ t_ok(isequal(T2.Properties.VariableNames, var_names(4)), [t 'VariableNames']); t_ok(isequal(table_values(T2), {v4(5)}), [t 'VariableValues']); t_ok(isequal(T2.Properties.RowNames, row_names(5)), [t 'RowNames']); - if skip_oct_tab || skip_ml_tab + if skip_oct_tab || skip_ml_tab || skip_oct_tab2 t_skip(1, [t 'DimensionNames not yet supported']) else t_ok(isequal(T2.Properties.DimensionNames, dim_names), [t 'DimensionNames']); @@ -345,7 +349,7 @@ t_ok(isequal(T2.Properties.VariableNames, var_names(6)), [t 'VariableNames']); t_ok(isequal(table_values(T2), {v6(3, :)}), [t 'VariableValues']); t_ok(isequal(T2.Properties.RowNames, row_names(3)), [t 'RowNames']); - if skip_oct_tab || skip_ml_tab + if skip_oct_tab || skip_ml_tab || skip_oct_tab2 t_skip(1, [t 'DimensionNames not yet supported']) else t_ok(isequal(T2.Properties.DimensionNames, dim_names), [t 'DimensionNames']); @@ -363,14 +367,14 @@ t_ok(isequal(T2.Properties.VariableNames, var_names(6)), [t 'VariableNames']); t_ok(isequal(table_values(T2), {v6(end, :)}), [t 'VariableValues']); t_ok(isequal(T2.Properties.RowNames, row_names(end)), [t 'RowNames']); - if skip_oct_tab || skip_ml_tab + if skip_oct_tab || skip_ml_tab || skip_oct_tab2 t_skip(1, [t 'DimensionNames not yet supported']) else t_ok(isequal(T2.Properties.DimensionNames, dim_names), [t 'DimensionNames']); end end - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(20, sprintf('%s : subsasgn () not yet supported.', cls)); else t = sprintf('%s : subsasgn () : T(ii,jj) : ', cls); @@ -459,7 +463,7 @@ t_ok(isequal(T4, T3), [t '[T1;T2]']); T5 = table_class([7;8],[7.7;8.8],{'seven';'eight'},1./[7;8],[-1;2]<=0, [70 75; 80 85], ... 'VariableNames', var_names); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(1, [t 'RowNames auto-generation not yet supported']); else T6 = [T5; T2; T1]; @@ -479,7 +483,7 @@ %% deleting variables t = sprintf('%s : delete variables : ', cls); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 || skip_ml_tab2 t_skip(1, [t 'not yet supported']); else T7 = T; @@ -501,7 +505,7 @@ T3 = T6(:, 6:7); ii = [5;3;1]; jj = [6:7]; - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(6, [t 'T{ii, :} syntax not yet supported']) else ex = horzcat(v3, var3); @@ -514,7 +518,7 @@ end t = sprintf('%s : more subsasgn {} : ', cls); - if skip_oct_tab + if skip_oct_tab || skip_oct_tab2 t_skip(6, [t 'T{ii, :} syntax not yet supported']) else T2{ii, :} = [var1(ii) var2(ii) var4(ii) var6(ii, :) var5(ii)]; @@ -541,21 +545,36 @@ t_is(T2.igr(2), 55, 12, t); %% nested tables - t = sprintf('%s : nested tables : ', cls); - T1 = table_class([1;2;3],[4;5;6]); - T2 = table_class({'one';'two';'three'},{'four';'five';'six'}); - T3 = table_class([10;20;30],{'uno';'dos';'tres'}, T1, T2); - t_ok(isequal(T3.T1, T1), [t 'T.T1 == T1']); - t_ok(isequal(T3.T2, T2), [t 'T.T2 == T2']); - t_ok(isequal(T3.T1([1;3], :), T1([1;3], :)), [t 'T.T1(ii, :) == T1(ii, :)']); - t_ok(isequal(T3.T2([1;3], :), T2([1;3], :)), [t 'T.T2(ii, :) == T2(ii, :)']); - t_ok(isequal(T3.T1.Var1, T1.Var1), [t 'T.T1.Var1 == T1.Var1']); - t_ok(isequal(T3.T2.Var2([1;3], :), T2.Var2([1;3], :)), [t 'T.T2.Var2(ii) == T.T2.Var2(ii)']); - %% check for Octave trouble in subsref - T4 = T3(:, :); - T5 = T3([1;2;3], :); - t_ok(isequal(T3, T4), [t 'T == T(:, :))']); - t_ok(isequal(T3, T5), [t 'T == T(ii, :))']); + for k2 = 1:nc + nested_table_class = table_classes{k2}; + nstcls = upper(class_names{k2}); + + t = sprintf('%s . %s : nested tables : ', cls, nstcls); + T1 = nested_table_class([1;2;3],[4;5;6]); + T2 = nested_table_class({'one';'two';'three'},{'four';'five';'six'}); + T3 = table_class([10;20;30],{'uno';'dos';'tres'}, T1, T2, ... + 'VariableNames', {'v1', 'v2', 'T1', 'T2'}); + t_ok(isequal(T3.T1, T1), [t 'T.T1 == T1']); + t_ok(isequal(T3.T2, T2), [t 'T.T2 == T2']); + if have_feature('matlab') && isa(T1, 'mp_table_subclass') && ... + isa(T3, 'mp_table_subclass') + t_skip(4, 'multiple indexing of nested mp_table_subclasses'); + else + t_ok(isequal(T3.T1([1;3], :), T1([1;3], :)), [t 'T.T1(ii, :) == T1(ii, :)']); + t_ok(isequal(T3.T2([1;3], :), T2([1;3], :)), [t 'T.T2(ii, :) == T2(ii, :)']); + t_ok(isequal(T3.T1.Var1, T1.Var1), [t 'T.T1.Var1 == T1.Var1']); + t_ok(isequal(T3.T2.Var2([1;3], :), T2.Var2([1;3], :)), [t 'T.T2.Var2(ii) == T.T2.Var2(ii)']); + end + %% check for Octave trouble in subsref + if (skip_oct_tab && isa(T3, 'table')) || skip_oct_tab2 + t_skip(2, 'mp_table_subclass does not handle this yet'); + else + T4 = T3(:, :); + T5 = T3([1;2;3], :); + t_ok(isequal(T3, T4), [t 'T == T(:, :))']); + t_ok(isequal(T3, T5), [t 'T == T(ii, :))']); + end + end end if nargout @@ -565,6 +584,9 @@ t_end; function Tv = table_values(T) +if isa(T, 'mp_table_subclass') + T = get_table(T); +end if isa(T, 'table') && have_feature('matlab') Tv = cellfun(@(c)T{:, c}, num2cell(1:size(T, 2)), 'UniformOutput', false); else