csvrecord 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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