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 unless (figure_data.is_a?(FigureData) or figure_data.is_a?(SmrAutofigureData))
43 raise ':figure_data must be of FigureData or SmrAutofigureData'
45 @last_figure_data = figure_data
46 @last_figure_var = @last_figure_data.FigureVar
48 # define locals to shorten the code below
49 name = @last_figure_var.name
50 id = @last_figure_var.id.to_i
51 year = @last_figure_data.time.year.to_i
52 period = @last_figure_data.period
54 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
55 # we do not know this figure yet, map_add to map
56 @map[name][year][id][period] = @last_figure_data.time
59 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
63 # known but more recent => update map
64 @map[name][year][id][period] = figure_data.time
71 # return name last FigureVar processed by map_add()
77 # return FigureData id of the last item processed by map_add()
78 def get_figure_data_id
83 # return period of the last item processed by map_add()
85 @last_figure_data.period
89 # return date as +Time+ of the last item processed by map_add()
91 @last_figure_data.time
96 # Builds FigureVar and FigureData objects into a form that is easy to process
99 # Use add() to push FigureData records into the table. When done, tell the object
100 # to process all data by calling render(). Thereafter all data is available for
101 # display through the +get_*+ methods.
103 # Note that methods return false or raise an exception when used their proper
106 class SmrFiguresDataTable < SmrFiguresDataMap
111 # mode of operation (input or display)
113 # The add() mgqethod is only active in input mode while all the +get_*+
114 # methods are only available in display mode. You must call render()
115 # before you can get any data out. A new object is in input mode.
118 # see add() and add_record()
119 @most_recent_figures = Array.new
121 # see add_record()a and update_data()
122 @less_recent_figures = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
124 # the final table created by render()
125 @table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
131 # CSS class string used for rows, altered by self.cycle_css_row()
132 @cycle_css_row = 'row1'
136 # Process a FigureData while in input mode.
138 raise 'add() only works in input mode, it stops working when render() was called' if not @input_mode
140 case self.map_add(figure_data)
141 when false then self.add_record(figure_data)
142 when 1 then self.add_record(figure_data, {:less_recent=>true})
143 when 2 then self.update_data(figure_data)
150 # End input mode and render all data ready for displaying
152 # NOTE: @table is a multi-dimensional array but we use only two dimensions,
153 # see http://stackoverflow.com/questions/10253105/dynamically-creating-a-multi-dimensional-hash-in-ruby
155 # Final table will look like when viewed by calling cell() on each
156 # +row+,+col+ coordinate:
158 # ______ ________________ ____________ ______________ _______________ _____
159 # | | | - Year - | - Quarter 1 -| - Quarter 2 - | ... |
160 # | year | FigureVar.name | FigureData | FigureData | FigureData | ... |
161 # |------|----------------|------------|--------------|---------------|-----|
164 # The text_table() method is very useful for debugging.
167 raise 'no data to render(), use add() first' if @most_recent_figures.empty?
169 @table.clear if @table.count > 0
172 periods = FigureData.new.get_periods
175 periods.each_index {|i| @table[0][i+2]=FigureData.new.translate_period(periods[i])}
177 # add most recent FigureVar
178 # - first: sort by FigureVar.time
179 # - second: add by period index + offset (because of the table head)
180 # ATTENTION: sorting by fd.FigureVar.name probably expensive, consider
181 # fs.id_figure_var as performant alternative
182 @most_recent_figures.sort_by! { |fd| [fd.time.year, fd.FigureVar.name] }
185 @most_recent_figures.each do |fd|
186 @table[row][0] = fd.time.year
187 @table[row][1] = fd.FigureVar.name
189 col = periods.index(fd.period) + 2
190 @table[row][col] = fd
195 # consolidate rows by years and figure name (first two columns)
198 @table.each do |k, row|
199 next if k == 0 # skip table head
201 if row[0]==prev_row[0] and row[1]==prev_row[1] then
202 # merge into prev_row since row is more recent, see sort_by above
203 @table[k] = prev_row.merge(row)
204 @table.delete(prev_key) if k > prev_key
210 # re-index @table since rows(), columns() and cell() rely on
211 # consecutive numbering
212 tmp_table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
214 @table.each_key do |k|
215 tmp_table[i] = @table[k]
222 # print table in text format (for debugging only!)
225 format = Array.new(self.columns, '%12s').join('|')
227 puts '----------- @table ------------'
228 for r in 0...self.rows
230 for c in 0...self.columns
231 row[c] = self.cell(r, c)
235 puts '-------------------------------'
239 # Returns number of rows after render().
241 raise 'rows() only works after render() was called' if @input_mode
246 # Returns number of columns after render().
247 # NOTE: the first row sets the number of columns of the table
249 raise 'columns() only works after render() was called' if @input_mode
254 # Returns content for a cell identified by row and col, nil if empty.
256 raise 'cell() only works after render() was called' if @input_mode
258 if c = self.raw_cell_content(row, col) then
259 if c.is_a?(FigureData) or c.is_a?(SmrAutofigureData)
262 # leftmost column is always a descriptive string, not to be
263 # rounded, scaled, whatever ...
264 if col==0 then c.to_s else c end
272 # Returns unit of value in cell or nil if there is nothing.
273 def cell_unit(row, col)
274 raise 'cell_unit() only works after render() was called' if @input_mode
276 if c = self.raw_cell_content(row, col) then
277 if c.is_a?(FigureData) or c.is_a?(SmrAutofigureData)
284 # Return rowspanning information for given cell (row, col) or false if this
285 # cell should not span at all
287 # rowspanning information for @datatable is stored in @rowspan and looks
289 # - two-dimensional array: #row, #col
290 # - a number indicates how many lines a cell should span down
291 # - a dash ('-') indicates that a cell is covered by another cell that
293 # - #row,#col coordinates are not set means the cell is not affected by
295 def rowspan(row, col)
296 raise 'rowspan() only works after render() was called' if @input_mode
301 # Return class information for given row or cell
303 # False is returned if there is no such information (the row or cell should
304 # not have any class attribute set).
306 # The +col+ paramenter is optional, if given you ask for a specific cell,
307 # if ommited (default) you ask for the entire row.
309 # CSS class information for @datatable is stored in @class and looks like
311 # - one-or-two-dimensional array: #row, #col
312 # - contains a string of CSS class names that should be set on a given row
314 # - false means that there shouldn't be any class attribute set
315 def class(row, col=false)
316 raise 'class() only works after render() was called' if @input_mode
321 # Return link to edit FigureVar of given field
323 # False is returned if that field is empty.
325 raise 'link() only works after render() was called' if @input_mode
327 if c = self.raw_cell_content(row, col) then
328 if c.is_a?(FigureData) then return('/figures/%i/edit' % c.id) end
337 # add FigureData to internal @most_recent_figures or @less_recent_figures
339 # @less_recent_figures contains all those where a more recent counterpart
340 # is available in @most_recent_figures. See get_summary() and
343 # @most_recent_figures is a regular +Array+
345 # @less_recent_figures is a multi-dimensional +Hash+ structured as
347 # @less_recent_figures[:year][:period][:id_figure_data] = Array.new
349 def add_record(figure_data, options={ :less_recent=>false })
350 y = figure_data.time.year
351 p = figure_data.period
354 if options[:less_recent] then
355 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
356 @less_recent_figures[y][p][id] << figure_data
358 @less_recent_figures[y][p][id] = [figure_data]
361 @most_recent_figures << figure_data
368 # update FigureData item in @most_recent_figures, make sure the old data is
369 # kept in @less_recent_figures.
370 # FIXME: to be implemented!
371 def update_data(figure_data)
372 #@most_recent_figures.find(:id_figure_var=>figure_data.id_figure_var, ...)
377 # Returns content for a cell identified by row and col.
379 # The content can be +nil+ if that cell is empty or whatever is there, ie.
380 # a FigureData, a +String+, etc....
381 def raw_cell_content(row, col)
382 if @table.has_key?(row) and @table[row].has_key?(col) then
389 # creates many (!) SmrAutofigureData objects from a FigureVar record
391 # A SmrAutofigure is based on a FigureVar record with the :expression field
392 # containing a math expression. That is each FigureVar becomes a SmrAutofigure
393 # by setting the :expression field. It will return to being a ordinary
394 # FigureVar by emptying the the :expression field.
396 # Each SmrAutofigure will create as many SmrAutofigureData objects as possible,
397 # depending on the information available to solve the given :expression. It
398 # might be hundreds if there is input data for, say, 100 fiscal quarters.
403 # Init with FigureVar and a list of variables contained in its :expression.
404 # See SmrDaemonClient#parse_math_expressions.
405 def initialize(figure_var, variables)
406 raise 'figure_var must be a FigureVar object' unless figure_var.is_a?(FigureVar)
407 raise 'figure_var must have :expression field set' if figure_var.expression.empty?
408 @autofigure = figure_var
409 @expression_vars = Hash.new
411 # internally we work with FigureVar ids, not with their names
412 FigureVar.select(:id, :name).where(:name=>variables).each do |v|
413 @expression_vars[v.id]=v.name
416 # collects FigureData by year and period, to know what we have (=>
417 # #add()) and still need (=> #get_missing_variables)
418 @datamatrix = Hash.new
420 # collects Time if most recent FigureData per row in @datamatrix,
421 # necessary to know how to time the SmrAutofigureData objects
422 @timematrix = Hash.new
428 # Add FigureData as input for solving the expression.
430 # Returns +true+ if it was added or +false+ if it did not help to fill this
433 # Note: data is overwritten. A FigureData of same +:year+ and +:period+
434 # will overwrite the previously passed one. So mind the order.
436 raise 'figure_data must be a FigureData object' unless figure_data.is_a?(FigureData)
438 if @id_stock and @id_stock != figure_data.id_stock
439 raise 'add()ing FigureData of another Stock should not happen, should it?'
441 @id_stock = figure_data.id_stock
444 i = '%s_%s' % [figure_data.time.year, figure_data.period ]
445 if @expression_vars.keys.include?(figure_data.id_figure_var)
446 newdata = { figure_data.id_figure_var=>figure_data.value }
447 @datamatrix[i] = if @datamatrix[i] then @datamatrix[i].merge(newdata) else newdata end
449 if not @timematrix[i] or @timematrix[i] < figure_data.time then
450 @timematrix[i] = figure_data.time
460 # Tells whats missing to solve all expressions.
461 def get_missing_variables
463 @datamatrix.each do |i,d|
464 diff = @expression_vars.keys - d.keys
466 missing << i.to_s + '_' + @expression_vars.select{|k,v| diff.include?(k) }.values.join('-')
473 # Returns collection of SmrAutofigureData objects with solvable
474 # expressions. Objects with non-solvable expressions (missing data) are
477 @datamatrix.collect do |i,d|
478 diff = @expression_vars.keys - d.keys
481 d.each{ |k,v| dn[@expression_vars[k]]=v }
482 SmrAutofigureData.new(@autofigure, dn, @id_stock, i.split('_').second, @timematrix[i])
490 # like a FigureData object without related database record
492 # A SmrAutofigureData object is ment to behave exactly like a FigureData
493 # object. Except that it can`t be saved or updated or trigger any other
494 # database operation. Its an entirely artificial record so to say.
495 class SmrAutofigureData
496 # compatibility with FigureData, always +nil+
499 # compatibility with FigureData, empty
500 attr_reader :analyst, :is_expected, :is_audited, :comment
502 # where this autofigure was derived from
503 attr_reader :id_figure_var, :id_stock, :period
505 # unix timestamp when this autofigure was made, also see time()
508 # the Hash of data variables and values necessary for solving
509 attr_reader :expression, :solving_data
511 # result of the expression or +nil+ if not yet solved
512 # Note: this must be calculated/set externaly
516 # Give FigureVar out of which this SmrAutofigureData is derived and a Hash
517 # of var=>value for solving the expression. +id_stock+ is to know what this
519 def initialize(autofigure, values, id_stock, period, time)
520 raise 'autofigure must be a FigureVar' unless autofigure.is_a?(FigureVar)
521 raise 'values must be a Hash' unless values.is_a?(Hash)
522 raise 'time must be of Time' unless time.is_a?(Time)
523 @autofigure = autofigure
524 @expression = autofigure.expression
525 @solving_data = values
527 # carry on meaningful fields
528 @id_figure_var = autofigure.id
532 @analyst = self.class
536 # Time when this autofigure was made
542 # wrapper for compatibility with FigureData
544 FigureData.new.get_periods
548 # wrapper for compatibility with FigureData
549 def translate_period(period=self.period)
550 FigureData.new.translate_period(period)
554 # wrapper for compatibility with FigureData