cellar 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.
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: []