csvrecord 0.4.0 → 0.4.1
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 +4 -4
- data/Manifest.txt +0 -1
- data/Rakefile +1 -0
- data/lib/csvrecord.rb +1 -1
- data/lib/csvrecord/base.rb +47 -278
- data/lib/csvrecord/version.rb +2 -2
- metadata +15 -2
- data/lib/csvrecord/builder.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f9b5900a09323251bb9cf7fef7211c5692a5ac3
|
4
|
+
data.tar.gz: db705e1ba91bcd30ac538947d1518521a92ce76a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34c7f0d23df73725a8f6616024cda30ab89938e94e1032727b8caee434263dc2551547c8f6599c6294f6fa9fe0d1a9255ccd957a2ef6dda277c6e7f044506bc2
|
7
|
+
data.tar.gz: 49a205a98277d5e7842f06a052c5306cb3ce475de9fe3fa056fef411f923a96bf2f254b70b07f70db771f09832630bd95b40b9675992bcb4c8b7a8a750ed7ba1
|
data/Manifest.txt
CHANGED
data/Rakefile
CHANGED
data/lib/csvrecord.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
###
|
4
4
|
# 3rd party gems
|
5
|
+
require 'record'
|
5
6
|
require 'csvreader'
|
6
7
|
|
7
8
|
|
@@ -9,7 +10,6 @@ require 'csvreader'
|
|
9
10
|
# our own code
|
10
11
|
require 'csvrecord/version' # let version always go first
|
11
12
|
require 'csvrecord/base'
|
12
|
-
require 'csvrecord/builder'
|
13
13
|
|
14
14
|
|
15
15
|
puts CsvRecord.banner # say hello
|
data/lib/csvrecord/base.rb
CHANGED
@@ -3,270 +3,10 @@
|
|
3
3
|
|
4
4
|
module CsvRecord
|
5
5
|
|
6
|
-
|
7
|
-
|
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
|
-
## zero-based position index (0,1,2,3,...)
|
15
|
-
## todo: use/rename to index/idx/pos - add an alias name - why?
|
16
|
-
attr_reader :num
|
17
|
-
|
18
|
-
def initialize( name, num, type )
|
19
|
-
## note: always symbol-ify (to_sym) name and type
|
20
|
-
|
21
|
-
## todo: add title or titles for header field/column title as is e.g. 'Team 1' etc.
|
22
|
-
## incl. numbers only or even an empty header title
|
23
|
-
@name = name.to_sym
|
24
|
-
@num = num
|
25
|
-
|
26
|
-
if type.is_a?( Class )
|
27
|
-
@type = type ## assign class to its own property - why? why not?
|
28
|
-
else
|
29
|
-
@type = Type.registry[type.to_sym]
|
30
|
-
if @type.nil?
|
31
|
-
puts "!!!! warn unknown type >#{type}< - no class mapping found; add missing type to CsvRecord::Type.registry[]"
|
32
|
-
## todo/fix: raise exception!!!!
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
|
-
def typecast( value ) ## cast (convert) from string value to type (e.g. float, integer, etc.)
|
39
|
-
## todo: add typecast to class itself (monkey patch String/Float etc. - why? why not)
|
40
|
-
## use __typecast or something?
|
41
|
-
## or use a new "wrapper" class Type::String or StringType - why? why not?
|
42
|
-
|
43
|
-
## convert string value to (field) type
|
44
|
-
if @type == String
|
45
|
-
value ## pass through as is
|
46
|
-
elsif @type == Float
|
47
|
-
## note: allow/check for nil values - why? why not?
|
48
|
-
float = (value.nil? || value.empty?) ? nil : value.to_f
|
49
|
-
puts "typecast >#{value}< to float number >#{float}<"
|
50
|
-
float
|
51
|
-
elsif @type == Integer
|
52
|
-
number = (value.nil? || value.empty?) ? nil : value.to_i(10) ## always use base10 for now (e.g. 010 => 10 etc.)
|
53
|
-
puts "typecast >#{value}< to integer number >#{number}<"
|
54
|
-
number
|
55
|
-
else
|
56
|
-
## todo/fix: raise exception about unknow type
|
57
|
-
pp @type
|
58
|
-
puts "!!!! unknown type >#{@type}< - don't know how to convert/typecast string value >#{value}<"
|
59
|
-
value
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end # class Field
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
class Type ## todo: use a module - it's just a namespace/module now - why? why not?
|
67
|
-
|
68
|
-
## e.g. use Type.registry[:string] = String etc.
|
69
|
-
## note use @@ - there is only one registry
|
70
|
-
def self.registry() @@registry ||={} end
|
71
|
-
|
72
|
-
## add built-in types:
|
73
|
-
registry[:string] = String
|
74
|
-
registry[:integer] = Integer ## todo/check: add :number alias for integer? why? why not?
|
75
|
-
registry[:float] = Float
|
76
|
-
## todo: add some more
|
77
|
-
end # class Type
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
def self.define( &block )
|
82
|
-
builder = Builder.new
|
83
|
-
if block.arity == 1
|
84
|
-
block.call( builder )
|
85
|
-
## e.g. allows "yield" dsl style e.g.
|
86
|
-
## CsvRecord.define do |rec|
|
87
|
-
## rec.string :team1
|
88
|
-
## rec.string :team2
|
89
|
-
## end
|
90
|
-
##
|
91
|
-
else
|
92
|
-
builder.instance_eval( &block )
|
93
|
-
## e.g. shorter "instance eval" dsl style e.g.
|
94
|
-
## CsvRecord.define do
|
95
|
-
## string :team1
|
96
|
-
## string :team2
|
97
|
-
## end
|
98
|
-
end
|
99
|
-
builder.to_record
|
100
|
-
end
|
101
|
-
|
102
|
-
|
103
|
-
###########################################
|
104
|
-
## "magic" lazy auto-build schema from headers versions
|
105
|
-
|
106
|
-
def self.build_class( headers ) ## check: find a better name - why? why not?
|
107
|
-
## (auto-)build record class from an array of headers
|
108
|
-
## add fields (all types will be string for now)
|
109
|
-
clazz = Class.new(Base)
|
110
|
-
headers.each do |header|
|
111
|
-
## downcase and remove all non-ascii chars etc.
|
112
|
-
## todo/fix: remove all non-ascii chars!!!
|
113
|
-
## todo: check if header starts with a number too!!
|
114
|
-
name = header.downcase.gsub( ' ', '_' )
|
115
|
-
name = name.to_sym ## symbol-ify
|
116
|
-
clazz.field( name )
|
117
|
-
end
|
118
|
-
clazz
|
119
|
-
end
|
120
|
-
|
121
|
-
def self.read( path, sep: Csv.config.sep )
|
122
|
-
headers = CsvReader.header( path, sep: sep )
|
123
|
-
|
124
|
-
clazz = build_class( headers )
|
125
|
-
clazz.read( path, sep: sep )
|
126
|
-
end
|
127
|
-
|
128
|
-
def self.foreach( path, sep: Csv.config.sep, &block )
|
129
|
-
headers = CsvReader.header( path, sep: sep )
|
130
|
-
|
131
|
-
clazz = build_class( headers )
|
132
|
-
clazz.foreach( path, sep: sep, &block )
|
133
|
-
end
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
class Base
|
139
|
-
|
140
|
-
def self.fields ## note: use class instance variable (@fields and NOT @@fields)!!!! (derived classes get its own copy!!!)
|
141
|
-
@fields ||= []
|
142
|
-
end
|
143
|
-
|
144
|
-
def self.field_names ## rename to header - why? why not?
|
145
|
-
## return header row, that is, all field names in an array
|
146
|
-
## todo: rename to field_names or just names - why? why not?
|
147
|
-
## note: names are (always) symbols!!!
|
148
|
-
fields.map {|field| field.name }
|
149
|
-
end
|
150
|
-
|
151
|
-
def self.field_types
|
152
|
-
## note: types are (always) classes!!!
|
153
|
-
fields.map {|field| field.type }
|
154
|
-
end
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
def self.field( name, type=:string )
|
159
|
-
num = fields.size ## auto-calc num(ber) / position index - always gets added at the end
|
160
|
-
field = Field.new( name, num, type )
|
161
|
-
fields << field
|
162
|
-
|
163
|
-
define_field( field ) ## auto-add getter,setter,parse/typecast
|
164
|
-
end
|
165
|
-
|
166
|
-
def self.define_field( field )
|
167
|
-
name = field.name ## note: always assumes a "cleaned-up" (symbol) name
|
168
|
-
num = field.num
|
169
|
-
|
170
|
-
define_method( name ) do
|
171
|
-
instance_variable_get( "@values" )[num]
|
172
|
-
end
|
173
|
-
|
174
|
-
define_method( "#{name}=" ) do |value|
|
175
|
-
instance_variable_get( "@values" )[num] = value
|
176
|
-
end
|
177
|
-
|
178
|
-
define_method( "parse_#{name}") do |value|
|
179
|
-
instance_variable_get( "@values" )[num] = field.typecast( value )
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
## column/columns aliases for field/fields
|
184
|
-
## use self << with alias_method - possible? works? why? why not?
|
185
|
-
def self.column( name, type=:string ) field( name, type ); end
|
186
|
-
def self.columns() fields; end
|
187
|
-
def self.column_names() field_names; end
|
188
|
-
def self.column_types() field_types; end
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
def self.build_hash( values ) ## find a better name - build_attrib? or something?
|
194
|
-
## convert to key-value (attribute) pairs
|
195
|
-
## puts "== build_hash:"
|
196
|
-
## pp values
|
197
|
-
|
198
|
-
## e.g. [[],[]] return zipped pairs in array as (attribute - name/value pair) hash
|
199
|
-
Hash[ field_names.zip(values) ]
|
200
|
-
end
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
def self.typecast( new_values )
|
205
|
-
values = []
|
206
|
-
|
207
|
-
##
|
208
|
-
## todo: check that new_values.size <= fields.size
|
209
|
-
##
|
210
|
-
## fields without values will get auto-filled with nils (or default field values?)
|
211
|
-
|
212
|
-
##
|
213
|
-
## use fields.zip( new_values ).map |field,value| ... instead - why? why not?
|
214
|
-
fields.each_with_index do |field,i|
|
215
|
-
value = new_values[i] ## note: returns nil if new_values.size < fields.size
|
216
|
-
values << field.typecast( value )
|
217
|
-
end
|
218
|
-
values
|
219
|
-
end
|
220
|
-
|
221
|
-
|
222
|
-
def parse( new_values ) ## use read (from array) or read_values or read_row - why? why not?
|
223
|
-
|
224
|
-
## todo: check if values overshadowing values attrib is ok (without warning?) - use just new_values (not values)
|
225
|
-
|
226
|
-
## todo/fix:
|
227
|
-
## check if values is a string
|
228
|
-
## use Csv.parse_line to convert to array
|
229
|
-
## otherwise assume array of (string) values
|
230
|
-
@values = self.class.typecast( new_values )
|
231
|
-
self ## return self for chaining
|
232
|
-
end
|
233
|
-
|
234
|
-
|
235
|
-
def values
|
236
|
-
## return array of all record values (typed e.g. float, integer, date, ..., that is,
|
237
|
-
## as-is and NOT auto-converted to string
|
238
|
-
## use to_csv or values for all string values)
|
239
|
-
@values
|
240
|
-
end
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
def [](key)
|
245
|
-
if key.is_a? Integer
|
246
|
-
@values[ key ]
|
247
|
-
elsif key.is_a? Symbol
|
248
|
-
## try attribute access
|
249
|
-
send( key )
|
250
|
-
else ## assume string
|
251
|
-
## downcase and symbol-ize
|
252
|
-
## remove spaces too -why? why not?
|
253
|
-
## todo/fix: add a lookup mapping for (string) titles (Team 1, etc.)
|
254
|
-
send( key.downcase.to_sym )
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
def to_h ## use to_hash - why? why not? - add attributes alias - why? why not?
|
261
|
-
self.class.build_hash( @values )
|
262
|
-
end
|
263
|
-
|
264
|
-
|
265
|
-
def to_csv ## use/rename/alias to to_row too - why? why not?
|
266
|
-
## todo/fix: check for date and use own date to string format!!!!
|
267
|
-
@values.map{ |value| value.to_s }
|
268
|
-
end
|
6
|
+
########################
|
7
|
+
# Base
|
269
8
|
|
9
|
+
class Base < Record::Base
|
270
10
|
|
271
11
|
def self.foreach( path, sep: Csv.config.sep, headers: true )
|
272
12
|
CsvReader.foreach( path, sep: sep, headers: headers ) do |row|
|
@@ -309,25 +49,54 @@ end
|
|
309
49
|
|
310
50
|
|
311
51
|
|
312
|
-
def
|
313
|
-
|
314
|
-
|
52
|
+
def to_csv ## use/rename/alias to to_row too - why? why not?
|
53
|
+
## todo/fix: check for date and use own date to string format!!!!
|
54
|
+
@values.map{ |value| value.to_s }
|
315
55
|
end
|
316
56
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
57
|
+
end # class Base
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
|
62
|
+
###########################################
|
63
|
+
## "magic" lazy auto-build schema from headers versions
|
64
|
+
|
65
|
+
def self.build_class( headers ) ## check: find a better name - why? why not?
|
66
|
+
## (auto-)build record class from an array of headers
|
67
|
+
## add fields (all types will be string for now)
|
68
|
+
clazz = Class.new( Base )
|
69
|
+
headers.each do |header|
|
70
|
+
## downcase and remove all non-ascii chars etc.
|
71
|
+
## todo/fix: remove all non-ascii chars!!!
|
72
|
+
## todo: check if header starts with a number too!!
|
73
|
+
name = header.downcase.gsub( ' ', '_' )
|
74
|
+
name = name.to_sym ## symbol-ify
|
75
|
+
clazz.field( name )
|
326
76
|
end
|
77
|
+
clazz
|
78
|
+
end
|
327
79
|
|
328
|
-
|
329
|
-
|
80
|
+
def self.read( path, sep: Csv.config.sep )
|
81
|
+
headers = CsvReader.header( path, sep: sep )
|
82
|
+
|
83
|
+
clazz = build_class( headers )
|
84
|
+
clazz.read( path, sep: sep )
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.foreach( path, sep: Csv.config.sep, &block )
|
88
|
+
headers = CsvReader.header( path, sep: sep )
|
89
|
+
|
90
|
+
clazz = build_class( headers )
|
91
|
+
clazz.foreach( path, sep: sep, &block )
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
#########
|
96
|
+
# alternative class (record) builder
|
97
|
+
|
98
|
+
def self.define( &block ) ## check: rename super_class to base - why? why not?
|
99
|
+
Record.define( Base, &block )
|
330
100
|
end
|
331
101
|
|
332
|
-
end # class Base
|
333
102
|
end # module CsvRecord
|
data/lib/csvrecord/version.rb
CHANGED
@@ -5,7 +5,7 @@ module CsvRecord
|
|
5
5
|
|
6
6
|
MAJOR = 0 ## todo: namespace inside version or something - why? why not??
|
7
7
|
MINOR = 4
|
8
|
-
PATCH =
|
8
|
+
PATCH = 1
|
9
9
|
VERSION = [MAJOR,MINOR,PATCH].join('.')
|
10
10
|
|
11
11
|
|
@@ -21,4 +21,4 @@ module CsvRecord
|
|
21
21
|
File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
|
22
22
|
end
|
23
23
|
|
24
|
-
end #
|
24
|
+
end # module CsvRecord
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csvrecord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
@@ -10,6 +10,20 @@ bindir: bin
|
|
10
10
|
cert_chain: []
|
11
11
|
date: 2018-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: record
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.1
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: csvreader
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,7 +84,6 @@ files:
|
|
70
84
|
- Rakefile
|
71
85
|
- lib/csvrecord.rb
|
72
86
|
- lib/csvrecord/base.rb
|
73
|
-
- lib/csvrecord/builder.rb
|
74
87
|
- lib/csvrecord/version.rb
|
75
88
|
- test/data/beer.csv
|
76
89
|
- test/data/beer11.csv
|
data/lib/csvrecord/builder.rb
DELETED
@@ -1,37 +0,0 @@
|
|
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.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 integer( name ) ## use number for alias for integer - why? why not???
|
23
|
-
puts " adding integer number field >#{name}<"
|
24
|
-
field( name, :integer )
|
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
|