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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 93e91d72d1ae47471f1be951fdf49bea1b5de6c0
4
- data.tar.gz: e6cee95ce83201559b16638e08eefb73b1cf89ba
3
+ metadata.gz: d2a00ba713d656ff500838729e9ea39ed91d71c5
4
+ data.tar.gz: 1b07a9638a677ca8265d6a146674d222d80b0470
5
5
  SHA512:
6
- metadata.gz: 196913721f0a56bd267fc1f1b17c13e8e4a3dde6251fb22c3a6ffecc4896bb510a9d24ecceb73712d53f50e5a26ed52b699a1fb988a392949a0813020da59f00
7
- data.tar.gz: e3919e144ebd2a1d86f4a42a0105c7bd946567e6caef4061e7d2bd75fb29266c50ed0b4fc3065ee10bb1d6766a380a3300578cdf41893fb58609f54f1c41b79d
6
+ metadata.gz: a48c2ed7910ffc5d4e0c8d23fcfec501e220c82ea7db87f8d88a30dbb500251b4e67dc8c2743d0380182fb75b47a4bbb204cb7440803925f375f9ecdbf0ba26a
7
+ data.tar.gz: 1bb2cee9412e484908a9e94d4095ecda288223b868b487259bdfa0c2d40b1cb8d2d0a4c50258d949d2ba1bc295003e987c98856134698a116e011724cf8dbc79
data/.gitignore CHANGED
@@ -37,3 +37,4 @@ Gemfile.lock
37
37
 
38
38
  # The db directory is used by the test suite to verify the rankle install generator
39
39
  /db
40
+ /bin
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/*_test.rb']
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
@@ -1,3 +1,3 @@
1
1
  module Rankle
2
- VERSION = '0.0.0'
2
+ VERSION = '0.0.1'
3
3
  end
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(child)
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
- rankle_index = RankleIndex.where(indexable_name: name.to_s, indexable_id: instance.id, indexable_type: instance.class).first_or_create!
23
- rankle_index_length = if name == :default
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(indexable_name: name.to_s).count
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
- position = rankle_index_length - 1 if position >= rankle_index_length
30
- rankle_index.update_attribute(:indexable_position, rankle_index_length - 1) unless rankle_index.indexable_position
31
- swap_distance = -1
32
- swap_distance *= -1 if rankle_index.indexable_position < position
33
- until rankle_index.indexable_position == position
34
- if name == :default
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
@@ -19,4 +19,4 @@ class TestDefaultBehavior < Minitest::Test
19
19
 
20
20
  assert_equal ['apple', 'orange'], Fruit.ranked.map(&:name)
21
21
  end
22
- end
22
+ end
@@ -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
@@ -1,5 +1,3 @@
1
- #require_relative '../../lib/rankle_index'
2
-
3
1
  class Fruit < ActiveRecord::Base
4
2
  has_many :rankle_indices, as: :indexable
5
3
  end
@@ -13,25 +13,23 @@ ActiveRecord::Base.establish_connection(
13
13
  rake = Rake.application
14
14
  rake.init
15
15
  rake.load_rakefile
16
- Dir.entries(File.dirname(__FILE__) + '/../../db/migrate').each do |filename|
17
- File.delete(File.dirname(__FILE__) + '/../../db/migrate/' + filename) rescue nil
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')[0]
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 = :truncation
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.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-01 00:00:00.000000000 Z
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