fat_table 0.2.4 → 0.2.6

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
  SHA1:
3
- metadata.gz: 772c4f7d95d5af94968f650b4bc98ebad4756626
4
- data.tar.gz: c15462e9a444de246690482da1a8acd3a46e0c60
3
+ metadata.gz: fc10b9aadaada3eb303831288c6cc65d9362a5c6
4
+ data.tar.gz: 1a4c9e66382b2879ca6306f955d7aba5c4b9eef1
5
5
  SHA512:
6
- metadata.gz: 4f1177b1568ece2027849b102f2e7db84db99846e7291882f943fba5fd400b75032847df303c528fada0683afb2fb5b0bc97a2a91f2e52ed74cbd6960da07ac2
7
- data.tar.gz: 813ddb4b57d893ce9bfb0ca17bb6d03d8807fa361b166396d0635fc5d23e70f4c1641fb69b0f3973678faa2e1bb87667de6bca4b8ff3c0033ceba38db0168658
6
+ metadata.gz: 2f263f677bdd964642a6700c5c139d689184059ee994e551941dba6fac2d1d3bc3830ac63c7b28e526356b0166ee63c255ffac00769a38ee6e71fba090216b9f
7
+ data.tar.gz: 5d02b955317232d48a78f8ddb73632a4aaea5c49da31f72a808303a362c295203b7f6d508b55f03b14687953b72c7870a840844ff2779bb0cbc1ae16a966bd22
data/.gitignore CHANGED
@@ -20,3 +20,6 @@
20
20
  /lib/GPATH
21
21
  /lib/GRTAGS
22
22
  /lib/GTAGS
23
+ /GPATH
24
+ /GRTAGS
25
+ /GTAGS
@@ -1,5 +1,17 @@
1
- sudo: false
2
1
  language: ruby
2
+ before_install:
3
+ - sudo apt-get -qq update
4
+ - sudo apt-get install -y texlive-latex-base texlive-latex-recommended
5
+ after_failure:
6
+ - "pwd"
7
+ - "cat ./spec/tmp/latex.err"
8
+ - "cat ./spec/tmp/example1.log"
9
+ - "cat ./spec/tmp/psql.out"
10
+ bundler_args: --without debug
11
+ services:
12
+ - postgresql
3
13
  rvm:
4
- - 2.3.0
5
- before_install: gem install bundler -v 1.14.3
14
+ - 2.2.2
15
+ - 2.3
16
+ - 2.4
17
+ - ruby-head
data/README.org CHANGED
@@ -1,6 +1,12 @@
1
1
  #+OPTIONS: :toc
2
2
  #+LATEX_HEADER: \usepackage[margin=0.75in]{geometry}
3
3
 
4
+ #+BEGIN_COMMENT
5
+ [![Build Status](https://travis-ci.org/ddoherty03/fat_table.svg?branch=master)](https://travis-ci.org/ddoherty03/fat_table)
6
+ #+END_COMMENT
7
+
8
+ [[https://travis-ci.org/ddoherty03/fat_table.svg?branch=master]]
9
+
4
10
  * Introduction
5
11
 
6
12
  ~FatTable~ is a gem that treats tables as a data type. It provides methods for
@@ -28,9 +34,9 @@ as a foundation for providing reporting functions where flexibility about the
28
34
  output medium can be quite useful. Finally ~FatTable~ can be used within Emacs
29
35
  ~org-mode~ files in code blocks targeting the Ruby language. Org mode tables are
30
36
  presented to a ruby code block as an array of arrays, so ~FatTable~ can read
31
- them in with its ~.from_aoa~ constructor. A ~FatTable~ table can output as an
32
- array of arrays with its ~.to_aoa~ output function and will be rendered in an
33
- org-mode buffer as an org-table, ready for processing by other code blocks.
37
+ them in with its ~.from_aoa~ constructor. A ~FatTable~ table output as an array
38
+ of arrays with its ~.to_aoa~ output function will be rendered in an org-mode
39
+ buffer as an org-table, ready for processing by other code blocks.
34
40
 
35
41
  * Installation
36
42
 
@@ -244,21 +250,24 @@ types as follows:
244
250
  regardless of case, are interpreted as ~FalseClass~, in either case
245
251
  resulting in a Boolean column. Empty strings in a column already having a
246
252
  Boolean type are converted to nil.
247
- - DateTime :: strings that contain patterns of 'yyyy-mm-dd' or 'yyyy/mm/dd' will
248
- be interpreted as a ~DateTime~ or a ~Date~ (if there are no sub-day time
249
- components present). The number of digits in the month and day can be one
250
- or two, but the year component must be four digits. Any time components are
251
- valid if they can be properly interpreted by ~DateTime.parse~. Org mode
252
- timestamps, active or inactive, are valid input strings for DateTime
253
- columns. Empty strings in a column already having the DateTime type are
254
- converted to nil.
255
- - Numeric :: all commas ',', underscores, '_', and '$' dollar signs are removed
256
- from the string and if the remaining string can be interpreted as a
257
- ~Numeric~, it will be. It is interpreted as an ~Integer~ if there are no
258
- decimal places in the remaining string, as a ~Rational~ if the string has
259
- the form '<number>:<number>' or '<number>/<number>', or as a ~BigDecimal~
260
- if there is a decimal point in the remaining string. Empty strings in a
261
- column already having the Numeric type are converted to nil.
253
+ - DateTime :: strings that contain patterns of 'yyyy-mm-dd' or 'yyyy/mm/dd' or
254
+ 'mm-dd-yyy' or 'mm/dd/yyyy' or any of the foregoing with an added
255
+ 'Thh:mm:ss' or 'Thh:mm' will be interpreted as a ~DateTime~ or a ~Date~ (if
256
+ there are no sub-day time components present). The number of digits in the
257
+ month and day can be one or two, but the year component must be four
258
+ digits. Any time components are valid if they can be properly interpreted
259
+ by ~DateTime.parse~. Org mode timestamps (any of the foregoing surrounded
260
+ by square '[]' or pointy '<>' brackets), active or inactive, are valid
261
+ input strings for DateTime columns. Empty strings in a column already
262
+ having the DateTime type are converted to nil.
263
+ - Numeric :: all commas ',', underscores, '_', and '$' dollar signs (or other
264
+ currency symbol as set by ~FatTable.currency_symbol~ are removed from the
265
+ string and if the remaining string can be interpreted as a ~Numeric~, it
266
+ will be. It is interpreted as an ~Integer~ if there are no decimal places
267
+ in the remaining string, as a ~Rational~ if the string has the form
268
+ '<number>:<number>' or '<number>/<number>', or as a ~BigDecimal~ if there
269
+ is a decimal point in the remaining string. Empty strings in a column
270
+ already having the Numeric type are converted to nil.
262
271
  - String :: if all else fails, ~FatTable~ applies ~#to_s~ to the input value
263
272
  and, treats it as an item of type ~String~. Empty strings in a column
264
273
  already having the String type are kept as empty strings.
@@ -497,10 +506,11 @@ or strings that can parsed into one of the permissible column types.
497
506
  *** From SQL queries
498
507
 
499
508
  Another way to initialize a ~FatTable~ table is with the results of a SQL query.
500
- ~FatTable~ uses the ~dbi~ gem to query databases. You must first set the
509
+ ~FatTable~ uses the ~sequel~ gem to query databases. You must first set the
501
510
  database parameters to be used for the queries.
502
511
 
503
512
  #+BEGIN_SRC ruby
513
+ # This automatically requires sequel.
504
514
  require 'fat_table'
505
515
  FatTable.set_db(driver: 'Pg',
506
516
  database: 'XXX_development',
@@ -522,6 +532,19 @@ The ~.set_db~ function need only be called once, and the database handle it
522
532
  creates will be used for all subsequent ~.from_sql~ calls until ~.set_db~ is
523
533
  called again.
524
534
 
535
+ Alternatively, you can build the ~Sequel~ connection with ~Sequel.connect~ or
536
+ with adapter-specific ~Sequel~ connection methods and let ~FatTable~ know to use
537
+ that connection:
538
+
539
+ #+BEGIN_SRC ruby
540
+ require 'fat_table'
541
+ FatTable.db = Sequel.connect('postgres://user:password@localhost/dbname')
542
+ FatTable.db = Sequel.ado(conn_string: 'Provider=Microsoft.ACE.OLEDB.12.0;Data Source=drive:\path\filename.accdb')
543
+ #+END_SRC
544
+
545
+ Consult ~Sequel's~ documentation for details on its connection methods.
546
+ [[http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html]]
547
+
525
548
  *** Marking Groups in Input
526
549
 
527
550
  The ~.from_aoa~ and ~.from_aoh~ functions take an optional keyword parameter
@@ -679,6 +702,8 @@ With the ~select~ method, you can select which existing columns should appear in
679
702
  the output table and create new columns in the output table that are a function
680
703
  of existing and new columns.
681
704
 
705
+ **** Selecting Existing Columns
706
+
682
707
  Here we select three existing columns by simply passing header symbols in the
683
708
  order we want them to appear in the output. Thus, one use of =select= is to
684
709
  filter and permute the order of existing columns. The =select= method preserves
@@ -716,6 +741,8 @@ any group boundaries present in the input table.
716
741
  | 8.25 | T016 | 100 |
717
742
  #+END_EXAMPLE
718
743
 
744
+ **** Adding New Columns
745
+
719
746
  More interesting is that ~select~ can take hash-like keyword arguments following
720
747
  all of the symbol arguments to create new columns in the output as functions of
721
748
  other columns. For each hash-like parameter, the keyword given must be a symbol,
@@ -810,9 +837,71 @@ second, chained call to ~select~:
810
837
  | T016 | 2016-11-02 | 8.25 | 100 | 825.0 |
811
838
  #+END_EXAMPLE
812
839
 
840
+ **** Custom Instance Variables and Hooks
841
+
842
+ As the above examples demonstrate, the instance variables ~@row~ and ~@group~
843
+ are available when evaluating expressions that add new columns. You can set up
844
+ your own instance variables as well for keeping track of things that cross row
845
+ boundaries, such as running sums.
846
+
847
+ To declare instance variables, you can use the ~ivars:~ hash parameter to
848
+ ~select~. Each key of the hash becomes an instance variable and each value
849
+ becomes its initial value before any rows are evaluated.
850
+
851
+ In addition, you can provide ~before_hook:~ and ~after_hook:~ parameters as
852
+ strings that are evaluated as ruby expressions before and after each row is
853
+ processed. You can use these to update instance variables. The values set in
854
+ the ~before_hook:~ can be used in expressions for adding new columns by
855
+ referencing them with the '@' prefix.
856
+
857
+ For example, suppose we wanted to not only add a cost column, but a column that
858
+ shows the cumulative cost after each transaction in our example table. The
859
+ following example uses the ~ivars:~ and ~before_hook:~ parameters to keep track
860
+ of the running cost of shares, then formats the table.
861
+
862
+ #+HEADER: :colnames no :session readme :hlines yes :wrap EXAMPLE :exports both
863
+ #+BEGIN_SRC ruby
864
+ tab = tab1.select(:ref, :price, :shares, traded_on: :date, \
865
+ cost: 'price * shares', cumulative: '@total_cost', \
866
+ ivars: { total_cost: 0 }, \
867
+ before_hook: '@total_cost += price * shares')
868
+ FatTable.to_aoa(tab) do |f|
869
+ f.format(price: '0.4', shares: '0.0,', cost: '0.2,', cumulative: '0.2,')
870
+ end
871
+ #+END_SRC
872
+
873
+ #+BEGIN_EXAMPLE
874
+ | Ref | Price | Shares | Traded On | Cost | Cumulative |
875
+ |------+--------+--------+------------+------------+------------|
876
+ | T001 | 7.7000 | 100 | 2016-11-01 | 770.00 | 770.00 |
877
+ | T002 | 7.7500 | 200 | 2016-11-01 | 1,550.00 | 2,320.00 |
878
+ | T003 | 7.5000 | 800 | 2016-11-01 | 6,000.00 | 8,320.00 |
879
+ | T003 | 7.5000 | 800 | 2016-11-01 | 6,000.00 | 14,320.00 |
880
+ |------+--------+--------+------------+------------+------------|
881
+ | T004 | 7.5500 | 6,811 | 2016-11-01 | 51,423.05 | 65,743.05 |
882
+ | T005 | 7.5000 | 4,000 | 2016-11-01 | 30,000.00 | 95,743.05 |
883
+ | T006 | 7.6000 | 1,000 | 2016-11-01 | 7,600.00 | 103,343.05 |
884
+ | T006 | 7.6000 | 1,000 | 2016-11-01 | 7,600.00 | 110,943.05 |
885
+ | T007 | 7.6500 | 200 | 2016-11-01 | 1,530.00 | 112,473.05 |
886
+ | T008 | 7.6500 | 2,771 | 2016-11-01 | 21,198.15 | 133,671.20 |
887
+ | T009 | 7.6000 | 9,550 | 2016-11-01 | 72,580.00 | 206,251.20 |
888
+ |------+--------+--------+------------+------------+------------|
889
+ | T010 | 7.5500 | 3,175 | 2016-11-01 | 23,971.25 | 230,222.45 |
890
+ | T011 | 7.4250 | 100 | 2016-11-02 | 742.50 | 230,964.95 |
891
+ | T012 | 7.5500 | 4,700 | 2016-11-02 | 35,485.00 | 266,449.95 |
892
+ | T012 | 7.5500 | 4,700 | 2016-11-02 | 35,485.00 | 301,934.95 |
893
+ | T013 | 7.3500 | 53,100 | 2016-11-02 | 390,285.00 | 692,219.95 |
894
+ |------+--------+--------+------------+------------+------------|
895
+ | T014 | 7.4500 | 5,847 | 2016-11-02 | 43,560.15 | 735,780.10 |
896
+ | T015 | 7.7500 | 500 | 2016-11-02 | 3,875.00 | 739,655.10 |
897
+ | T016 | 8.2500 | 100 | 2016-11-02 | 825.00 | 740,480.10 |
898
+ #+END_EXAMPLE
899
+
900
+ **** Argument Order and Boundaries
813
901
 
814
902
  Notice that ~select~ can take any number of arguments but all the symbol
815
- arguments must come first followed by all the hash-like keyword arguments.
903
+ arguments must come first followed by all the hash-like keyword arguments,
904
+ including the special arguments for instance variables and hooks.
816
905
 
817
906
  As the example illustrates, ~.select~ transmits any group boundaries in its
818
907
  input table to the result table.
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'fat_table/version'
@@ -74,8 +75,8 @@ org-mode buffer as an org-table, ready for processing by other code blocks.
74
75
  spec.add_runtime_dependency 'fat_core', '~> 4.0', '>= 4.1'
75
76
  spec.add_runtime_dependency 'activesupport'
76
77
  spec.add_runtime_dependency 'rainbow'
77
- spec.add_runtime_dependency 'dbi'
78
- spec.add_runtime_dependency 'dbd-pg'
79
- spec.add_runtime_dependency 'dbd-mysql'
80
- spec.add_runtime_dependency 'dbd-sqlite3'
78
+ spec.add_runtime_dependency 'sequel'
79
+ spec.add_runtime_dependency 'pg'
80
+ spec.add_runtime_dependency 'sqlite3'
81
+ spec.add_runtime_dependency 'mysql2'
81
82
  end
@@ -10,12 +10,13 @@ module FatTable
10
10
  require 'fat_core/hash'
11
11
  require 'fat_core/numeric'
12
12
  require 'csv'
13
- require 'dbi'
13
+ require 'sequel'
14
14
  require 'active_support'
15
15
  require 'active_support/core_ext'
16
16
  require 'active_support/number_helper'
17
17
 
18
18
  require 'fat_table/version'
19
+ require 'fat_table/patches'
19
20
  require 'fat_table/evaluator'
20
21
  require 'fat_table/column'
21
22
  require 'fat_table/table'
@@ -473,6 +473,9 @@ module FatTable
473
473
  end
474
474
  end
475
475
 
476
+ IS0_DATE_RE = %r{\b(\d\d\d\d)[-/](\d\d?)[-/](\d\d?)\s*(T\d\d:\d\d:\d\d(\+\d\d:\d\d)?)?\b}
477
+ AMR_DATE_RE = %r{\b(\d\d?)[-/](\d\d?)[-/](\d\d\d\d)\s*(T\d\d:\d\d:\d\d(\+\d\d:\d\d)?)?\b}
478
+
476
479
  # Convert the val to a DateTime if it is either a DateTime, a Date, or a
477
480
  # String that can be parsed as a DateTime, otherwise return nil. It only
478
481
  # recognizes strings that contain a something like '2016-01-14' or
@@ -485,8 +488,13 @@ module FatTable
485
488
  begin
486
489
  val = val.to_s.clean
487
490
  return nil if val.blank?
488
- return nil unless val =~ %r{\b\d\d\d\d[-/]\d\d?[-/]\d\d?\b}
489
- val = DateTime.parse(val.to_s.clean)
491
+ if val =~ IS0_DATE_RE
492
+ val = DateTime.parse(val)
493
+ elsif val =~ AMR_DATE_RE
494
+ val = DateTime.new($3.to_i, $1.to_i, $2.to_i)
495
+ else
496
+ return nil
497
+ end
490
498
  val = val.to_date if val.seconds_since_midnight.zero?
491
499
  val
492
500
  rescue ArgumentError
@@ -506,11 +514,11 @@ module FatTable
506
514
  val = val.to_s.clean.gsub(clean_re, '')
507
515
  return nil if val.blank?
508
516
  case val
509
- when /\A(\d+\.\d*)|(\d*\.\d+)\z/
517
+ when /(\A[-+]?\d+\.\d*\z)|(\A[-+]?\d*\.\d+\z)/
510
518
  BigDecimal.new(val.to_s.clean)
511
- when /\A[\d]+\z/
519
+ when /\A[-+]?[\d]+\z/
512
520
  val.to_i
513
- when %r{\A(\d+)\s*[:/]\s*(\d+)\z}
521
+ when %r{\A([-+]?\d+)\s*[:/]\s*([-+]?\d+)\z}
514
522
  Rational($1, $2)
515
523
  end
516
524
  end
@@ -1,18 +1,25 @@
1
1
  module FatTable
2
2
  class << self;
3
- # The +DBI+ database handle returned by the last successful call to
4
- # FatTable.set_db.
3
+ # The +Sequel+ database handle to use in calls to +FatTable.from_sql+.
5
4
  attr_accessor :handle
6
5
  end
7
6
 
8
- # This method must be called before calling FatTable.from_sql or
9
- # FatTable::Table.from_sql in order to specify the database to use. All of
10
- # the keyword parameters have a default except +database:+, which must contain
11
- # the name of the database to query.
7
+ # This method must be called before calling +FatTable.from_sql+ or
8
+ # +FatTable::Table.from_sql+ in order to specify the database to use.
9
+ #
10
+ # You can pass in a +Sequel+ connection with +db+, or have fat_table construct
11
+ # a uri from given components. In the latter case, all of the keyword
12
+ # parameters have a default except +database:+, which must contain the name of
13
+ # the database to query.
14
+ #
15
+ # +db+::
16
+ # Inject a Sequel connection constructed +Sequel.connect+ or one of
17
+ # Sequel's adapter-specific connection methods.
18
+ # http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html
12
19
  #
13
20
  # +driver+::
14
- # One of 'Pg' (for Postgresql), 'Mysql' (for Mysql), or 'SQLite3' (for
15
- # SQLite3) to specify the +DBI+ driver to use. You may have to install the
21
+ # One of 'pg' (for Postgresql), 'mysql' or 'mysql2' (for Mysql), or 'sqlite' (for
22
+ # SQLite3) to specify the +Sequel+ driver to use. You may have to install the
16
23
  # driver to make this work. By default use 'Pg'.
17
24
  #
18
25
  # +database+::
@@ -20,11 +27,11 @@ module FatTable
20
27
  #
21
28
  # +user+::
22
29
  # The user name to use for accessing the database. It defaults to nil,
23
- # which may be interpreted as a default user by the DBI driver being used.
30
+ # which may be interpreted as a default user by the Sequel driver being used.
24
31
  #
25
32
  # +password+::
26
33
  # The password to use for accessing the database. It defaults to nil,
27
- # which may be interpreted as a default password by the DBI driver being used.
34
+ # which may be interpreted as a default password by the Sequel driver being used.
28
35
  #
29
36
  # +host+::
30
37
  # The name of the host on which to look for the database connection,
@@ -38,44 +45,59 @@ module FatTable
38
45
  # The socket to use to access the database if the host is 'localhost'.
39
46
  # Defaults to the standard socket for the Pg driver, '/tmp/.s.PGSQL.5432'.
40
47
  #
41
- # If successful the database handle for DBI is return.
48
+ # If successful the database handle for Sequel is return.
42
49
  # Once called successfully, this establishes the database handle to use for
43
50
  # all subsequent calls to FatTable.from_sql or FatTable::Table.from_sql. You
44
51
  # can then access the handle if needed with FatTable.db.
45
- def self.set_db(driver: 'Pg',
52
+ def self.set_db(db: nil,
53
+ driver: 'postgres',
46
54
  database:,
47
- user: nil,
55
+ user: ENV['LOGNAME'],
48
56
  password: nil,
49
57
  host: 'localhost',
50
58
  port: '5432',
51
59
  socket: '/tmp/.s.PGSQL.5432')
52
- raise UserError, 'must supply database name to set_db' unless database
60
+ if db
61
+ self.handle = db
62
+ else
63
+ raise UserError, 'must supply database name to set_db' unless database
53
64
 
54
- valid_drivers = ['Pg', 'Mysql', 'SQLite3']
55
- unless valid_drivers.include?(driver)
56
- raise UserError, "set_db driver must be one of #{valid_drivers.join(' or ')}"
57
- end
58
- # In case port is given as an integer
59
- port = port.to_s if port
65
+ valid_drivers = ['postgres', 'mysql', 'mysql2', 'sqlite']
66
+ unless valid_drivers.include?(driver)
67
+ raise UserError, "'#{driver}' driver must be one of #{valid_drivers.join(' or ')}"
68
+ end
69
+ if database.blank?
70
+ raise UserError, "must supply database parameter to set_db"
71
+ end
60
72
 
61
- # Set the dsn for DBI
62
- dsn =
63
- if host == 'localhost'
64
- "DBI:Pg:database=#{database};host=#{host};socket=#{socket}"
73
+ if driver == 'sqlite'
74
+ dsn = "sqlite://#{database}"
65
75
  else
66
- "DBI:Pg:database=#{database};host=#{host};port=#{port}"
76
+ pw_part = password ? ":#{password}" : ''
77
+ hst_part = host ? "@#{host}" : ''
78
+ prt_part = port ? ":#{port}" : ''
79
+ dsn = "#{driver}:://#{user}#{pw_part}#{hst_part}#{prt_part}/#{database}"
80
+ end
81
+
82
+ # Set the dsn for Sequel
83
+ begin
84
+ # DB = Sequel.connect(dsn)
85
+ self.handle = Sequel.connect(dsn)
86
+ rescue => ex
87
+ raise TransientError, "#{dsn}: #{ex}"
67
88
  end
68
- begin
69
- self.handle = ::DBI.connect(dsn, user, password)
70
- rescue DBI::OperationalError => ex
71
- raise TransientError, "#{dsn}: #{ex}"
72
89
  end
73
90
  handle
74
91
  end
75
92
 
76
- # Return the +DBI+ database handle as returned by the last call to
77
- # FatTable.set_db.
93
+ # Return the +Sequel+ database handle.
78
94
  def self.db
79
95
  handle
80
96
  end
97
+
98
+ # Directly set the db handle to a Sequel connection formed without
99
+ # FatTable.set_db.
100
+ def self.db=(db)
101
+ self.handle = db
102
+ end
81
103
  end
@@ -17,24 +17,37 @@ module FatTable
17
17
  # subsequent calls to Evaluator.evaluate. The strings +before+ and +after+
18
18
  # are string expressions that will be evaluated before and after each
19
19
  # subsequent call to Evaluator.evaluate.
20
- def initialize(vars: {}, before: nil, after: nil)
20
+ def initialize(ivars: {}, before: nil, after: nil)
21
21
  @before = before
22
22
  @after = after
23
- set_instance_vars(vars)
23
+ set_instance_vars(ivars)
24
+ end
25
+
26
+ # Run the before hook after setting the @group to grp, passed in as a
27
+ # parameter. This is run before any column has been evaluated with the
28
+ # evaluate method.
29
+ def eval_before_hook(vars)
30
+ bdg = binding
31
+ set_local_vars(vars, bdg)
32
+ @group = vars[:__group]
33
+ eval(@before, bdg) if @before
34
+ end
35
+
36
+ # Run the after hook. This is run after each column in the row has been
37
+ # evaluated with the evaluate method.
38
+ def eval_after_hook(vars)
39
+ bdg = binding
40
+ set_local_vars(vars, bdg)
41
+ eval(@after, bdg) if @after
24
42
  end
25
43
 
26
44
  # Return the result of evaluating +expr+ as a Ruby expression in which the
27
45
  # instance variables set in Evaluator.new and any local variables set in the
28
- # Hash parameter +vars+ are available to the expression. Call any +before+
29
- # hook defined in Evaluator.new before evaluating +expr+ and any +after+
30
- # hook defined in Evaluator.new after evaluating +expr+.
46
+ # Hash parameter +vars+ are available to the expression.
31
47
  def evaluate(expr = '', vars: {})
32
48
  bdg = binding
33
49
  set_local_vars(vars, bdg)
34
- eval(@before, bdg) if @before
35
- result = eval(expr, bdg)
36
- eval(@after, bdg) if @after
37
- result
50
+ eval(expr, bdg)
38
51
  end
39
52
 
40
53
  private
@@ -60,7 +60,8 @@ module FatTable
60
60
  end
61
61
 
62
62
  def strip_ansi(str)
63
- str&.gsub(/\e\[[0-9;]+m/, '')
63
+ return '' unless str
64
+ str.gsub(/\e\[[0-9;]+m/, '')
64
65
  end
65
66
 
66
67
  # Add ANSI codes to string to implement the given decorations
@@ -0,0 +1,16 @@
1
+ unless {a: 1}.respond_to?(:fetch_values)
2
+ class Hash
3
+ def fetch_values(*keys)
4
+ result = []
5
+ keys.each do |k|
6
+ result <<
7
+ if block_given?
8
+ yield(self[k])
9
+ else
10
+ self[k]
11
+ end
12
+ end
13
+ result
14
+ end
15
+ end
16
+ end
@@ -153,9 +153,7 @@ module FatTable
153
153
  def self.from_sql(query)
154
154
  raise UserError, 'FatTable.db must be set with FatTable.set_db' if FatTable.db.nil?
155
155
  result = Table.new
156
- sth = FatTable.db.prepare(query)
157
- sth.execute
158
- sth.fetch_hash do |h|
156
+ FatTable.db[query].each do |h|
159
157
  result << h
160
158
  end
161
159
  result
@@ -595,7 +593,7 @@ module FatTable
595
593
  end
596
594
 
597
595
  # :category: Operators
598
-
596
+ #
599
597
  # Return a Table having the selected column expressions. Each expression can
600
598
  # be either a
601
599
  #
@@ -613,6 +611,12 @@ module FatTable
613
611
  # access the instance variable @row, as the row number of the row being
614
612
  # evaluated, and @group, as the group number of the row being evaluated.
615
613
  #
614
+ # 4. a hash in +new_cols+ with one of the special keys, +ivars: {literal
615
+ # hash}+, +before_hook: 'ruby-code'+, or +after_hook: 'ruby-code'+ for
616
+ # defining custom instance variables to be used during evaluation of
617
+ # parameters described in point 3 and hooks of ruby code snippets to be
618
+ # evaluated before and after processing each row.
619
+ #
616
620
  # The bare symbol arguments +cols+ (1) must precede any hash arguments
617
621
  # +new_cols+ (2 or 3). Each expression results in a column in the resulting
618
622
  # Table in the order given in the argument list. The expressions are
@@ -623,12 +627,84 @@ module FatTable
623
627
  # tab.select(:ref, :date, shares: :quantity) => rename :shares->:quantity
624
628
  # tab.select(:ref, :date, :shares, cost: 'price * shares') => new column
625
629
  # tab.select(:ref, :date, :shares, seq: '@row') => add sequential nums
630
+ #
631
+ # The instance variables and hooks mentioned in point 4 above allow you to
632
+ # keep track of things that cross row boundaries, such as running sums or
633
+ # the values of columns before or after construction of the new row. You can
634
+ # define instance variables other than the default @row and @group variables
635
+ # to be available when evaluating normal string expressions for constructing
636
+ # a new row.
637
+ #
638
+ # You define custom instance variables by passing a Hash to the ivars
639
+ # parameter. The names of the instance variables will be the keys and their
640
+ # initial values will be the values. For example, you can keep track of a
641
+ # running sum of the cost of shares and the number of shares in the prior
642
+ # row by adding two custom instance variables and the appropriate hooks:
643
+ #
644
+ # tab.select(:ref, :date, :shares, :price,
645
+ # cost: 'shares * price', cumulative_cost: '@total_cost'
646
+ # ivars: { total_cost: 0, prior_shares: 0},
647
+ # before_hook: '@total_cost += shares * price,
648
+ # after_hook: '@prior_shares = shares')
649
+ #
650
+ # Notice that in the +ivars:+ parameter, the '@' is not prefixed to the name
651
+ # since it is a symbol, but must be prefixed when the instance variable is
652
+ # referenced in an expression, otherwise it would be interpreted as a column
653
+ # name. You could include the '@' if you use a string as a key, e.g., +{
654
+ # '@total_cost' => 0 }+ The ivars values are evaluated once, before the
655
+ # first row is processed with the select statement.
656
+ #
657
+ # For each row, the +before_hook+ is evaluated, then the +new_cols+
658
+ # expressions for setting the new value of columns, then the +after_hook+ is
659
+ # evaluated.
660
+ #
661
+ # In the before_hook, the values of all columns are available as local
662
+ # variables as they were before processing the row. The values of all
663
+ # instance variables are available as well with the values they had after
664
+ # processing the prior row of the table.
665
+ #
666
+ # In the string expressions for new columns, all the instance variables are
667
+ # available with the values they have after the before_hook is evaluated.
668
+ # You could also modify instance variables in the new_cols expression, but
669
+ # remember, they are evaluated once for each new column expression. Also,
670
+ # the new column is assigned the value of the entire expression, so you must
671
+ # ensure that the last expression is the one you want assigned to the new
672
+ # column. You might want to use a semicolon: +cost: '@total_cost += shares *
673
+ # price; shares * price'
674
+ #
675
+ # In the after_hook, the new, updated values of all columns, old and new are
676
+ # available as local variables, and the instance variables are available
677
+ # with the values they had after executing the before_hook.
626
678
  def select(*cols, **new_cols)
679
+ # Set up the Evaluator
680
+ ivars = { row: 0, group: 0 }
681
+ if new_cols.key?(:ivars)
682
+ ivars = ivars.merge(new_cols[:ivars])
683
+ new_cols.delete(:ivars)
684
+ end
685
+ before_hook = '@row += 1'
686
+ if new_cols.key?(:before_hook)
687
+ before_hook += "; #{new_cols[:before_hook]}"
688
+ new_cols.delete(:before_hook)
689
+ end
690
+ after_hook = nil
691
+ if new_cols.key?(:after_hook)
692
+ after_hook = new_cols[:after_hook].to_s
693
+ new_cols.delete(:after_hook)
694
+ end
695
+ ev = Evaluator.new(ivars: ivars,
696
+ before: before_hook,
697
+ after: after_hook)
698
+ # Compute the new Table from this Table
627
699
  result = Table.new
628
700
  normalize_boundaries
629
- ev = Evaluator.new(vars: { row: 0, group: 1 },
630
- before: '@row = __row; @group = __group')
631
701
  rows.each_with_index do |old_row, old_k|
702
+ # Set the group number in the before hook and run the hook with the
703
+ # local variables set to the row before the new row is evaluated.
704
+ grp = row_index_to_group_index(old_k)
705
+ vars = old_row.merge(__group: grp)
706
+ ev.eval_before_hook(vars)
707
+ # Compute the new row.
632
708
  new_row = {}
633
709
  cols.each do |k|
634
710
  h = k.as_sym
@@ -638,8 +714,6 @@ module FatTable
638
714
  new_cols.each_pair do |key, val|
639
715
  key = key.as_sym
640
716
  vars = old_row.merge(new_row)
641
- vars[:__row] = old_k + 1
642
- vars[:__group] = row_index_to_group_index(old_k)
643
717
  case val
644
718
  when Symbol
645
719
  raise UserError, "Column '#{val}' in select does not exist" unless vars.keys.include?(val)
@@ -647,9 +721,13 @@ module FatTable
647
721
  when String
648
722
  new_row[key] = ev.evaluate(val, vars: vars)
649
723
  else
650
- raise UserError, 'Hash parameters to select must be a symbol or string'
724
+ raise UserError, "Hash parameter '#{key}' to select must be a symbol or string"
651
725
  end
652
726
  end
727
+ # Set the group number and run the hook with the local variables set to
728
+ # the row after the new row is evaluated.
729
+ vars = new_row.merge(__group: grp)
730
+ ev.eval_after_hook(vars)
653
731
  result << new_row
654
732
  end
655
733
  result.boundaries = boundaries
@@ -675,13 +753,14 @@ module FatTable
675
753
  col = Column.new(header: h)
676
754
  result.add_column(col)
677
755
  end
678
- ev = Evaluator.new(vars: { row: 0 },
679
- before: '@row = __row; @group = __group')
756
+ ev = Evaluator.new(ivars: { row: 0, group: 0 },
757
+ before: '@row += 1')
680
758
  rows.each_with_index do |row, k|
681
- vars = row.dup
682
- vars[:__row] = k + 1
683
- vars[:__group] = row_index_to_group_index(k)
759
+ grp = row_index_to_group_index(k)
760
+ vars = row.merge(__group: grp)
761
+ ev.eval_before_hook(vars)
684
762
  result << row if ev.evaluate(expr, vars: vars)
763
+ ev.eval_after_hook(vars)
685
764
  end
686
765
  result.normalize_boundaries
687
766
  result
@@ -1,4 +1,4 @@
1
1
  module FatTable
2
2
  # The current version of FatTable
3
- VERSION = '0.2.4'.freeze
3
+ VERSION = '0.2.6'.freeze
4
4
  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.2.4
4
+ version: 0.2.6
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: 2017-05-23 00:00:00.000000000 Z
11
+ date: 2017-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: simplecov
@@ -185,7 +185,7 @@ dependencies:
185
185
  - !ruby/object:Gem::Version
186
186
  version: '0'
187
187
  - !ruby/object:Gem::Dependency
188
- name: dbi
188
+ name: sequel
189
189
  requirement: !ruby/object:Gem::Requirement
190
190
  requirements:
191
191
  - - ">="
@@ -199,7 +199,7 @@ dependencies:
199
199
  - !ruby/object:Gem::Version
200
200
  version: '0'
201
201
  - !ruby/object:Gem::Dependency
202
- name: dbd-pg
202
+ name: pg
203
203
  requirement: !ruby/object:Gem::Requirement
204
204
  requirements:
205
205
  - - ">="
@@ -213,7 +213,7 @@ dependencies:
213
213
  - !ruby/object:Gem::Version
214
214
  version: '0'
215
215
  - !ruby/object:Gem::Dependency
216
- name: dbd-mysql
216
+ name: sqlite3
217
217
  requirement: !ruby/object:Gem::Requirement
218
218
  requirements:
219
219
  - - ">="
@@ -227,7 +227,7 @@ dependencies:
227
227
  - !ruby/object:Gem::Version
228
228
  version: '0'
229
229
  - !ruby/object:Gem::Dependency
230
- name: dbd-sqlite3
230
+ name: mysql2
231
231
  requirement: !ruby/object:Gem::Requirement
232
232
  requirements:
233
233
  - - ">="
@@ -303,6 +303,7 @@ files:
303
303
  - lib/fat_table/formatters/org_formatter.rb
304
304
  - lib/fat_table/formatters/term_formatter.rb
305
305
  - lib/fat_table/formatters/text_formatter.rb
306
+ - lib/fat_table/patches.rb
306
307
  - lib/fat_table/table.rb
307
308
  - lib/fat_table/version.rb
308
309
  homepage: https://github.com/ddoherty03/fat_table
@@ -326,7 +327,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
326
327
  version: '0'
327
328
  requirements: []
328
329
  rubyforge_project:
329
- rubygems_version: 2.5.2
330
+ rubygems_version: 2.6.13
330
331
  signing_key:
331
332
  specification_version: 4
332
333
  summary: Provides tools for working with tables as a data type.