table_fu 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.gitignore +4 -0
  2. data/LICENSE +20 -0
  3. data/README +37 -0
  4. data/Rakefile +58 -0
  5. data/VERSION.yml +5 -0
  6. data/doc/TableFu/Datum.html +743 -0
  7. data/doc/TableFu/Formatting.html +469 -0
  8. data/doc/TableFu/Header.html +198 -0
  9. data/doc/TableFu/Row.html +508 -0
  10. data/doc/TableFu.html +1806 -0
  11. data/doc/_index.html +154 -0
  12. data/doc/class_list.html +36 -0
  13. data/doc/css/common.css +1 -0
  14. data/doc/css/full_list.css +50 -0
  15. data/doc/css/style.css +268 -0
  16. data/doc/file.README.html +89 -0
  17. data/doc/file_list.html +38 -0
  18. data/doc/frames.html +13 -0
  19. data/doc/index.html +89 -0
  20. data/doc/js/app.js +99 -0
  21. data/doc/js/full_list.js +106 -0
  22. data/doc/js/jquery.js +19 -0
  23. data/doc/method_list.html +363 -0
  24. data/doc/top-level-namespace.html +85 -0
  25. data/documentation/css/dawn.css +121 -0
  26. data/documentation/css/styles.css +63 -0
  27. data/documentation/images/proplogo.png +0 -0
  28. data/documentation/index.html.erb +148 -0
  29. data/examples/columns.rb +7 -0
  30. data/examples/columns_hidden.rb +5 -0
  31. data/examples/faceting.rb +2 -0
  32. data/examples/formatting_options.rb +6 -0
  33. data/examples/last_name.rb +4 -0
  34. data/examples/link.rb +11 -0
  35. data/examples/only.rb +6 -0
  36. data/examples/rails_helpers.rb +3 -0
  37. data/examples/sort_by_column.rb +11 -0
  38. data/examples/sort_by_number.rb +0 -0
  39. data/examples/totals.rb +2 -0
  40. data/examples/zap_joyce.rb +3 -0
  41. data/index.html +210 -0
  42. data/lib/table_fu/formatting.rb +52 -0
  43. data/lib/table_fu.rb +386 -0
  44. data/spec/assets/sample.csv +476 -0
  45. data/spec/assets/test.csv +8 -0
  46. data/spec/assets/test_macro.csv +8 -0
  47. data/spec/rcov.opts +2 -0
  48. data/spec/readme_example_spec.rb +39 -0
  49. data/spec/spec.opts +4 -0
  50. data/spec/spec_helper.rb +4 -0
  51. data/spec/table_fu_spec.rb +221 -0
  52. data/table_fu.gemspec +112 -0
  53. metadata +141 -0
data/lib/table_fu.rb ADDED
@@ -0,0 +1,386 @@
1
+ require 'rubygems'
2
+ require 'fastercsv'
3
+
4
+
5
+ # TableFu turns a matric array(from a csv file for example) into a spreadsheet.
6
+ #
7
+ # Allows formatting, macros, sorting, and faceting.
8
+ #
9
+ # Documentation:
10
+ # http://propublica.github.com/table-fu
11
+ class TableFu
12
+
13
+ attr_reader :deleted_rows, :table, :totals, :column_headers
14
+ attr_accessor :faceted_on, :col_opts
15
+
16
+ # Should be initialized with a matrix array or a string containing a csv, and expects the first
17
+ # array in the matrix to be column headers.
18
+ def initialize(table, column_opts = {})
19
+ # Assume if we're past a string or filehandle we need to parse a csv
20
+ if table.is_a?(String) || table.is_a?(File)
21
+ table = FasterCSV.parse(table)
22
+ end
23
+ @column_headers = table.slice!(0)
24
+ @totals = {}
25
+ @table = table
26
+ @col_opts = column_opts
27
+ yield self if block_given?
28
+ end
29
+
30
+
31
+
32
+ # Pass it an array and it will delete it from the table, but save the data in
33
+ # @deleted_rows@ for later perusal.
34
+ #
35
+ # Returns:
36
+ # nothing
37
+ def delete_rows!(arr)
38
+ @deleted_rows ||= []
39
+ arr.map do |item|
40
+ @deleted_rows << @table[item] #account for header and 0 index
41
+ @table[item] = nil
42
+ end
43
+ @table.compact!
44
+ end
45
+
46
+
47
+ # Inverse slice: Only keep the rows in the range after sorting
48
+ def only!(range)
49
+ rows_to_exclude = rows.map do |row|
50
+ range.include?(row.row_num) ? nil : row.row_num
51
+ end
52
+ delete_rows!(rows_to_exclude.compact)
53
+ end
54
+
55
+ # Returns a Row object for the row at a certain index
56
+ def row_at(row_num)
57
+ TableFu::Row.new(@table[row_num], row_num, self)
58
+ end
59
+
60
+ # Returns all the Row objects for this object as a collection
61
+ def rows
62
+ all_rows = []
63
+ @table.each_with_index do |row, index|
64
+ all_rows << TableFu::Row.new(row, index, self)
65
+ end
66
+ all_rows.sort
67
+ end
68
+
69
+ # Return the headers defined in column headers or cherry picked from @col_opts
70
+ def columns
71
+ @col_opts[:columns] || column_headers
72
+ end
73
+
74
+ # Return the headers of the array
75
+ def headers
76
+ all_columns = []
77
+ columns.each do |header|
78
+ all_columns << TableFu::Header.new(header, header, nil, self)
79
+ end
80
+ all_columns
81
+ end
82
+
83
+ # Sum the values of a particular column
84
+ def sum_totals_for(column)
85
+ @totals[column.to_s] = rows.inject(0) { |sum, r| to_numeric(r.datum_for(column).value) + sum }
86
+ end
87
+
88
+ # Sum the values of a particular column and return a Datum
89
+ def total_for(column)
90
+ sum_totals_for(column)
91
+ Datum.new(@totals[column.to_s], column, nil, self)
92
+ end
93
+
94
+ # Return an array of TableFu instances grouped by a column.
95
+ def faceted_by(column, opts = {})
96
+ faceted_spreadsheets = {}
97
+ rows.each do |row|
98
+ unless row.column_for(column).value.nil?
99
+ faceted_spreadsheets[row.column_for(column).value] ||= []
100
+ faceted_spreadsheets[row.column_for(column).value] << row
101
+ end
102
+ end
103
+
104
+ # Create new table_fu instances for each facet
105
+ tables = []
106
+ faceted_spreadsheets.each do |key,value|
107
+ new_table = [@column_headers] + value
108
+ table = TableFu.new(new_table)
109
+ table.faceted_on = key
110
+ table.col_opts = @col_opts #formatting should be carried through
111
+ tables << table
112
+ end
113
+
114
+ tables.sort! do |a,b|
115
+ a.faceted_on <=> b.faceted_on
116
+ end
117
+
118
+ if opts[:total]
119
+ opts[:total].each do |c|
120
+ tables.each do |f|
121
+ f.sum_totals_for(c)
122
+ end
123
+ end
124
+ end
125
+
126
+ tables
127
+ end
128
+
129
+ # Return a numeric instance for a string number, or if it's a string we
130
+ # return 1, this way if we total up a series of strings it's a count
131
+ def to_numeric(num)
132
+ if num.nil?
133
+ 0
134
+ elsif num.kind_of? Integer
135
+ num
136
+ else
137
+ 1 # We count each instance of a string this way
138
+ end
139
+ end
140
+
141
+ # Return true if this table is faceted
142
+ def faceted?
143
+ not faceted_on.nil?
144
+ end
145
+
146
+ # Return the sorted_by column
147
+ def sorted_by
148
+ @col_opts[:sorted_by]
149
+ end
150
+
151
+ # Set the sorted_by column
152
+ def sorted_by=(header)
153
+ @col_opts[:sorted_by] = header
154
+ end
155
+
156
+ # Return the formatting hash
157
+ def formatting
158
+ @col_opts[:formatting]
159
+ end
160
+
161
+ # Set the formatting hash
162
+ def formatting=(headers)
163
+ @col_opts[:formatting] = headers
164
+ end
165
+
166
+ # Set up the cherry picked columns
167
+ def columns=(array)
168
+ @col_opts[:columns] = array
169
+ end
170
+
171
+
172
+
173
+ end
174
+
175
+ class TableFu
176
+ # TableFu::Row adds functionality to an row array in a TableFu instance
177
+ class Row < Array
178
+
179
+ attr_reader :row_num
180
+
181
+ def initialize(row, row_num, spreadsheet)
182
+ self.replace row
183
+ @row_num = row_num
184
+ @spreadsheet = spreadsheet
185
+ end
186
+
187
+ def columns
188
+ all_cols = []
189
+ @spreadsheet.columns.each do |column|
190
+ all_cols << datum_for(column)
191
+ end
192
+ all_cols
193
+ end
194
+
195
+ # This returns a Datum object for a header name. Will return a nil Datum object
196
+ # for nonexistant column names
197
+ #
198
+ # Parameters:
199
+ # header name
200
+ #
201
+ # Returns:
202
+ # Datum object
203
+ #
204
+ def datum_for(col_name)
205
+ if col_num = @spreadsheet.column_headers.index(col_name)
206
+ TableFu::Datum.new(self[col_num], col_name, @row_num, @spreadsheet)
207
+ else # Return a nil Datum object for non existant column names
208
+ TableFu::Datum.new(nil, col_name, @row_num, @spreadsheet)
209
+ end
210
+ end
211
+ alias_method :column_for, :datum_for
212
+
213
+ # Comparator for sorting a spreadsheet row.
214
+ #
215
+ def <=>(b)
216
+ if @spreadsheet.sorted_by
217
+ column = @spreadsheet.sorted_by.keys.first
218
+ order = @spreadsheet.sorted_by[@spreadsheet.sorted_by.keys.first]["order"]
219
+ format = @spreadsheet.sorted_by[@spreadsheet.sorted_by.keys.first]["format"]
220
+ a = column_for(column).value || ''
221
+ b = b.column_for(column).value || ''
222
+ if format
223
+ a = TableFu::Formatting.send(format, a) || ''
224
+ b = TableFu::Formatting.send(format, b) || ''
225
+ end
226
+ result = a <=> b
227
+ result = -1 if result.nil?
228
+ result = result * -1 if order == 'descending'
229
+ result
230
+ else
231
+ -1
232
+ end
233
+ end
234
+
235
+ end
236
+ # A Datum is an individual cell in the TableFu::Row
237
+ class Datum
238
+
239
+ attr_reader :options, :column_name
240
+
241
+ # Each piece of datum should know where it is by column and row number, along
242
+ # with the spreadsheet it's apart of. There's probably a better way to go
243
+ # about doing this. Subclass?
244
+ def initialize(datum, col_name, row_num, spreadsheet)
245
+ @datum = datum
246
+ @column_name = col_name
247
+ @row_num = row_num
248
+ @spreadsheet = spreadsheet
249
+ end
250
+
251
+ # Our standard formatter for the datum
252
+ #
253
+ # Returns:
254
+ # the formatted value, macro value, or a empty string
255
+ #
256
+ # First we test to see if this Datum has a macro attached to it. If so
257
+ # we let the macro method do it's magic
258
+ #
259
+ # Then we test for a simple formatter method.
260
+ #
261
+ # And finally we return a empty string object or the value.
262
+ #
263
+ def to_s
264
+ if macro_value
265
+ macro_value
266
+ elsif @spreadsheet.formatting && format_method = @spreadsheet.formatting[column_name]
267
+ TableFu::Formatting.send(format_method, @datum) || ''
268
+ else
269
+ @datum || ''
270
+ end
271
+ end
272
+
273
+ # Returns the macro'd format if there is one
274
+ #
275
+ # Returns:
276
+ # The macro value if it exists, otherwise nil
277
+ def macro_value
278
+ # Grab the macro method first
279
+ # Then get a array of the values in the columns listed as arguments
280
+ # Splat the arguments to the macro method.
281
+ # Example:
282
+ # @spreadsheet.col_opts[:formatting] =
283
+ # {'Total Appropriation' => :currency,
284
+ # 'AppendedColumn' => {'method' => 'append', 'arguments' => ['Projects','State']}}
285
+ #
286
+ # in the above case we handle the AppendedColumn in this method
287
+ if @row_num && @spreadsheet.formatting && @spreadsheet.formatting[@column_name].is_a?(Hash)
288
+ method = @spreadsheet.formatting[@column_name]['method']
289
+ arguments = @spreadsheet.formatting[@column_name]['arguments'].inject([]){|arr,arg| arr << @spreadsheet.rows[@row_num].column_for(arg); arr}
290
+ TableFu::Formatting.send(method, *arguments)
291
+ end
292
+ end
293
+
294
+ # Returns the raw value of a datum
295
+ #
296
+ # Returns:
297
+ # raw value of the datum, could be nil
298
+ def value
299
+ if @datum =~ /[0-9]+/
300
+ @datum.to_i
301
+ else
302
+ @datum
303
+ end
304
+ end
305
+
306
+ # This method missing looks for 4 matches
307
+ #
308
+ # First Option
309
+ # We have a column option by that method name and it applies to this column
310
+ # Example -
311
+ # >> @data.column_name
312
+ # => 'Total'
313
+ # >> @datum.style
314
+ # Finds col_opt[:style] = {'Total' => 'text-align:left;'}
315
+ # => 'text-align:left;'
316
+ #
317
+ # Second Option
318
+ # We have a column option by that method name, but no attribute
319
+ # >> @data.column_name
320
+ # => 'Total'
321
+ # >> @datum.style
322
+ # Finds col_opt[:style] = {'State' => 'text-align:left;'}
323
+ # => ''
324
+ #
325
+ # Third Option
326
+ # The boolean
327
+ # >> @data.invisible?
328
+ # And we've set it col_opts[:invisible] = ['Total']
329
+ # => true
330
+ #
331
+ # Fourth Option
332
+ # The boolean that's false
333
+ # >> @data.invisible?
334
+ # And it's not in the list col_opts[:invisible] = ['State']
335
+ # => false
336
+ #
337
+ def method_missing(method)
338
+ opts = indifferent_access @spreadsheet.col_opts
339
+ if val = opts[method] && opts[method][column_name]
340
+ val
341
+ elsif val = opts[method] && !opts[method][column_name]
342
+ ''
343
+ elsif method.to_s =~ /\?$/ && col_opts = opts[method.to_s.chop.to_sym]
344
+ col_opts.index(column_name) || false
345
+ elsif method.to_s =~ /\?$/ && !opts[method.to_s.chop.to_sym]
346
+ nil
347
+ else
348
+ super
349
+ end
350
+ end
351
+
352
+ private
353
+
354
+ # Enable string or symbol key access to col_opts
355
+ # from sinatra
356
+ def indifferent_access(params)
357
+ params = indifferent_hash.merge(params)
358
+ params.each do |key, value|
359
+ next unless value.is_a?(Hash)
360
+ params[key] = indifferent_access(value)
361
+ end
362
+ end
363
+
364
+ def indifferent_hash
365
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
366
+ end
367
+ end
368
+
369
+ # A header object needs to be a special kind of Datum, and
370
+ # we may want to extend this further, but currently we just
371
+ # need to ensure that when to_s is called on a @Header@ object
372
+ # that we don't run it through a macro, or a formatter.
373
+ class Header < Datum
374
+
375
+ def to_s
376
+ @datum
377
+ end
378
+
379
+ end
380
+
381
+ end
382
+
383
+ $:.unshift(File.dirname(__FILE__)) unless
384
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
385
+
386
+ require 'table_fu/formatting'