rankle 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Rakefile +7 -2
- data/lib/rankle/ranker.rb +28 -1
- data/lib/rankle/version.rb +1 -1
- data/lib/rankle.rb +2 -2
- data/lib/rankle_index.rb +14 -15
- data/test/default_behavior_test.rb +1 -1
- data/test/performance/rank_benchmark.rb +38 -0
- data/test/spec/ranker_test.rb +115 -0
- data/test/spec/rankle_index_test.rb +23 -0
- data/test/support/models.rb +0 -2
- data/test/support/test_helper.rb +7 -9
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2a00ba713d656ff500838729e9ea39ed91d71c5
|
4
|
+
data.tar.gz: 1b07a9638a677ca8265d6a146674d222d80b0470
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a48c2ed7910ffc5d4e0c8d23fcfec501e220c82ea7db87f8d88a30dbb500251b4e67dc8c2743d0380182fb75b47a4bbb204cb7440803925f375f9ecdbf0ba26a
|
7
|
+
data.tar.gz: 1bb2cee9412e484908a9e94d4095ecda288223b868b487259bdfa0c2d40b1cb8d2d0a4c50258d949d2ba1bc295003e987c98856134698a116e011724cf8dbc79
|
data/.gitignore
CHANGED
data/Rakefile
CHANGED
@@ -3,7 +3,12 @@ require 'yard'
|
|
3
3
|
require 'rake/testtask'
|
4
4
|
|
5
5
|
Rake::TestTask.new do |t|
|
6
|
-
t.test_files = FileList['test
|
6
|
+
t.test_files = FileList['test/**/*_test.rb']
|
7
|
+
t.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
Rake::TestTask.new :bench do |t|
|
11
|
+
t.test_files = FileList['test/**/*_benchmark.rb']
|
7
12
|
t.verbose = true
|
8
13
|
end
|
9
14
|
|
@@ -11,4 +16,4 @@ YARD::Rake::YardocTask.new do |t|
|
|
11
16
|
t.files = ['lib/rankle.rb']
|
12
17
|
end
|
13
18
|
|
14
|
-
task :default => [:test, :yard]
|
19
|
+
task :default => [:test, :bench, :yard]
|
data/lib/rankle/ranker.rb
CHANGED
@@ -1,9 +1,36 @@
|
|
1
1
|
module Rankle
|
2
2
|
class Ranker
|
3
|
+
MIN_INDEX = -2147483648
|
4
|
+
MAX_INDEX = 2147483647
|
5
|
+
|
3
6
|
attr_accessor :strategy
|
4
7
|
|
5
8
|
def initialize strategy
|
6
9
|
@strategy = strategy
|
7
10
|
end
|
11
|
+
|
12
|
+
def self.insert target_position, existing_elements
|
13
|
+
if existing_elements.count > MAX_INDEX - MIN_INDEX
|
14
|
+
raise IndexError
|
15
|
+
elsif existing_elements.empty?
|
16
|
+
return 0, []
|
17
|
+
elsif target_position <= 0
|
18
|
+
return (MIN_INDEX + existing_elements.first) / 2, existing_elements
|
19
|
+
elsif target_position >= existing_elements.count
|
20
|
+
return (MAX_INDEX + existing_elements.last) / 2, existing_elements
|
21
|
+
elsif existing_elements[target_position] - existing_elements[target_position - 1] > 1
|
22
|
+
return (existing_elements[target_position] + existing_elements[target_position - 1]) / 2, existing_elements
|
23
|
+
else
|
24
|
+
existing_elements = balance existing_elements
|
25
|
+
insert target_position, existing_elements
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.balance indices, options = {}
|
30
|
+
min_index = options[:min_index] || MIN_INDEX
|
31
|
+
max_index = options[:max_index] || MAX_INDEX
|
32
|
+
offset = (max_index - min_index) / (indices.count + 1)
|
33
|
+
indices.count.times.map { |index| min_index + (offset * (index + 1)) + index }
|
34
|
+
end
|
8
35
|
end
|
9
|
-
end
|
36
|
+
end
|
data/lib/rankle/version.rb
CHANGED
data/lib/rankle.rb
CHANGED
@@ -38,7 +38,7 @@ module Rankle
|
|
38
38
|
RankleIndex.set_default_position self
|
39
39
|
end
|
40
40
|
|
41
|
-
# Assigns an explicit position to the record
|
41
|
+
# Assigns an explicit position to the record using the default ranker
|
42
42
|
#
|
43
43
|
# @param format [Integer] the new position
|
44
44
|
# @return [Integer or Exception] the new position or an exception if the position could not be set
|
@@ -60,7 +60,7 @@ ActiveRecord::Base.extend Rankle::ClassMethods
|
|
60
60
|
ActiveRecord::Base.send :include, Rankle::InstanceMethods
|
61
61
|
|
62
62
|
class ActiveRecord::Base
|
63
|
-
def self.inherited
|
63
|
+
def self.inherited child
|
64
64
|
super
|
65
65
|
unless child == ActiveRecord::SchemaMigration || child == RankleIndex
|
66
66
|
child.send :after_create, :set_default_position
|
data/lib/rankle_index.rb
CHANGED
@@ -19,28 +19,27 @@ class RankleIndex < ActiveRecord::Base
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def self.rank instance, name, position
|
22
|
-
|
23
|
-
|
24
|
-
RankleIndex.where(indexable_name: name.to_s, indexable_type: instance.class).count
|
22
|
+
existing_indices = if name == :default
|
23
|
+
RankleIndex.where indexable_name: name.to_s, indexable_type: instance.class
|
25
24
|
else
|
26
|
-
RankleIndex.where
|
25
|
+
RankleIndex.where indexable_name: name.to_s
|
27
26
|
end
|
27
|
+
position = existing_indices.length - 1 if position > existing_indices.length
|
28
28
|
position = 0 if position < 0
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
swap(rankle_index, RankleIndex.where(indexable_name: name.to_s, indexable_type: instance.class, indexable_position: rankle_index.indexable_position + swap_distance).first)
|
36
|
-
else
|
37
|
-
swap(rankle_index, RankleIndex.where(indexable_name: name.to_s, indexable_position: rankle_index.indexable_position + swap_distance).first)
|
38
|
-
end
|
29
|
+
index = RankleIndex.where(indexable_name: name.to_s, indexable_id: instance.id, indexable_type: instance.class).first_or_initialize
|
30
|
+
existing_positions = existing_indices.pluck(:indexable_position).compact
|
31
|
+
existing_positions -= [index.indexable_position] unless index.new_record?
|
32
|
+
indexable_position, existing_positions = Rankle::Ranker.insert(position, existing_positions)
|
33
|
+
existing_positions.each_with_index do |position, index|
|
34
|
+
existing_indices[index].update_attribute(:indexable_position, position) unless existing_indices[index].indexable_position = position
|
39
35
|
end
|
36
|
+
index.indexable_position = indexable_position
|
37
|
+
index.save!
|
40
38
|
end
|
41
39
|
|
42
40
|
def self.position instance, name
|
43
|
-
where(indexable_name: name.to_s, indexable_id: instance.id, indexable_type: instance.class).first_or_create!.indexable_position
|
41
|
+
indexable_position = where(indexable_name: name.to_s, indexable_id: instance.id, indexable_type: instance.class).first_or_create!.indexable_position
|
42
|
+
where(indexable_name: name.to_s).where('indexable_position < ?', indexable_position).count
|
44
43
|
end
|
45
44
|
|
46
45
|
def self.ranked name
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative '../support/test_helper'
|
2
|
+
require 'minitest/benchmark'
|
3
|
+
|
4
|
+
class BenchmarkRank < Minitest::Benchmark
|
5
|
+
def self.bench_range
|
6
|
+
Minitest::Benchmark.bench_exp 16, 256, 2
|
7
|
+
end
|
8
|
+
|
9
|
+
def trials
|
10
|
+
1
|
11
|
+
end
|
12
|
+
|
13
|
+
def bench_insert_has_linear_performance
|
14
|
+
assert_performance_linear 0.99 do |n|
|
15
|
+
trials.times do
|
16
|
+
DatabaseCleaner.clean
|
17
|
+
n.times { |name| Fruit.create! name: name }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
DatabaseCleaner.clean
|
22
|
+
end
|
23
|
+
|
24
|
+
def bench_reverse_insert_has_power_performance
|
25
|
+
Fruit.send :ranks, ->(a, b) { a.name.to_i > b.name.to_i }
|
26
|
+
|
27
|
+
assert_performance_power 0.99 do |n|
|
28
|
+
trials.times do
|
29
|
+
DatabaseCleaner.clean
|
30
|
+
n.times { |name| Fruit.create! name: name }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# FIXME: This unfortunate hack reaches into the internals of RankleIndex to reset the test state
|
35
|
+
RankleIndex.instance_variable_set(:@rankers, {})
|
36
|
+
DatabaseCleaner.clean
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require_relative '../support/test_helper'
|
2
|
+
|
3
|
+
describe Rankle::Ranker do
|
4
|
+
describe '.insert into' do
|
5
|
+
describe 'empty array' do
|
6
|
+
describe 'at position 0' do
|
7
|
+
it{ assert_equal [0, []], Rankle::Ranker.insert(0, []) }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'at position -100' do
|
11
|
+
it{ assert_equal [0, []], Rankle::Ranker.insert(-100, []) }
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'at position 100' do
|
15
|
+
it{ assert_equal [0, []], Rankle::Ranker.insert(100, []) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'singleton array' do
|
20
|
+
describe 'at position 0' do
|
21
|
+
it{ assert_equal [-1073741824, [0]], Rankle::Ranker.insert(0, [0]) }
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'at position 1' do
|
25
|
+
it{ assert_equal [1073741823, [0]], Rankle::Ranker.insert(1, [0]) }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'at position -100' do
|
29
|
+
it{ assert_equal [-1073741824, [0]], Rankle::Ranker.insert(-100, [0]) }
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'at position 100' do
|
33
|
+
it{ assert_equal [1073741823, [0]], Rankle::Ranker.insert(100, [0]) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'between non-adjacent elements' do
|
38
|
+
it{ assert_equal [0, [-1, 1]], Rankle::Ranker.insert(1, [-1, 1]) }
|
39
|
+
it{ assert_equal [0, [-1, 2]], Rankle::Ranker.insert(1, [-1, 2]) }
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'saturated' do
|
43
|
+
before do
|
44
|
+
@min_index = Rankle::Ranker::MIN_INDEX
|
45
|
+
@max_index = Rankle::Ranker::MAX_INDEX
|
46
|
+
Rankle::Ranker.send :remove_const, :MIN_INDEX
|
47
|
+
Rankle::Ranker.send :remove_const, :MAX_INDEX
|
48
|
+
Rankle::Ranker::MIN_INDEX = -2
|
49
|
+
Rankle::Ranker::MAX_INDEX = 2
|
50
|
+
end
|
51
|
+
|
52
|
+
it{ assert_raises(IndexError) { Rankle::Ranker.insert(0, [-2, -1, 0, 1, 2]) } }
|
53
|
+
|
54
|
+
after do
|
55
|
+
Rankle::Ranker.send :remove_const, :MIN_INDEX
|
56
|
+
Rankle::Ranker.send :remove_const, :MAX_INDEX
|
57
|
+
Rankle::Ranker::MIN_INDEX = @min_index
|
58
|
+
Rankle::Ranker::MAX_INDEX = @max_index
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'collision' do
|
63
|
+
it{ assert_equal [0, [-715827883, 715827883]], Rankle::Ranker.insert(1, [-1, 0]) }
|
64
|
+
it{ assert_equal [0, [-715827883, 715827883]], Rankle::Ranker.insert(1, [ 0, 1]) }
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'cascading collision' do
|
68
|
+
it{ assert_equal [-858993459, [-1288490189, -429496729, 429496731, 1288490191]], Rankle::Ranker.insert(1, [-2, -1, 0, 1]) }
|
69
|
+
it{ assert_equal [-858993459, [-1288490189, -429496729, 429496731, 1288490191]], Rankle::Ranker.insert(1, [-1, 0, 1, 2]) }
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'half-saturated' do
|
73
|
+
before do
|
74
|
+
@min_index = Rankle::Ranker::MIN_INDEX
|
75
|
+
@max_index = Rankle::Ranker::MAX_INDEX
|
76
|
+
Rankle::Ranker.send :remove_const, :MIN_INDEX
|
77
|
+
Rankle::Ranker.send :remove_const, :MAX_INDEX
|
78
|
+
Rankle::Ranker::MIN_INDEX = -2
|
79
|
+
Rankle::Ranker::MAX_INDEX = 2
|
80
|
+
end
|
81
|
+
|
82
|
+
it{ assert_equal [-1, [-2, 0, 2]], Rankle::Ranker.insert(1, [-2, 0, 2]) }
|
83
|
+
it{ assert_equal [1, [-2, 0, 2]], Rankle::Ranker.insert(2, [-2, 0, 2]) }
|
84
|
+
|
85
|
+
after do
|
86
|
+
Rankle::Ranker.send :remove_const, :MIN_INDEX
|
87
|
+
Rankle::Ranker.send :remove_const, :MAX_INDEX
|
88
|
+
Rankle::Ranker::MIN_INDEX = @min_index
|
89
|
+
Rankle::Ranker::MAX_INDEX = @max_index
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '.balance' do
|
95
|
+
describe 'with default range' do
|
96
|
+
before do
|
97
|
+
@indices = {
|
98
|
+
[-1073741824] => [-1],
|
99
|
+
[1073741823] => [-1],
|
100
|
+
[-1, 1] => [-715827883, 715827883]
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
it do
|
105
|
+
@indices.each do |indices, expected|
|
106
|
+
assert_equal expected, Rankle::Ranker.balance(indices)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'with custom range' do
|
112
|
+
it{ assert_equal [3, 7], Rankle::Ranker.balance([0, 1], min_index: 0, max_index: 10) }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative '../support/test_helper'
|
2
|
+
|
3
|
+
describe RankleIndex do
|
4
|
+
describe 'position' do
|
5
|
+
describe 'when 1 record' do
|
6
|
+
describe 'when indexable_position is 0' do
|
7
|
+
it 'returns 0' do
|
8
|
+
apple = Fruit.create! name: 'apple'
|
9
|
+
RankleIndex.first.update_attribute :indexable_position, 0
|
10
|
+
assert_equal 0, apple.position
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'when indexable_position is 11' do
|
15
|
+
it 'returns 0' do
|
16
|
+
apple = Fruit.create! name: 'apple'
|
17
|
+
RankleIndex.first.update_attribute :indexable_position, 11
|
18
|
+
assert_equal 0, apple.position
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/test/support/models.rb
CHANGED
data/test/support/test_helper.rb
CHANGED
@@ -13,25 +13,23 @@ ActiveRecord::Base.establish_connection(
|
|
13
13
|
rake = Rake.application
|
14
14
|
rake.init
|
15
15
|
rake.load_rakefile
|
16
|
-
|
17
|
-
|
16
|
+
|
17
|
+
[File.dirname(__FILE__) + '/../../db/migrate',
|
18
|
+
'rankle.sqlite3'].each do |path|
|
19
|
+
FileUtils::rm_rf(path) if File.exist?(path)
|
18
20
|
end
|
21
|
+
|
19
22
|
Rails::Generators.invoke 'rankle:install'
|
20
|
-
require File.dirname(__FILE__) + '/../../db/migrate/' + Dir.entries(File.dirname(__FILE__) + '/../../db/migrate')
|
21
|
-
File.delete 'rankle.sqlite3'
|
23
|
+
require File.dirname(__FILE__) + '/../../db/migrate/' + Dir.entries(File.dirname(__FILE__) + '/../../db/migrate').sort.last
|
22
24
|
CreateRankleIndices.new.migrate :up
|
23
25
|
|
24
26
|
load File.dirname(__FILE__) + '/schema.rb'
|
25
27
|
load File.dirname(__FILE__) + '/models.rb'
|
26
28
|
|
27
|
-
DatabaseCleaner.strategy = :
|
29
|
+
DatabaseCleaner.strategy = :deletion
|
28
30
|
|
29
31
|
class Minitest::Test
|
30
32
|
def setup
|
31
|
-
DatabaseCleaner.start
|
32
|
-
end
|
33
|
-
|
34
|
-
def teardown
|
35
33
|
DatabaseCleaner.clean
|
36
34
|
end
|
37
35
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rankle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wil
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06-
|
11
|
+
date: 2015-06-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -145,8 +145,11 @@ files:
|
|
145
145
|
- test/default_behavior_test.rb
|
146
146
|
- test/multiple_resources_test.rb
|
147
147
|
- test/named_ranking_test.rb
|
148
|
+
- test/performance/rank_benchmark.rb
|
148
149
|
- test/scoped_ranking_test.rb
|
149
150
|
- test/simple_usage_test.rb
|
151
|
+
- test/spec/ranker_test.rb
|
152
|
+
- test/spec/rankle_index_test.rb
|
150
153
|
- test/support/models.rb
|
151
154
|
- test/support/schema.rb
|
152
155
|
- test/support/test_helper.rb
|
@@ -178,8 +181,11 @@ test_files:
|
|
178
181
|
- test/default_behavior_test.rb
|
179
182
|
- test/multiple_resources_test.rb
|
180
183
|
- test/named_ranking_test.rb
|
184
|
+
- test/performance/rank_benchmark.rb
|
181
185
|
- test/scoped_ranking_test.rb
|
182
186
|
- test/simple_usage_test.rb
|
187
|
+
- test/spec/ranker_test.rb
|
188
|
+
- test/spec/rankle_index_test.rb
|
183
189
|
- test/support/models.rb
|
184
190
|
- test/support/schema.rb
|
185
191
|
- test/support/test_helper.rb
|