csvrecord 0.0.1 → 0.1.0

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: e37c870fe8f3bd7dcd5321254961dcf62e13a36e
4
- data.tar.gz: 2fa49f5f6ab2d7d5ddd293654a70512afd51f9e8
3
+ metadata.gz: 7bec20faf657130128c9a70953bc48160b3e1d99
4
+ data.tar.gz: 92900195cb8f791286e4d07920b4f5d1ebb4b13a
5
5
  SHA512:
6
- metadata.gz: df82e31279d1c49404ccc25b314c85602f0537a4c4c503e170322a7a65feb72e33017b157a95b81f976dac23ee20d928c423f2f3f2ce3d1aa59a8a3321e8e316
7
- data.tar.gz: c92aefd53c73c43f7b14465643616b6ed7c2b464017a6ebedd01e81657ee01f5bd45d139f3e412b4b40f010d501288dda3cbc591ad855ff926905fe2188c7d1a
6
+ metadata.gz: c02849458381b9dd23643f0a6f9358c04984ef69aa5e2b72c61a791b3da7340a431049dd08a0059a09c0c188c5c5fbafedeaf815fe062d9ab4c61f8f704ce75d
7
+ data.tar.gz: b79e7e35a4d016ac38cbc281d59a534df054d508d8ccc45130f1b351850622ed0991e59336b564c42aa1919c14da62de973b1e797b44d8ae26e428e80db2a106
@@ -4,4 +4,9 @@ Manifest.txt
4
4
  README.md
5
5
  Rakefile
6
6
  lib/csvrecord.rb
7
+ lib/csvrecord/base.rb
8
+ lib/csvrecord/builder.rb
7
9
  lib/csvrecord/version.rb
10
+ test/data/beer.csv
11
+ test/helper.rb
12
+ test/test_beer.rb
data/README.md CHANGED
@@ -11,7 +11,120 @@
11
11
 
12
12
  ## Usage
13
13
 
14
- To be done
14
+ [`beer.csv`](test/data/beer.csv):
15
+
16
+ ```
17
+ Brewery,City,Name,Abv
18
+ Andechser Klosterbrauerei,Andechs,Doppelbock Dunkel,7%
19
+ Augustiner Bräu München,München,Edelstoff,5.6%
20
+ Bayerische Staatsbrauerei Weihenstephan,Freising,Hefe Weissbier,5.4%
21
+ Brauerei Spezial,Bamberg,Rauchbier Märzen,5.1%
22
+ Hacker-Pschorr Bräu,München,Münchner Dunkel,5.0%
23
+ Staatliches Hofbräuhaus München,München,Hofbräu Oktoberfestbier,6.3%
24
+ ```
25
+
26
+ Step 1: Define a (typed) struct for the comma-separated values (csv) records. Example:
27
+
28
+ ```ruby
29
+ require 'csvrecord'
30
+
31
+ Beer = CsvRecord.define do
32
+ field :brewery ## note: default type is :string
33
+ field :city
34
+ field :name
35
+ field :abv, Float ## allows type specified as class (or use :float)
36
+ end
37
+ ```
38
+
39
+ or in "classic" style:
40
+
41
+ ```ruby
42
+ class Beer < CsvRecord::Base
43
+ field :brewery
44
+ field :city
45
+ field :name
46
+ field :abv, Float
47
+ end
48
+ ```
49
+
50
+
51
+ Step 2: Read in the comma-separated values (csv) datafile. Example:
52
+
53
+ ```ruby
54
+ beers = Beer.read( 'beer.csv' ).to_a
55
+
56
+ puts "#{beers.size} beers:"
57
+ pp beers
58
+ ```
59
+
60
+ pretty prints (pp):
61
+
62
+ ```
63
+ 6 beers:
64
+ [#<Beer:0x302c760
65
+ @abv = 7.0,
66
+ @brewery = "Andechser Klosterbrauerei",
67
+ @city = "Andechs",
68
+ @name = "Doppelbock Dunkel">,
69
+ #<Beer:0x3026fe8
70
+ @abv = 5.6,
71
+ @brewery = "Augustiner Br\u00E4u M\u00FCnchen",
72
+ @city = "M\u00FCnchen",
73
+ @name = "Edelstoff">,
74
+ #<Beer:0x30257a0
75
+ @abv = 5.4,
76
+ @brewery = "Bayerische Staatsbrauerei Weihenstephan",
77
+ @city = "Freising",
78
+ @name = "Hefe Weissbier">,
79
+ ...
80
+ ]
81
+ ```
82
+
83
+ Or loop over the records. Example:
84
+
85
+ ``` ruby
86
+ Beer.read( data ).each do |rec|
87
+ puts "#{rec.name} (#{rec.abv}%) by #{rec.brewery}, #{rec.city}"
88
+ end
89
+ ```
90
+
91
+ printing:
92
+
93
+ ```
94
+ Doppelbock Dunkel (7.0%) by Andechser Klosterbrauerei, Andechs
95
+ Edelstoff (5.6%) by Augustiner Bräu München, München
96
+ Hefe Weissbier (5.4%) by Bayerische Staatsbrauerei Weihenstephan, Freising
97
+ Rauchbier Märzen (5.1%) by Brauerei Spezial, Bamberg
98
+ Münchner Dunkel (5.0%) by Hacker-Pschorr Bräu, München
99
+ Hofbräu Oktoberfestbier (6.3%) by Staatliches Hofbräuhaus München, München
100
+ ```
101
+
102
+
103
+ Or create new records from scratch. Example:
104
+
105
+ ``` ruby
106
+ beer = Beer.new( brewery: 'Andechser Klosterbrauerei',
107
+ city: 'Andechs',
108
+ name: 'Doppelbock Dunkel' )
109
+ pp beer
110
+
111
+ # -or-
112
+
113
+ beer = Beer.new
114
+ beer.update( abv: 12.7 )
115
+ beer.update( brewery: 'Andechser Klosterbrauerei',
116
+ city: 'Andechs',
117
+ name: 'Doppelbock Dunkel' )
118
+
119
+ # -or-
120
+
121
+ beer.abv = 12.7
122
+ beer.name = 'Doppelbock Dunkel'
123
+ beer.brewery = 'Andechser Klosterbrauerei'
124
+ ```
125
+
126
+
127
+ And so on. That's it.
15
128
 
16
129
 
17
130
 
@@ -1,8 +1,15 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'csv'
4
+ require 'json'
5
+ require 'pp'
6
+
7
+
3
8
  ###
4
9
  # our own code
5
10
  require 'csvrecord/version' # let version always go first
11
+ require 'csvrecord/base'
12
+ require 'csvrecord/builder'
6
13
 
7
14
 
8
15
 
@@ -0,0 +1,161 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ module CsvRecord
5
+
6
+ ## note on naming:
7
+ ## use naming convention from awk and tabular data package/json schema for now
8
+ ## use - records (use rows for "raw" untyped (string) data rows )
9
+ ## - fields (NOT columns or attributes) -- might add an alias later - why? why not?
10
+
11
+ class Field ## ruby record class field
12
+ attr_reader :name, :type
13
+
14
+ def initialize( name, type )
15
+ ## todo: always symbol-ify (to_sym) name and type - why? why not?
16
+ @name = name.to_sym
17
+
18
+ if type.is_a?( Class )
19
+ @type = type ## assign class to its own property - why? why not?
20
+ else
21
+ @type = type.to_sym
22
+ end
23
+ end
24
+ end # class Field
25
+
26
+
27
+
28
+ def self.define( &block )
29
+ builder = Builder.new
30
+ builder.instance_eval(&block)
31
+ builder.to_record
32
+ end
33
+
34
+
35
+ class Base
36
+
37
+ def self.fields ## note: use class instance variable (@fields and NOT @@fields)!!!! (derived classes get its own copy!!!)
38
+ @fields ||= []
39
+ end
40
+
41
+
42
+ def self.field( name, type=:string )
43
+
44
+ fields << Field.new( name, type )
45
+
46
+ define_method( name ) do
47
+ instance_variable_get( "@#{name}" )
48
+ end
49
+
50
+ define_method( "#{name}=" ) do |value|
51
+ instance_variable_set( "@#{name}", value )
52
+ end
53
+
54
+ define_method( "parse_#{name}") do |value|
55
+ instance_variable_set( "@#{name}", self.class.typecast( value, type ) )
56
+ end
57
+ end
58
+ def self.add_field( name, type ) field( name, type ); end ## add alias for builder
59
+
60
+
61
+
62
+ def self.typecast( value, type )
63
+ ## convert string value to (field) type
64
+ if type == :string || type == 'string' || type == String
65
+ value ## pass through as is
66
+ elsif type == :float || type == 'float' || type == Float
67
+ ## note: allow/check for nil values
68
+ float = (value.nil? || value.empty?) ? nil : value.to_f
69
+ puts "typecast >#{value}< to float number >#{float}<"
70
+ float
71
+ elsif type == :number || type == 'number' || type == Integer
72
+ number = (value.nil? || value.empty?) ? nil : value.to_i(10) ## always use base10 for now (e.g. 010 => 10 etc.)
73
+ puts "typecast >#{value}< to integer number >#{number}<"
74
+ number
75
+ else
76
+ ## raise exception about unknow type
77
+ puts "!!!! unknown type >#{type}< - don't know how to convert/typecast string value >#{value}<"
78
+ value
79
+ end
80
+ end
81
+
82
+
83
+ def self.build_hash( values ) ## find a better name - build_attrib? or something?
84
+ ## convert to key-value (attribute) pairs
85
+ ## puts "== build_hash:"
86
+ ## pp values
87
+
88
+ h = {}
89
+ values.each_with_index do |value,i|
90
+ field = fields[i]
91
+ ## pp field
92
+ h[ field.name ] = value
93
+ end
94
+ h
95
+ end
96
+
97
+
98
+ def parse( values ) ## use read (from array) or read_values or read_row - why? why not?
99
+ h = self.build_hash( values )
100
+ update( h )
101
+ end
102
+
103
+
104
+
105
+ def self.parse( txt_or_rows ) ## note: returns an (lazy) enumarator
106
+ if txt_or_rows.is_a? String
107
+ txt = txt_or_rows
108
+ rows = CSV.parse( txt, headers: true )
109
+ else
110
+ ### todo/fix: use only self.create( array-like ) for array-like data - why? why not?
111
+ rows = txt_or_rows ## assume array-like records that responds to :each
112
+ end
113
+
114
+ pp rows
115
+
116
+ Enumerator.new do |yielder|
117
+ rows.each do |row|
118
+ ## check - check for to_h - why? why not? supported/built-into by CSV::Row??
119
+ ## if row.respond_to?( :to_h )
120
+ ## else
121
+ ## pp row.fields
122
+ ## pp row.to_hash
123
+ ## fix/todo: use row.to_hash
124
+ h = build_hash( row.fields )
125
+ ## pp h
126
+ rec = new( h )
127
+ ## end
128
+ yielder.yield( rec )
129
+ end
130
+ end
131
+ end
132
+
133
+
134
+ def self.read( path ) ## not returns an enumarator
135
+ txt = File.open( path, 'r:utf-8' ).read
136
+ parse( txt )
137
+ end
138
+
139
+
140
+
141
+ def initialize( **kwargs )
142
+ update( kwargs )
143
+ end
144
+
145
+ def update( **kwargs )
146
+ pp kwargs
147
+ kwargs.each do |name,value|
148
+ ## note: only convert/typecast string values
149
+ if value.is_a?( String )
150
+ send( "parse_#{name}", value ) ## note: use parse_<name> setter (for typecasting)
151
+ else ## use "regular" plain/classic attribute setter
152
+ send( "#{name}=", value )
153
+ end
154
+ end
155
+
156
+ ## todo: check if args.first is an array (init/update from array)
157
+ self ## return self for chaining
158
+ end
159
+
160
+ end # class Base
161
+ end # module CsvRecord
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ ## (record) builder mini language / domain-specific language (dsl)
4
+
5
+ module CsvRecord
6
+
7
+ class Builder # check: rename to RecordDefinition or RecordDsl or similar - why? why not?
8
+ def initialize
9
+ @clazz = Class.new(Base)
10
+ end
11
+
12
+ def field( name, type=:string ) ## note: type defaults to string
13
+ puts " adding field >#{name}< with type >#{type}<"
14
+ @clazz.add_field( name, type ) ## auto-add getter and setter
15
+ end
16
+
17
+ def string( name )
18
+ puts " adding string field >#{name}<"
19
+ field( name, 'string' )
20
+ end
21
+
22
+ def number( name ) ## use for alias for integer ???
23
+ puts " adding number field >#{name}<"
24
+ field( name, 'number' )
25
+ end
26
+
27
+ def float( name )
28
+ puts " adding float number field >#{name}<"
29
+ field( name, 'float' )
30
+ end
31
+
32
+
33
+ def to_record ## check: rename to just record or obj or finish or end something?
34
+ @clazz
35
+ end
36
+ end # class Builder
37
+ end # module CsvRecord
@@ -1,13 +1,14 @@
1
1
  # encoding: utf-8
2
2
 
3
- ## note: for now CsvRecord is a class!!! NOT a module - change - why? why not?
4
- class CsvRecord
3
+
4
+ module CsvRecord
5
5
 
6
6
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
7
- MINOR = 0
8
- PATCH = 1
7
+ MINOR = 1
8
+ PATCH = 0
9
9
  VERSION = [MAJOR,MINOR,PATCH].join('.')
10
10
 
11
+
11
12
  def self.version
12
13
  VERSION
13
14
  end
@@ -0,0 +1,7 @@
1
+ Brewery,City,Name,Abv
2
+ Andechser Klosterbrauerei,Andechs,Doppelbock Dunkel,7%
3
+ Augustiner Bräu München,München,Edelstoff,5.6%
4
+ Bayerische Staatsbrauerei Weihenstephan,Freising,Hefe Weissbier,5.4%
5
+ Brauerei Spezial,Bamberg,Rauchbier Märzen,5.1%
6
+ Hacker-Pschorr Bräu,München,Münchner Dunkel,5.0%
7
+ Staatliches Hofbräuhaus München,München,Hofbräu Oktoberfestbier,6.3%
@@ -0,0 +1,16 @@
1
+ ## $:.unshift(File.dirname(__FILE__))
2
+
3
+ ## minitest setup
4
+
5
+ require 'minitest/autorun'
6
+
7
+
8
+ ## our own code
9
+ require 'csvrecord'
10
+
11
+ ## add test_data_dir helper
12
+ module CsvRecord
13
+ def self.test_data_dir
14
+ "#{root}/test/data"
15
+ end
16
+ end
@@ -0,0 +1,160 @@
1
+ # encoding: utf-8
2
+
3
+ ###
4
+ # to run use
5
+ # ruby -I ./lib -I ./test test/test_beer.rb
6
+
7
+
8
+ require 'helper'
9
+
10
+ class TestBeer < MiniTest::Test
11
+
12
+ def test_version
13
+ pp CsvRecord::VERSION
14
+ pp CsvRecord.banner
15
+ pp CsvRecord.root
16
+
17
+ assert true ## assume ok if we get here
18
+ end
19
+
20
+
21
+ def test_class_style1
22
+ clazz1 = CsvRecord.define do
23
+ field :brewery, :string # fix: do NOT use 'Brewery' - name SHOULD be a valid variable name
24
+ field :city, :string
25
+ field :name ## default type is :string
26
+ field :abv, Float ## allow type specified as class
27
+ end
28
+ pp clazz1
29
+ pp clazz1.fields
30
+
31
+ assert true ## assume ok if we get here
32
+ end
33
+
34
+ Beer = CsvRecord.define do
35
+ string :brewery # fix: do NOT use 'Brewery' - name SHOULD be a valid variable name
36
+ string :city
37
+ string :name
38
+ float :abv
39
+ end
40
+
41
+ class BeerClassic < CsvRecord::Base
42
+ field :brewery
43
+ field :city
44
+ field :name
45
+ field :abv, Float
46
+ end
47
+
48
+
49
+ def test_class_style2
50
+ clazz2 = CsvRecord.define do
51
+ string :brewery # fix: do NOT use 'Brewery' - name SHOULD be a valid variable name
52
+ string :city
53
+ string :name
54
+ float :abv
55
+ end
56
+ pp clazz2
57
+ pp clazz2.class.name
58
+ pp clazz2.fields
59
+
60
+ txt = File.open( "#{CsvRecord.test_data_dir}/beer.csv", 'r:utf-8' ).read
61
+ data = CSV.parse( txt, headers: true )
62
+ pp data
63
+ pp data.to_a ## note: includes header (first row with field names)
64
+
65
+ puts "== parse( data ).to_a:"
66
+ pp clazz2.parse( data ).to_a
67
+ pp Beer.parse( data ).to_a
68
+
69
+ puts "== parse( data ).each:"
70
+ ## loop over records
71
+ clazz2.parse( data ).each do |rec|
72
+ puts "#{rec.name} (#{rec.abv}%) by #{rec.brewery}, #{rec.city}"
73
+ end
74
+
75
+ puts "== parse( txt ).to_a:"
76
+ pp Beer.parse( txt ).to_a
77
+
78
+ pp clazz2.class.name
79
+ pp clazz2.class.name
80
+ pp Beer.class.name
81
+
82
+ assert true ## assume ok if we get here
83
+ end
84
+
85
+
86
+ def test_read
87
+ puts "== read( data ).to_a:"
88
+ beers = Beer.read( "#{CsvRecord.test_data_dir}/beer.csv" ).to_a
89
+ puts "#{beers.size} beers:"
90
+ pp beers
91
+
92
+ pp Beer.fields
93
+
94
+ assert true ## assume ok if we get here
95
+ end
96
+
97
+ def test_classic
98
+ puts "== read( data ).to_a:"
99
+ beers = BeerClassic.read( "#{CsvRecord.test_data_dir}/beer.csv" ).to_a
100
+ puts "#{beers.size} beers:"
101
+ pp beers
102
+
103
+ pp BeerClassic.fields
104
+
105
+ beer = BeerClassic.new
106
+ pp beer
107
+ beer.update( abv: 12.7 )
108
+ beer.update( brewery: 'Andechser Klosterbrauerei',
109
+ city: 'Andechs',
110
+ name: 'Doppelbock Dunkel' )
111
+ pp beer
112
+
113
+ assert_equal 12.7, beer.abv
114
+ assert_equal 'Andechser Klosterbrauerei', beer.brewery
115
+ assert_equal 'Andechs', beer.city
116
+ assert_equal 'Doppelbock Dunkel', beer.name
117
+ end
118
+
119
+
120
+ def test_new
121
+ beer = Beer.new
122
+ pp beer
123
+ beer.update( abv: 12.7 )
124
+ beer.update( brewery: 'Andechser Klosterbrauerei',
125
+ city: 'Andechs',
126
+ name: 'Doppelbock Dunkel' )
127
+ pp beer
128
+
129
+ assert_equal 12.7, beer.abv
130
+ assert_equal 'Andechser Klosterbrauerei', beer.brewery
131
+ assert_equal 'Andechs', beer.city
132
+ assert_equal 'Doppelbock Dunkel', beer.name
133
+
134
+
135
+ pp beer.abv
136
+ pp beer.abv = 12.7
137
+ pp beer.abv
138
+ assert_equal 12.7, beer.abv
139
+
140
+ pp beer.parse_abv( '12.8%' ) ## (auto-)converts/typecasts string to specified type (e.g. float)
141
+ assert_equal 12.8, beer.abv
142
+
143
+ pp beer.name
144
+ pp beer.name = 'Doppelbock Dunkel'
145
+ pp beer.name
146
+ assert_equal 'Doppelbock Dunkel', beer.name
147
+
148
+
149
+ beer2 = Beer.new( brewery: 'Andechser Klosterbrauerei',
150
+ city: 'Andechs',
151
+ name: 'Doppelbock Dunkel' )
152
+ pp beer2
153
+
154
+ assert_equal nil, beer2.abv
155
+ assert_equal 'Andechser Klosterbrauerei', beer2.brewery
156
+ assert_equal 'Andechs', beer2.city
157
+ assert_equal 'Doppelbock Dunkel', beer2.name
158
+ end
159
+
160
+ end # class TestBeer
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csvrecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-11 00:00:00.000000000 Z
11
+ date: 2018-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rdoc
@@ -55,7 +55,12 @@ files:
55
55
  - README.md
56
56
  - Rakefile
57
57
  - lib/csvrecord.rb
58
+ - lib/csvrecord/base.rb
59
+ - lib/csvrecord/builder.rb
58
60
  - lib/csvrecord/version.rb
61
+ - test/data/beer.csv
62
+ - test/helper.rb
63
+ - test/test_beer.rb
59
64
  homepage: https://github.com/csv11/csvrecord
60
65
  licenses:
61
66
  - Public Domain