cellar 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/README.md +32 -0
  4. data/cellar.gemspec +15 -0
  5. data/lib/cellar.rb +265 -0
  6. metadata +46 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 549bf01d6e8d969ae15c0a3a683c562a372a6916e2b73a4d98b4b215471cbc6d
4
+ data.tar.gz: 0df9839f7a98c0659100e9c431168ccaa27dd4c18e978d9e74a3d1996a95bbc4
5
+ SHA512:
6
+ metadata.gz: 854e6884861eaa8d3b94cd82ae618e61b791fe64ec11d6aa1d97c1bd7938ff2f722607293428b47270866b376f9d2d23329891ac5252190cfe043aa546700362
7
+ data.tar.gz: a8ab03f030093ac6d20da412fb60480b558c23f0227b4145533bf3439dd9e1a5eb9483436025e1446f3e4e38e8bddd97de1b224310339f3b70d0c566e69064de
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # cellar
2
+ Ruby gem to deal with cells of data in rows and columns (CSV, spreadsheets, etc.)
3
+
4
+ ### Example
5
+
6
+ Sample code:
7
+
8
+ ```ruby
9
+ # read CSV
10
+ data = DATA.read
11
+ rows = data.split("\n").map {|line| line.split(",") }
12
+
13
+ # show output
14
+ info = Cellar.new(rows)
15
+ info.each do
16
+ p info[:id, 4, "name", "gym".."age", 6..4, :color, "CaNdY"]
17
+ end
18
+
19
+ __END__
20
+ id,name,age,school,gym,color,candy,pet
21
+ 1,joe,13,Rockville,Gold's,yellow,gum,bird
22
+ 2,sally,8,Melville,24 Hour Fitness,pink,skittles,pig
23
+ 3,curly,44,Vegas,Couch,purple,Snicker's,lizard
24
+ ```
25
+
26
+ Sample output:
27
+
28
+ ```text
29
+ ["1", "Gold's", "joe", "Gold's", "Rockville", "13", "gum", "yellow", "Gold's", "yellow", "gum"]
30
+ ["2", "24 Hour Fitness", "sally", "24 Hour Fitness", "Melville", "8", "skittles", "pink", "24 Hour Fitness", "pink", "skittles"]
31
+ ["3", "Couch", "curly", "Couch", "Vegas", "44", "Snicker's", "purple", "Couch", "purple", "Snicker's"]
32
+ ```
data/cellar.gemspec ADDED
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "cellar"
5
+ s.version = `grep -m 1 '^\s*VERSION' lib/cellar.rb | head -1 | cut -f 2 -d '"'`
6
+ s.author = "Steve Shreeve"
7
+ s.email = "steve.shreeve@gmail.com"
8
+ s.summary = "A " +
9
+ s.description = "Ruby gem to deal with cells of data in rows and columns"
10
+ s.homepage = "https://github.com/shreeve/cellar"
11
+ s.license = "MIT"
12
+ s.platform = Gem::Platform::RUBY
13
+ s.files = `git ls-files`.split("\n") - %w[.gitignore]
14
+ s.required_ruby_version = Gem::Requirement.new(">= 3.0") if s.respond_to? :required_ruby_version=
15
+ end
data/lib/cellar.rb ADDED
@@ -0,0 +1,265 @@
1
+ # ============================================================================
2
+ # cellar - Ruby gem to deal with cells of data in rows and columns
3
+ #
4
+ # Author: Steve Shreeve (steve.shreeve@gmail.com)
5
+ # Date: October 2, 2024
6
+ #
7
+ # TODO:
8
+ # • Should we failover to empty strings like this: (value || "")
9
+ # ============================================================================
10
+
11
+ class Object
12
+ def val?
13
+ !nil?
14
+ end unless defined? val?
15
+
16
+ def blank?
17
+ respond_to?(:empty?) or return !self
18
+ empty? or respond_to?(:strip) && strip.empty?
19
+ end unless defined? blank?
20
+
21
+ def if_blank?(val)
22
+ blank? ? val : self
23
+ end unless defined? if_blank?
24
+ end
25
+
26
+ class Cellar
27
+ VERSION="0.1.0"
28
+
29
+ attr_reader :fields
30
+ attr_reader :values
31
+ attr_accessor :strict
32
+
33
+ def initialize(obj=nil, header: true, strict: true)
34
+ @fields = []
35
+ @values = []
36
+ @finder = {}
37
+ @seeker = {}
38
+ @index = nil
39
+ @widest = 0
40
+
41
+ if obj.is_a?(Array)
42
+ if obj.first.is_a?(Array)
43
+ self.fields = obj.shift if header
44
+ @rows = obj unless obj.empty?
45
+ elsif !obj.empty?
46
+ header ? (self.fields = obj) : (@rows = obj)
47
+ end
48
+ end
49
+
50
+ @strict = strict.nil? ? !@fields.empty? : !!strict
51
+ end
52
+
53
+ def add_field(field)
54
+ field = field.to_s
55
+ index = @fields.size
56
+ @fields << field
57
+ finders = @finder.size
58
+ @finder[field.downcase.gsub(/\W/,'_')] ||= index
59
+ @finder.size == finders + 1 or warn "field clash for #{field.inspect}"
60
+ @finder[field] ||= index
61
+ @widest = field.length if field.length > @widest
62
+ index
63
+ end
64
+
65
+ def index(field)
66
+ case field
67
+ when String, Symbol
68
+ field = field.to_s
69
+ index = @finder[field] || @finder[field.downcase.gsub(/\W/,'_')]
70
+ raise "no field #{field.inspect}" if !index && @strict
71
+ index
72
+ when Integer
73
+ raise "no field at index #{field}" if field >= @fields.size && @strict
74
+ field < 0 ? field % @fields.size : field
75
+ when Range
76
+ from = field.begin
77
+ till = field.end
78
+ from = from.blank? ? 0 : index(from)
79
+ till = till.blank? ? -1 : index(till)
80
+ case from <=> till
81
+ when 1 then field.exclude_end? ? [*(till+1)..from].reverse : [*till..from].reverse
82
+ when 0 then from
83
+ when -1 then field.exclude_end? ? from...till : from..till
84
+ else "no fields match #{field.inspect}"
85
+ end
86
+ else
87
+ raise "unable to index fields by #{field.class.inspect} [#{field.inspect}]"
88
+ end
89
+ end
90
+
91
+ def clear
92
+ @values = []
93
+ self
94
+ end
95
+
96
+ def [](*fields)
97
+ case fields.size
98
+ when 0
99
+ []
100
+ when 1
101
+ index = index(fields.first)
102
+ value = @values[index] if index
103
+ else
104
+ fields.inject([]) do |values, field|
105
+ index = index(field)
106
+ value = case index
107
+ when nil then nil
108
+ when Array then @values.values_at(*index)
109
+ else @values[index]
110
+ end
111
+ if Array === value
112
+ values.concat(value)
113
+ else
114
+ values.push(value)
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ def []=(*fields)
121
+ values = Array(fields.pop).dup
122
+ fields = fields.map {|field| Array(index(field) || add_field(field))}.flatten
123
+
124
+ if fields.empty?
125
+ @values.replace(values)
126
+ elsif values.size > fields.size
127
+ raise "unable to assign #{values.size} values to #{fields.size} fields for values=#{values.inspect}"
128
+ else
129
+ fields.each_with_index do |field, pos|
130
+ @values[field] = values[pos]
131
+ end
132
+ end
133
+
134
+ @values
135
+ end
136
+
137
+ def fields=(*list)
138
+ @fields.clear
139
+ @finder.clear
140
+ @widest = 0
141
+
142
+ list.flatten.each {|field| add_field(field)}
143
+ end
144
+
145
+ def values=(*list)
146
+ @values = list.flatten
147
+ end
148
+
149
+ def field(pos)
150
+ @fields[pos]
151
+ end
152
+
153
+ def method_missing(field, *args)
154
+ field = field.to_s
155
+ equal = field.chomp!("=")
156
+ index = index(field)
157
+ if equal
158
+ index ||= add_field(field)
159
+ value = @values[index] = args.first
160
+ elsif index
161
+ raise "variable lookup ignores arguments" unless args.empty?
162
+ value = @values[index]
163
+ else
164
+ value = ""
165
+ end
166
+ value
167
+ end
168
+
169
+ # ==[ Row handling ]==
170
+
171
+ def rows=(rows)
172
+ rows or raise "no rows defined"
173
+ @rows = rows
174
+ row(0) # returns self
175
+ end
176
+
177
+ def row=(row)
178
+ @rows or raise "no rows defined"
179
+ row(row) # returns self
180
+ end
181
+
182
+ def rows
183
+ @rows
184
+ end
185
+
186
+ def row(row=nil)
187
+ @rows or raise "no rows defined"
188
+ @values = row ? @rows[@row = row] : []
189
+ self
190
+ end
191
+
192
+ def <<(data)
193
+ @rows ||= []
194
+ @row = row = @rows.size
195
+ @rows << @values = []
196
+ raise "unable to << #{data.class} objects" unless self.class === data.class
197
+ self[*data.fields] = data.values
198
+ if @index
199
+ block = @index if @index.is_a?(Proc)
200
+ field = @index unless block
201
+ index = index(field) or raise "unknown index #{field.inspect}" if field
202
+ if key = block ? block.call(self) : @values[index]
203
+ @seeker[key] and raise "duplicate index: #{key.inspect}"
204
+ @seeker[key] = row
205
+ end
206
+ end
207
+ self
208
+ end
209
+
210
+ def index!(field=nil, &block)
211
+ @rows ||= []
212
+ @index = field || block or raise "index needs a field or a block"
213
+ index = index(field) or raise "unknown index #{field.inspect}" if field && @rows.size > 0
214
+ @seeker.clear
215
+ @rows.each_with_index do |values, row|
216
+ if key = block ? yield(row(row)) : values[index]
217
+ @seeker[key] and raise "duplicate index: #{key.inspect}"
218
+ @seeker[key] = row
219
+ end
220
+ end
221
+ self
222
+ end
223
+
224
+ def seek!(seek)
225
+ @rows or raise "no rows defined"
226
+ @seeker.empty? and raise "not indexed"
227
+
228
+ if row = @seeker[seek]
229
+ row(row)
230
+ else
231
+ @values = []
232
+ @row = nil
233
+ end
234
+ end
235
+
236
+ def each
237
+ @rows or raise "no rows defined"
238
+ @rows.each_with_index {|values, row| yield(row(row)) }
239
+ end
240
+
241
+ def map
242
+ @rows or raise "no rows defined"
243
+ @rows.map.with_index {|values, row| yield(row(row)) }
244
+ end
245
+
246
+ def from_array(list)
247
+ clear
248
+ @values = list.map {|v| v.to_s.strip if v }
249
+ self
250
+ end
251
+
252
+ def from_hash(hash)
253
+ clear
254
+ hash.each {|k,v| self[k] = v.to_s if v }
255
+ self
256
+ end
257
+
258
+ def to_hash!
259
+ @fields.size.times.inject({}) do |h, i|
260
+ v = @values[i]
261
+ h[@fields[i].downcase.gsub(/\W/,'_')] = v if !v.blank?
262
+ h
263
+ end
264
+ end
265
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cellar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Steve Shreeve
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-10-03 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby gem to deal with cells of data in rows and columns
14
+ email: steve.shreeve@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - Gemfile
20
+ - README.md
21
+ - cellar.gemspec
22
+ - lib/cellar.rb
23
+ homepage: https://github.com/shreeve/cellar
24
+ licenses:
25
+ - MIT
26
+ metadata: {}
27
+ post_install_message:
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '3.0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubygems_version: 3.5.19
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: A Ruby gem to deal with cells of data in rows and columns
46
+ test_files: []