ohm-zset 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gems +15 -0
- data/.gitignore +2 -0
- data/.rvmrc +1 -0
- data/CHANGELOG +24 -0
- data/LICENSE +20 -0
- data/README.md +172 -0
- data/Rakefile +2 -0
- data/lib/ohm-zset.rb +406 -0
- data/lib/suppress-warnings.rb +16 -0
- data/test/helper.rb +4 -0
- data/test/unit/ohm-zset.rb +663 -0
- metadata +79 -0
data/.gems
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# .gems generated gem export file. Note that any env variable settings will be missing. Append these after using a ';' field separator
|
2
|
+
|
3
|
+
file-tail -v1.0.10
|
4
|
+
minitest -v3.2.0
|
5
|
+
nest -v1.1.1
|
6
|
+
ohm -v1.0.2
|
7
|
+
redis -v3.0.1
|
8
|
+
redis -v2.2.2
|
9
|
+
ruby2ruby -v1.3.1
|
10
|
+
ruby_parser -v2.3.1
|
11
|
+
scrivener -v0.0.3
|
12
|
+
sexp_processor -v3.2.0
|
13
|
+
sourcify -v0.5.0
|
14
|
+
tins -v0.4.3
|
15
|
+
uuidtools -v2.1.3
|
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create use 1.9.3@ohm-zset
|
data/CHANGELOG
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
v 0.2
|
2
|
+
|
3
|
+
- refactored code
|
4
|
+
|
5
|
+
v 0.1.4
|
6
|
+
|
7
|
+
- included option to intersect and union multiple sets with specified name
|
8
|
+
|
9
|
+
v 0.1.3
|
10
|
+
|
11
|
+
- included save_set, and load_set, and cleaned code
|
12
|
+
|
13
|
+
v 0.1.2
|
14
|
+
|
15
|
+
- included duplicate and ZSet.generate_uuid
|
16
|
+
|
17
|
+
v 0.1.1
|
18
|
+
|
19
|
+
- included UUID generation for keys of new ZSets for union and intersection
|
20
|
+
|
21
|
+
v 0.1.0
|
22
|
+
|
23
|
+
- stable
|
24
|
+
- Ohm basic support for Redis sorted sets
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Joshua Arvin Lat
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
# ohm-zset
|
2
|
+
|
3
|
+
Ohm Sorted Set (ZSET) support for Redis.
|
4
|
+
|
5
|
+
## Basic Usage
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class Big < Ohm::Model
|
9
|
+
set :smalls, :Small
|
10
|
+
zset :zsmalls, :Small, :size
|
11
|
+
end
|
12
|
+
|
13
|
+
class Small < Ohm::Model
|
14
|
+
attribute :name
|
15
|
+
attribute :size
|
16
|
+
end
|
17
|
+
|
18
|
+
b = Big.create
|
19
|
+
s1 = Small.create(name: 'S1', size: 5)
|
20
|
+
s2 = Small.create(name: 'S2', size: 4)
|
21
|
+
s3 = Small.create(name: 'S3', size: 3)
|
22
|
+
s4 = Small.create(name: 'S4', size: 2)
|
23
|
+
s5 = Small.create(name: 'S5', size: 1)
|
24
|
+
|
25
|
+
b.zsmalls.add_list(s1, s2, s3, s4, s5)
|
26
|
+
|
27
|
+
b.zsmalls.size
|
28
|
+
# => 5
|
29
|
+
|
30
|
+
b.zsmalls.to_a.map(&:name)
|
31
|
+
# => ['S5', 'S4', 'S3', 'S2', 'S1']
|
32
|
+
|
33
|
+
b.zsmalls.to_a.map(&:size)
|
34
|
+
# => ['1', '2', '3', '4', '5']
|
35
|
+
```
|
36
|
+
|
37
|
+
## Interacting with the elements of the set
|
38
|
+
**Ohm-ZSET** includes *get*, *rank*, *revrank*, *score*, *range*, *revrange*, *rangebyscore*, *revrangebyscore*
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
|
42
|
+
b.zsmalls.get(0).name
|
43
|
+
# => 'S5'
|
44
|
+
|
45
|
+
b.zsmalls.range(0, 3).to_a.map(&:name)
|
46
|
+
# => ['S5', 'S4', 'S3', 'S2']
|
47
|
+
|
48
|
+
b.zsmalls.revrange(0, 3).each do |small|
|
49
|
+
puts "#{small.name} - #{small.size}"
|
50
|
+
end
|
51
|
+
# => S1 - 5
|
52
|
+
# => S2 - 4
|
53
|
+
# => S3 - 3
|
54
|
+
# => S4 - 2
|
55
|
+
|
56
|
+
s6 = Small.create(name:'S6', size:3.5)
|
57
|
+
b.zsmalls.add(s6)
|
58
|
+
b.zsmalls.to_a.map(&:name)
|
59
|
+
# => ['S5', 'S4', 'S3', 'S6', 'S2', 'S1']
|
60
|
+
```
|
61
|
+
|
62
|
+
You can update the score of an element by using *update*. There is also a *count* function that returns the number of elements with scores inside a specified range.
|
63
|
+
|
64
|
+
## Deleting Elements
|
65
|
+
**Ohm-ZSET** includes *delete* for deleting a single element, *clear* for deleting all elements, and *remrangebyrank* and *remrangebyscore* for deleting selected elements.
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
b.zsmalls.delete(s6)
|
69
|
+
b.zsmalls.to_a.map(&:name)
|
70
|
+
# => ['S5', 'S4', 'S3', 'S2', 'S1']
|
71
|
+
|
72
|
+
b.zsmalls.include? s6
|
73
|
+
# => false
|
74
|
+
|
75
|
+
b.zsmalls.clear
|
76
|
+
b.zsmalls.to_a.map(&:name)
|
77
|
+
# => []
|
78
|
+
```
|
79
|
+
|
80
|
+
It also has *destroy!* to delete the key of the sorted set.
|
81
|
+
|
82
|
+
## Set Intersection and Union
|
83
|
+
Set intersection between sorted sets and sets are allowed. You can use *intersect*, *intersect_multiple*, *union*, and *union_multiple* between sets and sorted sets.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
b.smalls.add(s1)
|
87
|
+
b.smalls.add(s2)
|
88
|
+
b.smalls.add(s4)
|
89
|
+
|
90
|
+
# Intersect ['S5', 'S4', 'S3', 'S2', 'S1'] with ['S4', 'S2', 'S1']
|
91
|
+
b.zsmalls.intersect(b.smalls).to_a.map(&:name)
|
92
|
+
# => ['S4', 'S2', 'S1']
|
93
|
+
```
|
94
|
+
|
95
|
+
The sorted set allows union and intersection of multiple sets and sorted sets with weights.
|
96
|
+
Result of intersection and union is another ZSET.
|
97
|
+
|
98
|
+
It also allows intersection and union to a new set with the specified name.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
zunion = Ohm::ZSet.union_multiple('zunion', [zlittles, slittles, zlittles2, zlittles3])
|
102
|
+
|
103
|
+
zunion.key
|
104
|
+
# => 'zunion'
|
105
|
+
```
|
106
|
+
|
107
|
+
## Scoring Functions
|
108
|
+
**Ohm-ZSET** also supports custom scoring functions. The default scoring function is ZScore::Integer.
|
109
|
+
There are also available built-in scoring functions in the module.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class Bigger < Ohm::Model
|
113
|
+
include Ohm::ZScores
|
114
|
+
|
115
|
+
zset :zlittles, :Little, :score
|
116
|
+
|
117
|
+
# Custom scoring function
|
118
|
+
zset :zsmalls, :Small, :value, lambda{ |x| Integer(x) + 1 }
|
119
|
+
|
120
|
+
# Built-in scoring functions
|
121
|
+
zset :zlittles, :Little, :score, ZScore::Float
|
122
|
+
zset :zbools, :Bool, :is_valid, ZScore::Boolean
|
123
|
+
zset :zdts, :DT, :last_login, ZScore::DateTime
|
124
|
+
|
125
|
+
# Built-in string sorting functions
|
126
|
+
zset :zbools2, :Bool, :name, ZScore::String
|
127
|
+
zset :zbools3, :Bool, :name, ZScore::StringInsensitive
|
128
|
+
zset :zbools4, :Bool, :name, ZScore::StringInsensitiveHigh
|
129
|
+
end
|
130
|
+
|
131
|
+
class Bool < Ohm::Model
|
132
|
+
attribute :name
|
133
|
+
attribute :is_valid
|
134
|
+
end
|
135
|
+
|
136
|
+
class DT < Ohm::Model
|
137
|
+
attribute :name
|
138
|
+
attribute :last_login
|
139
|
+
end
|
140
|
+
|
141
|
+
class Small < Ohm::Model
|
142
|
+
attribute :name
|
143
|
+
attribute :value
|
144
|
+
end
|
145
|
+
|
146
|
+
class Little < Ohm::Model
|
147
|
+
attribute :name
|
148
|
+
attribute :score
|
149
|
+
end
|
150
|
+
|
151
|
+
```
|
152
|
+
|
153
|
+
Redis allows only numerical scores so DateTime and strings objects are first converted to numbers and then stored as scores.
|
154
|
+
|
155
|
+
**Note**: The string scoring algorithm is limited only to the first 9 characters because of the floating point accuracy limit.
|
156
|
+
You can use the *starts_with* function when dealing with string sorted sets. It returns all the elements of the set with a score field value that starts with the specified string.
|
157
|
+
|
158
|
+
## Saving and Loading ZSet instances by name / key
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
|
162
|
+
sorted_set = b.zlittles
|
163
|
+
sorted_set.save_set
|
164
|
+
|
165
|
+
sorted_set_2 = Ohm::ZSet.load_set(sorted_set.key)
|
166
|
+
sorted_set_2.add(Little.create(name: 'X1',score: 29))
|
167
|
+
|
168
|
+
# sorted_set and sorted_set_2 are pointing to same ZSet instance
|
169
|
+
```
|
170
|
+
|
171
|
+
## Copyright
|
172
|
+
Copyright (c) 2012 [Joshua Arvin Lat](http://www.joshualat.com). See LICENSE for more details.
|
data/Rakefile
ADDED
data/lib/ohm-zset.rb
ADDED
@@ -0,0 +1,406 @@
|
|
1
|
+
require 'ohm'
|
2
|
+
require 'time'
|
3
|
+
require 'uuidtools'
|
4
|
+
require 'sourcify'
|
5
|
+
|
6
|
+
# http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
|
7
|
+
class Object
|
8
|
+
def meta_def(name, &blk)
|
9
|
+
(class << self; self; end).instance_eval { define_method(name, &blk) }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Ohm
|
14
|
+
class Model
|
15
|
+
def self.zset(name, model, *score_field)
|
16
|
+
|
17
|
+
define_method name do
|
18
|
+
model = Utils.const(self.class, model)
|
19
|
+
Ohm::ZSet.new(key[name], model.key, model, score_field)
|
20
|
+
end
|
21
|
+
|
22
|
+
meta_def name do
|
23
|
+
model = Utils.const(self, model)
|
24
|
+
Ohm::ZSet.new(key[name], model.key, model, score_field)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Utils
|
31
|
+
class << self
|
32
|
+
def proc_to_string(function)
|
33
|
+
function.to_source
|
34
|
+
end
|
35
|
+
|
36
|
+
def string_to_proc(function)
|
37
|
+
eval function
|
38
|
+
end
|
39
|
+
|
40
|
+
def score_field_to_string(score_field)
|
41
|
+
string_list = []
|
42
|
+
|
43
|
+
lambda_function = score_field.each do |field|
|
44
|
+
break field if field.is_a? Proc
|
45
|
+
|
46
|
+
string_list.push(field.to_s)
|
47
|
+
|
48
|
+
break lambda{ |x| x.to_i } if field == score_field.last
|
49
|
+
end
|
50
|
+
|
51
|
+
string_list.push Ohm::Utils.proc_to_string(lambda_function)
|
52
|
+
string_list.join(":")
|
53
|
+
end
|
54
|
+
|
55
|
+
def string_to_score_field(score_field)
|
56
|
+
string_list = score_field.split(":")
|
57
|
+
return_list = []
|
58
|
+
|
59
|
+
string_list.each do |field|
|
60
|
+
if field.include? 'proc'
|
61
|
+
return_list.push Ohm::Utils.string_to_proc field
|
62
|
+
else
|
63
|
+
return_list.push field.to_sym
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return_list
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module ZScores
|
73
|
+
module ZScore
|
74
|
+
Integer = lambda{ |x| Integer(x) }
|
75
|
+
Float = lambda{ |x| Float(x) }
|
76
|
+
Boolean = lambda{ |x| Ohm::ZScores.boolean(x) }
|
77
|
+
DateTime = lambda{ |x| Ohm::ZScores.datetime(x) }
|
78
|
+
String = lambda{ |x| Ohm::ZScores.string(x) }
|
79
|
+
StringInsensitive = lambda{ |x| Ohm::ZScores.string_insensitive(x) }
|
80
|
+
StringInsensitiveHigh = lambda{ |x| Ohm::ZScores.string_insensitive_high(x) }
|
81
|
+
end
|
82
|
+
|
83
|
+
class << self
|
84
|
+
def boolean(val)
|
85
|
+
case val
|
86
|
+
when "true", "1", true
|
87
|
+
1
|
88
|
+
when "false", "0", false, nil
|
89
|
+
0
|
90
|
+
else
|
91
|
+
1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def datetime(val)
|
96
|
+
::DateTime.parse(val.to_s).to_time.to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
# scoring accurate until 9th character
|
100
|
+
def string(val)
|
101
|
+
total_score = 0
|
102
|
+
|
103
|
+
val.each_char.with_index do |c, i|
|
104
|
+
total_score += (c.ord-31) * ((126-32) ** (10 - i))
|
105
|
+
break if i == 9
|
106
|
+
end
|
107
|
+
|
108
|
+
total_score.to_f
|
109
|
+
end
|
110
|
+
|
111
|
+
# scoring accurate until 9th character
|
112
|
+
def string_insensitive(val)
|
113
|
+
total_score = 0
|
114
|
+
|
115
|
+
val.each_char.with_index do |c, i|
|
116
|
+
char_ord = c.ord-31
|
117
|
+
if ('a'..'z').include? c
|
118
|
+
char_ord -= 32
|
119
|
+
end
|
120
|
+
total_score += (char_ord) * ((126-32) ** (10 - i))
|
121
|
+
break if i == 9
|
122
|
+
end
|
123
|
+
|
124
|
+
total_score.to_f
|
125
|
+
end
|
126
|
+
|
127
|
+
# scoring accurate until 9th character
|
128
|
+
def string_insensitive_high(val)
|
129
|
+
total_score = 0
|
130
|
+
|
131
|
+
val.each_char.with_index do |c, i|
|
132
|
+
char_ord = c.ord-31
|
133
|
+
if ('a'..'z').include? c
|
134
|
+
char_ord -= 31.5
|
135
|
+
end
|
136
|
+
total_score += (char_ord) * ((126-32) ** (10 - i))
|
137
|
+
break if i == 9
|
138
|
+
end
|
139
|
+
|
140
|
+
total_score.to_f
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class ZSet < Struct.new(:key, :namespace, :model, :score_field)
|
146
|
+
include PipelinedFetch
|
147
|
+
include Enumerable
|
148
|
+
|
149
|
+
class << self
|
150
|
+
def intersect_multiple(new_key, sets, weights = [])
|
151
|
+
base_set = sets[0]
|
152
|
+
weights = [1.0] * sets.length if weights = []
|
153
|
+
|
154
|
+
new_set = Ohm::ZSet.new(new_key, base_set.model.key, base_set.model, base_set.score_field)
|
155
|
+
sets = sets.map(&:key)
|
156
|
+
|
157
|
+
Ohm.redis.zinterstore(new_key, sets, :weights => weights)
|
158
|
+
new_set
|
159
|
+
end
|
160
|
+
|
161
|
+
def load_set(name)
|
162
|
+
new_model, new_score_field = Ohm.redis.hmget("ZSet:" + name, "model", "score_field")
|
163
|
+
return nil if new_model == nil && new_score_field == nil
|
164
|
+
|
165
|
+
new_model = Ohm::Utils.const(self.class, new_model.to_sym)
|
166
|
+
new_score_field = Ohm::Utils.string_to_score_field new_score_field
|
167
|
+
return_instance = Ohm::ZSet.new(name, new_model.key, new_model, new_score_field)
|
168
|
+
end
|
169
|
+
|
170
|
+
def union_multiple(new_key, sets, weights = [])
|
171
|
+
base_set = sets[0]
|
172
|
+
weights = [1.0] * sets.length if weights = []
|
173
|
+
|
174
|
+
new_set = Ohm::ZSet.new(new_key, base_set.model.key, base_set.model, base_set.score_field)
|
175
|
+
sets = sets.map(&:key)
|
176
|
+
|
177
|
+
Ohm.redis.zunionstore(new_key, sets, :weights => weights)
|
178
|
+
|
179
|
+
new_set
|
180
|
+
end
|
181
|
+
|
182
|
+
def generate_uuid
|
183
|
+
"ZSet:" + UUIDTools::UUID.random_create.to_s
|
184
|
+
end
|
185
|
+
|
186
|
+
def random_instance(model, score_field)
|
187
|
+
self.new_instance(Ohm::ZSet.generate_uuid, model, score_field)
|
188
|
+
end
|
189
|
+
|
190
|
+
def new_instance(name, model, score_field)
|
191
|
+
self.new(name, model.key, model, score_field)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def size
|
196
|
+
db.zcard(key)
|
197
|
+
end
|
198
|
+
|
199
|
+
def count(a = "-inf", b = "+inf")
|
200
|
+
return size if a == "-inf" && b == "+inf"
|
201
|
+
|
202
|
+
db.zcount(key, a, b)
|
203
|
+
end
|
204
|
+
|
205
|
+
def ids(a = 0, b = -1)
|
206
|
+
execute { |key| db.zrange(key, a, b) }
|
207
|
+
end
|
208
|
+
|
209
|
+
def range(a = 0, b = -1)
|
210
|
+
fetch(ids(a, b))
|
211
|
+
end
|
212
|
+
|
213
|
+
def revrange(a = 0, b = -1)
|
214
|
+
fetch(execute { |key| db.zrevrange(key, a, b) })
|
215
|
+
end
|
216
|
+
|
217
|
+
# Fetch data from Redis
|
218
|
+
def to_a
|
219
|
+
fetch(ids)
|
220
|
+
end
|
221
|
+
|
222
|
+
def include?(model)
|
223
|
+
!rank(model).nil?
|
224
|
+
end
|
225
|
+
|
226
|
+
def get(i)
|
227
|
+
range(i, i)[0] rescue nil
|
228
|
+
end
|
229
|
+
|
230
|
+
def each
|
231
|
+
to_a.each { |element| yield element }
|
232
|
+
end
|
233
|
+
|
234
|
+
def empty?
|
235
|
+
size == 0
|
236
|
+
end
|
237
|
+
|
238
|
+
def add(model)
|
239
|
+
score_value = model
|
240
|
+
|
241
|
+
lambda_function = score_field.each do |field|
|
242
|
+
break field if field.is_a? Proc
|
243
|
+
|
244
|
+
score_value = model.send(field)
|
245
|
+
|
246
|
+
break lambda{ |x| x.to_i } if field == score_field.last
|
247
|
+
end
|
248
|
+
|
249
|
+
db.zadd(key, lambda_function.call(score_value), model.id)
|
250
|
+
end
|
251
|
+
|
252
|
+
def add_list(*models)
|
253
|
+
models.each do |model|
|
254
|
+
add(model)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def update(model)
|
259
|
+
add (model)
|
260
|
+
end
|
261
|
+
|
262
|
+
def delete(model)
|
263
|
+
db.zrem(key, model.id)
|
264
|
+
|
265
|
+
model
|
266
|
+
end
|
267
|
+
|
268
|
+
def remrangebyrank(a, b)
|
269
|
+
db.zremrangebyrank(key, a, b)
|
270
|
+
end
|
271
|
+
|
272
|
+
def remrangebyscore(a, b)
|
273
|
+
db.zremrangebyscore(key, a, b)
|
274
|
+
end
|
275
|
+
|
276
|
+
def intersect(set, w1=1.0, w2=1.0)
|
277
|
+
new_key = generate_uuid
|
278
|
+
new_set = Ohm::ZSet.new(new_key, model.key, model, score_field)
|
279
|
+
|
280
|
+
db.zinterstore(new_set.key, [key, set.key], :weights => [w1, w2])
|
281
|
+
new_set
|
282
|
+
end
|
283
|
+
|
284
|
+
def intersect_multiple(sets, weights = [])
|
285
|
+
sets = sets.map(&:key)
|
286
|
+
sets.push(key)
|
287
|
+
weights = [1.0] * sets.length if weights = []
|
288
|
+
|
289
|
+
new_key = generate_uuid
|
290
|
+
new_set = Ohm::ZSet.new(new_key, model.key, model, score_field)
|
291
|
+
|
292
|
+
db.zinterstore(new_set.key, sets, :weights => weights)
|
293
|
+
|
294
|
+
new_set
|
295
|
+
end
|
296
|
+
|
297
|
+
def intersect_multiple!(sets, weights = [])
|
298
|
+
weights = [1.0] * sets.length if weights = []
|
299
|
+
db.zinterstore(key, sets.map(&:key), :weights => weights)
|
300
|
+
self
|
301
|
+
end
|
302
|
+
|
303
|
+
def union(set, w1=1.0, w2=1.0)
|
304
|
+
new_key = generate_uuid
|
305
|
+
new_set = Ohm::ZSet.new(new_key, model.key, model, score_field)
|
306
|
+
|
307
|
+
db.zunionstore(new_set.key, [key, set.key], :weights => [w1, w2])
|
308
|
+
|
309
|
+
new_set
|
310
|
+
end
|
311
|
+
|
312
|
+
def union_multiple(sets, weights = [])
|
313
|
+
sets = sets.map(&:key)
|
314
|
+
sets.push(key)
|
315
|
+
|
316
|
+
weights = [1.0] * sets.length if weights = []
|
317
|
+
new_key = generate_uuid
|
318
|
+
new_set = Ohm::ZSet.new(new_key, model.key, model, score_field)
|
319
|
+
|
320
|
+
db.zunionstore(new_set.key, sets, :weights => weights)
|
321
|
+
|
322
|
+
new_set
|
323
|
+
end
|
324
|
+
|
325
|
+
def rank(model)
|
326
|
+
db.zrank(key, model.id)
|
327
|
+
end
|
328
|
+
|
329
|
+
def revrank(model)
|
330
|
+
db.zrevrank(key, model.id)
|
331
|
+
end
|
332
|
+
|
333
|
+
def score(model)
|
334
|
+
db.zscore(key, model.id).to_i
|
335
|
+
end
|
336
|
+
|
337
|
+
def rangebyscore(a = "-inf", b = "+inf", limit = {})
|
338
|
+
limit[:offset] ||= 0
|
339
|
+
limit[:count] ||= -1
|
340
|
+
|
341
|
+
fetch(execute { |key| db.zrangebyscore(key, a, b, :limit => [limit[:offset], limit[:count]]) })
|
342
|
+
end
|
343
|
+
|
344
|
+
def revrangebyscore(a = "+inf", b = "-inf", limit = {})
|
345
|
+
limit[:offset] ||= 0
|
346
|
+
limit[:count] ||= -1
|
347
|
+
|
348
|
+
fetch(execute { |key| db.zrevrangebyscore(key, a, b, :limit => [limit[:offset], limit[:count]]) })
|
349
|
+
end
|
350
|
+
|
351
|
+
def starts_with(query, limit = {})
|
352
|
+
start_query = ZScores.string(query)
|
353
|
+
end_query = "(" + ZScores.string(query.succ).to_s
|
354
|
+
|
355
|
+
rangebyscore(start_query, end_query, limit)
|
356
|
+
end
|
357
|
+
|
358
|
+
def first
|
359
|
+
range(0, 1)[0] rescue nil
|
360
|
+
end
|
361
|
+
|
362
|
+
def last
|
363
|
+
revrange(0, 1)[0] rescue nil
|
364
|
+
end
|
365
|
+
|
366
|
+
def destroy!
|
367
|
+
db.del(key)
|
368
|
+
end
|
369
|
+
|
370
|
+
def clear
|
371
|
+
remrangebyrank(0, -1)
|
372
|
+
end
|
373
|
+
|
374
|
+
def generate_uuid
|
375
|
+
ZSet.generate_uuid
|
376
|
+
end
|
377
|
+
|
378
|
+
def save_set
|
379
|
+
db.hmset("ZSet:" + key, *get_hmset_attrs)
|
380
|
+
end
|
381
|
+
|
382
|
+
def duplicate
|
383
|
+
intersect(self, 1.0, 0.0)
|
384
|
+
end
|
385
|
+
|
386
|
+
def get_hmset_attrs
|
387
|
+
return_list = []
|
388
|
+
return_list << "model" << model.to_s
|
389
|
+
return_list << "score_field" << Utils.score_field_to_string(score_field)
|
390
|
+
return_list
|
391
|
+
end
|
392
|
+
|
393
|
+
def expire(seconds)
|
394
|
+
db.expire(self.key, seconds)
|
395
|
+
end
|
396
|
+
|
397
|
+
private
|
398
|
+
def db
|
399
|
+
model.db
|
400
|
+
end
|
401
|
+
|
402
|
+
def execute
|
403
|
+
yield key
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Kernel
|
2
|
+
def suppress_warnings
|
3
|
+
original_verbosity = $VERBOSE
|
4
|
+
$VERBOSE = nil
|
5
|
+
result = yield
|
6
|
+
$VERBOSE = original_verbosity
|
7
|
+
return result
|
8
|
+
end
|
9
|
+
|
10
|
+
def require_suppress(library)
|
11
|
+
suppress_warnings{
|
12
|
+
require library
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,663 @@
|
|
1
|
+
require_relative "../helper"
|
2
|
+
|
3
|
+
class Big < Ohm::Model
|
4
|
+
include Ohm::ZScores
|
5
|
+
|
6
|
+
collection :smalls, :Small
|
7
|
+
zset :zlittles, :Little, :score, ZScore::Float
|
8
|
+
zset :zsmalls, :Small, :value
|
9
|
+
zset :zlittles2, :Little, :score
|
10
|
+
zset :zlittles3, :Little, :score
|
11
|
+
zset :zbools, :Bool, :is_valid, ZScore::Boolean
|
12
|
+
set :slittles, :Little
|
13
|
+
zset :zdts, :DT, :last_login, ZScore::DateTime
|
14
|
+
zset :zbools2, :Bool, :name, ZScore::String
|
15
|
+
zset :zbools3, :Bool, :name, ZScore::StringInsensitive
|
16
|
+
zset :zbools4, :Bool, :name, ZScore::StringInsensitiveHigh
|
17
|
+
end
|
18
|
+
|
19
|
+
class Bool < Ohm::Model
|
20
|
+
attribute :name
|
21
|
+
attribute :is_valid
|
22
|
+
end
|
23
|
+
|
24
|
+
class DT < Ohm::Model
|
25
|
+
attribute :name
|
26
|
+
#attribute :date_updated
|
27
|
+
attribute :last_login
|
28
|
+
end
|
29
|
+
|
30
|
+
class Small < Ohm::Model
|
31
|
+
attribute :name
|
32
|
+
attribute :value
|
33
|
+
end
|
34
|
+
|
35
|
+
class Little < Ohm::Model
|
36
|
+
attribute :name
|
37
|
+
attribute :score
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_to_zset(model, zset_name, items, item_class)
|
41
|
+
z = model.send(zset_name)
|
42
|
+
|
43
|
+
items.each do |i|
|
44
|
+
z.send(:add, item_class.create(i))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def setup_big_1
|
49
|
+
b = Big.create
|
50
|
+
|
51
|
+
l1 = Little.create(name: 'L1', score: 1)
|
52
|
+
l2 = Little.create(name: 'L2', score: 2)
|
53
|
+
l3 = Little.create(name: 'L3', score: 3)
|
54
|
+
l4 = Little.create(name: 'L4', score: 4)
|
55
|
+
|
56
|
+
Big.zlittles.clear
|
57
|
+
Big.zlittles.add(l1)
|
58
|
+
Big.zlittles.add(l2)
|
59
|
+
Big.zlittles.add(l3)
|
60
|
+
Big.zlittles.add(l4)
|
61
|
+
|
62
|
+
b.slittles.add(l1)
|
63
|
+
b.slittles.add(l2)
|
64
|
+
b.slittles.add(Little.create(name: 'E3', score: 24))
|
65
|
+
b.slittles.add(l4)
|
66
|
+
|
67
|
+
Big.zlittles2.clear
|
68
|
+
Big.zlittles2.add(l2)
|
69
|
+
Big.zlittles2.add(l3)
|
70
|
+
Big.zlittles2.add(l4)
|
71
|
+
|
72
|
+
Big.zlittles3.clear
|
73
|
+
Big.zlittles3.add(l1)
|
74
|
+
Big.zlittles3.add(l4)
|
75
|
+
Big.zlittles3.add(Little.create(name: 'E1', score: 21))
|
76
|
+
Big.zlittles3.add(Little.create(name: 'E2', score: 22))
|
77
|
+
|
78
|
+
Big.zsmalls.clear
|
79
|
+
Big.zsmalls.add(Small.create(name: 'S1', value: 8))
|
80
|
+
Big.zsmalls.add(Small.create(name: 'S2', value: 7))
|
81
|
+
Big.zsmalls.add(Small.create(name: 'S3', value: 6))
|
82
|
+
Big.zsmalls.add(Small.create(name: 'S4', value: 5))
|
83
|
+
|
84
|
+
b
|
85
|
+
end
|
86
|
+
|
87
|
+
def setup_1
|
88
|
+
b = Big.create
|
89
|
+
|
90
|
+
l1 = Little.create(name: 'L1', score: 1)
|
91
|
+
l2 = Little.create(name: 'L2', score: 2)
|
92
|
+
l3 = Little.create(name: 'L3', score: 3)
|
93
|
+
l4 = Little.create(name: 'L4', score: 4)
|
94
|
+
|
95
|
+
b.slittles.add(l1)
|
96
|
+
b.slittles.add(l2)
|
97
|
+
b.slittles.add(Little.create(name: 'E3',score: 24))
|
98
|
+
b.slittles.add(l4)
|
99
|
+
|
100
|
+
b.zlittles.add(l1)
|
101
|
+
b.zlittles.add(l2)
|
102
|
+
b.zlittles.add(l3)
|
103
|
+
b.zlittles.add(l4)
|
104
|
+
|
105
|
+
b.zlittles2.add(l2)
|
106
|
+
b.zlittles2.add(l3)
|
107
|
+
b.zlittles2.add(l4)
|
108
|
+
|
109
|
+
b.zlittles3.add(l1)
|
110
|
+
b.zlittles3.add(l4)
|
111
|
+
b.zlittles3.add(Little.create(name: 'E1', score: 21))
|
112
|
+
b.zlittles3.add(Little.create(name: 'E2', score: 22))
|
113
|
+
|
114
|
+
b.zsmalls.add(Small.create(name: 'S1', value: 8))
|
115
|
+
b.zsmalls.add(Small.create(name: 'S2', value: 7))
|
116
|
+
b.zsmalls.add(Small.create(name: 'S3', value: 6))
|
117
|
+
b.zsmalls.add(Small.create(name: 'S4', value: 5))
|
118
|
+
|
119
|
+
|
120
|
+
b.zlittles.add(l1)
|
121
|
+
b.zlittles.add(l2)
|
122
|
+
b.zlittles.add(l3)
|
123
|
+
b.zlittles.add(l4)
|
124
|
+
|
125
|
+
b
|
126
|
+
end
|
127
|
+
|
128
|
+
def setup_2
|
129
|
+
b = Big.create
|
130
|
+
|
131
|
+
l1 = Little.create(name: 'L1', score: 1)
|
132
|
+
l2 = Little.create(name: 'L2', score: 2)
|
133
|
+
l3 = Little.create(name: 'L3', score: 3)
|
134
|
+
l4 = Little.create(name: 'L4', score: 4)
|
135
|
+
|
136
|
+
b.zlittles.add(l1)
|
137
|
+
b.zlittles.add(l2)
|
138
|
+
b.zlittles.add(l3)
|
139
|
+
b.zlittles.add(l4)
|
140
|
+
|
141
|
+
[b, l1, l2, l3, l4]
|
142
|
+
end
|
143
|
+
|
144
|
+
def setup_3
|
145
|
+
b = Big.create
|
146
|
+
|
147
|
+
b1 = Bool.create(name: 'B1', is_valid: "false")
|
148
|
+
b2 = Bool.create(name: 'B2', is_valid: "true")
|
149
|
+
b3 = Bool.create(name: 'B3', is_valid: "false")
|
150
|
+
b4 = Bool.create(name: 'B4', is_valid: "true")
|
151
|
+
|
152
|
+
b.zbools.add(b1)
|
153
|
+
b.zbools.add(b2)
|
154
|
+
b.zbools.add(b3)
|
155
|
+
b.zbools.add(b4)
|
156
|
+
|
157
|
+
b
|
158
|
+
end
|
159
|
+
|
160
|
+
def setup_4
|
161
|
+
b = Big.create
|
162
|
+
|
163
|
+
d1 = DT.create(name: 'D1', last_login: "2012-07-29 06:24:20 +0800")
|
164
|
+
d2 = DT.create(name: 'D2', last_login: "2012-07-29 05:24:20 +0800")
|
165
|
+
d3 = DT.create(name: 'D3', last_login: "2012-07-29 04:24:20 +0800")
|
166
|
+
d4 = DT.create(name: 'D4', last_login: "2012-08-29")
|
167
|
+
b.zdts.add_list(d1, d2, d3, d4)
|
168
|
+
b
|
169
|
+
end
|
170
|
+
|
171
|
+
def setup_5
|
172
|
+
b = Big.create
|
173
|
+
|
174
|
+
items = [
|
175
|
+
{name: 'Apple', is_valid: 'false'},
|
176
|
+
{name: 'Coconut', is_valid: 'true'},
|
177
|
+
{name: 'Dragonfruit', is_valid: 'false'},
|
178
|
+
{name: 'Banana', is_valid: 'true'},
|
179
|
+
{name: 'apples', is_valid: 'true'},
|
180
|
+
{name: 'coconuts', is_valid: 'true'},
|
181
|
+
{name: 'durians', is_valid: 'false'},
|
182
|
+
{name: 'bananas', is_valid: 'true'},
|
183
|
+
{name: 'apple', is_valid: 'true'},
|
184
|
+
{name: 'coconut', is_valid: 'true'},
|
185
|
+
{name: 'banana', is_valid: 'false'},
|
186
|
+
{name: 'durian', is_valid: 'true'},
|
187
|
+
]
|
188
|
+
|
189
|
+
add_to_zset(b, 'zbools2', items, Bool)
|
190
|
+
|
191
|
+
b
|
192
|
+
end
|
193
|
+
|
194
|
+
def setup_6
|
195
|
+
b = Big.create
|
196
|
+
|
197
|
+
items = [
|
198
|
+
{name: 'apple', is_valid: 'false'},
|
199
|
+
{name: 'Coconut', is_valid: 'true'},
|
200
|
+
{name: 'duria', is_valid: 'false'},
|
201
|
+
{name: 'Banana', is_valid: 'true'},
|
202
|
+
{name: 'Apples', is_valid: 'true'},
|
203
|
+
{name: 'coconuts', is_valid: 'true'},
|
204
|
+
{name: 'durians', is_valid: 'false'},
|
205
|
+
{name: 'bananas', is_valid: 'true'},
|
206
|
+
{name: 'Apple', is_valid: 'true'},
|
207
|
+
{name: 'coconut', is_valid: 'true'},
|
208
|
+
{name: 'banana', is_valid: 'false'},
|
209
|
+
{name: 'Durian', is_valid: 'true'},
|
210
|
+
]
|
211
|
+
|
212
|
+
add_to_zset(b, 'zbools3', items, Bool)
|
213
|
+
|
214
|
+
b
|
215
|
+
end
|
216
|
+
|
217
|
+
def setup_7
|
218
|
+
b = Big.create
|
219
|
+
|
220
|
+
items = [
|
221
|
+
{name: 'apple', is_valid: 'false'},
|
222
|
+
{name: 'Coconut', is_valid: 'true'},
|
223
|
+
{name: 'duria', is_valid: 'false'},
|
224
|
+
{name: 'Banana', is_valid: 'true'},
|
225
|
+
{name: 'Apples', is_valid: 'true'},
|
226
|
+
{name: 'coconuts', is_valid: 'true'},
|
227
|
+
{name: 'durians', is_valid: 'false'},
|
228
|
+
{name: 'bananas', is_valid: 'true'},
|
229
|
+
{name: 'Apple', is_valid: 'true'},
|
230
|
+
{name: 'coconut', is_valid: 'true'},
|
231
|
+
{name: 'banana', is_valid: 'false'},
|
232
|
+
{name: 'Durian', is_valid: 'true'},
|
233
|
+
]
|
234
|
+
|
235
|
+
add_to_zset(b, 'zbools4', items, Bool)
|
236
|
+
|
237
|
+
b
|
238
|
+
end
|
239
|
+
|
240
|
+
# Tests for the class methods
|
241
|
+
# TODO: figure out how to DRY up the class and instance method tests. Probably move the method body to a common method and test that common method instead
|
242
|
+
describe Ohm do
|
243
|
+
it "can add objects and get the size for the class" do
|
244
|
+
setup_big_1
|
245
|
+
|
246
|
+
assert_equal 4, Big.zlittles.size
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Tests for the instance methods
|
251
|
+
describe Ohm do
|
252
|
+
it "can add objects and get the size" do
|
253
|
+
b = setup_1
|
254
|
+
|
255
|
+
assert_equal 4, b.zlittles.size
|
256
|
+
end
|
257
|
+
|
258
|
+
it "can add objects with scores and automatically sorts them by score" do
|
259
|
+
b = setup_1
|
260
|
+
|
261
|
+
assert_equal ["L1", "L2", "L3", "L4"], b.zlittles.to_a.map(&:name)
|
262
|
+
end
|
263
|
+
|
264
|
+
it "can return a range of sorted elements" do
|
265
|
+
b = setup_1
|
266
|
+
|
267
|
+
assert_equal ["L2", "L3"], b.zlittles.range(1, 2).map(&:name)
|
268
|
+
assert_equal ["L1", "L2", "L3"], b.zlittles.range(0, 2).map(&:name)
|
269
|
+
end
|
270
|
+
|
271
|
+
it "can return a range of sorted elements in reverse" do
|
272
|
+
b = setup_1
|
273
|
+
|
274
|
+
assert_equal ["L3", "L2"], b.zlittles.revrange(1, 2).map(&:name)
|
275
|
+
assert_equal ["L4", "L3", "L2"], b.zlittles.revrange(0, 2).map(&:name)
|
276
|
+
end
|
277
|
+
|
278
|
+
it "can iterate over the elements of a specified range of the set" do
|
279
|
+
b = setup_1
|
280
|
+
|
281
|
+
expected_items = ["L2", "L3", "L4"]
|
282
|
+
|
283
|
+
b.zlittles.range(1, 3).each_with_index do |e, i|
|
284
|
+
assert_equal expected_items[i], e.name
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
it "can iterate over the elements of a specified range of the reverse sorted set" do
|
289
|
+
b = setup_1
|
290
|
+
expected_items = ["L3", "L2", "L1"]
|
291
|
+
|
292
|
+
b.zlittles.revrange(1, 3).each_with_index do |e, i|
|
293
|
+
assert_equal expected_items[i], e.name
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
it "can get an element with a specified index" do
|
298
|
+
b = setup_1
|
299
|
+
|
300
|
+
assert_equal "L3", b.zlittles.get(2).name
|
301
|
+
assert_nil b.zlittles.get(5)
|
302
|
+
end
|
303
|
+
|
304
|
+
it "can delete an element" do
|
305
|
+
b = setup_1
|
306
|
+
|
307
|
+
x = b.zlittles.get(2)
|
308
|
+
deleted_element = b.zlittles.delete(x)
|
309
|
+
|
310
|
+
assert_equal x, deleted_element
|
311
|
+
assert_equal 3, b.zlittles.size
|
312
|
+
end
|
313
|
+
|
314
|
+
it "knows if it includes a specified element" do
|
315
|
+
b, l1, l2, l3, l4 = setup_2
|
316
|
+
|
317
|
+
assert b.zlittles.include?(l1)
|
318
|
+
|
319
|
+
b.zlittles.delete(l2)
|
320
|
+
|
321
|
+
refute b.zlittles.include?(l2)
|
322
|
+
assert b.zlittles.include?(l3)
|
323
|
+
assert b.zlittles.include?(l4)
|
324
|
+
assert b.zlittles.include?(l1)
|
325
|
+
|
326
|
+
b.zlittles.add(l2)
|
327
|
+
|
328
|
+
assert b.zlittles.include?(l2)
|
329
|
+
end
|
330
|
+
|
331
|
+
it "can delete a range of elements by rank" do
|
332
|
+
b = setup_1
|
333
|
+
assert_equal ["L1", "L2", "L3", "L4"], b.zlittles.to_a.map(&:name)
|
334
|
+
|
335
|
+
b.zlittles.remrangebyrank(0, 1)
|
336
|
+
assert_equal ["L3", "L4"], b.zlittles.to_a.map(&:name)
|
337
|
+
|
338
|
+
b.zlittles.remrangebyrank(0, 0)
|
339
|
+
assert_equal ["L4"], b.zlittles.to_a.map(&:name)
|
340
|
+
end
|
341
|
+
|
342
|
+
it "can delete a range of elements by score" do
|
343
|
+
b = setup_1
|
344
|
+
assert_equal ["L1", "L2", "L3", "L4"], b.zlittles.to_a.map(&:name)
|
345
|
+
|
346
|
+
b.zlittles.remrangebyscore(2, 3)
|
347
|
+
assert_equal ["L1", "L4"], b.zlittles.to_a.map(&:name)
|
348
|
+
end
|
349
|
+
|
350
|
+
it "can allow intersection of 2 zsets" do
|
351
|
+
b = setup_1
|
352
|
+
|
353
|
+
zlittles = b.zlittles
|
354
|
+
zlittles2 = b.zlittles2
|
355
|
+
zintersection = b.zlittles.intersect(zlittles2)
|
356
|
+
zlittles3 = b.zlittles3
|
357
|
+
zintersection2 = b.zlittles.intersect(zlittles3)
|
358
|
+
zintersection3 = zintersection.intersect(zintersection2)
|
359
|
+
zintersection4 = zintersection.intersect(zintersection)
|
360
|
+
|
361
|
+
assert_equal ["L1", "L4"], zintersection2.to_a.map(&:name)
|
362
|
+
assert_equal ["L2", "L3", "L4"], zintersection.to_a.map(&:name)
|
363
|
+
assert_equal ["L4"], zintersection3.to_a.map(&:name)
|
364
|
+
assert_equal zintersection.to_a.map(&:name), zintersection4.to_a.map(&:name)
|
365
|
+
end
|
366
|
+
|
367
|
+
it "can allow intersection of a zset and a set" do
|
368
|
+
b = setup_1
|
369
|
+
|
370
|
+
zlittles = b.zlittles
|
371
|
+
zlittles2 = b.zlittles2
|
372
|
+
slittles = b.slittles
|
373
|
+
|
374
|
+
zintersection = b.zlittles.intersect(slittles)
|
375
|
+
zintersection2 = b.zlittles2.intersect(slittles)
|
376
|
+
|
377
|
+
assert_equal ["L1","L2","L4"], zintersection.to_a.map(&:name)
|
378
|
+
assert_equal ["L2","L4"], zintersection2.to_a.map(&:name)
|
379
|
+
end
|
380
|
+
|
381
|
+
it "can allow intersection of multiple zsets and sets" do
|
382
|
+
b = setup_1
|
383
|
+
|
384
|
+
zlittles = b.zlittles
|
385
|
+
zlittles2 = b.zlittles2
|
386
|
+
zlittles3 = b.zlittles3
|
387
|
+
slittles = b.slittles
|
388
|
+
|
389
|
+
zintersection = b.zlittles.intersect_multiple([zlittles2, zlittles3])
|
390
|
+
|
391
|
+
assert_equal ["L4"], zintersection.to_a.map(&:name)
|
392
|
+
end
|
393
|
+
|
394
|
+
it "can allow intersection of multiple zsets and sets to a new named set" do
|
395
|
+
b = setup_1
|
396
|
+
|
397
|
+
zlittles = b.zlittles
|
398
|
+
zlittles2 = b.zlittles2
|
399
|
+
zlittles3 = b.zlittles3
|
400
|
+
slittles = b.slittles
|
401
|
+
zintersection = Ohm::ZSet.intersect_multiple("zintersection", [zlittles, zlittles2, zlittles3])
|
402
|
+
|
403
|
+
assert_equal ["L4"], zintersection.to_a.map(&:name)
|
404
|
+
assert_equal "zintersection", zintersection.key
|
405
|
+
end
|
406
|
+
|
407
|
+
it "can allow union of 2 zsets" do
|
408
|
+
b = setup_1
|
409
|
+
|
410
|
+
zlittles = b.zlittles
|
411
|
+
zlittles2 = b.zlittles2
|
412
|
+
zunion = b.zlittles.union(zlittles2)
|
413
|
+
|
414
|
+
assert_equal ["L1","L2","L3","L4"], zunion.to_a.map(&:name)
|
415
|
+
end
|
416
|
+
|
417
|
+
it "can allow union of a zset and a set" do
|
418
|
+
b = setup_1
|
419
|
+
|
420
|
+
zlittles = b.zlittles
|
421
|
+
zlittles2 = b.zlittles2
|
422
|
+
slittles = b.slittles
|
423
|
+
|
424
|
+
zunion = b.zlittles.union(slittles)
|
425
|
+
zunion2 = b.zlittles2.union(slittles)
|
426
|
+
|
427
|
+
assert_equal ["E3","L1","L2","L3","L4"], zunion.to_a.map(&:name)
|
428
|
+
end
|
429
|
+
|
430
|
+
it "can allow union of multiple zsets and sets" do
|
431
|
+
b = setup_1
|
432
|
+
|
433
|
+
zlittles = b.zlittles
|
434
|
+
zlittles2 = b.zlittles2
|
435
|
+
zlittles3 = b.zlittles3
|
436
|
+
slittles = b.slittles
|
437
|
+
|
438
|
+
zunion = b.zlittles.union_multiple([slittles, zlittles2, zlittles3])
|
439
|
+
|
440
|
+
assert_equal ["E3","L1","L2","L3","L4","E1","E2"], zunion.to_a.map(&:name)
|
441
|
+
end
|
442
|
+
|
443
|
+
it "can allow union of multiple zsets and sets to a newly named set" do
|
444
|
+
b = setup_1
|
445
|
+
|
446
|
+
zlittles = b.zlittles
|
447
|
+
zlittles2 = b.zlittles2
|
448
|
+
zlittles3 = b.zlittles3
|
449
|
+
slittles = b.slittles
|
450
|
+
|
451
|
+
zunion = Ohm::ZSet.union_multiple("zunion", [zlittles, slittles, zlittles2, zlittles3])
|
452
|
+
|
453
|
+
assert_equal ["E3","L1","L2","L3","L4","E1","E2"], zunion.to_a.map(&:name)
|
454
|
+
assert_equal "zunion", zunion.key
|
455
|
+
end
|
456
|
+
|
457
|
+
it "can get the rank of an element" do
|
458
|
+
b, l1, l2, l3, l4 = setup_2
|
459
|
+
|
460
|
+
assert_equal 0, b.zlittles.rank(l1)
|
461
|
+
assert_equal 1, b.zlittles.rank(l2)
|
462
|
+
assert_equal 2, b.zlittles.rank(l3)
|
463
|
+
assert_equal 3, b.zlittles.rank(l4)
|
464
|
+
end
|
465
|
+
|
466
|
+
it "can get the rank of specified element from the reverse sorted list" do
|
467
|
+
b, l1, l2, l3, l4 = setup_2
|
468
|
+
|
469
|
+
assert_equal 3, b.zlittles.revrank(l1)
|
470
|
+
assert_equal 2, b.zlittles.revrank(l2)
|
471
|
+
assert_equal 1, b.zlittles.revrank(l3)
|
472
|
+
assert_equal 0, b.zlittles.revrank(l4)
|
473
|
+
end
|
474
|
+
|
475
|
+
it "can get the score of an element" do
|
476
|
+
b, l1, l2, l3, l4 = setup_2
|
477
|
+
|
478
|
+
assert_equal 1, b.zlittles.score(l1)
|
479
|
+
assert_equal 2, b.zlittles.score(l2)
|
480
|
+
assert_equal 3, b.zlittles.score(l3)
|
481
|
+
assert_equal 4, b.zlittles.score(l4)
|
482
|
+
end
|
483
|
+
|
484
|
+
it "can get a range of elements by score" do
|
485
|
+
b, = setup_2
|
486
|
+
|
487
|
+
assert_equal ["L1", "L2", "L3", "L4"], b.zlittles.rangebyscore(0, 4).to_a.map(&:name)
|
488
|
+
assert_equal ["L1", "L2"], b.zlittles.rangebyscore(1, 2).to_a.map(&:name)
|
489
|
+
assert_equal ["L3", "L4"], b.zlittles.rangebyscore(3, 4).to_a.map(&:name)
|
490
|
+
assert_equal ["L1", "L2", "L3"], b.zlittles.rangebyscore(1, 3).to_a.map(&:name)
|
491
|
+
assert_equal ["L1", "L2"], b.zlittles.rangebyscore(0, 4, offset: 0, count: 2).to_a.map(&:name)
|
492
|
+
assert_equal ["L3", "L4"], b.zlittles.rangebyscore(0, 4, offset: 2, count: 2).to_a.map(&:name)
|
493
|
+
assert_equal ["L2", "L3", "L4"], b.zlittles.rangebyscore(0, 4, offset: 1, count: 3).to_a.map(&:name)
|
494
|
+
assert_equal ["L3"], b.zlittles.rangebyscore(1, 3, offset: 2, count: 4).to_a.map(&:name)
|
495
|
+
assert_equal ["L1", "L2", "L3", "L4"], b.zlittles.rangebyscore(0, "+inf").to_a.map(&:name)
|
496
|
+
assert_equal ["L4"], b.zlittles.rangebyscore(2, "+inf", offset: 2).to_a.map(&:name)
|
497
|
+
assert_equal ["L1", "L2"], b.zlittles.rangebyscore("-inf", 2).to_a.map(&:name)
|
498
|
+
assert_equal ["L4"], b.zlittles.rangebyscore(2, "+inf", offset: 2, count: 2).to_a.map(&:name)
|
499
|
+
end
|
500
|
+
|
501
|
+
it "can get a range of elements by score in reverse" do
|
502
|
+
b, = setup_2
|
503
|
+
|
504
|
+
assert_equal ["L4", "L3", "L2", "L1"], b.zlittles.revrangebyscore(4, 0).to_a.map(&:name)
|
505
|
+
assert_equal ["L2", "L1"], b.zlittles.revrangebyscore(2, 1).to_a.map(&:name)
|
506
|
+
assert_equal ["L4", "L3"], b.zlittles.revrangebyscore(4, 3).to_a.map(&:name)
|
507
|
+
assert_equal ["L3", "L2", "L1"], b.zlittles.revrangebyscore(3, 1).to_a.map(&:name)
|
508
|
+
assert_equal ["L4", "L3"], b.zlittles.revrangebyscore(4, 0, offset: 0, count: 2).to_a.map(&:name)
|
509
|
+
assert_equal ["L2", "L1"], b.zlittles.revrangebyscore(4, 0, offset: 2, count: 2).to_a.map(&:name)
|
510
|
+
assert_equal ["L3", "L2", "L1"], b.zlittles.revrangebyscore(4, 0, offset: 1, count: 3).to_a.map(&:name)
|
511
|
+
assert_equal ["L1"], b.zlittles.revrangebyscore(3, 1, offset: 2, count: 4).to_a.map(&:name)
|
512
|
+
assert_equal ["L4", "L3", "L2", "L1"], b.zlittles.revrangebyscore("+inf", 0).to_a.map(&:name)
|
513
|
+
assert_equal ["L2"], b.zlittles.revrangebyscore("+inf", 2, offset: 2).to_a.map(&:name)
|
514
|
+
assert_equal ["L2", "L1"], b.zlittles.revrangebyscore(2, "-inf").to_a.map(&:name)
|
515
|
+
assert_equal ["L2"], b.zlittles.revrangebyscore("+inf", 2, offset: 2, count: 2).to_a.map(&:name)
|
516
|
+
end
|
517
|
+
|
518
|
+
it "can get the number of elements between 2 given scores" do
|
519
|
+
b, = setup_2
|
520
|
+
|
521
|
+
assert_equal 2, b.zlittles.count(1, 2)
|
522
|
+
assert_equal 2, b.zlittles.count(3, 4)
|
523
|
+
assert_equal 4, b.zlittles.count(1, 4)
|
524
|
+
assert_equal 3, b.zlittles.count(2, "+inf")
|
525
|
+
assert_equal 4, b.zlittles.count
|
526
|
+
assert_equal 4, b.zlittles.count("-inf", "+inf")
|
527
|
+
assert_equal 2, b.zlittles.count("-inf", 2)
|
528
|
+
end
|
529
|
+
|
530
|
+
it "can update the sorted set properly upon updating the score of an element" do
|
531
|
+
b, l1, l2, l3, l4 = setup_2
|
532
|
+
assert_equal ["L1", "L2", "L3", "L4"], b.zlittles.to_a.map(&:name)
|
533
|
+
|
534
|
+
l1.score = 5
|
535
|
+
l1.save
|
536
|
+
b.zlittles.update(l1)
|
537
|
+
assert_equal ["L2", "L3", "L4", "L1"], b.zlittles.to_a.map(&:name)
|
538
|
+
|
539
|
+
l3.score = -3
|
540
|
+
l3.save
|
541
|
+
b.zlittles.update(l3)
|
542
|
+
assert_equal ["L3", "L2", "L4", "L1"], b.zlittles.to_a.map(&:name)
|
543
|
+
|
544
|
+
end
|
545
|
+
|
546
|
+
it "can sort elements by date" do
|
547
|
+
b = setup_4
|
548
|
+
|
549
|
+
assert_equal ["D3", "D2", "D1", "D4"], b.zdts.to_a.map(&:name)
|
550
|
+
end
|
551
|
+
|
552
|
+
it "can sort elements by name" do
|
553
|
+
b = setup_5
|
554
|
+
|
555
|
+
assert_equal ["Apple", "Banana", "Coconut", "Dragonfruit",
|
556
|
+
"apple", "apples", "banana", "bananas",
|
557
|
+
"coconut", "coconuts", "durian", "durians"],
|
558
|
+
b.zbools2.to_a.map(&:name)
|
559
|
+
end
|
560
|
+
|
561
|
+
it "can sort elements by name (insensitive)" do
|
562
|
+
b = setup_6
|
563
|
+
|
564
|
+
assert_equal ["apple", "Apple", "Apples",
|
565
|
+
"Banana", "banana", "bananas",
|
566
|
+
"Coconut", "coconut", "coconuts",
|
567
|
+
"duria", "Durian", "durians"],
|
568
|
+
b.zbools3.to_a.map(&:name)
|
569
|
+
end
|
570
|
+
|
571
|
+
it "can sort elements by name (insensitive high)" do
|
572
|
+
b = setup_7
|
573
|
+
|
574
|
+
assert_equal ["Apple", "Apples", "apple",
|
575
|
+
"Banana", "banana", "bananas",
|
576
|
+
"Coconut", "coconut", "coconuts",
|
577
|
+
"Durian", "duria", "durians"],
|
578
|
+
b.zbools4.to_a.map(&:name)
|
579
|
+
end
|
580
|
+
|
581
|
+
it "can find elements that starts with specified string" do
|
582
|
+
b = setup_5
|
583
|
+
|
584
|
+
assert_equal ["durian", "durians"], b.zbools2.starts_with("du").map(&:name)
|
585
|
+
assert_equal ["durians"], b.zbools2.starts_with("d",offset:1,limit:1).map(&:name)
|
586
|
+
assert_equal ["bananas"], b.zbools2.starts_with("bananas").map(&:name)
|
587
|
+
end
|
588
|
+
|
589
|
+
it "can return the first and last elements" do
|
590
|
+
b = setup_1
|
591
|
+
|
592
|
+
assert_equal "L1", b.zlittles.first.name
|
593
|
+
assert_equal "L4", b.zlittles.last.name
|
594
|
+
end
|
595
|
+
|
596
|
+
it "can be deleted / destroyed" do
|
597
|
+
b = setup_1
|
598
|
+
|
599
|
+
b.zlittles.destroy!
|
600
|
+
|
601
|
+
assert_equal [], b.zlittles.to_a.map(&:name)
|
602
|
+
end
|
603
|
+
|
604
|
+
it "can delete all elements at once" do
|
605
|
+
b = setup_1
|
606
|
+
|
607
|
+
b.zlittles.clear
|
608
|
+
|
609
|
+
assert_equal [], b.zlittles.to_a.map(&:name)
|
610
|
+
end
|
611
|
+
|
612
|
+
it "can clone itself to another instance" do
|
613
|
+
b, l1, l2, l3, l4 = setup_2
|
614
|
+
|
615
|
+
clone = b.zlittles.duplicate
|
616
|
+
|
617
|
+
assert_equal ["L1", "L2", "L3", "L4"], clone.to_a.map(&:name)
|
618
|
+
assert_equal 1, clone.score(l1)
|
619
|
+
assert_equal 2, clone.score(l2)
|
620
|
+
assert_equal 3, clone.score(l3)
|
621
|
+
assert_equal 4, clone.score(l4)
|
622
|
+
end
|
623
|
+
|
624
|
+
it "can convert a procedure block to string and back" do
|
625
|
+
x = lambda { |x| x + 1 }
|
626
|
+
y = Ohm::Utils.proc_to_string x
|
627
|
+
|
628
|
+
assert_equal "proc { |x| (x + 1) }", y
|
629
|
+
|
630
|
+
z = Ohm::Utils.string_to_proc y
|
631
|
+
|
632
|
+
assert_equal 3, z.call(2)
|
633
|
+
end
|
634
|
+
|
635
|
+
it "can convert a score_field list to string and back" do
|
636
|
+
score_field = [:n1, :n2, :n3, lambda { |x| x + 1 }]
|
637
|
+
score_field_string = Ohm::Utils.score_field_to_string score_field
|
638
|
+
|
639
|
+
assert_equal "n1:n2:n3:proc { |x| (x + 1) }", score_field_string
|
640
|
+
|
641
|
+
score_field_list = Ohm::Utils.string_to_score_field score_field_string
|
642
|
+
|
643
|
+
assert_equal :n1, score_field_list[0]
|
644
|
+
assert_equal :n2, score_field_list[1]
|
645
|
+
assert_equal :n3, score_field_list[2]
|
646
|
+
|
647
|
+
assert_equal 3, score_field_list[3].call(2)
|
648
|
+
end
|
649
|
+
|
650
|
+
it "can save and load sets by name" do
|
651
|
+
b = setup_1
|
652
|
+
|
653
|
+
sorted_set = b.zlittles
|
654
|
+
sorted_set.save_set
|
655
|
+
sorted_set_2 = Ohm::ZSet.load_set(sorted_set.key)
|
656
|
+
|
657
|
+
assert_equal sorted_set_2.to_a.map(&:name), sorted_set.to_a.map(&:name)
|
658
|
+
|
659
|
+
sorted_set_2.add(Little.create(name:'X1',score:29))
|
660
|
+
|
661
|
+
assert_equal sorted_set_2.to_a.map(&:name), sorted_set.to_a.map(&:name)
|
662
|
+
end
|
663
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ohm-zset
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.2'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Josh
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ohm
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Adds ZSet support to Ohm
|
31
|
+
email:
|
32
|
+
- akosijoshualat@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gems
|
38
|
+
- .gitignore
|
39
|
+
- .rvmrc
|
40
|
+
- CHANGELOG
|
41
|
+
- LICENSE
|
42
|
+
- README.md
|
43
|
+
- Rakefile
|
44
|
+
- lib/.DS_Store
|
45
|
+
- lib/ohm-zset.rb
|
46
|
+
- lib/suppress-warnings.rb
|
47
|
+
- test/.DS_Store
|
48
|
+
- test/helper.rb
|
49
|
+
- test/unit/.DS_Store
|
50
|
+
- test/unit/ohm-zset.rb
|
51
|
+
homepage: ''
|
52
|
+
licenses: []
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.8.24
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Adds ZSet support to Ohm
|
75
|
+
test_files:
|
76
|
+
- test/.DS_Store
|
77
|
+
- test/helper.rb
|
78
|
+
- test/unit/.DS_Store
|
79
|
+
- test/unit/ohm-zset.rb
|