Commit 79d4b888 authored by Houtan Bastani's avatar Houtan Bastani

add basic functionality for tables with non time series data

parent 58fd8ec1
Pipeline #653 failed with stage
in 25 seconds
function o = addData(o, varargin)
%function o = addData(o, varargin)
% Add data to the current section of the current page in the report
%
% INPUTS
% o [report] report object
% varargin arguments to @report_table/addData
%
% OUTPUTS
% o [report] updated report object
%
% SPECIAL REQUIREMENTS
% none
% Copyright (C) 2019 Dynare Team
%
% This file is part of Dynare.
%
% Dynare 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.
%
% Dynare 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 Dynare. If not, see <http://www.gnu.org/licenses/>.
assert(~isempty(o.pages) , ...
['@report.addData: Before adding data, you must add a page, ' ...
'section, and a table.']);
assert(~isempty(o.pages{end}.sections) , ...
['@report.addData: Before adding data, you must add a section and ' ...
'a table']);
assert(~isempty(o.pages{end}.sections{end}.elements), ...
'@report.addData: Before adding data, you must add a table');
assert(isa(o.pages{end}.sections{end}.elements{end}, 'report_table'), ...
'@report.addData: you can only add data to a report_table object');
o.pages{end}.sections{end}.elements{end} = ...
o.pages{end}.sections{end}.elements{end}.addData(varargin{:});
end
function display(o)
%function display(o)
% Display a Report_Series object
%
% INPUTS
% o [report_series] report_series object
%
% OUTPUTS
% none
%
% SPECIAL REQUIREMENTS
% none
% Copyright (C) 2013-2015 Dynare Team
%
% This file is part of Dynare.
%
% Dynare 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.
%
% Dynare 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 Dynare. If not, see <http://www.gnu.org/licenses/>.
display_reporting_object(o);
end
\ No newline at end of file
function o = printData(o, fid, data, precision)
%function printData(o, fid, dser, precision)
% function to print a row of data, contained in data
%
% INPUTS
% fid [int] file id
% data [string] value to be printed
% dates [dates] dates for report_series slice
% precision [float] precision with which to print the data
%
%
% OUTPUTS
% o [report_series] report_series object
%
% SPECIAL REQUIREMENTS
% none
% Copyright (C) 2019 Dynare Team
%
% This file is part of Dynare.
%
% Dynare 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.
%
% Dynare 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 Dynare. If not, see <http://www.gnu.org/licenses/>.
dataString = sprintf('%%.%df', precision);
precision = 10^precision;
data = setDataToZeroFromZeroTol(o, data);
for i=1:size(data,1)
fprintf(fid, '&');
output = round(data(i)*precision)/precision;
  • What's the purpose of this line? The string format (store in dataString) should already truncate to the right precision.

  • to round the result. fprintf only truncates

  • This is not correct:

    >> sprintf('%.2f', 0.016)
    
    ans =
    
        '0.02'
  • Sometimes it rounds and sometimes it doesn't. Don't know what determines that

    >> sprintf('%.5f', 0.878795)
    
    ans =
    
        '0.87879'
  • It's because the last digit before truncation is 5. There is a special rounding rule for that digit, see https://en.wikipedia.org/wiki/Rounding#Round_half_to_even

  • Actually MATLAB seems to implement “round half to odd”, which is similar.

  • Then we must decide what makes more sense in a report. As we are not summing over rounded numbers, rather just displaying them, it seems that the current way is more consistent (and is what we'd expect when doing it on pen and paper).

  • It is also what we had decided a long time ago when first creating the reporting functionality.

  • I doubt we made a decision on the rounding rules for breaking ties!

    I think the defaults provided by fprintf are quite reasonable, I don't see the need for working around it.

  • I think I did that with Ferhat for GPM

  • Fair enough. In my opinion, we can drop that now. Unless we have a strong opinion on the tie-breaking rounding rules (which I don't), I'd rather keep the fprintf defaults.

    Edited by Sébastien Villemot
  • If you really want to keep that, then at least please document in the code that this implements “round half up” (instead of fprintf's “round half odd”).

  • Actually it implements "round half away from zero" (because of negative numbers).

    Edited by Sébastien Villemot
  • Thanks for the doc in the manual. Please also add it in the code (at the two places), because it's really not obvious what this code does.

Please register or sign in to reply
if isnan(output)
fprintf(fid, '%s', o.tableNaNSymb);
else
fprintf(fid, dataString, output);
end
end
end
function o = report_data(varargin)
%function o = report_data(varargin)
% Report_Data Class Constructor
%
% INPUTS
% varargin 0 args : empty report_data object
% 1 arg : must be report_data object (return a copy of arg)
% > 1 args: option/value pairs (see structure below for
% options)
%
% OUTPUTS
% o [report_data] report_data object
%
% SPECIAL REQUIREMENTS
% none
% Copyright (C) 2019 Dynare Team
%
% This file is part of Dynare.
%
% Dynare 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.
%
% Dynare 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 Dynare. If not, see <http://www.gnu.org/licenses/>.
o = struct;
o.data = '';
o.tableSubSectionHeader = '';
o.tableAlignRight = false;
o.tableRowColor = 'white';
o.tableRowIndent = 0;
o.tableNaNSymb = 'NaN';
o.tablePrecision = '';
o.zeroTol = 1e-6;
if nargin == 1
assert(isa(varargin{1}, 'report_data'), ...
'@report_data.report_data: with one arg you must pass a report_data object');
o = varargin{1};
return;
elseif nargin > 1
if round(nargin/2) ~= nargin/2
error('@report_data.report_data: options must be supplied in name/value pairs.');
end
optNames = fieldnames(o);
% overwrite default values
for pair = reshape(varargin, 2, [])
ind = find(strcmpi(optNames, pair{1}));
assert(isempty(ind) || length(ind) == 1);
if ~isempty(ind)
o.(optNames{ind}) = pair{2};
else
error('@report_data.report_data: %s is not a recognized option.', pair{1});
end
end
end
% Create report_data object
o = class(o, 'report_data');
end
function B = subsasgn(A, S, V)
% function B = subsasgn(A, S, V)
% Copyright (C) 2019 Dynare Team
%
% This file is part of Dynare.
%
% Dynare 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.
%
% Dynare 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 Dynare. If not, see <http://www.gnu.org/licenses/>.
B = A;
if length(S) > 1
for i=1:(length(S)-1)
B = subsref(B, S(i));
end
B = subsasgn(B, S(end), V);
B = subsasgn(A, S(1:(end-1)), B);
return
end
switch S.type
case '.'
switch S.subs
case fieldnames(A)
B.(S.subs) = V;
otherwise
error(['@report_data.subsasgn: field ' S.subs 'does not exist']);
end
otherwise
error('@report_data.subsasgn: syntax error');
end
end
\ No newline at end of file
function A = subsref(A, S)
%function A = subsref(A, S)
% Copyright (C) 2019 Dynare Team
%
% This file is part of Dynare.
%
% Dynare 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.
%
% Dynare 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 Dynare. If not, see <http://www.gnu.org/licenses/>.
switch S(1).type
case '.'
switch S(1).subs
case fieldnames(A)
A = A.(S(1).subs);
case methods(A)
if areParensNext(S)
A = feval(S(1).subs, A, S(2).subs{:});
S = shiftS(S,1);
else
A = feval(S(1).subs, A);
end
otherwise
error(['@report_data.subsref: unknown field or method: ' S(1).subs]);
end
case {'()', '{}'}
error(['@report_data.subsref: ' S(1).type ' indexing not supported.']);
otherwise
error('@report_data.subsref: impossible case')
end
S = shiftS(S,1);
if length(S) >= 1
A = subsref(A, S);
end
end
function o = writeDataForTable(o, fid, precision)
%function o = writeDataForTable(o, fid, precision)
% Write Table Data
%
% INPUTS
% o [report_data] report_data object
% fid [int] file id
% precision [float] precision with which to print the data
%
%
% OUTPUTS
% o [report_data] report_data object
%
% SPECIAL REQUIREMENTS
% none
% Copyright (C) 2019 Dynare Team
%
% This file is part of Dynare.
%
% Dynare 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.
%
% Dynare 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 Dynare. If not, see <http://www.gnu.org/licenses/>.
%% Validate options passed to function
assert(fid ~= -1);
assert(isint(precision));
%% Validate options provided by user
assert(ischar(o.tableSubSectionHeader), '@report_data.writeDataForTable: tableSubSectionHeader must be a string');
if isempty(o.tableSubSectionHeader)
assert(~isempty(o.data) && iscell(o.data), ...
'@report_data.writeDataForTable: must provide data as a cell');
end
assert(ischar(o.tableRowColor), '@report_data.writeDataForTable: tableRowColor must be a string');
assert(isint(o.tableRowIndent) && o.tableRowIndent >= 0, ...
'@report_data.writeDataForTable: tableRowIndent must be an integer >= 0');
assert(islogical(o.tableAlignRight), '@report_data.writeDataForTable: tableAlignRight must be true or false');
assert(ischar(o.tableNaNSymb), '@report_data.writeDataForTable: tableNaNSymb must be a string');
if ~isempty(o.tablePrecision)
assert(isint(o.tablePrecision) && o.tablePrecision >= 0, ...
'@report_data.writeDataForTable: tablePrecision must be a non-negative integer');
precision = o.tablePrecision;
end
rounding = 10^precision;
%% Write Output
fprintf(fid, '%% Table Data (report_data)\n');
nrows = length(o.data{1});
ncols = length(o.data);
for i = 1:nrows
if ~isempty(o.tableRowColor) && ~strcmpi(o.tableRowColor, 'white')
fprintf(fid, '\\rowcolor{%s}', o.tableRowColor);
else
fprintf(fid, '\\rowcolor{%s}', o.tableRowColor);
end
if ~isempty(o.tableSubSectionHeader)
fprintf(fid, '\\textbf{%s}', o.tableSubSectionHeader);
for i=1:ncols-1
fprintf(fid, ' &');
end
fprintf(fid, '\\\\%%\n');
return;
end
if o.tableAlignRight
fprintf(fid, '\\multicolumn{1}{r}{');
end
if o.tableRowIndent == 0
fprintf(fid, '\\noindent ');
else
for j=1:o.tableRowIndent
fprintf(fid,'\\indent ');
end
end
if o.tableAlignRight
fprintf(fid, '}');
end
for j = 1:ncols
val = o.data{j}(i);
if iscell(val)
val = val{:};
end
if isnan(val)
val = o.tableNaNSymb;
dataString = '%s';
elseif isnumeric(val)
dataString = sprintf('%%.%df', precision);
if val < o.zeroTol && val > -o.zeroTol
val = 0;
end
val = round(val*rounding)/rounding;
Please register or sign in to reply
if isnan(val)
val = o.tableNaNSymb;
dataString = '%s';
end
else
dataString = '%s';
val = regexprep(val, '_', '\\_');
end
fprintf(fid, dataString, val);
if j ~= ncols
fprintf(fid, ' & ');
else
fprintf(fid, '\\\\%%\n');
end
end
end
end
function o = addData(o, varargin)
% function o = addData(o, varargin)
% Copyright (C) 2019 Dynare Team
%
% This file is part of Dynare.
%
% Dynare 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.
%
% Dynare 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 Dynare. If not, see <http://www.gnu.org/licenses/>.
o.table_data{end+1} = report_data(varargin{:});
end
\ No newline at end of file
......@@ -12,7 +12,7 @@ function o = report_table(varargin)
% SPECIAL REQUIREMENTS
% none
% Copyright (C) 2013-2017 Dynare Team
% Copyright (C) 2013-2019 Dynare Team
%
% This file is part of Dynare.
%
......@@ -53,6 +53,11 @@ o.writeCSV = false;
o.highlightRows = {''};
o.preamble = {''};
o.afterward = {''};
o.column_names = '';
o.table_data = {};
if nargin == 1
assert(isa(varargin{1}, 'report_table'),['With one arg to Report_Table constructor, ' ...
'you must pass a report_table object']);
......@@ -89,6 +94,7 @@ end
if ischar(o.title)
o.title = {o.title};
end
if ischar(o.titleFormat)
o.titleFormat = {o.titleFormat};
end
......
......@@ -33,7 +33,8 @@ function o = writeTableFile(o, pg, sec, row, col)
% along with Dynare. If not, see <http://www.gnu.org/licenses/>.
ne = length(o.series);
if ne == 0
is_data_table = ~isempty(o.table_data);
if ne == 0 && ~is_data_table
warning('@report_table.write: no series to plot, returning');
return;
end
......@@ -49,6 +50,10 @@ if fid == -1
error(['@report_table.writeTableFile: ' msg]);
end
fprintf(fid, '%% Report_Table Object\n');
fprintf(fid, '\\setlength{\\parindent}{6pt}\n');
fprintf(fid, '\\setlength{\\tabcolsep}{4pt}\n');
%number of left-hand columns, 1 until we allow the user to group data,
% e.g.: GDP Europe
% GDP France
......@@ -56,140 +61,166 @@ end
% this example would be two lh columns, with GDP Europe spanning both
nlhc = 1;
if isempty(o.range)
dates = getMaxRange(o.series);
o.range = {dates};
else
dates = o.range{1};
end
ndates = dates.ndat;
fprintf(fid, '%% Report_Table Object\n');
fprintf(fid, '\\setlength{\\parindent}{6pt}\n');
fprintf(fid, '\\setlength{\\tabcolsep}{4pt}\n');
fprintf(fid, '\\begin{tabular}{@{}l');
if ~is_data_table
fprintf(fid, '\\begin{tabular}{@{}l');
if isempty(o.range)
dates = getMaxRange(o.series);
o.range = {dates};
else
dates = o.range{1};
end
ndates = dates.ndat;
for i=1:ndates
fprintf(fid, 'r');
if o.showVlines
fprintf(fid, '|');
elseif o.vlineAfterEndOfPeriod && dates(i).time(2) == dates(i).freq
fprintf(fid, '|');
elseif ~isempty(o.vlineAfter)
for j=1:length(o.vlineAfter)
if dates(i) == o.vlineAfter{j}
fprintf(fid, '|');
for i=1:ndates
fprintf(fid, 'r');
if o.showVlines
fprintf(fid, '|');
elseif o.vlineAfterEndOfPeriod && dates(i).time(2) == dates(i).freq
fprintf(fid, '|');
elseif ~isempty(o.vlineAfter)
for j=1:length(o.vlineAfter)
if dates(i) == o.vlineAfter{j}
fprintf(fid, '|');
end
end
end
end
end
datedata = dates.time;
years = unique(datedata(:, 1));
if length(o.range) > 1
rhscols = strings(o.range{2});
if o.range{2}.freq == 1
rhscols = strrep(rhscols, 'Y', '');
datedata = dates.time;
years = unique(datedata(:, 1));
if length(o.range) > 1
rhscols = strings(o.range{2});
if o.range{2}.freq == 1
rhscols = strrep(rhscols, 'Y', '');
end
else
rhscols = {};
end
else
rhscols = {};
end
for i=1:length(rhscols)
fprintf(fid, 'r');
if o.showVlines
fprintf(fid, '|');
for i=1:length(rhscols)
fprintf(fid, 'r');
if o.showVlines
fprintf(fid, '|');
end
end
end
nrhc = length(rhscols);
ncols = ndates+nlhc+nrhc;
fprintf(fid, '@{}}%%\n');
for i=1:length(o.title)
if ~isempty(o.title{i})
fprintf(fid, '\\multicolumn{%d}{c}{%s %s}\\\\\n', ...
nrhc = length(rhscols);
ncols = ndates+nlhc+nrhc;
fprintf(fid, '@{}}%%\n');
for i=1:length(o.title)
if ~isempty(o.title{i})
fprintf(fid, '\\multicolumn{%d}{c}{%s %s}\\\\\n', ...
ncols, o.titleFormat{i}, o.title{i});
end
end
end
fprintf(fid, '\\toprule%%\n');
fprintf(fid, '\\toprule%%\n');
% Column Headers
thdr = num2cell(years, size(years, 1));
if dates.freq == 1
for i=1:size(thdr, 1)
fprintf(fid, ' & %d', thdr{i, 1});
end
for i=1:length(rhscols)
fprintf(fid, ' & %s', rhscols{i});
end
else
thdr{1, 2} = datedata(:, 2)';
if size(thdr, 1) > 1
for i=2:size(thdr, 1)
split = find(thdr{i-1, 2} == dates.freq, 1, 'first');
assert(~isempty(split), '@report_table.writeTableFile: Shouldn''t arrive here');
thdr{i, 2} = thdr{i-1, 2}(split+1:end);
thdr{i-1, 2} = thdr{i-1, 2}(1:split);
% Column Headers
thdr = num2cell(years, size(years, 1));
if dates.freq == 1
for i=1:size(thdr, 1)
fprintf(fid, ' & %d', thdr{i, 1});
end
end
for i=1:size(thdr, 1)
fprintf(fid, ' & \\multicolumn{%d}{c}{%d}', size(thdr{i,2}, 2), thdr{i,1});
end
for i=1:length(rhscols)
fprintf(fid, ' & %s', rhscols{i});
end
fprintf(fid, '\\\\\n');
switch dates.freq
case 4
sep = 'Q';
case 12
sep = 'M';
case 52
sep = 'W';
otherwise
error('@report_table.writeTableFile: Invalid frequency.');
end
for i=1:size(thdr, 1)
period = thdr{i, 2};
for j=1:size(period, 2)
fprintf(fid, ' & \\multicolumn{1}{c');
if o.showVlines
fprintf(fid, '|');
elseif o.vlineAfterEndOfPeriod && j == size(period, 2)
fprintf(fid, '|');
elseif ~isempty(o.vlineAfter)
for k=1:length(o.vlineAfter)
if o.vlineAfter{k}.time(1) == thdr{i} && ...
o.vlineAfter{k}.time(2) == period(j)
fprintf(fid, '|');
for i=1:length(rhscols)
fprintf(fid, ' & %s', rhscols{i});
end
else
thdr{1, 2} = datedata(:, 2)';
if size(thdr, 1) > 1
for i=2:size(thdr, 1)
split = find(thdr{i-1, 2} == dates.freq, 1, 'first');
assert(~isempty(split), '@report_table.writeTableFile: Shouldn''t arrive here');
thdr{i, 2} = thdr{i-1, 2}(split+1:end);
thdr{i-1, 2} = thdr{i-1, 2}(1:split);
end
end
for i=1:size(thdr, 1)
fprintf(fid, ' & \\multicolumn{%d}{c}{%d}', size(thdr{i,2}, 2), thdr{i,1});
end
for i=1:length(rhscols)
fprintf(fid, ' & %s', rhscols{i});
end
fprintf(fid, '\\\\\n');
switch dates.freq
case 4
sep = 'Q';
case 12
sep = 'M';
case 52
sep = 'W';
otherwise
error('@report_table.writeTableFile: Invalid frequency.');
end
for i=1:size(thdr, 1)
period = thdr{i, 2};
for j=1:size(period, 2)
fprintf(fid, ' & \\multicolumn{1}{c');
if o.showVlines
fprintf(fid, '|');
elseif o.vlineAfterEndOfPeriod && j == size(period, 2)
fprintf(fid, '|');
elseif ~isempty(o.vlineAfter)
for k=1:length(o.vlineAfter)
if o.vlineAfter{k}.time(1) == thdr{i} && ...
o.vlineAfter{k}.time(2) == period(j)
fprintf(fid, '|');
end
end
end
fprintf(fid, '}{%s%d}', sep, period(j));
end
fprintf(fid, '}{%s%d}', sep, period(j));
end
end
else
fprintf(fid, '\\begin{tabular}{|');
for i = 1:length(o.column_names)
if isempty(o.column_names{i})
fprintf(fid, 'l');
else
fprintf(fid, 'r');
end