manage bookmarks, bugfixes
[smr.git] / gui / lib / smr / figures.rb
blobe5e3814747704241c8f32d12bc165338255cebe3
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
7 # version.
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/>.
16 require 'math_engine'
17   
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
21 # organization class.
22 class Smr::FiguresDataMap
24     def initialize
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
28     end
30     ##
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).
33     #
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
37     #
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
40     # or ignore it.
41     #
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'
45         end
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
58             return false
59         else
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
61                 # known but older
62                 return 1
63             else
64                 # known but more recent => update map
65                 @map[name][year][id][period] = figure_data.time
66                 return 2
67             end
68         end
69     end
71     ##
72     # Return name last FigureVar processed by #map_add.
73     def get_name
74         @last_figure_var.name
75     end
77     ##
78     # Return FigureData id of the last item processed by #map_add.
79     def get_figure_data_id
80         @last_figure_data.id
81     end
83     ##
84     # Return period of the last item processed by #map_add.
85     def get_period
86         @last_figure_data.period
87     end
89     ##
90     # Return date as +Time+ of the last item processed by #map_add.
91     def get_date
92         @last_figure_data.time
93     end
94 end
95   
97 # Builds FigureVar and FigureData objects into a form that is easy to process
98 # in views.
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
105 # mode.
106 class Smr::FiguresDataTable < Smr::FiguresDataMap
108     ##
109     # mode of operation (input or display)
110     #
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
116     ##
117     # Most recent figures, see #add and #add_record.
118     attr_reader :most_recent_figures
120     ##
121     # All figures not most recent, see #add and #add_record.
122     attr_reader :less_recent_figures
124     ##
125     # Collection of years the table contains data for.
126     attr_reader :years
128     def initialize
129         super
131         @input_mode = true;
132         @most_recent_figures = Array.new
133         @less_recent_figures = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
134         @years = Array.new
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
140         @rowspan = Hash.new
141     end
143     ##
144     # Process a FigureData while in input mode.
145     def add(figure_data)
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)
152         end
154         true
155     end
157     ##
158     # End input mode and render all data ready for displaying.
159     #
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
162     #
163     # Final table will look like when viewed by calling #cell on each
164     # +row+,+col+ coordinate:
165     #
166     #    ______ ________________ ____________ ______________ _______________ _____
167     #   |      |                | -  Year -  | - Quarter 1 -| - Quarter 2 - | ... |
168     #   | year | FigureVar.name | FigureData |  FigureData  |   FigureData  | ... |
169     #   |------|----------------|------------|--------------|---------------|-----|
170     #     ...
171     #
172     # The #text_table method is very useful for debugging.
173     #
174     def render
175         raise 'no data to #render, use add() first' if @most_recent_figures.empty?
176         @input_mode = false
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] }
188         row=1
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
196             row += 1
197         end
199         # consolidate rows by years and figure name (first two columns)
200         prev_row = @table[1]
201         prev_key = 1
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
207             end
208             prev_row = @table[k]
209             prev_key = k
210         end
212         # init rowspanning
213         for row in 1..@table.count do
214             @rowspan.store(row, 0)
215         end
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) }
223         tmp = Array.new
224         row=1
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)
233                # figure rowspanning
234                firstrowyear = row if next_year
235                @rowspan[firstrowyear] += 1
236                next_year = false
237                row += 1
238             end
239             next_year = true;
240         end
241         @table = tmp_table
243         # blank over-spanned lines with +false+ in first column
244         span = 0
245         for row in 2..@table.count do
246             if @rowspan[row]==0
247                if not @rowspan[row-1].is_a?(FalseClass)
248                    span = @rowspan[row-1] if @rowspan[row-1] > 0
249                end
250                @rowspan[row]=false if span>0
251             end
252             span -= 1
253         end
255         # create table head
256         # - this will be row 0
257         # - table starts always with 0 and data follows from row-index 1+
258         @table[0][0] = ''
259         @table[0][1] = ''
260         periods.each_index {|i| @table[0][i+2]=FigureData.new.translate_period(periods[i])}
262         true
263     end
265     ##
266     # print table in text format (for debugging only!)
267     def text_table
268         render
269         format = Array.new(columns, '%15s').join('|')
271         puts '-' * ((columns+1) * 15)
272         for r in 0...rows
273             rowstr = Array.new
274             rowstr[0] = 'sp:%5s %4s ' % [rowspan(r), cell(r, 0)]
275             for c in 1..columns
276                 rowstr[c] = cell(r, c)
277             end
278             puts(format % rowstr)
279         end
280         puts '-' * ((columns+1) * 15)
281         true
282     end
284     ##
285     # Returns number of rows after #render.
286     def rows
287         raise '#rows only works after #render was called' if @input_mode
288         @table.count
289     end
291     ##
292     # Returns number of columns after #render.
293     # NOTE: the first row sets the number of columns of the table
294     def columns
295         raise '#columns only works after #render was called' if @input_mode
296         @table[0].count
297     end
299     ##
300     # Returns content for a cell identified by row and col, nil if empty.
301     def cell(row, col)
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)
306                 c.value
307             else
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
311             end
312         else
313             nil
314         end
315     end
317     ##
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)
324                 c.FigureVar.unit
325             end
326         end
327     end
329     ##
330     # Return rowspanning information for given row.
331     def rowspan(row)
332         raise '#rowspan only works after #render was called' if @input_mode
333         if @rowspan.key?(row) then @rowspan[row] else 1 end
334     end
336     ##
337     # Return link to edit FigureVar of given field.
338     #
339     # False is returned if that field is empty.
340     def link(row, col)
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
345         end
347         false
348     end
350   protected
352     ##
353     # Add FigureData to internal @most_recent_figures or @less_recent_figures.
354     #
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.
357     #
358     # #most_recent_figures is a regular +Array+.
359     #
360     # #less_recent_figures is a multi-dimensional +Hash+ structured as:
361     #
362     #   @less_recent_figures[:year][:period][:id_figure_data] = Array.new
363     #
364     def add_record(figure_data, options={ :less_recent=>false })
365         y  = figure_data.time.year
366         p  = figure_data.period
367         id = figure_data.id
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
372             else
373                 @less_recent_figures[y][p][id] = [figure_data]
374             end
375         else
376             @most_recent_figures << figure_data
377             @years << y if not @years.include?(y)
378         end
380         true
381     end
383     ##
384     # Update FigureData item in #most_recent_figures, make sure the old data is
385     # kept in #less_recent_figures.
386     #
387     # FIXME: to be implemented!
388     def update_data(figure_data)
389         #@most_recent_figures.find(:id_figure_var=>figure_data.id_figure_var, ...)
390         true
391     end
393     ##
394     # Returns content for a cell identified by row and col.
395     #
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
400                 @table[row][col]
401         else nil end
402     end
404   
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.
417 class Smr::Autofigure
419     # collection of human readdable errors, emtpy if all went well
420     attr_reader :errors
422     # Math functions users can use in the :expression field
423     SUPPORTED_FUNCTIONS = [:ceil, :floor, :sqrt, :cos, :sin, :tan, :exp, :log]
425     ##
426     # Init with FigureVar and a list of variables contained in its :expression.
427     # See SmrDaemonClient#parse_math_expressions.
428     def initialize(figure_var)
429         raise 'figure_var must be a FigureVar object' unless figure_var.is_a?(FigureVar)
430         raise 'figure_var must have :expression field set' if figure_var.expression.blank?
431         @autofigure = figure_var
432         @expression_vars = Hash.new
433         @errors = Array.new
435         # find variables used in expression
436         # - that is FigureVar objects that match by name used in :expression
437         # - internally variables are tracked by :id, not by :name (avoids db
438         #   queries later, when building maps)
439         variables = @autofigure.expression.strip.split(/\W+/).delete_if do |x|
440               x.to_f != 0 or SUPPORTED_FUNCTIONS.include?(x.to_sym)
441         end
442         FigureVar.select(:id, :name).where(:name=>variables).each do |v|
443             @expression_vars[v.id]=v.name
444         end
446         # collects FigureData by year and period, to know what we have (=>
447         # #add()) and still need (=> #get_missing_variables)
448         @datamatrix = Hash.new
450         # collects Time if most recent FigureData per row in @datamatrix,
451         # necessary to know how to time the SmrAutofigureData objects
452         @timematrix = Hash.new
454         @id_security = nil
455     end
457     ##
458     # Add FigureData as input for solving the expression.
459     #
460     # Returns +true+ if it was added or +false+ if it did not help to fill this
461     # expression.
462     #
463     # Note: data is overwritten. A FigureData of same +:year+ and +:period+
464     # will overwrite the previously passed one. So mind the order.
465     def add(figure_data)
466         raise 'figure_data must be a FigureData object' unless figure_data.is_a?(FigureData)
468         if @id_security and @id_security != figure_data.id_security
469             raise 'add()ing FigureData of another Security should not happen, should it?'
470         else @id_security = figure_data.id_security end
472         i = '%s_%s' % [figure_data.time.year, figure_data.period ]
473         if @expression_vars.keys.include?(figure_data.id_figure_var)
474             newdata = { figure_data.id_figure_var=>figure_data.value }
475             @datamatrix[i] = if @datamatrix[i] then @datamatrix[i].merge(newdata) else newdata end
477             if not @timematrix[i] or @timematrix[i] < figure_data.time then
478                 @timematrix[i] = figure_data.time
479             end
481             return true
482         end
484         return false
485     end
487     ##
488     # Tells whats missing to solve all expressions.
489     def get_missing_variables
490         missing = Array.new
491         @datamatrix.each do |i,d|
492             diff = @expression_vars.keys - d.keys
493             if not diff.empty?
494                 missing << i.to_s + '_' + @expression_vars.select{|k,v| diff.include?(k) }.values.join('-')
495             end
496         end
497         missing
498     end
500     ##
501     # Returns collection of AutofigureData objects with solved
502     # expressions.
503     #
504     # Objects with non-solvable expressions due to missing data are
505     # skipped silently (see #get_missing_variables).
506     #
507     # Exceptions from MathEngine will be catched and collected in #errors.
508     def get
509         @datamatrix.collect do |i,d|
510             diff = @expression_vars.keys - d.keys
511             if diff.empty?
512                 e = MathEngine.new
513                 e.context.include_library Smr::AutofigureMathFunctions.new
515                 af, time, period, id = @autofigure, @timematrix[i], i.split('_').second, @id_security
517               begin
518                 # init variables in MathEngine instance, then ...
519                 d.each{ |k,v|
520                   e.context.set(@expression_vars[k].to_sym,v)
521                 }
523                 # ... solve the expression as result of a new AutofigureData
524                 Smr::AutofigureData.new(af, e.evaluate(af.expression), id, period, time)
525               rescue #=> exception
526 # p exception.backtrace
527 # p '=E=> %s' %  e.inspect
528                  @errors << 'solving %s(%s-%s) failed with: %s' % [af.name, time.year, period, $! ]
529                  nil
530               end
531             end
532         end.compact
533     end
535   
537 # Like a FigureData object in behaviour but without related database record.
539 # A AutofigureData object is ment to behave exactly like a FigureData
540 # object. Except that it can`t be saved or updated or trigger any other
541 # database operation. Its an entirely artificial record so to say.
542 class Smr::AutofigureData
543     # compatibility with FigureData, always +nil+
544     attr_reader :id
546     # compatibility with FigureData, empty
547     attr_reader :analyst, :is_expected, :is_audited, :comment
549     # where this autofigure was derived from
550     attr_reader :id_figure_var, :id_security, :period, :expression
552     # unix timestamp when this autofigure was made, also see time()
553     attr_reader :date
555     # result of the expression or +nil+ if not yet solved
556     # Note: must be calculated/set externaly, Smr::Autofigure#get does this
557     attr_accessor :value
559     ##
560     # Give FigureVar out of which this Smr::AutofigureData was derived and the result
561     # from solving the expression.
562     # - :id_security+ is to know what this is for
563     # - the given FigureVar must have a :expression, otherwise its not a autofigure
564     def initialize(autofigure, result, id_security, period, time)
565         raise ':autofigure must be a FigureVar' unless autofigure.is_a? FigureVar
566         raise ':autofigure :expression must be set' if autofigure.expression.blank?
567         raise ':result must be numerical' unless result.is_a? Numeric
568         raise ':time must be of Time' unless time.is_a? Time
569         @autofigure = autofigure
570         @expression = autofigure.expression
571         @value = result
573         # carry on meaningful fields
574         @id_figure_var = autofigure.id
575         @id_security = id_security
576         @date = time.to_i
577         @period = period
578         @analyst = self.class
579     end
581     ##
582     # Time when this autofigure was made.
583     def time
584         Time.at(@date)
585     end
587     ##
588     # Wrapper for compatibility with FigureData.
589     def get_periods
590         FigureData.new.get_periods
591     end
593     ##
594     # Wrapper for compatibility with FigureData.
595     def translate_period(period=@period)
596         FigureData.new.translate_period(period)
597     end
599     ##
600     # Wrapper for compatibility with FigureData.
601     def FigureVar
602         @autofigure
603     end
607 # additional methods available to users in FigureVar#expression field.
608 class Smr::AutofigureMathFunctions
609   def ceil(v)
610       v.to_f.ceil
611   end
613   def floor(v)
614       v.to_f.floor
615   end