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/>.
20 # Implements a mapping mechanism to keep track of the FigureData records of
21 # most recent relevance. Its most useful when inherited from the actual
26 @map = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
27 @last_figure_var = FigureVar.new
28 @last_figure_data = FigureData.new
32 # Add a FigureData to the map, return code indicates whether this data is
33 # already known (1), known and more recent (2) or not known at all (false).
35 # Map items are categorized into (:name, :year, :id, :period). This is a
36 # multi-dimensional Hash, see
37 # http://stackoverflow.com/questions/10253105/dynamically-creating-a-multi-dimensional-hash-in-ruby
39 # Unknown items will be map_added to the map. If a FigureData with the same
40 # categorization is passed, the map uses :time to decide whether to update
43 def map_add(figure_data)
44 unless (figure_data.is_a?(FigureData) or figure_data.is_a?(Smr::AutofigureData))
45 raise ':figure_data must be of FigureData or SmrAutofigureData'
47 @last_figure_data = figure_data
48 @last_figure_var = @last_figure_data.FigureVar
50 # define locals to shorten the code below
51 name = @last_figure_var.name
52 id = @last_figure_var.id.to_i
53 year = @last_figure_data.time.year.to_i
54 period = @last_figure_data.period
56 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
57 # we do not know this figure yet, map_add to map
58 @map[name][year][id][period] = @last_figure_data.time
61 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
65 # known but more recent => update map
66 @map[name][year][id][period] = figure_data.time
73 # Return name last FigureVar processed by #map_add.
79 # Return FigureData id of the last item processed by #map_add.
80 def get_figure_data_id
85 # Return period of the last item processed by #map_add.
87 @last_figure_data.period
91 # Return date as +Time+ of the last item processed by #map_add.
93 @last_figure_data.time
98 # Builds FigureVar and FigureData objects into a form that is easy to process
101 # Use #add to push FigureData records into the table. When done, tell the object
102 # to process all data by calling #render. Thereafter all data is available for
103 # display through the +get_*+ methods.
105 # Note that methods return false or raise an exception when used their proper
107 class FiguresDataTable < FiguresDataMap
110 # mode of operation (input or display)
112 # The #add method is only active in input mode while all the +get_*+
113 # methods are only available in display mode. You must call #render
114 # before you can get any data out. A new object is in input mode.
115 attr_reader :input_mode
118 # Most recent figures, see #add and #add_record.
119 attr_reader :most_recent_figures
122 # All figures not most recent, see #add and #add_record.
123 attr_reader :less_recent_figures
126 # Collection of years the table contains data for.
133 @most_recent_figures = Array.new
134 @less_recent_figures = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
137 # the final table created by #render
138 @table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
140 # rowspanning information, see and use #rowspan
145 # Process a FigureData while in input mode.
147 raise 'add() only works in input mode, it stops working when #render was called' if not @input_mode
149 case map_add(figure_data)
150 when false then add_record(figure_data)
151 when 1 then add_record(figure_data, {:less_recent=>true})
152 when 2 then update_data(figure_data)
159 # End input mode and render all data ready for displaying.
161 # NOTE: @table is a multi-dimensional array but we use only two dimensions,
162 # see http://stackoverflow.com/questions/10253105/dynamically-creating-a-multi-dimensional-hash-in-ruby
164 # Final table will look like when viewed by calling #cell on each
165 # +row+,+col+ coordinate:
167 # ______ ________________ ____________ ______________ _______________ _____
168 # | | | - Year - | - Quarter 1 -| - Quarter 2 - | ... |
169 # | year | FigureVar.name | FigureData | FigureData | FigureData | ... |
170 # |------|----------------|------------|--------------|---------------|-----|
173 # The #text_table method is very useful for debugging.
176 raise 'no data to #render, use add() first' if @most_recent_figures.empty?
178 @table.clear if @table.count > 0
179 periods = FigureData.new.get_periods
182 # add most recent FigureVar
183 # - first: sort by FigureVar.time
184 # - second: add by period index + offset (because of the table head)
185 # ATTENTION: sorting by fd.FigureVar.name probably expensive, consider
186 # fs.id_figure_var as performant alternative
187 @most_recent_figures.sort_by! { |fd| [fd.time.year, fd.FigureVar.name] }
190 @most_recent_figures.each do |fd|
191 @table[row][0] = fd.time.year
192 @table[row][1] = fd.FigureVar.name
194 col = periods.index(fd.period) + 2
195 @table[row][col] = fd
200 # consolidate rows by years and figure name (first two columns)
203 @table.each do |k, row|
204 if row[0]==prev_row[0] and row[1]==prev_row[1] then
205 # merge into prev_row since row is more recent, see sort_by above
206 @table[k] = prev_row.merge(row)
207 @table.delete(prev_key) if k > prev_key
214 for row in 1..@table.count do
215 @rowspan.store(row, 0)
218 # sort table by years
219 # - all elements within a year keep their order as is
220 # - our @years accessor is ordered the same way
221 # - this also makes a new row-index beginning with 1
222 @years.sort!.reverse!
223 tmp_table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
227 @years.each{|y| tmp << @table.select {|k, v| v[0] == y } }
229 next_year = true; firstrowyear = 1;
230 for yi in 0..@years.count-1 do
231 tmp[yi].each do |k,v|
232 tmp_table.store(row, v)
235 firstrowyear = row if next_year
236 @rowspan[firstrowyear] += 1
244 # blank over-spanned lines with +false+ in first column
246 for row in 2..@table.count do
248 if not @rowspan[row-1].is_a?(FalseClass)
249 span = @rowspan[row-1] if @rowspan[row-1] > 0
251 @rowspan[row]=false if span>0
257 # - this will be row 0
258 # - table starts always with 0 and data follows from row-index 1+
261 periods.each_index {|i| @table[0][i+2]=FigureData.new.translate_period(periods[i])}
267 # print table in text format (for debugging only!)
270 format = Array.new(columns, '%15s').join('|')
272 puts '-' * ((columns+1) * 15)
275 rowstr[0] = 'sp:%5s %4s ' % [rowspan(r), cell(r, 0)]
277 rowstr[c] = cell(r, c)
279 puts(format % rowstr)
281 puts '-' * ((columns+1) * 15)
286 # Returns number of rows after #render.
288 raise '#rows only works after #render was called' if @input_mode
293 # Returns number of columns after #render.
294 # NOTE: the first row sets the number of columns of the table
296 raise '#columns only works after #render was called' if @input_mode
301 # Returns content for a cell identified by row and col, nil if empty.
303 raise '#cell only works after #render was called' if @input_mode
305 if c = raw_cell_content(row, col) then
306 if c.is_a?(FigureData) or c.is_a?(Smr::AutofigureData)
309 # leftmost column is always a descriptive string, not to be
310 # rounded, scaled, whatever ...
311 if col==0 then c.to_s else c end
319 # Returns unit of value in cell or nil if there is nothing.
320 def cell_unit(row, col)
321 raise '#cell_unit only works after #render was called' if @input_mode
323 if c = raw_cell_content(row, col) then
324 if c.is_a?(FigureData) or c.is_a?(Smr::AutofigureData)
331 # Return rowspanning information for given row.
333 raise '#rowspan only works after #render was called' if @input_mode
334 if @rowspan.key?(row) then @rowspan[row] else 1 end
338 # Return link to edit FigureVar of given field.
340 # False is returned if that field is empty.
342 raise '#link only works after #render was called' if @input_mode
344 if c = raw_cell_content(row, col) then
345 if c.is_a?(FigureData) then return('/figures/%i/edit' % c.id) end
354 # Add FigureData to internal @most_recent_figures or @less_recent_figures.
356 # #less_recent_figures contains all those where a more recent counterpart
357 # is available in #most_recent_figures. See #get_summary and #have_summary.
359 # #most_recent_figures is a regular +Array+.
361 # #less_recent_figures is a multi-dimensional +Hash+ structured as:
363 # @less_recent_figures[:year][:period][:id_figure_data] = Array.new
365 def add_record(figure_data, options={ :less_recent=>false })
366 y = figure_data.time.year
367 p = figure_data.period
370 if options[:less_recent] then
371 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
372 @less_recent_figures[y][p][id] << figure_data
374 @less_recent_figures[y][p][id] = [figure_data]
377 @most_recent_figures << figure_data
378 @years << y if not @years.include?(y)
385 # Update FigureData item in #most_recent_figures, make sure the old data is
386 # kept in #less_recent_figures.
388 # FIXME: to be implemented!
389 def update_data(figure_data)
390 #@most_recent_figures.find(:id_figure_var=>figure_data.id_figure_var, ...)
395 # Returns content for a cell identified by row and col.
397 # The content can be +nil+ if that cell is empty or whatever is there, ie.
398 # a FigureData, a +String+, etc....
399 def raw_cell_content(row, col)
400 if @table.has_key?(row) and @table[row].has_key?(col) then
407 # Creates many (!) AutofigureData objects from a FigureVar record.
409 # A Autofigure is based on a FigureVar record with the :expression field
410 # containing a math expression. That is each FigureVar becomes a Autofigure
411 # by setting the :expression field. It will return to being a ordinary
412 # FigureVar by emptying the the :expression field.
414 # Each Autofigure will create as many AutofigureData objects as possible,
415 # depending on the information available to solve the given :expression. It
416 # might be hundreds if there is input data for, say, 100 fiscal quarters.
420 # collection of human readdable errors, emtpy if all went well
423 # Math functions users can use in the :expression field
424 SUPPORTED_FUNCTIONS = [:ceil, :floor, :sqrt, :cos, :sin, :tan, :exp, :log]
427 # Init with FigureVar and a list of variables contained in its :expression.
428 # See SmrDaemonClient#parse_math_expressions.
429 def initialize(figure_var)
430 raise 'figure_var must be a FigureVar object' unless figure_var.is_a?(FigureVar)
431 raise 'figure_var must have :expression field set' if figure_var.expression.blank?
432 @autofigure = figure_var
433 @expression_vars = Hash.new
436 # find variables used in expression
437 # - that is FigureVar objects that match by name used in :expression
438 # - internally variables are tracked by :id, not by :name (avoids db
439 # queries later, when building maps)
440 variables = @autofigure.expression.strip.split(/\W+/).delete_if do |x|
441 x.to_f != 0 or SUPPORTED_FUNCTIONS.include?(x.to_sym)
443 FigureVar.select(:id, :name).where(:name=>variables).each do |v|
444 @expression_vars[v.id]=v.name
447 # collects FigureData by year and period, to know what we have (=>
448 # #add()) and still need (=> #get_missing_variables)
449 @datamatrix = Hash.new
451 # collects Time if most recent FigureData per row in @datamatrix,
452 # necessary to know how to time the SmrAutofigureData objects
453 @timematrix = Hash.new
459 # Add FigureData as input for solving the expression.
461 # Returns +true+ if it was added or +false+ if it did not help to fill this
464 # Note: data is overwritten. A FigureData of same +:year+ and +:period+
465 # will overwrite the previously passed one. So mind the order.
467 raise 'figure_data must be a FigureData object' unless figure_data.is_a?(FigureData)
469 if @id_security and @id_security != figure_data.id_security
470 raise 'add()ing FigureData of another Security should not happen, should it?'
471 else @id_security = figure_data.id_security end
473 i = '%s_%s' % [figure_data.time.year, figure_data.period ]
474 if @expression_vars.keys.include?(figure_data.id_figure_var)
475 newdata = { figure_data.id_figure_var=>figure_data.value }
476 @datamatrix[i] = if @datamatrix[i] then @datamatrix[i].merge(newdata) else newdata end
478 if not @timematrix[i] or @timematrix[i] < figure_data.time then
479 @timematrix[i] = figure_data.time
489 # Tells whats missing to solve all expressions.
490 def get_missing_variables
492 @datamatrix.each do |i,d|
493 diff = @expression_vars.keys - d.keys
495 missing << i.to_s + '_' + @expression_vars.select{|k,v| diff.include?(k) }.values.join('-')
502 # Returns collection of AutofigureData objects with solved
505 # Objects with non-solvable expressions due to missing data are
506 # skipped silently (see #get_missing_variables).
508 # Exceptions from MathEngine will be catched and collected in #errors.
510 @datamatrix.collect do |i,d|
511 diff = @expression_vars.keys - d.keys
514 e.context.include_library Smr::AutofigureMathFunctions.new
516 af, time, period, id = @autofigure, @timematrix[i], i.split('_').second, @id_security
519 # init variables in MathEngine instance, then ...
521 e.context.set(@expression_vars[k].to_sym,v)
524 # ... solve the expression as result of a new AutofigureData
525 AutofigureData.new(af, e.evaluate(af.expression), id, period, time)
527 # p exception.backtrace
528 # p '=E=> %s' % e.inspect
529 @errors << 'solving %s(%s-%s) failed with: %s' % [af.name, time.year, period, $! ]
539 # Like a FigureData object in behaviour but without related database record.
541 # A AutofigureData object is ment to behave exactly like a FigureData
542 # object. Except that it can`t be saved or updated or trigger any other
543 # database operation. Its an entirely artificial record so to say.
545 # compatibility with FigureData, always +nil+
548 # compatibility with FigureData, empty
549 attr_reader :analyst, :is_expected, :is_audited, :comment
551 # where this autofigure was derived from
552 attr_reader :id_figure_var, :id_security, :period, :expression
554 # unix timestamp when this autofigure was made, also see time()
557 # result of the expression or +nil+ if not yet solved
558 # Note: must be calculated/set externaly, Smr::Autofigure#get does this
562 # Give FigureVar out of which this Smr::AutofigureData was derived and the result
563 # from solving the expression.
564 # - :id_security+ is to know what this is for
565 # - the given FigureVar must have a :expression, otherwise its not a autofigure
566 def initialize(autofigure, result, id_security, period, time)
567 raise ':autofigure must be a FigureVar' unless autofigure.is_a? FigureVar
568 raise ':autofigure :expression must be set' if autofigure.expression.blank?
569 raise ':result must be numerical' unless result.is_a? Numeric
570 raise ':time must be of Time' unless time.is_a? Time
571 @autofigure = autofigure
572 @expression = autofigure.expression
575 # carry on meaningful fields
576 @id_figure_var = autofigure.id
577 @id_security = id_security
580 @analyst = self.class
584 # Time when this autofigure was made.
590 # Wrapper for compatibility with FigureData.
592 FigureData.new.get_periods
596 # Wrapper for compatibility with FigureData.
597 def translate_period(period=@period)
598 FigureData.new.translate_period(period)
602 # Wrapper for compatibility with FigureData.
609 # additional methods available to users in FigureVar#expression field.
610 class AutofigureMathFunctions