2 # This file is part of SMR.
4 # SMR is free software: you can redistribute it and/or modify it under the
5 # terms of the GNU General Public License as published by the Free Software
6 # Foundation, either version 3 of the License, or (at your option) any later
9 # SMR is distributed in the hope that it will be useful, but WITHOUT ANY
10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along with
14 # SMR. If not, see <http://www.gnu.org/licenses/>.
18 # Implements a mapping mechanism to keep track of the FigureData records of
19 # most recent relevance. Its most useful when inherited from the actual
21 class SmrFiguresDataMap
24 @map = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
25 @last_figure_var = FigureVar.new
26 @last_figure_data = FigureData.new
30 # Add a FigureData to the map, return code indicates whether this data is
31 # already known (1), known and more recent (2) or not known at all (false).
33 # Map items are categorized into (:name, :year, :id, :period). This is a
34 # multi-dimensional Hash, see
35 # http://stackoverflow.com/questions/10253105/dynamically-creating-a-multi-dimensional-hash-in-ruby
37 # Unknown items will be map_added to the map. If a FigureData with the same
38 # categorization is passed, the map uses :time to decide whether to update
41 def map_add(figure_data)
42 @last_figure_data = figure_data
43 @last_figure_var = @last_figure_data.FigureVar
45 # define locals to shorten the code below
46 name = @last_figure_var.name
47 id = @last_figure_var.id.to_i
48 year = @last_figure_data.time.year.to_i
49 period = @last_figure_data.period
51 if not (@map.has_key?(name) and @map[name].has_key?(year) and @map[name][year].has_key?(id) and @map[name][year][id].has_key?(period)) then
52 # we do not know this figure yet, map_add to map
53 @map[name][year][id][period] = @last_figure_data.time
56 if (@map.has_key?(name) and @map[name].has_key?(year) and @map[name][year].has_key?(id) and @map[name][year][id].has_key?(period)) and @map[name][year][id][period] > figure_data.time then
60 # known but more recent => update map
61 @map[name][year][id][period] = figure_data.time
68 # return name last FigureVar processed by map_add()
74 # return FigureData id of the last item processed by map_add()
75 def get_figure_data_id
80 # return list of all uniq FigureVar id's known by the map or false in case there is none.
94 # if ids.empty? then false else ids end
98 # return period of the last item processed by map_add()
100 @last_figure_data.period
104 # return date as +Time+ of the last item processed by map_add()
106 @last_figure_data.time
111 # Builds FigureVar and FigureData objects into a form that is easy to process
114 # Use add() to push FigureData records into the table. When done, tell the object
115 # to process all data by calling render(). Thereafter all data is available for
116 # display through the +get_*+ methods.
118 # Note that methods return false or raise an exception when used their proper
121 class SmrFiguresDataTable < SmrFiguresDataMap
126 # mode of operation (input or display)
128 # The add() mgqethod is only active in input mode while all the +get_*+
129 # methods are only available in display mode. You must call render()
130 # before you can get any data out. A new object is in input mode.
136 # list of periods that exist in SMR
138 # Used as @datatable header, see data_table().
139 # @periods_existing = FigureData.new.get_periods
141 # see add() and add_record()
142 @most_recent_figures = Array.new
144 # see add_record()a and update_data()
145 @less_recent_figures = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
147 # the final table created by render()
148 # - tmp_table is same thing, just to be used for temprary operation on
150 @table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
159 # CSS class string used for rows, altered by self.cycle_css_row()
160 @cycle_css_row = 'row1'
164 # Process a FigureData while in input mode.
166 raise 'add() only works in input mode, it stops working when render() was called' if not @input_mode
168 case self.map_add(figure_data)
169 when false then self.add_record(figure_data)
170 when 1 then self.add_record(figure_data, {:less_recent=>true})
171 when 2 then self.update_data(figure_data)
174 # if not @years.include?(figure_data.time.year)
175 # @years << figure_data.time.year
182 # End input mode and render all data ready for displaying
184 # NOTE: @table is a multi-dimensional array but we use only two dimensions,
185 # see http://stackoverflow.com/questions/10253105/dynamically-creating-a-multi-dimensional-hash-in-ruby
187 # Final table will look like when viewed by calling cell() on each
188 # +row+,+col+ coordinate:
190 # ______ ________________ ____________ ______________ _______________ _____
191 # | | | - Year - | - Quarter 1 -| - Quarter 2 - | ... |
192 # | year | FigureVar.name | FigureData | FigureData | FigureData | ... |
193 # |------|----------------|------------|--------------|---------------|-----|
196 # The text_table() method is very useful for debugging.
199 raise 'no data to render(), use add() first' if @most_recent_figures.empty?
201 @table.clear if @table.count > 0
204 periods = FigureData.new.get_periods
207 periods.each_index {|i| @table[0][i+2]=FigureData.new.translate_period(periods[i])}
209 # add most recent FigureVar
210 # - first: sort by FigureVar.time
211 # - second: add by period index + offset (because of the table head)
212 # ATTENTION: sorting by fd.FigureVar.name probably expensive, consider
213 # fs.id_figure_var as performant alternative
214 @most_recent_figures.sort_by! { |fd| [fd.time.year, fd.FigureVar.name] }
217 @most_recent_figures.each do |fd|
218 @table[row][0] = fd.time.year
219 @table[row][1] = fd.FigureVar.name
221 col = periods.index(fd.period) + 2
222 @table[row][col] = fd
227 # consolidate rows by years and figure name (first two columns)
230 @table.each do |k, row|
231 next if k == 0 # skip table head
233 if row[0]==prev_row[0] and row[1]==prev_row[1] then
234 # merge into prev_row since row is more recent, see sort_by above
235 @table[k] = prev_row.merge(row)
236 #p "==> %s" % prev_row
238 #p "=== %s" % @table[k]
240 @table.delete(prev_key) if k > prev_key
246 # re-index @table since rows(), columns() and cell() rely on
247 # consecutive numbering
248 tmp_table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
250 @table.each_key do |k|
251 tmp_table[i] = @table[k]
260 # print table in text format (for debugging only!)
263 format = Array.new(self.columns, '%12s').join('|')
265 puts '----------- @table ------------'
266 for r in 0...self.rows
268 for c in 0...self.columns
269 row[c] = self.cell(r, c)
273 puts '-------------------------------'
277 # Returns number of rows after render().
279 raise 'rows() only works after render() was called' if @input_mode
284 # Returns number of columns after render().
285 # NOTE: the first row sets the number of columns of the table
287 raise 'columns() only works after render() was called' if @input_mode
292 # Returns content for a cell identified by row and col
294 # The content can be +nil+ if that cell is emtpy or a +String+
296 raise 'cell() only works after render() was called' if @input_mode
298 if @table.has_key?(row) and @table[row].has_key?(col) then
299 if @table[row][col].is_a?(FigureData)
300 @table[row][col].value.to_s
302 @table[row][col].to_s
310 # Return rowspanning information for given cell (row, col) or false if this
311 # cell should not span at all
313 # rowspanning information for @datatable is stored in @rowspan and looks
315 # - two-dimensional array: #row, #col
316 # - a number indicates how many lines a cell should span down
317 # - a dash ('-') indicates that a cell is covered by another cell that
319 # - #row,#col coordinates are not set means the cell is not affected by
321 def rowspan(row, col)
322 raise 'rowspan() only works after render() was called' if @input_mode
327 # Return class information for given row or cell
329 # False is returned if there is no such information (the row or cell should
330 # not have any class attribute set).
332 # The +col+ paramenter is optional, if given you ask for a specific cell,
333 # if ommited (default) you ask for the entire row.
335 # CSS class information for @datatable is stored in @class and looks like
337 # - one-or-two-dimensional array: #row, #col
338 # - contains a string of CSS class names that should be set on a given row
340 # - false means that there shouldn't be any class attribute set
341 def class(row, col=false)
342 raise 'class() only works after render() was called' if @input_mode
348 # returns list off years for which we have figures available
350 # return false if not @input_mode
355 # # returns list off all periods that exist SMR
356 # def get_periods_existing
357 # return false if not @input_mode
365 # retrieve less recent fundamental figures by +row+/+col+
367 # These are those figures that have a more recent counterpart in
368 # get_value(). Returns false if there is no such data available. See add().
369 # def get_summary(row, col)
370 # return false if not @input_mode
373 # if val = self.get_value(row, col) then
376 # id = val.id_figure_var
378 # retval = if @summary[y][p][id] then @summary[y][p][id] end
386 # add FigureData to internal @most_recent_figures or @less_recent_figures
388 # @less_recent_figures contains all those where a more recent counterpart
389 # is available in @most_recent_figures. See get_summary() and
392 # @most_recent_figures is a regular +Array+
394 # @less_recent_figures is a multi-dimensional +Hash+ structured as
396 # @less_recent_figures[:year][:period][:id_figure_data] = Array.new
398 def add_record(figure_data, options={ :less_recent=>false })
399 y = figure_data.time.year
400 p = figure_data.period
403 if options[:less_recent] then
404 if @less_recent_figures.has_key?(y) and @less_recent_figures[y].has_key?(p) and @less_recent_figures[y][p].has_key?(id) then
405 @less_recent_figures[y][p][id] << figure_data
407 @less_recent_figures[y][p][id] = Array.new(figure_data)
410 @most_recent_figures << figure_data
417 # update FigureData item in @most_recent_figures, make sure the old data is
418 # kept in @less_recent_figures.
419 # FIXME: to be implemented!
420 def update_data(figure_data)
421 #@most_recent_figures.find(:id_figure_var=>figure_data.id_figure_var, ...)
426 # # returns cycled CSS class string to be used to rows, usefull when
427 # # looping through rows
429 # if @cycle_css_row == 'row1' then
430 # @cycle_css_row = 'row2'
432 # @cycle_css_row = 'row1'
439 # creates many (!) SmrAutoFigureData objects from a FigureVar record
441 # A SmrAutoFigure is based on a FigureVar record with the :expression field
442 # containing a math expression. That is each FigureVar becomes a SmrAutoFigure
443 # by setting the :expression field. It will return to being a ordinary
444 # FigureVar by emptying the the :expression field.
446 # Each SmrAutoFigure will create as many SmrAutoFigureData objects as possible,
447 # depending on the information available to solve the given :expression. It
448 # might be hundreds if there is input data for, say, 100 fiscal quarters.
451 def initialize(figure_var)
456 # like a FigureData object without related database record
458 # A SmrAutoFigureData object is ment to behave <u>exactly</u> like a FigureData
459 # object. Except that it can`t be +save()+ed or +update()+ed or trigger any
460 # other database operation. Its an entirely artificial record so to say.
462 class SmrAutoFigureData