basic usermanagement and various fixes
[smr.git] / gui / lib / smr / figures.rb
blob9c5d703dbfc40354e3fbaf8dcfab8e52701cda07
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   
17 module Smr  #:nodoc:
18   ##
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 FiguresDataMap
23   
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
29   
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
48   
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
54   
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
70   
71       ##
72       # Return name last FigureVar processed by #map_add.
73       def get_name
74           @last_figure_var.name
75       end
76   
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
82   
83       ##
84       # Return period of the last item processed by #map_add.
85       def get_period
86           @last_figure_data.period
87       end
88   
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   
96   ##
97   # Builds FigureVar and FigureData objects into a form that is easy to process
98   # in views.
99   #
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.
103   #
104   # Note that methods return false or raise an exception when used their proper
105   # mode.
106   class FiguresDataTable < 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
147   
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
153   
154           true
155       end
156   
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
180   
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] }
187   
188           row=1
189           @most_recent_figures.each do |fd|
190               @table[row][0] = fd.time.year
191               @table[row][1] = fd.FigureVar.name
192   
193               col = periods.index(fd.period) + 2
194               @table[row][col] = fd
195       
196               row += 1
197           end
198   
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
264   
265       ##
266       # print table in text format (for debugging only!)
267       def text_table
268           render
269           format = Array.new(columns, '%15s').join('|')
270   
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
283   
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
290   
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
298   
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
303   
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
316   
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
321   
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
328   
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
335   
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
342         
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
346         
347           false
348       end
349   
350   protected
351   
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
368   
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
379   
380           true
381       end
382   
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
392   
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
403   end
404   
405   ##
406   # Creates many (!) AutofigureData objects from a FigureVar record.
407   #
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.
412   #
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.
416   #
417   class Autofigure
418   
419       ##
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
427   
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
431           end
432   
433           # collects FigureData by year and period, to know what we have (=>
434           # #add()) and still need (=> #get_missing_variables)
435           @datamatrix = Hash.new
436   
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
440   
441           @id_stock = nil
442       end
443   
444       ##
445       # Add FigureData as input for solving the expression.
446       #
447       # Returns +true+ if it was added or +false+ if it did not help to fill this
448       # expression.
449       #
450       # Note: data is overwritten. A FigureData of same +:year+ and +:period+
451       # will overwrite the previously passed one. So mind the order.
452       def add(figure_data)
453           raise 'figure_data must be a FigureData object' unless figure_data.is_a?(FigureData)
454   
455           if @id_stock and @id_stock != figure_data.id_stock
456               raise 'add()ing FigureData of another Stock should not happen, should it?'
457           else
458               @id_stock = figure_data.id_stock
459           end
460   
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
465   
466               if not @timematrix[i] or @timematrix[i] < figure_data.time then
467                   @timematrix[i] = figure_data.time
468               end
469   
470               return true
471           end
472   
473           return false
474       end
475   
476       ##
477       # Tells whats missing to solve all expressions.
478       def get_missing_variables
479           missing = Array.new
480           @datamatrix.each do |i,d|
481               diff = @expression_vars.keys - d.keys
482               if not diff.empty?
483                   missing << i.to_s + '_' + @expression_vars.select{|k,v| diff.include?(k) }.values.join('-')
484               end
485           end
486           missing
487       end
488   
489       ##
490       # Returns collection of AutofigureData objects with solvable
491       # expressions. Objects with non-solvable expressions (missing data) are
492       # skipped silently.
493       def get
494           @datamatrix.collect do |i,d|
495               diff = @expression_vars.keys - d.keys
496               if diff.empty?
497                   dn = Hash.new
498                   d.each{ |k,v|  dn[@expression_vars[k]]=v }
499                   AutofigureData.new(@autofigure, dn, @id_stock, i.split('_').second, @timematrix[i])
500               end
501           end.compact
502       end
503   
504   end
505   
506   ##
507   # Like a FigureData object in behaviour but without related database record.
508   #
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.
512   class AutofigureData
513       # compatibility with FigureData, always +nil+
514       attr_reader :id
515   
516       # compatibility with FigureData, empty
517       attr_reader :analyst, :is_expected, :is_audited, :comment
518   
519       # where this autofigure was derived from
520       attr_reader :id_figure_var, :id_stock, :period
521   
522       # unix timestamp when this autofigure was made, also see time()
523       attr_reader :date
524   
525       # the Hash of data variables and values necessary for solving
526       attr_reader :expression, :solving_data
527   
528       # result of the expression or +nil+ if not yet solved
529       # Note: this must be calculated/set externaly
530       attr_accessor :value
531   
532       ##
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
535       # is for.
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
543   
544           # carry on meaningful fields
545           @id_figure_var = autofigure.id
546           @id_stock = id_stock
547           @date = time.to_i
548           @period = period
549           @analyst = self.class
550       end
551   
552       ##
553       # Time when this autofigure was made.
554       def time
555           Time.at(@date)
556       end
557   
558       ##
559       # Wrapper for compatibility with FigureData.
560       def get_periods
561           FigureData.new.get_periods
562       end
563   
564       ##
565       # Wrapper for compatibility with FigureData.
566       def translate_period(period=@period)
567           FigureData.new.translate_period(period)
568       end
569   
570       ##
571       # Wrapper for compatibility with FigureData.
572       def FigureVar
573           @autofigure
574       end
575   end
576   
577 end # module