fat_table 0.5.4 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ecb8cb3fb29acb95b0fe11e65c5b587379a5a3070ab68acbc6a2c1785c8f229
4
- data.tar.gz: f3cc9e7242ded9fd0970159eee8da4208faade0e6e4c37a324a45e1596c4eeae
3
+ metadata.gz: f045f2196dd5d6ad7584b877d847945f99cc8079b86b8a810af8528d2c18074c
4
+ data.tar.gz: cb8f031c4f48d472af658f111221524882f5d2bb6574858b1e988e2384d7ba56
5
5
  SHA512:
6
- metadata.gz: 8d404bd69ba3a1c3c9774f634e4f0935987ff4827803d59e89b9adbdc4806b25ef87e5dbcb9f22d7e3475c86dceebf097cf73e0ad1287f91d21415e5b0a5a3b3
7
- data.tar.gz: 8d8df3651b9cdcdeaefa0527cd4fc1987c1ec491bca2a88651b255032eea16ee29c82285b9d7f9bae2ecb9b6d90c6fc4d766980af5362dc89045d553e66dec03
6
+ metadata.gz: f906e8841e268263635dc4231573b07cb48cd48b1f33fd2a0d7812e65f1e9720ac8f8556129601b702a828c5c6a7a7a04c00344016c2ebe0004a892d1f524ab9
7
+ data.tar.gz: 694f65455ed63c1304f0bc626192c2770968278ec0599c0e9c5f57c5fcb091a351ed4e09d28fe6cbda12278f1f9ed669e128339e6e99b4fc2a1a21467d182892
data/README.org CHANGED
@@ -32,7 +32,7 @@ The following is for org.
32
32
  #+end_src
33
33
 
34
34
  #+begin_EXAMPLE
35
- Current version is: 0.5.3
35
+ Current version is: 0.6.0
36
36
  #+end_EXAMPLE
37
37
 
38
38
  * Introduction
@@ -148,6 +148,7 @@ org-mode buffer as an org-table, ready for processing by other code blocks.
148
148
  - [[#type-and-column-priority][Type and Column priority]]
149
149
  - [[#footers][Footers]]
150
150
  - [[#adding-footers][Adding Footers]]
151
+ - [[#dynamic-labels][Dynamic Labels]]
151
152
  - [[#aggregators][Aggregators]]
152
153
  - [[#footer-objects][Footer objects]]
153
154
  - [[#footer-examples][Footer Examples]]
@@ -442,11 +443,26 @@ or nil. There are only five permissible types for a ~Column~:
442
443
  5. *NilClass* (for the undetermined column type).
443
444
 
444
445
  When a ~Table~ is constructed from an external source, all ~Columns~ start out
445
- having a type of ~NilClass~, that is, their type is as yet undetermined. When a
446
- string or object of one of the four determined types is added to a ~Column~, it
447
- fixes the type of the column and all further items added to the ~Column~ must
448
- either be ~nil~ (indicating no value) or be capable of being coerced to the
449
- column's type. Otherwise, ~FatTable~ raises an exception.
446
+ having a type of ~NilClass~, that is, their type is as yet undetermined. When
447
+ a string or object of one of the four determined types is added to a ~Column~
448
+ and it can be converted into one of the permissible types, it fixes the type
449
+ of the column, and all further items added to the ~Column~ must either be
450
+ ~nil~ (indicating no value) or be capable of being coerced to the column's
451
+ type. Otherwise, ~FatTable~ raises an exception.
452
+
453
+ The strictness of requiring all items to be of the same type can be relaxed by
454
+ declaring a column to be "tolerant." You can do so when you create the table
455
+ by adding a tolerant_columns keyword parameter. If a Column is tolerant,
456
+ ~FatTable~ tries to convert new items into a type other than a ~String~ and,
457
+ if it can do so, sets /that/ as the Column's type. Any later items that
458
+ cannot be converted into the Column's type are converted to strings. These
459
+ interloper strings are treated like nils for purposes of sorting and
460
+ evaluation, but are displayed according to any string formatting on output.
461
+ See [[*Designating "Tolerant" Columns][Designating "Tolerant" Columns]] below.
462
+
463
+ It is also possible to force ~FatTable~ to treat a column as a String type,
464
+ even its items look like one of the other types. See [[*Forcing String Type][Forcing String Type]]
465
+ below.
450
466
 
451
467
  Items of input must be either one of the permissible ruby objects or strings. If
452
468
  they are strings, ~FatTable~ attempts to parse them as one of the permissible
@@ -519,6 +535,7 @@ You can create an empty table with ~FatTable::Table.new~ or, the shorter form,
519
535
  in the added rows determine the names of the headers:
520
536
 
521
537
  #+BEGIN_SRC ruby :results silent
538
+ require 'fat_table'
522
539
  tab = FatTable.new
523
540
  tab << { a: 1, b: 2, c: "<2017-01-21>", d: 'f', e: '' }
524
541
  tab << { a: 3.14, b: 2.17, c: '[2016-01-21 Thu]', d: 'Y', e: nil }
@@ -603,10 +620,18 @@ columns to be created:
603
620
  **** Forcing String Type
604
621
  Occasionally, ~FatTable~'s automatic type detection can get in the way and you
605
622
  just want it to treat one or more columns as Strings regardless of their
606
- appearance. Think, for example, of zip codes. At any time after creating a
607
- table, you can have it force the String type on any number of columns with the
608
- ~force_string!~ method. When you do so, all exisiting items in the column are
609
- converted to strings with the #to_s method.
623
+ appearance. Think, for example, of zip codes. If headers are given when a
624
+ table is contructed, you can designate a forced-string column by appending a
625
+ ~!~ to the end of the header. It will not become part of the header, it will
626
+ just mark it as a forced-string Column.
627
+
628
+ #+begin_SRC emacs-lisp :wrap EXAMPLE
629
+ tab = FatTable.new(:a, 'b', 'C!', :d, :zip!)
630
+ #+end_SRC
631
+
632
+ In addition, at any time after creating a table, you can force the String type
633
+ on any number of columns with the ~force_string!~ method. When you do so, all
634
+ exisiting items in the column are converted to strings with the #to_s method.
610
635
 
611
636
  #+begin_src ruby :wrap EXAMPLE
612
637
  tab = FatTable.new(:a, 'b', 'C', :d, :zip)
@@ -1731,8 +1756,6 @@ available in ~ft_console~ as ~@tab_a~ and ~@tab_b~:
1731
1756
  | 2 | Engineering | 2 |
1732
1757
  | 3 | Finance | 7 |
1733
1758
  EOS
1734
-
1735
- tab_b = FatTable.from_org_string(tab_b_str)
1736
1759
  #+END_SRC
1737
1760
 
1738
1761
  Here is ~tab_a~:
@@ -2442,7 +2465,6 @@ Finance&
2442
2465
  {:id=>"3", :dept=>"Finance", :emp_id=>"7"}]
2443
2466
  #+end_EXAMPLE
2444
2467
 
2445
-
2446
2468
  *** Formatting Directives
2447
2469
  The formatting methods explained in the next section all take formatting
2448
2470
  directives as strings in which letters and other characters signify what
@@ -2683,12 +2705,27 @@ but not for other nils, such as in the last row of the ~:join_date~ column.
2683
2705
 
2684
2706
  *** Footers
2685
2707
  **** Adding Footers
2686
- You can call the ~footer,~ ~gfooter, foot, and gfoot~ methods on ~Formatter~
2687
- objects to add footers and group footers. Note that all of these methods
2688
- return a ~Footer~ object that can be accessed to extract the computed values.
2689
- All of these methods return the ~FatTable::Footer~ object so constructed. It
2690
- can be used to access the values and other attributes of the footer
2691
- computed. Their signatures are:
2708
+ You can call the ~foot~, ~gfoot~, ~footer,~ or ~gfooter~, methods on
2709
+ ~Formatter~ objects to add footers and group footers. Note that all of these
2710
+ methods return a ~Footer~ object that can be accessed to extract the computed
2711
+ values. All of these methods return the ~FatTable::Footer~ object so
2712
+ constructed. It can be used to access the values and other attributes of the
2713
+ footer computed. Their signatures are:
2714
+
2715
+ - ~foot(label: label, label_col: nil, **agg_cols)~ :: where ~label~ is a label
2716
+ to be placed in the column with header ~label_col~, or, if ommitted, in the
2717
+ first cell of the footer (unless that column is named as one of the
2718
+ ~agg_cols~, in which case the label is ignored), and ~**agg_cols~ is zero or
2719
+ more hash-like parameters with a column symbol as a key and a valid
2720
+ aggregate as the value. This causes a table-wide header to be added at the
2721
+ bottom of the table applying ~agg~, to the ~agg_cols~. A table can have any
2722
+ number of footers attached, and they will appear at the bottom of the output
2723
+ table in the order they are given.
2724
+
2725
+ - ~gfoot(label: 'Group Total', label_col: nil, **agg_cols)~ :: where the
2726
+ parameters have the same meaning as for the ~foot~ method, but results in a
2727
+ footer for each group in the table rather than the table as a whole. These
2728
+ will appear in the output table just below each group.
2692
2729
 
2693
2730
  - ~footer(label, *sum_cols, **agg_cols)~ :: where ~label~ is a label to be
2694
2731
  placed in the first cell of the footer (unless that column is named as one
@@ -2701,26 +2738,11 @@ computed. Their signatures are:
2701
2738
  number of footers attached, and they will appear at the bottom of the output
2702
2739
  table in the order they are given.
2703
2740
 
2704
- - ~foot(label, label_col, **agg_cols)~ :: where ~label~ is a label to be
2705
- placed in the column with header ~label_col~, or, if ommitted, in the first
2706
- cell of the footer (unless that column is named as one of the ~agg_cols~, in
2707
- which case the label is ignored), and ~**agg_cols~ is zero or more hash-like
2708
- parameters with a column symbol as a key and a valid aggregate as the
2709
- value. This causes a table-wide header to be added at the bottom of the
2710
- table applying ~agg~, to the ~agg_cols~. A table can have any number of
2711
- footers attached, and they will appear at the bottom of the output table in
2712
- the order they are given.
2713
-
2714
2741
  - ~gfooter(label, *sum_cols, **agg_cols)~ :: where the parameters have the
2715
2742
  same meaning as for the ~footer~ method, but results in a footer for each
2716
2743
  group in the table rather than the table as a whole. These will appear in
2717
2744
  the output table just below each group.
2718
2745
 
2719
- - ~gfoot(label, label_col, **agg_cols)~ :: where the parameters have the same
2720
- meaning as for the ~foot~ method, but results in a footer for each group in
2721
- the table rather than the table as a whole. These will appear in the output
2722
- table just below each group.
2723
-
2724
2746
  There are also a number of convenience methods for adding common footers:
2725
2747
  - ~sum_footer(*cols)~ :: Add a footer summing the given columns with the label
2726
2748
  'Total'.
@@ -2739,6 +2761,39 @@ There are also a number of convenience methods for adding common footers:
2739
2761
  - ~max_gfooter(*cols)~ :: Add a group footer showing the maximum for the given
2740
2762
  columns with the label 'Group Maximum'.
2741
2763
 
2764
+ **** Dynamic Labels
2765
+ Most of the time, you will want a fixed string as the label. However,
2766
+ especially in the case of a group footer, you might want a dynamically
2767
+ contructed label. You can use a proc or lambda for a label, and it will be
2768
+ computed for you. In the case of non-group footers, the proc takes a single
2769
+ parameter, the footer object itself. This allows you to make the label a
2770
+ function of other footer values, for example, you could make the label
2771
+ include the most recent year from the date column:
2772
+
2773
+ #+begin_src ruby
2774
+ fmtr.foot(label: -> (f) { "Average (latest year #{f.column(:date).max.year})" },
2775
+ temp: :avg)
2776
+ #+end_src
2777
+
2778
+ In the case of a group footer, the lambda or proc may take either one or qtwo parameters.
2779
+ If it takes one, the parameter is simply the 0-based number of the group:
2780
+
2781
+ #+begin_src ruby
2782
+ fmtr.gfoot(label: -> (k) { "Group #{(k+1).to_roman} Average" }, temp: :avg)
2783
+ #+end_src
2784
+ This would format the label with a roman numeral (assuming you defined a
2785
+ method to do so) for the group number.
2786
+
2787
+ If it takes two arguments, the second argument is the footer itself, as with
2788
+ non-group footers:
2789
+
2790
+ #+begin_src ruby
2791
+ fmtr.gfoot(label: -> (k, f) { "Year #{f.column(:date, k).max.year} Group #{(k+1).to_roman} Average" },
2792
+ temp: :avg)
2793
+ #+end_src
2794
+ This would add the group's year to label, assuming the :date column of the
2795
+ footer's table had the same year for each item in the group.
2796
+
2742
2797
  **** Aggregators
2743
2798
  When adding a footer with the above methods, you can specify an aggregator for
2744
2799
  each column named in the ~agg_cols~ parameter. There are several candidates
@@ -2755,6 +2810,9 @@ for what you can use for an aggregator:
2755
2810
  In the case of datetime columns, these aggrgators convert the dates to
2756
2811
  julian date numbers, perform the calculation, then convert the result back
2757
2812
  to a datetime object.
2813
+ Apart from the built-in aggrgators, you could define your own by opening the
2814
+ FatTable::Column class and adding a suitable instance method. In that
2815
+ case, the symbol could also refer to the method you defined.
2758
2816
  - String :: using a string as an aggrgegator can result in:
2759
2817
  + the string being converted to an object matching the type of the column
2760
2818
  (for example, using '$1,888' in a numeric column puts the constant number
@@ -2769,11 +2827,16 @@ for what you can use for an aggregator:
2769
2827
  - A Lambda :: finally, you can provide a lambda for performing arbitrary
2770
2828
  calculations and placing the result in the footer field. The number of
2771
2829
  arguments the lambda takes can vary:
2772
- * If the lambda is used in a group footer, it must take a single integer
2773
- argument that is set to the group number being calculated and /can/ take a
2774
- second argument for the column symbol in which it appears, or
2775
- * If the lambda is used in an ordinary footer, it either takes no arguments,
2776
- or a single argument for the column symbol in which it appears.
2830
+ * If the lambda is used in an ordinary footer column, it can take 0, 1, or 2
2831
+ arguments: (1) the first argument, if given, will be set to the
2832
+ FatTable::Column object for that column and (2) the second argument, if
2833
+ given, will be set to the Footer object itself.
2834
+ * If the lambda is used in a group footer column, it can 0, 1, 2, or 3
2835
+ arguments: (1) the first argument, if given, will be set to the group's
2836
+ 0-based index number, (2) the second argument, if given, will be set to a
2837
+ FatTable::Column object consisting of those items in the group's column,
2838
+ and (3) the third argument, if given, will be set to the Footer object
2839
+ itself.
2777
2840
 
2778
2841
  **** Footer objects
2779
2842
  Each of the methods for adding a footer to a ~Formatter~ returns a ~Footer~ object
@@ -2797,7 +2860,6 @@ their computed values. Here are the accessors available on a
2797
2860
  to the footer value for that column, nil for unused columns. Use the index
2798
2861
  ~k~ to specify which group to access in the case of a group footer.
2799
2862
 
2800
-
2801
2863
  **** Footer Examples
2802
2864
  As a reminder, here is the table, ~tab_a~ defined earlier:
2803
2865
 
@@ -2958,18 +3020,20 @@ But it can be any type. Here we pick a lottery winner from the employee ids.
2958
3020
  #+end_EXAMPLE
2959
3021
 
2960
3022
  ***** Lambdas
2961
- Perhaps the most flexible form of aggregator is a lambda form. They require 2
2962
- or 3 parameters in non-group and group footers, respectively:
2963
-
2964
- - ~->(f, c) {...}~ :: in a normal, non-group footer, you must provide for two
2965
- paramters: the first, ~f~, will be bound to the footer in which the lambda
2966
- appears and the second, ~c~, will be bound to the column header to which the
2967
- lambda is attached.
2968
- - ~->(f, c, k)~ :: in a group footer, you must provide for three paramters:
2969
- the first, ~f~, will be bound to the footer in which the lambda appears, the
2970
- second, ~c~, will be bound to the column header to which the lambda is
2971
- attached, and the third, ~k~ will be bound to the group number of the group
2972
- being evaluated.
3023
+ Perhaps the most flexible form of aggregator is a lambda form. They can take
3024
+ up to 2 or up to 3 parameters in non-group and group footers, respectively:
3025
+
3026
+ - ~->(c, f) {...}~ :: in a normal, non-group footer, you may provide for up to
3027
+ two paramters: the first, ~c~, if given, will be bound to the column header
3028
+ to which the lambda is attached and and the second, ~f~, if given will be
3029
+ bound to the footer in which the lambda appears. A lambda with no
3030
+ parameters can be provided as well if none are needed.
3031
+ - ~->(k, c, f)~ :: in a group footer, you may provide for up to three
3032
+ paramters: the the first, ~k~, if provided, will be bound to the group
3033
+ number of the group being evaluated, the second, ~c~, if provided, will be
3034
+ bound to the column header to which the lambda is attached, and the third,
3035
+ ~f~, will be bound to the footer in which the lambda appears. A lambda with
3036
+ no parameters can be provided as well if none are needed.
2973
3037
 
2974
3038
  With the first argument, the footer itself becomes available and with it all
2975
3039
  the things accessible with the footers, including the items in the current
@@ -2980,7 +3044,7 @@ Compute the summ of the squares if the items in the ~:age~ column:
2980
3044
  tab_a.to_text do |f|
2981
3045
  f.format(numeric: '0.0R,', datetime: 'd[%v]D[%v]')
2982
3046
  f.footer('Average', age: :avg, salary: :avg, join_date: :avg)
2983
- f.footer('SSQ', age: ->(f, c) { sa = f.items(c).map {|x| x * x}.sum; Math.sqrt(sa) })
3047
+ f.footer('SSQ', age: ->(c) { sa = c.items.map {|x| x * x}.sum; Math.sqrt(sa) })
2984
3048
  end
2985
3049
  #+END_SRC
2986
3050
 
@@ -3009,8 +3073,8 @@ summ of the squares if the ages in each group:
3009
3073
  tab_a.order_with('join_date.year').to_text do |f|
3010
3074
  f.format(numeric: '0.0R,', datetime: 'd[%v]D[%v]', sort_key: '0.0~,')
3011
3075
  f.footer('Average', age: :avg, salary: :avg, join_date: :avg)
3012
- f.gfooter('Group SSQ', age: ->(f, c, k) { sa = f.items(c, k).map {|x| x * x}.sum; Math.sqrt(sa) })
3013
- f.footer('Total SSQ', age: ->(f, c) { sa = f.items(c).map {|x| x * x}.sum; Math.sqrt(sa) })
3076
+ f.gfooter('Group SSQ', age: ->(k, c, f) { sa = c.items.map {|x| x * x}.sum; Math.sqrt(sa) })
3077
+ f.footer('Total SSQ', age: ->(c, f) { sa = c.items.map {|x| x * x}.sum; Math.sqrt(sa) })
3014
3078
  end
3015
3079
  #+END_SRC
3016
3080
 
@@ -3022,19 +3086,19 @@ summ of the squares if the ages in each group:
3022
3086
  +-----------+-------+-----+------------+--------+-------------+----------+
3023
3087
  | Group SSQ | | 45 | | | | |
3024
3088
  +-----------+-------+-----+------------+--------+-------------+----------+
3025
- | 1 | Paul | 32 | California | 20,000 | 13-JUL-2001 | 2001 |
3089
+ | 1 | Paul | 32 | California | 20,000 | 13-JUL-2001 | 2001 |
3026
3090
  +-----------+-------+-----+------------+--------+-------------+----------+
3027
3091
  | Group SSQ | | 32 | | | | |
3028
3092
  +-----------+-------+-----+------------+--------+-------------+----------+
3029
- | 2 | Allen | 25 | Texas | | 13-JUL-2005 | 2005 |
3030
- | 8 | Paul | 24 | Houston | 20,000 | 13-JUL-2005 | 2005 |
3031
- | 9 | James | 44 | Norway | 5,000 | 13-JUL-2005 | 2005 |
3093
+ | 2 | Allen | 25 | Texas | | 13-JUL-2005 | 2005 |
3094
+ | 8 | Paul | 24 | Houston | 20,000 | 13-JUL-2005 | 2005 |
3095
+ | 9 | James | 44 | Norway | 5,000 | 13-JUL-2005 | 2005 |
3032
3096
  +-----------+-------+-----+------------+--------+-------------+----------+
3033
3097
  | Group SSQ | | 56 | | | | |
3034
3098
  +-----------+-------+-----+------------+--------+-------------+----------+
3035
- | 3 | Teddy | 23 | Norway | 20,000 | 13-DEC-2007 | 2007 |
3036
- | 4 | Mark | 25 | Rich-Mond | 65,000 | 13-DEC-2007 | 2007 |
3037
- | 5 | David | 27 | Texas | 85,000 | 13-DEC-2007 | 2007 |
3099
+ | 3 | Teddy | 23 | Norway | 20,000 | 13-DEC-2007 | 2007 |
3100
+ | 4 | Mark | 25 | Rich-Mond | 65,000 | 13-DEC-2007 | 2007 |
3101
+ | 5 | David | 27 | Texas | 85,000 | 13-DEC-2007 | 2007 |
3038
3102
  +-----------+-------+-----+------------+--------+-------------+----------+
3039
3103
  | Group SSQ | | 43 | | | | |
3040
3104
  +-----------+-------+-----+------------+--------+-------------+----------+
data/TODO.org CHANGED
@@ -1,10 +1,37 @@
1
+
2
+ * TODO Specify Column Widths
3
+ Allow a formatter to specify column widths. This could be a number of
4
+ characters, which would be interpreted as a number of "ems" for LaTeX.
5
+ Cell content larger than the width would be truncated. Any column without a
6
+ width specified would be set at the width of the longest value in that cell,
7
+ after initial formatting.
8
+
9
+ #+begin_SRC ruby
10
+ tab.to_text do |f|
11
+ f.widths(a: 13, b: 30)
12
+ end
13
+ #+end_SRC
14
+
15
+ Possible enhancements:
16
+ - specify an overall width and column widths as decimal or fractions, so that
17
+ a column's width would be that fraction of the overall width.
18
+ - specify a Range for a width, so that the column would at least min and at
19
+ most max, otherwise the width of its largest cell.
20
+
1
21
  * TODO Conversion to Spreadsheets
2
22
  - State "TODO" from [2017-04-21 Fri 10:36]
3
23
  This is a [[https://github.com/westonganger/spreadsheet_architect][gem]] that I can include into the Table model to convert a table into
4
24
  a spread-sheet, or even a sheet in a multi-sheet spreadsheet file.
5
25
 
6
- * TODO Add from_yql for fetching from Yahoo
26
+ * TODO Add Quandl or EODDATA Queries
27
+ Possible replacements for YQL.
28
+
29
+ * CNCL Add from_yql for fetching from Yahoo
30
+ CLOSED: [2022-01-30 Sun 06:03]
7
31
  - State "TODO" from [2017-04-21 Fri 10:35]
32
+
33
+ Cancelled because Yahoo shut down the YQL api service.
34
+
8
35
  Add a constructor to allow fetching stock data from yql. Perhaps grab all
9
36
  available fields, then allow a select of those of interest.
10
37
 
@@ -173,7 +173,12 @@ module FatTable
173
173
  # Column. This makes Columns Enumerable, so all the Enumerable methods are
174
174
  # available on a Column.
175
175
  def each
176
- items.each { |itm| yield itm }
176
+ if block_given?
177
+ items.each { |itm| yield itm }
178
+ self
179
+ else
180
+ to_enum(:each)
181
+ end
177
182
  end
178
183
 
179
184
  ##########################################################################
@@ -191,8 +196,11 @@ module FatTable
191
196
 
192
197
  # :category: Aggregates
193
198
 
194
- # Return the first non-nil item in the Column. Works with any Column type.
199
+ # Return the first non-nil item in the Column, or nil if all items are
200
+ # nil. Works with any Column type.
195
201
  def first
202
+ return nil if items.all?(&:nil?)
203
+
196
204
  if type == 'String'
197
205
  items.reject(&:blank?).first
198
206
  else
@@ -204,6 +212,8 @@ module FatTable
204
212
 
205
213
  # Return the last non-nil item in the Column. Works with any Column type.
206
214
  def last
215
+ return nil if items.all?(&:nil?)
216
+
207
217
  if type == 'String'
208
218
  items.reject(&:blank?).last
209
219
  else
@@ -213,9 +223,11 @@ module FatTable
213
223
 
214
224
  # :category: Aggregates
215
225
 
216
- # Return a count of the non-nil items in the Column. Works with any Column
217
- # type.
226
+ # Return a count of the non-nil items in the Column, or the size of the
227
+ # column if all items are nil. Works with any Column type.
218
228
  def count
229
+ return items.size if items.all?(&:nil?)
230
+
219
231
  if type == 'String'
220
232
  items.reject(&:blank?).count.to_d
221
233
  else
@@ -225,8 +237,8 @@ module FatTable
225
237
 
226
238
  # :category: Aggregates
227
239
 
228
- # Return the smallest non-nil, non-blank item in the Column. Works with
229
- # numeric, string, and datetime Columns.
240
+ # Return the smallest non-nil, non-blank item in the Column, or nil if all
241
+ # items are nil. Works with numeric, string, and datetime Columns.
230
242
  def min
231
243
  only_with('min', 'NilClass', 'Numeric', 'String', 'DateTime')
232
244
  if type == 'String'
@@ -238,8 +250,8 @@ module FatTable
238
250
 
239
251
  # :category: Aggregates
240
252
 
241
- # Return the largest non-nil, non-blank item in the Column. Works with
242
- # numeric, string, and datetime Columns.
253
+ # Return the largest non-nil, non-blank item in the Column, or nil if all
254
+ # items are nil. Works with numeric, string, and datetime Columns.
243
255
  def max
244
256
  only_with('max', 'NilClass', 'Numeric', 'String', 'DateTime')
245
257
  if type == 'String'
@@ -251,19 +263,24 @@ module FatTable
251
263
 
252
264
  # :category: Aggregates
253
265
 
254
- # Return a Range object for the smallest to largest value in the column.
255
- # Works with numeric, string, and datetime Columns.
266
+ # Return a Range object for the smallest to largest value in the column,
267
+ # or nil if all items are nil. Works with numeric, string, and datetime
268
+ # Columns.
256
269
  def range
257
270
  only_with('range', 'NilClass', 'Numeric', 'String', 'DateTime')
271
+ return nil if items.all?(&:nil?)
272
+
258
273
  Range.new(min, max)
259
274
  end
260
275
 
261
276
  # :category: Aggregates
262
277
 
263
- # Return the sum of the non-nil items in the Column. Works with numeric and
264
- # string Columns. For a string Column, it will return the concatenation of
265
- # the non-nil items.
278
+ # Return the sum of the non-nil items in the Column, or 0 if all items are
279
+ # nil. Works with numeric and string Columns. For a string Column, it
280
+ # will return the concatenation of the non-nil items.
266
281
  def sum
282
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
283
+
267
284
  only_with('sum', 'Numeric', 'String')
268
285
  if type == 'String'
269
286
  items.reject(&:blank?).join(' ')
@@ -274,11 +291,13 @@ module FatTable
274
291
 
275
292
  # :category: Aggregates
276
293
 
277
- # Return the average value of the non-nil items in the Column. Works with
278
- # numeric and datetime Columns. For datetime Columns, it converts each date
279
- # to its Julian day number, computes the average, and then converts the
280
- # average back to a DateTime.
294
+ # Return the average value of the non-nil items in the Column, or 0 if all
295
+ # items are nil. Works with numeric and datetime Columns. For datetime
296
+ # Columns, it converts each date to its Julian day number, computes the
297
+ # average, and then converts the average back to a DateTime.
281
298
  def avg
299
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
300
+
282
301
  only_with('avg', 'DateTime', 'Numeric')
283
302
  itms = items.filter_to_type(type)
284
303
  size = itms.size.to_d
@@ -293,11 +312,14 @@ module FatTable
293
312
  # :category: Aggregates
294
313
 
295
314
  # Return the sample variance (the unbiased estimator of the population
296
- # variance using a divisor of N-1) as the average squared deviation from the
297
- # mean, of the non-nil items in the Column. Works with numeric and datetime
298
- # Columns. For datetime Columns, it converts each date to its Julian day
299
- # number and computes the variance of those numbers.
315
+ # variance using a divisor of N-1) as the average squared deviation from
316
+ # the mean, of the non-nil items in the Column, or 0 if all items are
317
+ # nil. Works with numeric and datetime Columns. For datetime Columns, it
318
+ # converts each date to its Julian day number and computes the variance of
319
+ # those numbers.
300
320
  def var
321
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
322
+
301
323
  only_with('var', 'DateTime', 'Numeric')
302
324
  all_items =
303
325
  if type == 'DateTime'
@@ -319,10 +341,13 @@ module FatTable
319
341
 
320
342
  # Return the population variance (the biased estimator of the population
321
343
  # variance using a divisor of N) as the average squared deviation from the
322
- # mean, of the non-nil items in the Column. Works with numeric and datetime
323
- # Columns. For datetime Columns, it converts each date to its Julian day
324
- # number and computes the variance of those numbers.
344
+ # mean, of the non-nil items in the Column, or 0 if all items are
345
+ # nil. Works with numeric and datetime Columns. For datetime Columns, it
346
+ # converts each date to its Julian day number and computes the variance of
347
+ # those numbers.
325
348
  def pvar
349
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
350
+
326
351
  only_with('var', 'DateTime', 'Numeric')
327
352
  n = items.filter_to_type(type).size.to_d
328
353
  return BigDecimal('0.0') if n <= 1
@@ -333,11 +358,13 @@ module FatTable
333
358
 
334
359
  # Return the sample standard deviation (the unbiased estimator of the
335
360
  # population standard deviation using a divisor of N-1) as the square root
336
- # of the sample variance, of the non-nil items in the Column. Works with
337
- # numeric and datetime Columns. For datetime Columns, it converts each date
338
- # to its Julian day number and computes the standard deviation of those
339
- # numbers.
361
+ # of the sample variance, of the non-nil items in the Column, or 0 if all
362
+ # items are nil. Works with numeric and datetime Columns. For datetime
363
+ # Columns, it converts each date to its Julian day number and computes the
364
+ # standard deviation of those numbers.
340
365
  def dev
366
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
367
+
341
368
  only_with('dev', 'DateTime', 'Numeric')
342
369
  var.sqrt(20)
343
370
  end
@@ -345,12 +372,14 @@ module FatTable
345
372
  # :category: Aggregates
346
373
 
347
374
  # Return the population standard deviation (the biased estimator of the
348
- # population standard deviation using a divisor of N) as the square root of
349
- # the population variance, of the non-nil items in the Column. Works with
350
- # numeric and datetime Columns. For datetime Columns, it converts each date
351
- # to its Julian day number and computes the standard deviation of those
352
- # numbers.
375
+ # population standard deviation using a divisor of N) as the square root
376
+ # of the population variance, of the non-nil items in the Column, or 0 if
377
+ # all items are nil. Works with numeric and datetime Columns. For datetime
378
+ # Columns, it converts each date to its Julian day number and computes the
379
+ # standard deviation of those numbers.
353
380
  def pdev
381
+ return 0 if type == 'NilClass' || items.all?(&:nil?)
382
+
354
383
  only_with('dev', 'DateTime', 'Numeric')
355
384
  Math.sqrt(pvar)
356
385
  end
@@ -358,8 +387,10 @@ module FatTable
358
387
  # :category: Aggregates
359
388
 
360
389
  # Return true if any of the items in the Column are true; otherwise return
361
- # false. Works only with boolean Columns.
390
+ # false, or false if all items are nil. Works only with boolean Columns.
362
391
  def any?
392
+ return false if type == 'NilClass' || items.all?(&:nil?)
393
+
363
394
  only_with('any?', 'Boolean')
364
395
  items.filter_to_type(type).any?
365
396
  end
@@ -367,17 +398,22 @@ module FatTable
367
398
  # :category: Aggregates
368
399
 
369
400
  # Return true if all of the items in the Column are true; otherwise return
370
- # false. Works only with boolean Columns.
401
+ # false, or false if all items are nil. Works only with boolean Columns.
371
402
  def all?
403
+ return false if type == 'NilClass' || items.all?(&:nil?)
404
+
372
405
  only_with('all?', 'Boolean')
373
406
  items.filter_to_type(type).all?
374
407
  end
375
408
 
376
409
  # :category: Aggregates
377
410
 
378
- # Return true if none of the items in the Column are true; otherwise return
379
- # false. Works only with boolean Columns.
411
+ # Return true if none of the items in the Column are true; otherwise
412
+ # return false, or true if all items are nil. Works only with boolean
413
+ # Columns.
380
414
  def none?
415
+ return true if type == 'NilClass' || items.all?(&:nil?)
416
+
381
417
  only_with('none?', 'Boolean')
382
418
  items.filter_to_type(type).none?
383
419
  end
@@ -387,6 +423,8 @@ module FatTable
387
423
  # Return true if precisely one of the items in the Column is true;
388
424
  # otherwise return false. Works only with boolean Columns.
389
425
  def one?
426
+ return false if type == 'NilClass' || items.all?(&:nil?)
427
+
390
428
  only_with('one?', 'Boolean')
391
429
  items.filter_to_type(type).one?
392
430
  end
@@ -395,6 +433,7 @@ module FatTable
395
433
 
396
434
  def only_with(agg, *valid_types)
397
435
  return self if valid_types.include?(type)
436
+
398
437
  msg = "aggregate '#{agg}' cannot be applied to a #{type} column"
399
438
  raise UserError, msg
400
439
  end
@@ -436,7 +475,12 @@ module FatTable
436
475
  private
437
476
 
438
477
  def convert_and_set_type(val)
439
- new_val = Convert.convert_to_type(val, type, tolerant: tolerant?)
478
+ begin
479
+ new_val = Convert.convert_to_type(val, type, tolerant: tolerant?)
480
+ rescue IncompatibleTypeError
481
+ err_msg = "attempt to add '#{val}' to column '#{header}' already typed as #{type}"
482
+ raise IncompatibleTypeError, err_msg
483
+ end
440
484
  if new_val && (type == 'NilClass' || type == 'String')
441
485
  @type =
442
486
  if [true, false].include?(new_val)
@@ -36,8 +36,7 @@ module FatTable
36
36
  else
37
37
  new_val = convert_to_boolean(val)
38
38
  if new_val.nil?
39
- msg = "attempt to add '#{val}' to a column already typed as #{type}"
40
- raise IncompatibleTypeError, msg
39
+ raise IncompatibleTypeError
41
40
  end
42
41
  new_val
43
42
  end
@@ -47,8 +46,7 @@ module FatTable
47
46
  else
48
47
  new_val = convert_to_date_time(val)
49
48
  if new_val.nil?
50
- msg = "attempt to add '#{val}' to a column already typed as #{type}"
51
- raise IncompatibleTypeError, msg
49
+ raise IncompatibleTypeError
52
50
  end
53
51
  new_val
54
52
  end
@@ -58,8 +56,7 @@ module FatTable
58
56
  else
59
57
  new_val = convert_to_numeric(val)
60
58
  if new_val.nil?
61
- msg = "attempt to add '#{val}' to a column already typed as #{type}"
62
- raise IncompatibleTypeError, msg
59
+ raise IncompatibleTypeError
63
60
  end
64
61
  new_val
65
62
  end
@@ -82,8 +79,7 @@ module FatTable
82
79
  else
83
80
  new_val = convert_to_string(val)
84
81
  if new_val.nil?
85
- msg = "attempt to add '#{val}' to a column already typed as #{type}"
86
- raise IncompatibleTypeError, msg
82
+ raise IncompatibleTypeError
87
83
  end
88
84
  new_val
89
85
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module FatTable
4
4
  class Footer
5
- attr_reader :table, :label, :label_col, :values, :group
5
+ attr_reader :table, :label_col, :values, :group
6
6
 
7
7
  ###########################################################################
8
8
  # Constructors
@@ -15,6 +15,7 @@ module FatTable
15
15
  # for the footer are added later with the #add_value method.
16
16
  def initialize(label = 'Total', table, label_col: nil, group: false)
17
17
  @label = label
18
+
18
19
  unless table.is_a?(Table)
19
20
  raise ArgumentError, 'Footer.new needs a table argument'
20
21
  end
@@ -30,14 +31,7 @@ module FatTable
30
31
  @group = group
31
32
  @group_cols = {}
32
33
  @values = {}
33
- if group
34
- @values[@label_col] = []
35
- table.number_of_groups.times do
36
- @values[@label_col] << @label
37
- end
38
- else
39
- @values[@label_col] = [@label]
40
- end
34
+ insert_labels_in_label_col
41
35
  make_accessor_methods
42
36
  end
43
37
 
@@ -71,6 +65,11 @@ module FatTable
71
65
  end
72
66
  end
73
67
 
68
+ # Return the value of the label, for the kth group if grouped.
69
+ def label(k = 0)
70
+ @values[@label_col][k]
71
+ end
72
+
74
73
  # :category: Accessors
75
74
 
76
75
  # Return the value of under the +key+ header, or if this is a group
@@ -108,8 +107,10 @@ module FatTable
108
107
  if group && k.nil?
109
108
  raise ArgumentError, 'Footer#column(h, k) missing the group number argument k'
110
109
  end
110
+
111
111
  if group
112
- k.nil? ? @group_cols[h] : @group_cols[h][k]
112
+ @group_cols[h] ||= table.group_cols(h)
113
+ @group_cols[h][k]
113
114
  else
114
115
  table.column(h)
115
116
  end
@@ -151,14 +152,13 @@ module FatTable
151
152
 
152
153
  # Evaluate the given agg for the header col and, in the case of a group
153
154
  # footer, the group k.
154
- def calc_val(agg, col, k = nil)
155
- column =
156
- if group
157
- @group_cols[col] ||= table.group_cols(col)
158
- @group_cols[col][k]
159
- else
160
- table.column(col)
161
- end
155
+ def calc_val(agg, h, k = nil)
156
+ column = column(h, k)
157
+
158
+ # Convert Date and Time objects to DateTime
159
+ if [Date, Time].include?(agg.class)
160
+ agg = agg.to_datetime
161
+ end
162
162
 
163
163
  case agg
164
164
  when Symbol
@@ -166,7 +166,7 @@ module FatTable
166
166
  when String
167
167
  begin
168
168
  converted_val = Convert.convert_to_type(agg, column.type)
169
- rescue UserError
169
+ rescue UserError, IncompatibleTypeError
170
170
  converted_val = false
171
171
  end
172
172
  if converted_val
@@ -179,29 +179,77 @@ module FatTable
179
179
  when Proc
180
180
  result =
181
181
  if group
182
- unless agg.arity == 3
183
- msg = 'a lambda used in a group footer must have three arguments: (f, c, k)'
182
+ case agg.arity
183
+ when 0
184
+ agg.call
185
+ when 1
186
+ agg.call(k)
187
+ when 2
188
+ agg.call(k, column)
189
+ when 3
190
+ agg.call(k, column, self)
191
+ else
192
+ msg = 'a lambda used in a group footer may have 0 to 3 three arguments: (k, c, f)'
184
193
  raise ArgumentError, msg
185
194
  end
186
- agg.call(self, col, k)
187
195
  else
188
- unless agg.arity == 2
189
- msg = 'a lambda used in a non-group footer must have two arguments: (f, c)'
196
+ case agg.arity
197
+ when 0
198
+ agg.call
199
+ when 1
200
+ agg.call(column)
201
+ when 2
202
+ agg.call(column, self)
203
+ else
204
+ msg = 'a lambda used in a non-group footer may have 0 to 2 arguments: (c, f)'
190
205
  raise ArgumentError, msg
191
206
  end
192
- agg.call(self, col)
193
207
  end
194
- # Make sure the result returned can be inserted into footer field.
195
- case result
196
- when Symbol, String
197
- calc_val(result, col, k)
198
- when column.type.constantize
199
- result
208
+ # Pass the result back into this method as the new agg
209
+ calc_val(result, h, k)
210
+ else
211
+ agg.to_s
212
+ end
213
+ end
214
+
215
+ # Insert a possibly calculated value for the label in the appropriate
216
+ # @values column.
217
+ def insert_labels_in_label_col
218
+ if group
219
+ @values[@label_col] = []
220
+ table.number_of_groups.times do |k|
221
+ @values[@label_col] << calc_label(k)
222
+ end
223
+ else
224
+ @values[@label_col] = [calc_label]
225
+ end
226
+ end
227
+
228
+ # Calculate the label for the kth group, using k = nil for non-group
229
+ # footers. If the label is a proc, call it with the group number.
230
+ def calc_label(k = nil)
231
+ case @label
232
+ when Proc
233
+ case @label.arity
234
+ when 0
235
+ @label.call
236
+ when 1
237
+ k ? @label.call(k) : @label.call(self)
238
+ when 2
239
+ if k
240
+ @label.call(k, self)
241
+ else
242
+ raise ArgumentError, "a non-group footer label proc may only have 1 argument for the containing footer f"
243
+ end
200
244
  else
201
- raise ArgumentError, "lambda cannot return an object of class #{result.class}"
245
+ if k
246
+ raise ArgumentError, "group footer label proc may only have 0, 1, or 2 arguments for group number k and containing footer f"
247
+ else
248
+ raise ArgumentError, "a non-group footer label proc may only have 0 or 1 arguments for the containing footer f"
249
+ end
202
250
  end
203
251
  else
204
- agg
252
+ @label.to_s
205
253
  end
206
254
  end
207
255
 
@@ -181,41 +181,74 @@ module FatTable
181
181
  end
182
182
 
183
183
  # :category: Add Footers
184
-
185
- # A simpler method for adding a footer to the formatted output having the
186
- # label +label+ placed in the column with the header +label_col+ or in the
187
- # first column if +label_col+ is ommitted. The remaining hash arguments
188
- # apply an aggregate to the values of the column, which can be:
189
- #
190
- # (1) a symbol representing one of the builtin aggregates, i.e., :first,
191
- # :last, :range, :sum, :count, :min, :max, :avg, :var, :pvar, :dev, :pdev,
192
- # :any?, :all?, :none?, and :one?,
193
184
  #
194
- # (2) an arbitrary value of one of the four supported types: Boolean,
195
- # DateTime, Numeric, or String: true, false, 3.14159, 'Total debits', or a
196
- # string that is convertible into one of these types by the usual methods
197
- # used in contructing a table,
185
+ # A keyword method for adding a footer to the formatted output having the
186
+ # label +label:+ (default 'Total') placed in the column with the header
187
+ # +label_col:+ or in the first column if +label_col+ is ommitted.
188
+ # This assigns a fixed group label to be placed in the :date column:
189
+ # #+begin_src ruby
190
+ # fmtr.foot(label: "Year's Average", label_col: :date, temp: avg)
191
+ # #+end_src
192
+ #
193
+ # Besides being a fixed string, the +label:+ can also be a proc or lambda
194
+ # taking one argument, the foooter itself.
195
+ # Thus, a label such as:
196
+ #
197
+ # #+begin_src ruby
198
+ # fmtr.foot(label: -> (f) { "Average (latest year #{f.column(:date).max.year})" },
199
+ # temp: :avg)
200
+ # #+end_src
201
+ # And this would add the highest number to label, assuming the :date column
202
+ # of the footer's table had the year for each item.
203
+ #
204
+ # The remaining hash arguments apply an aggregate to the values of the
205
+ # column, which can be:
206
+ #
207
+ # 1. a symbol representing one of the builtin aggregates, i.e., :first,
208
+ # :last, :range, :sum, :count, :min, :max, :avg, :var, :pvar, :dev,
209
+ # :pdev, :any?, :all?, :none?, and :one?, or a symbol for your own
210
+ # aggregate defined as an instance method on FatTable::Column.
211
+ # 2. a fixed string, but it the string can be converted into the Column's
212
+ # type, it will be converted, so the string '3.14159' will be converted
213
+ # to 3.14159 in a Numeric column.
214
+ # 3. a value of the Column's type, so Date.today would simply be evaluated
215
+ # for a Numeric column.
216
+ # 4. most flexibly of all, a proc or lambda taking arguments: f, the
217
+ # footer object itself; c, the column (or in the case of a group
218
+ # footer, the sub-column) corresponding to the current header, and in
219
+ # the case of a group footer, k, the number of the group (0-based).
220
+ # 5. Any other value is converted to a string with #to_s.
198
221
  #
199
222
  # Examples:
200
223
  #
201
224
  # Put the label in the :dickens column of the footer and the maximum value
202
225
  # from the :alpha column in the :alpha column of the footer.
203
226
  #
204
- # fmtr.foot('Best', :dickens, alpha: :max)
227
+ # fmtr.foot(label: 'Best', label_col: :dickens, alpha: :max)
205
228
  #
206
229
  # Put the label 'Today' in the first column of the footer and today's date
207
230
  # in the :beta column.
208
231
  #
209
- # fmtr.foot('Today', beta: Date.today)
232
+ # fmtr.foot(label: 'Today', beta: Date.today)
210
233
  #
211
234
  # Put the label 'Best' in the :dickens column of the footer and the string
212
235
  # 'Tale of Two Cities' in the :alpha column of the footer. Since it can't
213
236
  # be interpreted as Boolean, Numeric, or DateTime, it is placed in the
214
237
  # footer literally.
215
238
  #
216
- # fmtr.foot('Best', :dickens, alpha: 'A Tale of Two Cities')
239
+ # fmtr.foot(label: 'Best', label_col: :dickens, alpha: 'A Tale of Two
240
+ # Cities')
241
+ #
242
+ # Use a lambda to calculate the value to be placed in the column :gamma.
243
+ #
244
+ # fmtr.foot(label: 'Gamma', beta: :avg, gamma: ->(f, c) {
245
+ # (Math.gamma(c.count) + f[:beta] } )
217
246
  #
218
- def foot(label, label_col = nil, **agg_cols)
247
+ # Note that this way a footer can be made a function of the other footer
248
+ # values (using f[:other_col]) as well as the Column object corresponding
249
+ # to the lamda's column.
250
+ #
251
+ def foot(label: 'Total', label_col: nil, **agg_cols)
219
252
  foot = Footer.new(label, table, label_col: label_col)
220
253
  agg_cols.each_pair do |h, agg|
221
254
  foot.add_value(h, agg)
@@ -255,9 +288,92 @@ module FatTable
255
288
  foot
256
289
  end
257
290
 
258
- # Add a group footer to the formatted output. This method has the same
259
- # usage as the #foot method, but it adds group footers.
260
- def gfoot(label, label_col = nil, **agg_cols)
291
+ # :category: Add Footers
292
+ #
293
+ # A keyword method for adding a group footer to the formatted output
294
+ # having the label +label:+ (default 'Total') placed in the column with
295
+ # the header +label_col:+ or in the first column if +label_col+ is
296
+ # ommitted.
297
+ #
298
+ # This assigns a fixed group label to be placed in the :date column:
299
+ # #+begin_src ruby
300
+ # fmtr.gfoot(label: "Year's Average", label_col: :date, temp: avg)
301
+ # #+end_src
302
+ #
303
+ # Besides being a fixed string, the +label:+ can also be a proc or lambda
304
+ # taking one or two arguments. In the one argument form, the argument is
305
+ # the group number k. If a second argument is specified, the foooter
306
+ # itself is passed as the argument. Thus, a label such as:
307
+ #
308
+ # #+begin_src ruby
309
+ # fmtr.gfoot(label: -> (k) { "Group #{(k+1).to_roman} Average" }, temp: :avg)
310
+ # #+end_src
311
+ # This would format the label with a roman numeral (assuming you defined a
312
+ # method to do so) for the group number.
313
+ #
314
+ # #+begin_src ruby
315
+ # fmtr.gfoot(label: -> (k, f) { "Year #{f.column(:date, k).max.year} Group #{(k+1).to_roman} Average" },
316
+ # temp: :avg)
317
+ # #+end_src
318
+ # And this would add the group's year to label, assuming the :date column
319
+ # of the footer's table had the same year for each item in the group.
320
+ #
321
+ #
322
+ # The remaining hash arguments apply an aggregate to the values
323
+ # of the column, which can be:
324
+ #
325
+ # 1. a symbol representing one of the builtin aggregates, i.e., :first,
326
+ # :last, :range, :sum, :count, :min, :max, :avg, :var, :pvar, :dev,
327
+ # :pdev, :any?, :all?, :none?, and :one?, or a symbol for your own
328
+ # aggregate defined as an instance method on FatTable::Column.
329
+ # 2. a fixed string, but it the string can be converted into the Column's
330
+ # type, it will be converted, so the string '3.14159' will be converted
331
+ # to 3.14159 in a Numeric column.
332
+ # 3. a value of the Column's type, so Date.today would simply be evaluated
333
+ # for a Numeric column.
334
+ # 4. most flexibly of all, a proc or lambda taking arguments: f, the
335
+ # footer object itself; c, the column (or in the case of a group
336
+ # footer, the sub-column) corresponding to the current header, and k,
337
+ # this group's group number (0-based).
338
+ # 5. Any other value is converted to a string with #to_s.
339
+ #
340
+ # Examples:
341
+ #
342
+ # Put the label in the :dickens column of the footer and the maximum value
343
+ # from the :alpha column in the :alpha column of the footer.
344
+ #
345
+ # #+begin_src ruby
346
+ # fmtr.gfoot(label: 'Best', label_col: :dickens, alpha: :max)
347
+ # #+end_src
348
+ #
349
+ # Put the label 'Today' in the first column of the footer and today's date
350
+ # in the :beta column.
351
+ #
352
+ # #+begin_src ruby
353
+ # fmtr.gfoot(label: 'Today', beta: Date.today)
354
+ # #+end_src
355
+ #
356
+ # Put the label 'Best' in the :dickens column of the footer and the string
357
+ # 'Tale of Two Cities' in the :alpha column of the footer. Since it can't
358
+ # be interpreted as Boolean, Numeric, or DateTime, it is placed in the
359
+ # footer literally.
360
+ #
361
+ # #+begin_src ruby
362
+ # fmtr.gfoot(label: 'Best', label_col: :dickens, alpha: 'A Tale of Two
363
+ # Cities')
364
+ # #+end_src
365
+ #
366
+ # Use a lambda to calculate the value to be placed in the column :gamma.
367
+ #
368
+ # #+begin_src ruby
369
+ # fmtr.gfoot(label: 'Gamma', beta: :avg, gamma: ->(f, c) {
370
+ # (Math.gamma(c.count) + f[:beta] } )
371
+ # #+end_src
372
+ #
373
+ # Note that this way a footer can be made a function of the other footer
374
+ # values (using f[:other_col]) as well as the Column object corresponding
375
+ # to the lamda's column.
376
+ def gfoot(label: 'Group Total', label_col: nil, **agg_cols)
261
377
  foot = Footer.new(label, table, label_col: label_col, group: true)
262
378
  agg_cols.each_pair do |h, agg|
263
379
  foot.add_value(h, agg)
@@ -532,7 +648,7 @@ module FatTable
532
648
  valid_keys = table.headers + %i[string numeric datetime boolean nil]
533
649
  invalid_keys = (fmts.keys - valid_keys).uniq
534
650
  unless invalid_keys.empty?
535
- msg = "invalid #{location} column or type: #{invalid_keys.join(',')}"
651
+ msg = "invalid #{location} column or type: #{invalid_keys.join(', ')}"
536
652
  raise UserError, msg
537
653
  end
538
654
 
@@ -1004,9 +1120,14 @@ module FatTable
1004
1120
  # converted to strings formatted according to the Formatter's formatting
1005
1121
  # directives given in Formatter.format_for or Formatter.format.
1006
1122
  def output
1007
- # This results in a hash of two-element arrays. The key is the header and
1008
- # the value is an array of the header and formatted header. We do the
1009
- # latter so the structure parallels the structure for rows explained next.
1123
+ # If there are neither headers nor any rows in the table, return an
1124
+ # empty string.
1125
+ return '' if table.empty? && table.headers.empty?
1126
+
1127
+ # This results in a hash of two-element arrays. The key
1128
+ # is the header and the value is an array of the header and formatted
1129
+ # header. We do the latter so the structure parallels the structure for
1130
+ # rows explained next.
1010
1131
  formatted_headers = build_formatted_headers
1011
1132
 
1012
1133
  # These produce an array with each element representing a row of the
@@ -20,7 +20,7 @@ module FatTable
20
20
  end
21
21
 
22
22
  def pre_header(widths)
23
- result = '|'
23
+ result = +'|'
24
24
  widths.each_value do |w|
25
25
  result += '-' * (w + 2) + '+'
26
26
  end
@@ -53,7 +53,7 @@ module FatTable
53
53
  end
54
54
 
55
55
  def hline(widths)
56
- result = '|'
56
+ result = +'|'
57
57
  widths.each_value do |w|
58
58
  result += '-' * (w + 2) + '+'
59
59
  end
@@ -62,7 +62,7 @@ module FatTable
62
62
  end
63
63
 
64
64
  def post_footers(widths)
65
- result = '|'
65
+ result = +'|'
66
66
  widths.each_value do |w|
67
67
  result += '-' * (w + 2) + '+'
68
68
  end
@@ -221,7 +221,7 @@ module FatTable
221
221
  end
222
222
 
223
223
  def pre_header(widths)
224
- result = upper_left
224
+ result = +upper_left
225
225
  widths.each_value do |w|
226
226
  result += double_rule * (w + 2) + upper_tee
227
227
  end
@@ -255,7 +255,7 @@ module FatTable
255
255
  end
256
256
 
257
257
  def hline(widths)
258
- result = left_tee
258
+ result = +left_tee
259
259
  widths.each_value do |w|
260
260
  result += horizontal_rule * (w + 2) + single_cross
261
261
  end
@@ -289,7 +289,7 @@ module FatTable
289
289
  end
290
290
 
291
291
  def post_footers(widths)
292
- result = lower_left
292
+ result = +lower_left
293
293
  widths.each_value do |w|
294
294
  result += double_rule * (w + 2) + lower_tee
295
295
  end
@@ -16,7 +16,7 @@ module FatTable
16
16
  end
17
17
 
18
18
  def pre_header(widths)
19
- result = '+'
19
+ result = +'+'
20
20
  widths.each_value do |w|
21
21
  result += '=' * (w + 2) + '+'
22
22
  end
@@ -49,7 +49,7 @@ module FatTable
49
49
  end
50
50
 
51
51
  def hline(widths)
52
- result = '+'
52
+ result = +'+'
53
53
  widths.each_value do |w|
54
54
  result += '-' * (w + 2) + '+'
55
55
  end
@@ -82,7 +82,7 @@ module FatTable
82
82
  end
83
83
 
84
84
  def post_footers(widths)
85
- result = '+'
85
+ result = +'+'
86
86
  widths.each_value do |w|
87
87
  result += '=' * (w + 2) + '+'
88
88
  end
@@ -105,7 +105,7 @@ module FatTable
105
105
  unless heads.empty?
106
106
  heads.each do |h|
107
107
  if h.to_s.end_with?('!') || @tolerant_columns.include?(h)
108
- @columns << Column.new(header: h.to_s.sub(/!\s*\z/, ''), tolerant: true)
108
+ @columns << Column.new(header: h.to_s.sub(/!\s*\z/, ''), type: 'String')
109
109
  else
110
110
  @columns << Column.new(header: h)
111
111
  end
@@ -120,7 +120,7 @@ module FatTable
120
120
  # though FatTable::Table objects have no instance variables, a class that
121
121
  # inherits from it might.
122
122
  def empty_dup
123
- self.dup.__empty!
123
+ dup.__empty!
124
124
  end
125
125
 
126
126
  def __empty!
@@ -530,8 +530,13 @@ module FatTable
530
530
 
531
531
  # Yield each row of the table as a Hash with the column symbols as keys.
532
532
  def each
533
- rows.each do |row|
534
- yield row
533
+ if block_given?
534
+ rows.each do |row|
535
+ yield row
536
+ end
537
+ self
538
+ else
539
+ to_enum(:each)
535
540
  end
536
541
  end
537
542
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FatTable
4
4
  # The current version of FatTable
5
- VERSION = '0.5.4'
5
+ VERSION = '0.6.1'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fat_table
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel E. Doherty
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-27 00:00:00.000000000 Z
11
+ date: 2022-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler