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 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