rankle 0.0.0 → 0.0.1
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 +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
|