christiank-turntable 0.4.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/turntable.rb +109 -198
- metadata +2 -2
data/turntable.rb
CHANGED
@@ -1,221 +1,132 @@
|
|
1
1
|
#
|
2
|
-
# turntable v0.
|
3
|
-
# Christian Koch <
|
4
|
-
#
|
5
|
-
# This is free software. You are permitted to modify, implement, and otherwise
|
6
|
-
# use this source code freely (as in beer and freedom).
|
2
|
+
# turntable v0.5
|
3
|
+
# Christian Koch <ckoch002@student.ucr.edu>
|
7
4
|
#
|
8
5
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
self.save_to @filename
|
27
|
-
end
|
28
|
-
|
29
|
-
# Loads an external database file and returns the Turntable object.
|
30
|
-
def Turntable.load filename
|
31
|
-
raise "\"#{filename}\": file does not exist" unless File.exists?(filename)
|
32
|
-
|
33
|
-
@filename = File.expand_path(filename)
|
34
|
-
f = File.open @filename, 'r'
|
35
|
-
return Marshal.load(f)
|
36
|
-
f.close
|
37
|
-
end
|
38
|
-
|
39
|
-
# Returns the version number.
|
40
|
-
def Turntable.version
|
41
|
-
@@version
|
42
|
-
end
|
43
|
-
|
44
|
-
# Reader methods for the object's columns and filename.
|
45
|
-
attr_reader :columns
|
46
|
-
attr_reader :filename
|
47
|
-
|
48
|
-
# Pushes a new column. Fills the new fields with the empty string
|
49
|
-
# (because filling them with nil creates issues). Returns the the new table.
|
50
|
-
def add_column column_name
|
51
|
-
@columns.push column_name
|
52
|
-
@table.each { |row| row[column_name] = '' }
|
53
|
-
end
|
54
|
-
|
55
|
-
# Returns true or false, depending on whether the result is empty.
|
56
|
-
def contains? hash
|
57
|
-
!self.select(hash).empty?
|
6
|
+
require 'pstore'
|
7
|
+
require 'ostruct'
|
8
|
+
|
9
|
+
class Turntable < PStore
|
10
|
+
|
11
|
+
@@version = 'v0.5'
|
12
|
+
|
13
|
+
# Creates a new Turntable object and corresponding PStore.
|
14
|
+
def initialize file, *columns
|
15
|
+
raise ArgumentError, 'Turntable.new() requires a filename and at least one column' if columns.empty?
|
16
|
+
|
17
|
+
super(file)
|
18
|
+
@filename = File.expand_path(file)
|
19
|
+
@columns = columns
|
20
|
+
|
21
|
+
# A quick hack for loading databases
|
22
|
+
@columns = columns[0] if columns.length == 1
|
58
23
|
end
|
59
|
-
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
# Return the deleted row here:
|
71
|
-
return before_table - what_wasnt_deleted
|
24
|
+
|
25
|
+
# Loads an existing database if it exists, complains otherwise.
|
26
|
+
# Tries to reconstruct what @columns should be by looking at the first row.
|
27
|
+
def Turntable.load file
|
28
|
+
raise "#{file}: file cannot be found" unless File.exists?(file)
|
29
|
+
|
30
|
+
@filename = File.expand_path(file)
|
31
|
+
db = Turntable.new @filename, :foo
|
32
|
+
@columns = db.transaction { db[0].columns }
|
33
|
+
|
34
|
+
return Turntable.new(@filename, @columns)
|
72
35
|
end
|
73
|
-
|
74
|
-
#
|
75
|
-
|
76
|
-
|
77
|
-
|
36
|
+
|
37
|
+
# Reader methods for this database's metadata.
|
38
|
+
attr_reader :filename, :columns
|
39
|
+
def version; @@version; end
|
40
|
+
|
41
|
+
# This hack should eliminate the need for transactions
|
42
|
+
#def in_transaction; true; end
|
43
|
+
#def in_transaction_wr; true; end
|
44
|
+
|
45
|
+
# The next best thing is to make transactions more domain-specific...
|
46
|
+
alias :update :transaction
|
47
|
+
|
48
|
+
# Returns the entire database as an array.
|
49
|
+
def all
|
50
|
+
as_array = []
|
51
|
+
|
52
|
+
self.transaction do
|
53
|
+
self.roots.each { |root| as_array.push self[root] }
|
54
|
+
end
|
55
|
+
|
56
|
+
return as_array
|
78
57
|
end
|
79
|
-
|
80
|
-
|
81
|
-
#
|
82
|
-
# Columns named :id
|
58
|
+
alias :select_all :all
|
59
|
+
|
60
|
+
# Pushes self with a new row.
|
61
|
+
# Columns named :id are autoincrement fields.
|
83
62
|
def insert *args
|
84
|
-
|
85
|
-
raise "wrong number of arguments (#{args.length} for #{
|
86
|
-
end
|
87
|
-
|
88
|
-
row = {}
|
89
|
-
|
90
|
-
@columns.each do |column_name|
|
91
|
-
if column_name == :id
|
92
|
-
if @table.empty?
|
93
|
-
row[:id] = 0
|
94
|
-
else
|
95
|
-
row[:id] = @table.last[:id] + 1
|
96
|
-
end
|
97
|
-
|
98
|
-
args.shift
|
99
|
-
else
|
100
|
-
row[column_name] = args.shift
|
101
|
-
end
|
63
|
+
if args.length != @columns.length
|
64
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for #{columns.length})"
|
102
65
|
end
|
103
|
-
|
104
|
-
|
105
|
-
self.
|
66
|
+
|
67
|
+
row = Row.new
|
68
|
+
self.all.empty? ? (row.id = 0) : (row.id = self.all.last.id + 1)
|
69
|
+
row.columns = @columns
|
70
|
+
@columns.each { |column| eval "row.#{column} = args.shift" }
|
71
|
+
|
72
|
+
self.transaction { self[row.id] = row }
|
73
|
+
|
106
74
|
return row
|
107
75
|
end
|
108
|
-
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
76
|
+
|
77
|
+
# The select statement. Equivalent to self.all.select() if there's a block,
|
78
|
+
# otherwise just return the specified row. In a perfect world, we should be
|
79
|
+
# able to access individual rows with just db[x].
|
80
|
+
def select *id
|
81
|
+
if block_given?
|
82
|
+
result = []
|
83
|
+
self.all.each { |row| result.push(row) if yield(row) }
|
84
|
+
return result
|
85
|
+
else
|
86
|
+
raise ArgumentError, 'select requires a block or 1 id number' if id.length != 1
|
87
|
+
return self.all[ id[0] ]
|
88
|
+
end
|
119
89
|
end
|
120
|
-
|
90
|
+
|
91
|
+
# Prints a plain text representation of the table to STDOUT
|
121
92
|
def to_stdout
|
122
|
-
|
93
|
+
self.all.each do |row|
|
94
|
+
print '|'
|
95
|
+
print row.id
|
123
96
|
@columns.each do |column|
|
124
|
-
print
|
97
|
+
print '|'
|
98
|
+
print eval("row.#{column}")
|
125
99
|
end
|
126
|
-
puts
|
100
|
+
puts '|'
|
127
101
|
end
|
128
102
|
return nil
|
129
103
|
end
|
130
|
-
|
131
|
-
# Returns a subset of the table which satisfies given conditions.
|
132
|
-
# TODO: get compound conditions to work somehow
|
133
|
-
def select hash
|
134
|
-
result_set = []
|
135
|
-
|
136
|
-
hash.each do |column, value|
|
137
|
-
this_set = @table.collect do |row|
|
138
|
-
row = row if row[column] == value
|
139
|
-
end
|
140
|
-
|
141
|
-
result_set += this_set.compact
|
142
|
-
end
|
143
|
-
|
144
|
-
return result_set
|
145
|
-
end
|
146
|
-
|
147
|
-
# Returns the entire table, an array of hashes.
|
148
|
-
def select_all
|
149
|
-
@table
|
150
|
-
end
|
151
|
-
|
152
|
-
alias :all :select_all
|
153
|
-
|
154
|
-
# Replaces many columns at a time, but currently only on one row at a time.
|
155
|
-
def update hash
|
156
|
-
raise 'update() requires at least :id => some_value' unless hash.has_key?(:id)
|
157
|
-
|
158
|
-
id = hash[:id]
|
159
|
-
hash.delete_if { |column, value| column == :id }
|
160
|
-
|
161
|
-
hash.each do |column, value|
|
162
|
-
@table[id][column] = value
|
163
|
-
end
|
164
|
-
|
165
|
-
self.save_to @filename
|
166
|
-
return @table[id]
|
167
|
-
end
|
168
|
-
|
169
|
-
protected
|
170
|
-
# Marshals the database object to an external file.
|
171
|
-
def save_to filename
|
172
|
-
if File.exists? filename
|
173
|
-
f = File.open filename, 'w'
|
174
|
-
else
|
175
|
-
f = File.new filename, 'w'
|
176
|
-
end
|
177
|
-
|
178
|
-
Marshal.dump self, f
|
179
|
-
f.close
|
180
|
-
end
|
104
|
+
|
181
105
|
end
|
182
106
|
|
183
|
-
class
|
184
|
-
|
185
|
-
# Rearranges an array of hashes according to a specified column name.
|
186
|
-
# TODO: how to check for @columns, even though it's a Turntable variable?
|
187
|
-
def order_by column_name
|
188
|
-
|
189
|
-
# Convert all row-hashes into arrays
|
190
|
-
rearranged_table = self.collect { |row| row.to_a }
|
191
|
-
|
192
|
-
# Convert all column-name-symbols into strings,
|
193
|
-
# then switch the first element of each row the desired order_by column
|
194
|
-
# (because the first element is where sort() looks)
|
195
|
-
rearranged_table.each do |row|
|
196
|
-
row.each { |column| column[0] = column[0].to_s if (column[0].class == Symbol) }
|
197
|
-
row.switch 0, ( row.index(row.assoc(column_name.to_s)) )
|
198
|
-
end
|
199
|
-
|
200
|
-
# Sort everything properly, and convert it back to hashes and symbols
|
201
|
-
rearranged_table.sort.collect do |row|
|
202
|
-
this_row_hash = {}
|
203
|
-
this_column_hash = {}
|
204
|
-
|
205
|
-
row.each do |column|
|
206
|
-
column[0] = column[0].to_sym
|
207
|
-
this_column_hash[ column[0] ] = column[1]
|
208
|
-
end
|
107
|
+
class Row < OpenStruct
|
209
108
|
|
210
|
-
|
109
|
+
# Custom inspect() to make dealing with rows in IRB cleaner
|
110
|
+
def inspect
|
111
|
+
str = "#<#{self.class}"
|
112
|
+
Thread.current[InspectKey] ||= []
|
113
|
+
if Thread.current[InspectKey].include?(self) then
|
114
|
+
str << " ..."
|
115
|
+
else
|
116
|
+
for k,v in @table
|
117
|
+
Thread.current[InspectKey] << v
|
118
|
+
begin
|
119
|
+
str << " #{k}=#{v.inspect}," unless k == :columns
|
120
|
+
ensure
|
121
|
+
Thread.current[InspectKey].pop
|
122
|
+
end
|
123
|
+
end
|
211
124
|
end
|
125
|
+
str[-1, 1] = ""
|
126
|
+
str << ">"
|
212
127
|
end
|
213
|
-
|
214
|
-
# Exchanges two elements' positions within an array.
|
215
|
-
def switch index1, index2
|
216
|
-
old_array = self.dup
|
217
|
-
self[index1] = old_array[index2]
|
218
|
-
self[index2] = old_array[index1]
|
219
|
-
end
|
220
|
-
|
128
|
+
|
221
129
|
end
|
130
|
+
|
131
|
+
# Quick hack to shut up Object#id deprecation warnings
|
132
|
+
class Object; undef id; end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: christiank-turntable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: "0.5"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christian Koch
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-06-08 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|