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/>.
19 # Implements a mapping mechanism to keep track of the FigureData records of
20 # most recent relevance. Its most useful when inherited from the actual
25 @map = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
26 @last_figure_var = FigureVar.new
27 @last_figure_data = FigureData.new
31 # Add a FigureData to the map, return code indicates whether this data is
32 # already known (1), known and more recent (2) or not known at all (false).
34 # Map items are categorized into (:name, :year, :id, :period). This is a
35 # multi-dimensional Hash, see
36 # http://stackoverflow.com/questions/10253105/dynamically-creating-a-multi-dimensional-hash-in-ruby
38 # Unknown items will be map_added to the map. If a FigureData with the same
39 # categorization is passed, the map uses :time to decide whether to update
42 def map_add(figure_data)
43 unless (figure_data.is_a?(FigureData) or figure_data.is_a?(Smr::AutofigureData))
44 raise ':figure_data must be of FigureData or SmrAutofigureData'
46 @last_figure_data = figure_data
47 @last_figure_var = @last_figure_data.FigureVar
49 # define locals to shorten the code below
50 name = @last_figure_var.name
51 id = @last_figure_var.id.to_i
52 year = @last_figure_data.time.year.to_i
53 period = @last_figure_data.period
55 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
56 # we do not know this figure yet, map_add to map
57 @map[name][year][id][period] = @last_figure_data.time
60 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
64 # known but more recent => update map
65 @map[name][year][id][period] = figure_data.time
72 # Return name last FigureVar processed by #map_add.
78 # Return FigureData id of the last item processed by #map_add.
79 def get_figure_data_id
84 # Return period of the last item processed by #map_add.
86 @last_figure_data.period
90 # Return date as +Time+ of the last item processed by #map_add.
92 @last_figure_data.time
97 # Builds FigureVar and FigureData objects into a form that is easy to process
100 # Use #add to push FigureData records into the table. When done, tell the object
101 # to process all data by calling #render. Thereafter all data is available for
102 # display through the +get_*+ methods.
104 # Note that methods return false or raise an exception when used their proper
106 class FiguresDataTable < FiguresDataMap
109 # mode of operation (input or display)
111 # The #add method is only active in input mode while all the +get_*+
112 # methods are only available in display mode. You must call #render
113 # before you can get any data out. A new object is in input mode.
114 attr_reader :input_mode
117 # Most recent figures, see #add and #add_record.
118 attr_reader :most_recent_figures
121 # All figures not most recent, see #add and #add_record.
122 attr_reader :less_recent_figures
125 # Collection of years the table contains data for.
132 @most_recent_figures = Array.new
133 @less_recent_figures = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
136 # the final table created by #render
137 @table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
139 # rowspanning information, see and use #rowspan
144 # Process a FigureData while in input mode.
146 raise 'add() only works in input mode, it stops working when #render was called' if not @input_mode
148 case map_add(figure_data)
149 when false then add_record(figure_data)
150 when 1 then add_record(figure_data, {:less_recent=>true})
151 when 2 then update_data(figure_data)
158 # End input mode and render all data ready for displaying.
160 # NOTE: @table is a multi-dimensional array but we use only two dimensions,
161 # see http://stackoverflow.com/questions/10253105/dynamically-creating-a-multi-dimensional-hash-in-ruby
163 # Final table will look like when viewed by calling #cell on each
164 # +row+,+col+ coordinate:
166 # ______ ________________ ____________ ______________ _______________ _____
167 # | | | - Year - | - Quarter 1 -| - Quarter 2 - | ... |
168 # | year | FigureVar.name | FigureData | FigureData | FigureData | ... |
169 # |------|----------------|------------|--------------|---------------|-----|
172 # The #text_table method is very useful for debugging.
175 raise 'no data to #render, use add() first' if @most_recent_figures.empty?
177 @table.clear if @table.count > 0
178 periods = FigureData.new.get_periods
181 # add most recent FigureVar
182 # - first: sort by FigureVar.time
183 # - second: add by period index + offset (because of the table head)
184 # ATTENTION: sorting by fd.FigureVar.name probably expensive, consider
185 # fs.id_figure_var as performant alternative
186 @most_recent_figures.sort_by! { |fd| [fd.time.year, fd.FigureVar.name] }
189 @most_recent_figures.each do |fd|
190 @table[row][0] = fd.time.year
191 @table[row][1] = fd.FigureVar.name
193 col = periods.index(fd.period) + 2
194 @table[row][col] = fd
199 # consolidate rows by years and figure name (first two columns)
202 @table.each do |k, row|
203 if row[0]==prev_row[0] and row[1]==prev_row[1] then
204 # merge into prev_row since row is more recent, see sort_by above
205 @table[k] = prev_row.merge(row)
206 @table.delete(prev_key) if k > prev_key
213 for row in 1..@table.count do
214 @rowspan.store(row, 0)
217 # sort table by years
218 # - all elements within a year keep their order as is
219 # - our @years accessor is ordered the same way
220 # - this also makes a new row-index beginning with 1
221 @years.sort!.reverse!
222 tmp_table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
226 @years.each{|y| tmp << @table.select {|k, v| v[0] == y } }
228 next_year = true; firstrowyear = 1;
229 for yi in 0..@years.count-1 do
230 tmp[yi].each do |k,v|
231 tmp_table.store(row, v)
234 firstrowyear = row if next_year
235 @rowspan[firstrowyear] += 1
243 # blank over-spanned lines with +false+ in first column
245 for row in 2..@table.count do
247 if not @rowspan[row-1].is_a?(FalseClass)
248 span = @rowspan[row-1] if @rowspan[row-1] > 0
250 @rowspan[row]=false if span>0
256 # - this will be row 0
257 # - table starts always with 0 and data follows from row-index 1+
260 periods.each_index {|i| @table[0][i+2]=FigureData.new.translate_period(periods[i])}
266 # print table in text format (for debugging only!)
269 format = Array.new(columns, '%15s').join('|')
271 puts '-' * ((columns+1) * 15)
274 rowstr[0] = 'sp:%5s %4s ' % [rowspan(r), cell(r, 0)]
276 rowstr[c] = cell(r, c)
278 puts(format % rowstr)
280 puts '-' * ((columns+1) * 15)
285 # Returns number of rows after #render.
287 raise '#rows only works after #render was called' if @input_mode
292 # Returns number of columns after #render.
293 # NOTE: the first row sets the number of columns of the table
295 raise '#columns only works after #render was called' if @input_mode
300 # Returns content for a cell identified by row and col, nil if empty.
302 raise '#cell only works after #render was called' if @input_mode
304 if c = raw_cell_content(row, col) then
305 if c.is_a?(FigureData) or c.is_a?(Smr::AutofigureData)
308 # leftmost column is always a descriptive string, not to be
309 # rounded, scaled, whatever ...
310 if col==0 then c.to_s else c end
318 # Returns unit of value in cell or nil if there is nothing.
319 def cell_unit(row, col)
320 raise '#cell_unit only works after #render was called' if @input_mode
322 if c = raw_cell_content(row, col) then
323 if c.is_a?(FigureData) or c.is_a?(Smr::AutofigureData)
330 # Return rowspanning information for given row.
332 raise '#rowspan only works after #render was called' if @input_mode
333 if @rowspan.key?(row) then @rowspan[row] else 1 end
337 # Return link to edit FigureVar of given field.
339 # False is returned if that field is empty.
341 raise '#link only works after #render was called' if @input_mode
343 if c = raw_cell_content(row, col) then
344 if c.is_a?(FigureData) then return('/figures/%i/edit' % c.id) end
353 # Add FigureData to internal @most_recent_figures or @less_recent_figures.
355 # #less_recent_figures contains all those where a more recent counterpart
356 # is available in #most_recent_figures. See #get_summary and #have_summary.
358 # #most_recent_figures is a regular +Array+.
360 # #less_recent_figures is a multi-dimensional +Hash+ structured as:
362 # @less_recent_figures[:year][:period][:id_figure_data] = Array.new
364 def add_record(figure_data, options={ :less_recent=>false })
365 y = figure_data.time.year
366 p = figure_data.period
369 if options[:less_recent] then
370 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
371 @less_recent_figures[y][p][id] << figure_data
373 @less_recent_figures[y][p][id] = [figure_data]
376 @most_recent_figures << figure_data
377 @years << y if not @years.include?(y)
384 # Update FigureData item in #most_recent_figures, make sure the old data is
385 # kept in #less_recent_figures.
387 # FIXME: to be implemented!
388 def update_data(figure_data)
389 #@most_recent_figures.find(:id_figure_var=>figure_data.id_figure_var, ...)
394 # Returns content for a cell identified by row and col.
396 # The content can be +nil+ if that cell is empty or whatever is there, ie.
397 # a FigureData, a +String+, etc....
398 def raw_cell_content(row, col)
399 if @table.has_key?(row) and @table[row].has_key?(col) then
406 # Creates many (!) AutofigureData objects from a FigureVar record.
408 # A Autofigure is based on a FigureVar record with the :expression field
409 # containing a math expression. That is each FigureVar becomes a Autofigure
410 # by setting the :expression field. It will return to being a ordinary
411 # FigureVar by emptying the the :expression field.
413 # Each Autofigure will create as many AutofigureData objects as possible,
414 # depending on the information available to solve the given :expression. It
415 # might be hundreds if there is input data for, say, 100 fiscal quarters.
420 # Init with FigureVar and a list of variables contained in its :expression.
421 # See SmrDaemonClient#parse_math_expressions.
422 def initialize(figure_var, variables)
423 raise 'figure_var must be a FigureVar object' unless figure_var.is_a?(FigureVar)
424 raise 'figure_var must have :expression field set' if figure_var.expression.empty?
425 @autofigure = figure_var
426 @expression_vars = Hash.new
428 # internally we work with FigureVar ids, not with their names
429 FigureVar.select(:id, :name).where(:name=>variables).each do |v|
430 @expression_vars[v.id]=v.name
433 # collects FigureData by year and period, to know what we have (=>
434 # #add()) and still need (=> #get_missing_variables)
435 @datamatrix = Hash.new
437 # collects Time if most recent FigureData per row in @datamatrix,
438 # necessary to know how to time the SmrAutofigureData objects
439 @timematrix = Hash.new
445 # Add FigureData as input for solving the expression.
447 # Returns +true+ if it was added or +false+ if it did not help to fill this
450 # Note: data is overwritten. A FigureData of same +:year+ and +:period+
451 # will overwrite the previously passed one. So mind the order.
453 raise 'figure_data must be a FigureData object' unless figure_data.is_a?(FigureData)
455 if @id_stock and @id_stock != figure_data.id_stock
456 raise 'add()ing FigureData of another Stock should not happen, should it?'
458 @id_stock = figure_data.id_stock
461 i = '%s_%s' % [figure_data.time.year, figure_data.period ]
462 if @expression_vars.keys.include?(figure_data.id_figure_var)
463 newdata = { figure_data.id_figure_var=>figure_data.value }
464 @datamatrix[i] = if @datamatrix[i] then @datamatrix[i].merge(newdata) else newdata end
466 if not @timematrix[i] or @timematrix[i] < figure_data.time then
467 @timematrix[i] = figure_data.time
477 # Tells whats missing to solve all expressions.
478 def get_missing_variables
480 @datamatrix.each do |i,d|
481 diff = @expression_vars.keys - d.keys
483 missing << i.to_s + '_' + @expression_vars.select{|k,v| diff.include?(k) }.values.join('-')
490 # Returns collection of AutofigureData objects with solvable
491 # expressions. Objects with non-solvable expressions (missing data) are
494 @datamatrix.collect do |i,d|
495 diff = @expression_vars.keys - d.keys
498 d.each{ |k,v| dn[@expression_vars[k]]=v }
499 AutofigureData.new(@autofigure, dn, @id_stock, i.split('_').second, @timematrix[i])
507 # Like a FigureData object in behaviour but without related database record.
509 # A AutofigureData object is ment to behave exactly like a FigureData
510 # object. Except that it can`t be saved or updated or trigger any other
511 # database operation. Its an entirely artificial record so to say.
513 # compatibility with FigureData, always +nil+
516 # compatibility with FigureData, empty
517 attr_reader :analyst, :is_expected, :is_audited, :comment
519 # where this autofigure was derived from
520 attr_reader :id_figure_var, :id_stock, :period
522 # unix timestamp when this autofigure was made, also see time()
525 # the Hash of data variables and values necessary for solving
526 attr_reader :expression, :solving_data
528 # result of the expression or +nil+ if not yet solved
529 # Note: this must be calculated/set externaly
533 # Give FigureVar out of which this SmrAutofigureData is derived and a Hash
534 # of var=>value for solving the expression. +id_stock+ is to know what this
536 def initialize(autofigure, values, id_stock, period, time)
537 raise 'autofigure must be a FigureVar' unless autofigure.is_a?(FigureVar)
538 raise 'values must be a Hash' unless values.is_a?(Hash)
539 raise 'time must be of Time' unless time.is_a?(Time)
540 @autofigure = autofigure
541 @expression = autofigure.expression
542 @solving_data = values
544 # carry on meaningful fields
545 @id_figure_var = autofigure.id
549 @analyst = self.class
553 # Time when this autofigure was made.
559 # Wrapper for compatibility with FigureData.
561 FigureData.new.get_periods
565 # Wrapper for compatibility with FigureData.
566 def translate_period(period=@period)
567 FigureData.new.translate_period(period)
571 # Wrapper for compatibility with FigureData.