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.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/README.md +32 -0
- data/cellar.gemspec +15 -0
- data/lib/cellar.rb +265 -0
- 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
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: []
|