added pagination gem, many fixes
[smr.git] / gui / lib / smr_figures.rb
blob368ae932ec243e31132aada2a02eb3fc3a3eb42b
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/>.
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
20 # organization class.
21 class SmrFiguresDataMap
23     def initialize
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
27     end
29     ##
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).
32     #
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
36     #
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
39     # or ignore it.
40     #
41     def map_add(figure_data)
42         @last_figure_data = figure_data
43         @last_figure_var = @last_figure_data.FigureVar
45         # define locals to shorten the code below
46         name = @last_figure_var.name
47         id = @last_figure_var.id.to_i
48         year = @last_figure_data.time.year.to_i
49         period = @last_figure_data.period
51         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
52             # we do not know this figure yet, map_add to map
53             @map[name][year][id][period] = @last_figure_data.time
54             return false
55         else
56             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
57                 # known but older
58                 return 1
59             else
60                 # known but more recent => update map
61                 @map[name][year][id][period] = figure_data.time
62                 return 2
63             end
64         end
65     end
67     ##
68     # return name last FigureVar processed by map_add()
69     def get_name
70         @last_figure_var.name
71     end
73     ##
74     # return FigureData id of the last item processed by map_add()
75     def get_figure_data_id
76         @last_figure_data.id
77     end
79     ##
80     # return list of all uniq FigureVar id's known by the map or false in case there is none.
81 #    def get_uniq_ids
82 #        ids = Array.new
84 #        @map.each do |n|
85 #            n.each do |y|
86 #                y.each do |id, val|
87 #                    if id or id>0 then
88 #                        ids[id] = id
89 #                    end
90 #                end
91 #            end
92 #        end
94 #        if ids.empty? then false else ids end
95 #    end
97     ##
98     # return period of the last item processed by map_add()
99     def get_period
100         @last_figure_data.period
101     end
103     ##
104     # return date as +Time+ of the last item processed by map_add()
105     def get_date
106         @last_figure_data.time
107     end
111 # Builds FigureVar and FigureData objects into a form that is easy to process
112 # in views.
114 # Use add() to push FigureData records into the table. When done, tell the object
115 # to process all data by calling render(). Thereafter all data is available for
116 # display through the +get_*+ methods.
118 # Note that methods return false or raise an exception when used their proper
119 # mode.
121 class SmrFiguresDataTable < SmrFiguresDataMap
122     def initialize
123         super
125         ##
126         # mode of operation (input or display)
127         #
128         # The add() mgqethod is only active in input mode while all the +get_*+
129         # methods are only available in display mode. You must call render()
130         # before you can get any data out. A new object is in input mode.
131         @input_mode = true
133         # see get_years()
134 #        @years = Array.new
136         # list of periods that exist in SMR
137         #
138         # Used as @datatable header, see data_table().
139 #        @periods_existing = FigureData.new.get_periods
141         # see add() and add_record()
142             @most_recent_figures = Array.new
144         # see add_record()a and update_data()
145         @less_recent_figures = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
147         # the final table created by render()
148         # - tmp_table is same thing, just to be used for temprary operation on
149         #   @table
150         @table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
152         # see get_rowspan()
153         @rowspan = Array.new
155         # see get_class()
156         @class = Array.new
158         ##
159         # CSS class string used for rows, altered by self.cycle_css_row()
160         @cycle_css_row = 'row1'
161     end
163     ##
164     # Process a FigureData while in input mode.
165     def add(figure_data)
166         raise 'add() only works in input mode, it stops working when render() was called' if not @input_mode
168         case self.map_add(figure_data)
169             when false then self.add_record(figure_data)
170             when 1     then self.add_record(figure_data, {:less_recent=>true})
171             when 2     then self.update_data(figure_data)
172         end
174 #        if not @years.include?(figure_data.time.year)
175 #            @years << figure_data.time.year
176 #        end
178         true
179     end
181     ##
182     # End input mode and render all data ready for displaying
183     #
184     # NOTE: @table is a multi-dimensional array but we use only two dimensions,
185     #       see http://stackoverflow.com/questions/10253105/dynamically-creating-a-multi-dimensional-hash-in-ruby
186     #
187     # Final table will look like when viewed by calling cell() on each
188     # +row+,+col+ coordinate:
189     #
190     #    ______ ________________ ____________ ______________ _______________ _____
191     #   |      |                | -  Year -  | - Quarter 1 -| - Quarter 2 - | ... |
192     #   | year | FigureVar.name | FigureData |  FigureData  |   FigureData  | ... |
193     #   |------|----------------|------------|--------------|---------------|-----|
194     #     ...
195     #
196     # The text_table() method is very useful for debugging.
197     #
198     def render
199         raise 'no data to render(), use add() first' if @most_recent_figures.empty?
200         @input_mode = false
201         @table.clear if @table.count > 0
203         # create table head
204         periods = FigureData.new.get_periods
205         @table[0][0] = ''
206         @table[0][1] = ''
207         periods.each_index {|i| @table[0][i+2]=FigureData.new.translate_period(periods[i])}
209         # add most recent FigureVar
210         # - first: sort by FigureVar.time
211         # - second: add by period index + offset (because of the table head)
212         # ATTENTION: sorting by fd.FigureVar.name probably expensive, consider
213         #            fs.id_figure_var as performant alternative
214         @most_recent_figures.sort_by! { |fd| [fd.time.year, fd.FigureVar.name] }
216         row=1
217         @most_recent_figures.each do |fd|
218             @table[row][0] = fd.time.year
219             @table[row][1] = fd.FigureVar.name
221             col = periods.index(fd.period) + 2
222             @table[row][col] = fd
223     
224             row += 1
225         end
227         # consolidate rows by years and figure name (first two columns)
228         prev_row = @table[1]
229         prev_key = 1
230         @table.each do |k, row|
231             next if k == 0 # skip table head
233             if row[0]==prev_row[0] and row[1]==prev_row[1] then
234                 # merge into prev_row since row is more recent, see sort_by above
235                 @table[k] = prev_row.merge(row)
236 #p "==> %s" % prev_row
237 #p "==+ %s" % row
238 #p "=== %s" % @table[k]
239 #p "____"
240                 @table.delete(prev_key) if k > prev_key
241             end
242             prev_row = @table[k]
243             prev_key = k
244         end
246         # re-index @table since rows(), columns() and cell() rely on
247         # consecutive numbering
248         tmp_table = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
249         i=0
250         @table.each_key do |k|
251             tmp_table[i] = @table[k]
252             i += 1
253         end
254         @table = tmp_table
256         true
257     end
259     ##
260     # print table in text format (for debugging only!)
261     def text_table
262         self.render
263         format = Array.new(self.columns, '%12s').join('|')
265         puts '----------- @table ------------'
266         for r in 0...self.rows
267             row = Array.new
268             for c in 0...self.columns
269                 row[c] = self.cell(r, c)
270             end
271             puts(format % row)
272         end
273         puts '-------------------------------'
274     end
276     ##
277     # Returns number of rows after render().
278     def rows
279         raise 'rows() only works after render() was called' if @input_mode
280         @table.count
281     end
283     ##
284     # Returns number of columns after render().
285     # NOTE: the first row sets the number of columns of the table
286     def columns
287         raise 'columns() only works after render() was called' if @input_mode
288         @table[0].count
289     end
291     ##
292     # Returns content for a cell identified by row and col
293     #
294     # The content can be +nil+ if that cell is emtpy or a +String+
295     def cell(row, col)
296         raise 'cell() only works after render() was called' if @input_mode
298         if @table.has_key?(row) and @table[row].has_key?(col) then
299             if @table[row][col].is_a?(FigureData)
300                 @table[row][col].value.to_s
301             else
302                 @table[row][col].to_s
303             end
304         else
305             nil
306         end
307     end
309     ##
310     # Return rowspanning information for given cell (row, col) or false if this
311     # cell should not span at all
312     #
313     # rowspanning information for @datatable is stored in @rowspan and looks
314     # this way:
315     # - two-dimensional array: #row, #col
316     # - a number indicates how many lines a cell should span down
317     # - a dash ('-') indicates that a cell is covered by another cell that
318     #   spans above it
319     # - #row,#col coordinates are not set means the cell is not affected by
320     #   rowspanning at all
321     def rowspan(row, col)
322         raise 'rowspan() only works after render() was called' if @input_mode
323         false
324     end
326     ##
327     # Return class information for given row or cell
328     #
329     # False is returned if there is no such information (the row or cell should
330     # not have any class attribute set).
331     #
332     # The +col+ paramenter is optional, if given you ask for a specific cell,
333     # if ommited (default) you ask for the entire row.
334     #
335     # CSS class information for @datatable is stored in @class and looks like
336     # this:
337     # - one-or-two-dimensional array: #row, #col
338     # - contains a string of CSS class names that should be set on a given row
339     #   and/or cell
340     # - false means that there shouldn't be any class attribute set
341     def class(row, col=false)
342         raise 'class() only works after render() was called' if @input_mode
343         false
344     end
347     ##
348     # returns list off years for which we have figures available
349 #    def get_years
350 #        return false if not @input_mode 
351 #        @years
352 #    end
354 #    ##
355 #    # returns list off all periods that exist SMR
356 #    def get_periods_existing
357 #        return false if not @input_mode 
358 #        @periods_existing
359 #    end
364     ##
365     # retrieve less recent fundamental figures by +row+/+col+
366     #
367     # These are those figures that have a more recent counterpart in
368     # get_value(). Returns false if there is no such data available. See add().
369 #    def get_summary(row, col) 
370 #        return false if not @input_mode
371 #        retval = false
373 #        if val = self.get_value(row, col) then
374 #            y  = val.year;
375 #            p  = val.period;
376 #            id = val.id_figure_var
378 #            retval = if @summary[y][p][id] then @summary[y][p][id] end
379 #        end
380 #        return retval
381 #    end
383 protected
385     ##
386     # add FigureData to internal @most_recent_figures or @less_recent_figures
387     #
388     # @less_recent_figures contains all those where a more recent counterpart
389     # is available in @most_recent_figures. See get_summary() and
390     # have_summary().
391     #
392     # @most_recent_figures is a regular +Array+
393     #
394     # @less_recent_figures is a multi-dimensional +Hash+ structured as
395     # 
396     #   @less_recent_figures[:year][:period][:id_figure_data] = Array.new
397     #
398     def add_record(figure_data, options={ :less_recent=>false })
399         y  = figure_data.time.year
400         p  = figure_data.period
401         id = figure_data.id
403         if options[:less_recent] then
404             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
405                @less_recent_figures[y][p][id] << figure_data
406             else
407                 @less_recent_figures[y][p][id] = Array.new(figure_data)
408             end
409         else
410             @most_recent_figures << figure_data
411         end
413         true
414     end
416     ##
417     # update FigureData item in @most_recent_figures, make sure the old data is
418     # kept in @less_recent_figures.
419     # FIXME: to be implemented!
420     def update_data(figure_data)
421         #@most_recent_figures.find(:id_figure_var=>figure_data.id_figure_var, ...)
422         true
423     end
425 #    ##
426 #    # returns cycled CSS class string to be used to rows, usefull when
427 #    # looping through rows
428 #    def cycle_css_row
429 #        if @cycle_css_row == 'row1' then
430 #            @cycle_css_row = 'row2'
431 #        else
432 #            @cycle_css_row = 'row1'
433 #        end
434 #        @cycle_css_row
435 #    end
439 # creates many (!) SmrAutoFigureData objects from a FigureVar record
441 # A SmrAutoFigure is based on a FigureVar record with the :expression field
442 # containing a math expression. That is each FigureVar becomes a SmrAutoFigure
443 # by setting the :expression field. It will return to being a ordinary
444 # FigureVar by emptying the the :expression field.
446 # Each SmrAutoFigure will create as many SmrAutoFigureData objects as possible,
447 # depending on the information available to solve the given :expression. It
448 # might be hundreds if there is input data for, say, 100 fiscal quarters.
450 class SmrAutoFigure
451     def initialize(figure_var)
452     end
456 # like a FigureData object without related database record
458 # A SmrAutoFigureData object is ment to behave <u>exactly</u> like a FigureData
459 # object. Except that it can`t be +save()+ed or +update()+ed or trigger any
460 # other database operation. Its an entirely artificial record so to say.
462 class SmrAutoFigureData