File: column_width_calculator.rb

package info (click to toggle)
ruby-prawn-table 0.2.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 532 kB
  • sloc: ruby: 3,355; makefile: 5
file content (182 lines) | stat: -rw-r--r-- 7,268 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# encoding: utf-8

module Prawn
  class Table
    # @private
    class ColumnWidthCalculator
      def initialize(cells)
        @cells = cells

        @widths_by_column        = Hash.new(0)
        @rows_with_a_span_dummy  = Hash.new(false)

        #calculate for each row if it includes a Cell:SpanDummy
        @cells.each do |cell|
          @rows_with_a_span_dummy[cell.row] = true if cell.is_a?(Cell::SpanDummy)
        end
      end

      # does this row include a Cell:SpanDummy?
      #
      # @param row - the row that should be checked for Cell:SpanDummy elements
      #
      def has_a_span_dummy?(row)
        @rows_with_a_span_dummy[row]
      end

      # helper method
      # column widths are stored in the values array
      # a cell may span cells whose value is only partly given
      # this function handles this special case
      #
      # @param values - The columns widths calculated up until now
      # @param cell - The current cell
      # @param index - The current column
      # @param meth - Meth (min/max); used to calculate values to be filled
      #
      def fill_values_if_needed(values, cell, index, meth)
        #have all spanned indices been filled with a value?
        #e.g. values[0], values[1] and values[2] don't return nil given a index of 0 and a colspan of 3
        number_of_nil_values = 0
        cell.colspan.times do |i|
          number_of_nil_values += 1 if values[index+i].nil?
        end

        #nothing to do? because
        #a) all values are filled
        return values if number_of_nil_values == 0
        #b) no values are filled
        return values if number_of_nil_values == cell.colspan
        #c) I am not sure why this line is needed FIXXME
        #some test cases manage to this line even though there is no dummy cell in the row
        #I'm not sure if this is a sign for a further underlying bug.
        return values unless has_a_span_dummy?(cell.row)
        #fill up the values array

        #calculate the new sum
        new_sum = cell.send(meth) * cell.colspan
        #substract any calculated values
        cell.colspan.times do |i|
          new_sum -= values[index+i] unless values[index+i].nil?
        end

        #calculate value for the remaining - not yet filled - cells.
        new_value = new_sum.to_f / number_of_nil_values
        #fill the not yet filled cells
        cell.colspan.times do |i|
          values[index+i] = new_value if values[index+i].nil?
        end
        return values
      end

      def natural_widths
        #calculate natural column width for all rows that do not include a span dummy
        @cells.each do |cell|
          unless has_a_span_dummy?(cell.row)
            @widths_by_column[cell.column] =
              [@widths_by_column[cell.column], cell.width.to_f].max
          end
        end

        #integrate natural column widths for all rows that do include a span dummy
        @cells.each do |cell|
          next unless has_a_span_dummy?(cell.row)
          #the width of a SpanDummy cell will be calculated by the "mother" cell
          next if cell.is_a?(Cell::SpanDummy)

          if cell.colspan == 1
            @widths_by_column[cell.column] =
              [@widths_by_column[cell.column], cell.width.to_f].max
          else
            #calculate the current with of all cells that will be spanned by the current cell
            current_width_of_spanned_cells =
              @widths_by_column.to_a[cell.column..(cell.column + cell.colspan - 1)]
                               .collect{|key, value| value}.inject(0, :+)

            #update the Hash only if the new with is at least equal to the old one
            #due to arithmetic errors we need to ignore a small difference in the new and the old sum
            #the same had to be done in the column_widht_calculator#natural_width
            update_hash = ((cell.width.to_f - current_width_of_spanned_cells) >
                           Prawn::FLOAT_PRECISION)

            if update_hash
              # Split the width of colspanned cells evenly by columns
              width_per_column = cell.width.to_f / cell.colspan
              # Update the Hash
              cell.colspan.times do |i|
                @widths_by_column[cell.column + i] = width_per_column
              end
            end
          end
        end

        @widths_by_column.sort_by { |col, _| col }.map { |_, w| w }
      end

      # get column widths (either min or max depending on meth)
      # used in cells.rb
      #
      # @param row_or_column - you may call this on either rows or columns
      # @param meth - min/max
      # @param aggregate - functions from cell.rb to be used to aggregate e.g. avg_spanned_min_width
      #
      def aggregate_cell_values(row_or_column, meth, aggregate)
        values = {}

        #calculate values for all cells that do not span accross multiple cells
        #this ensures that we don't have a problem if the first line includes
        #a cell that spans across multiple cells
        @cells.each do |cell|
          #don't take spanned cells
          if cell.colspan == 1 and cell.class != Prawn::Table::Cell::SpanDummy
            index = cell.send(row_or_column)
            values[index] = [values[index], cell.send(meth)].compact.send(aggregate)
          end
        end

        # if there are only colspanned or rowspanned cells in a table
        spanned_width_needs_fixing = true

        @cells.each do |cell|
          index = cell.send(row_or_column)
          if cell.colspan > 1
            #special treatment if some but not all spanned indices in the values array have been calculated
            #only applies to rows
            values = fill_values_if_needed(values, cell, index, meth) if row_or_column == :column
            #calculate current (old) return value before we do anything
            old_sum = 0
            cell.colspan.times { |i|
              old_sum += values[index+i] unless values[index+i].nil?
            }

            #calculate future return value
            new_sum = cell.send(meth) * cell.colspan

            #due to float rounding errors we need to ignore a small difference in the new
            #and the old sum the same had to be done in
            #the column_width_calculator#natural_width
            spanned_width_needs_fixing = ((new_sum - old_sum) > Prawn::FLOAT_PRECISION)

            if spanned_width_needs_fixing
              #not entirely sure why we need this line, but with it the tests pass
              values[index] = [values[index], cell.send(meth)].compact.send(aggregate)
              #overwrite the old values with the new ones, but only if all entries existed
              entries_exist = true
              cell.colspan.times { |i| entries_exist = false if values[index+i].nil? }
              cell.colspan.times { |i|
                values[index+i] = cell.send(meth) if entries_exist
              }
            end
          else
            if spanned_width_needs_fixing && cell.class == Prawn::Table::Cell::SpanDummy
              values[index] = [values[index], cell.send(meth)].compact.send(aggregate)
            end
          end
        end

        return values.values.inject(0, &:+)
      end
    end

  end
end