function cosmo_disp(x, varargin)
% display the input as a string representation
%
% cosmo_disp(x,opt)
%
% Inputs:
% x any type of data element (can be a dataset struct)
% opt Optional struct with fields
% .threshold If the number of values in an array along a dimension
% exceeds threshold, then an array is showed in summary
% style along that dimension. Default: 5
% .edgeitems When an array is shown in summary style, edgeitems sets
% the number of items at the beginning and end of the
% array to be shown (separated by '...' in rows and by ':'
% in columns).
% Default: 3
% .precision Numeric precision, indicating number of decimals after
% the floating point
% Default: 3
% .strlen Maximal string length, if a string is longer the
% beginning and end are shown separated by ' ... '.
% Default: 20
% .depth Maximum recursion depth
% Default: 6
%
% Side effect: Calling this function caused the representation of x
% to be displayed.
%
%
% Examples:
% % display a complicated data structure
% x=struct();
% x.a_cell={[],{'cell within cell',[1 2; 3 4]}};
% x.small_matrix=[10 11 12; 13 14 15];
% x.big_matrix=reshape(1:200,10,20);
% x.huge=2^40;
% x.tiny=2^-40;
% x.a_string='hello world';
% x.a_struct.another_struct.name='me';
% x.a_struct.another_struct.func=@abs;
% cosmo_disp(x);
% %|| .a_cell
% %|| { [ ] { 'cell within cell' [ 1 2
% %|| 3 4 ] } }
% %|| .small_matrix
% %|| [ 10 11 12
% %|| 13 14 15 ]
% %|| .big_matrix
% %|| [ 1 11 21 ... 171 181 191
% %|| 2 12 22 ... 172 182 192
% %|| 3 13 23 ... 173 183 193
% %|| : : : : : :
% %|| 8 18 28 ... 178 188 198
% %|| 9 19 29 ... 179 189 199
% %|| 10 20 30 ... 180 190 200 ]@10x20
% %|| .huge
% %|| [ 1.1e+12 ]
% %|| .tiny
% %|| [ 9.09e-13 ]
% %|| .a_string
% %|| 'hello world'
% %|| .a_struct
% %|| .another_struct
% %|| .name
% %|| 'me'
% %|| .func
% %|| @abs
% %
% cosmo_disp(x.a_cell)
% %|| { [ ] { 'cell within cell' [ 1 2
% %|| 3 4 ] } }
% cosmo_disp(x.a_cell{2}{2})
% %|| [ 1 2
% %|| 3 4 ]
%
% % make a cell in a cell in a cell in a cell ...
% m={{{{{{{{{{{'hello'}}}}}}}}}}};
% cosmo_disp(m)
% %|| { { { { { { <cell> } } } } } }
% cosmo_disp(m,'depth',8)
% %|| { { { { { { { { <cell> } } } } } } } }
% cosmo_disp(m,'depth',Inf)
% %|| { { { { { { { { { { { 'hello' } } } } } } } } } } }
%
% % illustrate 'threshold' and 'edgeitems' arguments
% cosmo_disp(num2cell('a':'k'))
% %|| { 'a' 'b' 'c' ... 'i' 'j' 'k' }@1x11
% cosmo_disp(num2cell('a':'k'),'threshold',Inf)
% %|| { 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' }
% cosmo_disp(num2cell('a':'k'),'edgeitems',2)
% %|| { 'a' 'b' ... 'j' 'k' }@1x11
%
% % illustrate 'precision' argument
% cosmo_disp(pi*[1 2],'precision',1);
% %|| [ 3 6 ]
% cosmo_disp(pi*[1 2],'precision',3);
% %|| [ 3.14 6.28 ]
% cosmo_disp(pi*[1 2],'precision',5);
% %|| [ 3.1416 6.2832 ]
% cosmo_disp(pi*[1 2],'precision',7);
% %|| [ 3.141593 6.283185 ]
%
% % illustrate n-dimensional arrays
% x=zeros([2 2 1 2 3]);
% x(:)=2*(1:numel(x));
% cosmo_disp(x)
% %|| <double>@2x2x1x2x3
% %|| (:,:,1,1,1) = [ 2 6
% %|| 4 8 ]
% %|| (:,:,1,2,1) = [ 10 14
% %|| 12 16 ]
% %|| (:,:,1,1,2) = [ 18 22
% %|| 20 24 ]
% %|| (:,:,1,2,2) = [ 26 30
% %|| 28 32 ]
% %|| (:,:,1,1,3) = [ 34 38
% %|| 36 40 ]
% %|| (:,:,1,2,3) = [ 42 46
% %|| 44 48 ]
% cosmo_disp(reshape(char(65:72),[2 2 2]))
% %|| <char>@2x2x2
% %|| (:,:,1) = 'AC
% %|| BD'
% %|| (:,:,2) = 'EG
% %|| FH'
% cosmo_disp(zeros([2 3 5 7 0 2]))
% %|| <double>@2x3x5x7x0x2 (empty)
%
% % illustrate non-singleton structs
% x=struct('x',{1 2; 3 4});
% cosmo_disp(x);
% %|| <struct>@2x2
% %|| (1,1).x
% %|| [ 1 ]
% %|| (2,1).x
% %|| [ 3 ]
% %|| (1,2).x
% %|| [ 2 ]
% %|| (2,2).x
% %|| [ 4 ]
% x3=cat(3,x,x,x);
% cosmo_disp(x3);
% %|| <struct>@2x2x3
% %|| (1,1,1).x
% %|| [ 1 ]
% %|| (2,1,1).x
% %|| [ 3 ]
% %|| (1,2,1).x
% %|| [ 2 ]
% %|| : :
% %|| (2,1,3).x
% %|| [ 3 ]
% %|| (1,2,3).x
% %|| [ 2 ]
% %|| (2,2,3).x
% %|| [ 4 ]
%
% Notes:
% - Unlike the builtin 'disp' function, this function shows the contents
% of the input using recursion. For example if a cell contains a
% struct, then the contents of that struct is shown as well
% - A use case is displaying dataset structs
%
% # For CoSMoMVPA's copyright information and license terms, #
% # see the COPYING file distributed with CoSMoMVPA. #
defaults.threshold = 5; % max #items before triggering summary style
defaults.edgeitems = 3; %#items at edges in summary style
defaults.precision = 3; % show floats with 3 decimals
defaults.strlen = 20; % insert '...' with strings more than 20 chars
defaults.depth = 6; % maximal depth
defaults.show_size = false; % whether to show size of matrices
opt = cosmo_structjoin(defaults, varargin);
% get string representation of x
s = disp_helper(x, opt);
% print string representation of x
disp(s);
function s = disp_helper(x, opt)
% general helper function to get a string representation. Unlike the
% main function this function returns a string, which makes it suitable
% for recursion
depth = opt.depth;
if depth <= 0
s = any2summary_str(x, opt);
return
end
opt.depth = depth - 1;
if ~has_size(x)
s = any2summary_str(x, opt);
else
s = nd_any2str(x, opt);
end
function s = any2summary_str(x, unused)
if has_size(x)
sz = size(x);
else
sz = [1 1];
end
s = surround_with(true, '<', class(x), '>', sz);
function tf = has_size(x)
% helper, because some classes have no 'size'
try
size(x);
tf = true;
catch
tf = false;
end
function y = nd_any2str(x, opt)
if isstruct(x)
if numel(x) <= 1
y = struct2str(x, opt);
else
y = multi_any2string(x, 0, opt);
end
elseif numel(size(x)) == 2
if iscell(x)
y = cell2str(x, opt);
elseif isnumeric(x) || islogical(x)
y = matrix2str(x, opt);
elseif ischar(x)
y = string2str(x, opt);
elseif isa(x, 'function_handle')
y = function_handle2str(x, opt);
else
y = any2summary_str(x, opt);
end
else
y = multi_any2string(x, 2, opt);
end
function y = multi_any2string(x, ndim_post, opt)
sz = size(x);
sz_rest = sz((ndim_post + 1):end);
ndim_rest = numel(sz_rest);
n_rest = prod(sz_rest);
parts = cell(ndim_rest, 1);
for k = 1:ndim_rest
parts{k} = num2cell(1:sz_rest(k));
end
p = cosmo_cartprod(parts);
if ndim_post > 0
xflat = reshape(x, [sz(1:ndim_post) n_rest]);
else
xflat = x(:);
end
[pre, post] = get_mx_idxs(xflat, opt.edgeitems, opt.threshold, ndim_post + 1);
header = any2summary_str(x, opt);
s_pre = nd_any2str_helper(xflat, p, pre, opt);
if isempty(post)
s_post = {'', ''};
s_dots = {'', ''};
else
s_post = nd_any2str_helper(xflat, p, post, opt);
s_dots = cell(1, 2);
for k = 1:2
szs = cellfun(@(x)size(x, 2), s_post(:, k));
pos = round(max(szs) / 2);
s_dots{k} = [spaces(1, pos) ':'];
end
end
s_all = cat(1, s_pre, s_dots, s_post);
y = strcat_({header; strcat_(s_all)});
function s = nd_any2str_helper(xflat, p, idxs, opt)
n_rest = numel(idxs);
s = cell(n_rest, 2);
sz = size(xflat);
npre = numel(sz) - 1;
for k = 1:n_rest
idx = idxs(k);
switch npre
case 1
% for struct
v = xflat(idx);
idx_prefix = '';
idx_postfix = '';
case 2
% anything else
v = xflat(:, :, idx);
idx_prefix = ':,:,';
idx_postfix = ' = ';
otherwise
assert(false);
end
v_str = disp_helper(v, opt);
idx_str = sprintf(',%d', p(idx, :));
s{k, 1} = sprintf(' (%s%s)%s', idx_prefix, idx_str(2:end), idx_postfix);
s{k, 2} = v_str;
end
function y = strcat_(xs)
if isempty(xs)
y = '';
return
end
% all elements in xs are char
[nr, nc] = size(xs);
ys = cell(1, nc);
% sizes for each element
width_per_col = max_element_size(xs, 2);
height_per_row = max_element_size(xs, 1);
for k = 1:nc
xcol = cell(nr, 1);
width = width_per_col(k);
row_pos = 0;
for j = 1:nr
height = height_per_row(j);
if height == 0
continue
end
x = xs{j, k};
if ~ischar(x) && isempty(x)
x = '';
end
sx = size(x);
to_add = [height width] - sx;
% pad with spaces
row_pos = row_pos + 1;
xcol{row_pos} = [[x spaces(sx(1), to_add(2))]; ...
spaces(to_add(1), width)];
end
ys{k} = char(xcol{1:row_pos});
end
y = [ys{:}];
function m = max_element_size(x, dim)
% faster than cellfun
n = numel(x);
sizes = zeros(size(x));
for k = 1:n
sizes(k) = size(x{k}, dim);
end
m = max(sizes, [], 3 - dim);
function y = spaces(nx, ny)
% faster than repmat(' ',nx,ny)
if nx > 0 && ny > 0
y(nx, ny) = ' ';
y(:) = ' ';
else
if nx < 0
nx = 0;
end
if ny < 0
ny = 0;
end
y = reshape('', nx, ny);
end
function y = struct2str(x, opt)
if numel(x) == 0
show_size = opt.show_size;
y = [surround_with(show_size, '', 'struct', '', size(x)) ' (empty)'];
return
end
assert(numel(x) == 1);
fns = fieldnames(x);
n = numel(fns);
if n == 0
show_size = opt.show_size;
y = [surround_with(show_size, '', 'struct', '', size(x)) ' (empty)'];
else
r = cell(n * 2, 1);
for k = 1:n
fn = fns{k};
r{k * 2 - 1} = ['.' fn];
d = disp_helper(x.(fn), opt);
r{k * 2} = [spaces(size(d, 1), 2) d];
end
y = strcat_(r);
end
function s = function_handle2str(x, opt)
s_with_quotes = string2str(func2str(x), opt);
s = ['@' s_with_quotes(2:(end - 1))];
function s = string2str(x, opt)
if ~ischar(x)
error('expected a char');
end
[nrows, ncols] = size(x);
if ncols > opt.strlen
infix = ' ... ';
h = floor((opt.strlen - numel(infix)) / 2);
x = strcat_({x(:, 1:h), infix, x(:, ncols + ((1 - h):0))});
end
quote = '''';
pre = quote;
post = [spaces(nrows - 1, 1); quote];
s = strcat_({pre, x, post});
function s = cell2str(x, opt)
% display a cell
edgeitems = opt.edgeitems;
threshold = opt.threshold;
% get indices of rows and columns to show
[r_pre, r_post] = get_mx_idxs(x, edgeitems, threshold, 1);
[c_pre, c_post] = get_mx_idxs(x, edgeitems, threshold, 2);
part_idxs = {{r_pre, r_post}, {c_pre, c_post}};
nrows = numel([r_pre r_post]) + ~isempty(r_post);
ncols = numel([c_pre c_post]) + ~isempty(c_post);
sinfix = cell(nrows, ncols * 2 + 1);
for k = 1:(ncols - 1)
sinfix{1, k * 2 + 2} = ' ';
end
cpos = 1;
for cpart = 1:2
col_idxs = part_idxs{2}{cpart};
nc = numel(col_idxs);
rpos = 0;
for rpart = 1:2
row_idxs = part_idxs{1}{rpart};
nr = numel(row_idxs);
if nr == 0
continue
end
for ci = 1:nc
col_idx = col_idxs(ci);
trgc = cpos + ci * 2;
for ri = 1:nr
row_idx = row_idxs(ri);
sinfix{rpos + ri, trgc} = disp_helper(x{row_idx, ...
col_idx}, opt);
if cpart == 2 && ci == 1 && nc > 0
sinfix{rpos + ri, cpos + ci * 2 - 1} = ' ... ';
end
end
if rpart == 2
max_length = max(cellfun(@numel, sinfix(:, trgc)));
pre_spaces = spaces(1, floor(max_length / 2 - 1));
sinfix{rpos, cpos + ci * 2} = [pre_spaces ':'];
end
end
rpos = rpos + nr + 1;
end
cpos = cpos + nc * 2;
end
show_size = opt.show_size || ~isempty(r_post) || ~isempty(c_post);
s = surround_with(show_size, '{ ', strcat_(sinfix), ' }', size(x));
function pre_infix_post = surround_with(show_size, pre, infix, post, matrix_sz)
% surround infix by pre and post, doing
n = prod(matrix_sz);
if show_size && n ~= 1
size_str = sprintf('x%d', matrix_sz);
size_str(1) = '@';
if n == 0
size_str = [size_str ' (empty)'];
end
else
size_str = '';
end
post = strcat_({spaces(size(infix, 1) - 1, 1); [post size_str]});
pre_infix_post = strcat_({pre, infix, post});
function s = matrix2str(x, opt)
if isempty(x)
show_size = opt.show_size;
s = surround_with(show_size, '[', ' ', ']', size(x));
return
end
% display a matrix
edgeitems = opt.edgeitems;
threshold = opt.threshold;
precision = opt.precision;
% get indices of rows and columns to show
[r_pre, r_post] = get_mx_idxs(x, edgeitems, threshold, 1);
[c_pre, c_post] = get_mx_idxs(x, edgeitems, threshold, 2);
% data to be shown
y = x([r_pre r_post], [c_pre c_post]);
% convert to string
s = num2str(y, precision);
% number of characters in first and second dimension
[nc_row, nc_col] = size(s);
% see where each column is a space; that's a potential split point
sp_col = sum(s == ' ', 1) == nc_row;
% col_index has value k for characters in the k-th column, else zero
col_index = zeros(1, nc_col);
col_count = 1;
in_num = true;
for k = 1:nc_col
if in_num
if sp_col(k)
col_count = col_count + 1;
in_num = false;
else
col_index(k) = col_count;
end
elseif ~sp_col(k)
in_num = true;
col_index(k) = col_count;
end
end
% deal with rows
row_blocks = cell(3, 1);
if isempty(r_post)
row_blocks{1, 1} = s;
else
% insert ':' for each column
line = spaces(1, nc_col);
for k = 1:max(col_index)
idxs = find(col_index == k);
median_pos = round(mean(idxs));
line(median_pos) = ':';
end
row_blocks{1} = s(1:edgeitems, :);
row_blocks{2} = line;
row_blocks{3} = s(edgeitems + (1:edgeitems), :);
end
% deal with columns
row_and_col_blocks = cell(3, 3);
for row = 1:3
if isempty(c_post)
row_and_col_blocks{row} = row_blocks{row};
else
% insert ' ... ' halfway each row
pre_end = find(col_index == edgeitems, 1, 'last') + 1;
post_start = find(col_index == (edgeitems + 1), 1, 'first') - 1;
r = row_blocks{row, 1};
if isempty(r)
continue
end
row_and_col_blocks{row, 1} = r(:, 1:pre_end);
if row ~= 2
row_and_col_blocks{row, 2} = repmat(' ... ', size(r, 1), 1);
end
row_and_col_blocks{row, 3} = r(:, post_start:end);
end
end
show_size = opt.show_size || ~isempty(r_post) || ~isempty(c_post);
s = surround_with(show_size, '[ ', strcat_(row_and_col_blocks), ' ]', ...
size(x));
function [pre, post] = get_mx_idxs(x, edgeitems, threshold, dim)
% returns the first and last indices for showing an array along
% dimension dim. If size(x,dim)<2*edgeitems, then pre has all the
% indices, otherwise pre and post have the first and last edgeitems
% indices, respectively
n = size(x, dim);
if n > max(threshold, 2 * edgeitems) % properly deal with Inf values
pre = 1:edgeitems;
post = n - edgeitems + (1:edgeitems);
else
pre = 1:n;
post = [];
end