Skip to content

Commit cea0610

Browse files
authored
Fix Heap comparison not working on some Objects (#60)
1 parent b9d2a15 commit cea0610

File tree

2 files changed

+187
-1
lines changed

2 files changed

+187
-1
lines changed

lib/containers/heap.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def consolidate
368368
degree += 1
369369
end
370370
degrees[degree] = root
371-
min = root if min.key == root.key # this fixes a bug with duplicate keys not being in the right order
371+
min = root if !@compare_fn[min.key, root.key] # this fixes a bug with duplicate keys not being in the right order
372372
end
373373
end
374374
@next = min

spec/heap_spec.rb

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,196 @@
11
$: << File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib')
22
require 'algorithms'
33

4+
include Comparable
5+
6+
class Book
7+
attr_reader :title, :year_published
8+
9+
def initialize(title, year_published)
10+
@title = title
11+
@year_published = year_published
12+
end
13+
14+
def <=>(other)
15+
return nil unless other.is_a?(Book)
16+
17+
# 1. Compare by year first
18+
year_comparison = self.year_published <=> other.year_published
19+
20+
# 2. If years are different, return that comparison result
21+
return year_comparison unless year_comparison == 0
22+
23+
# 3. If years are the same (tie), compare by title alphabetically
24+
# (Ensure title comparison returns -1, 0, or 1)
25+
self.title <=> other.title
26+
end
27+
28+
29+
def to_s
30+
"\"#{title}\" (#{year_published})" # Simplified for test output
31+
end
32+
33+
# Add an equality method for clearer test failures if needed,
34+
# though <=> returning 0 handles equality for sorting/heap purposes.
35+
def ==(other)
36+
other.is_a?(Book) &&
37+
self.title == other.title &&
38+
self.year_published == other.year_published
39+
end
40+
alias eql? ==
41+
42+
def hash
43+
[title, year_published].hash
44+
end
45+
46+
end
47+
48+
describe ::Containers::Heap do
49+
50+
# --- Test Data Setup ---
51+
let!(:book_1932) { Book.new("Brave New World", 1932) }
52+
let!(:book_1949) { Book.new("1984", 1949) }
53+
let!(:book_1951a) { Book.new("Foundation", 1951) }
54+
# Add another book from the same year to test handling of equal priority items
55+
let!(:book_1951b) { Book.new("The Illustrated Man", 1951) }
56+
let!(:book_1965) { Book.new("Dune", 1965) }
57+
let!(:book_1989) { Book.new("Hyperion", 1989) }
58+
59+
# Use shuffle to ensure tests don't depend on insertion order
60+
let(:books) { [book_1965, book_1951a, book_1989, book_1949, book_1932, book_1951b].shuffle }
61+
62+
let(:expected_min_order) { books.sort }
63+
64+
let(:expected_max_order) { books.sort.reverse }
65+
66+
context "Min-Heap (using default <=> via Comparable)" do
67+
# Initialize heap with books; it should use Book#<=> by default
68+
let(:min_heap) { ::Containers::Heap.new(books) }
69+
70+
it "initializes with the correct size" do
71+
expect(min_heap.size).to eq(books.size)
72+
expect(min_heap.empty?).to be false
73+
end
74+
75+
it "gets the next minimum element (earliest year) without removing it" do
76+
# next should return the element with the smallest year_published
77+
expect(min_heap.next).to eq(book_1932)
78+
# next should not change the size
79+
expect(min_heap.size).to eq(books.size)
80+
end
81+
82+
it "pops elements in ascending order of year_published" do
83+
popped_books = []
84+
while (book = min_heap.pop)
85+
popped_books << book
86+
end
87+
88+
# Verify the popped order matches the expected sorted order
89+
expect(popped_books).to eq(expected_min_order)
90+
91+
# Verify the heap is now empty
92+
expect(min_heap.size).to eq(0)
93+
expect(min_heap.empty?).to be true
94+
expect(min_heap.pop).to be_nil # Popping an empty heap
95+
expect(min_heap.next).to be_nil # Getting next from an empty heap
96+
end
97+
98+
it "correctly updates size after popping" do
99+
expect(min_heap.size).to eq(books.size)
100+
min_heap.pop
101+
expect(min_heap.size).to eq(books.size - 1)
102+
end
103+
end
104+
105+
106+
context "Max-Heap (using a custom comparison block)" do
107+
# Initialize heap with books and a block for max-heap behavior
108+
# The block returns true if x should have higher priority (be "larger") than y
109+
let(:max_heap) { ::Containers::Heap.new(books) { |x, y| x > y } } # Use > from Comparable
110+
111+
it "initializes with the correct size" do
112+
expect(max_heap.size).to eq(books.size)
113+
expect(max_heap.empty?).to be false
114+
end
115+
116+
it "gets the next maximum element (latest year) without removing it" do
117+
# next should return the element with the largest year_published
118+
expect(max_heap.next).to eq(book_1989)
119+
# next should not change the size
120+
expect(max_heap.size).to eq(books.size)
121+
end
122+
123+
it "pops elements in descending order of year_published" do
124+
popped_books = []
125+
while (book = max_heap.pop)
126+
popped_books << book
127+
end
128+
129+
# Verify the popped order matches the expected reversed sorted order
130+
expect(popped_books).to eq(expected_max_order)
131+
132+
# Verify the heap is now empty
133+
expect(max_heap.size).to eq(0)
134+
expect(max_heap.empty?).to be true
135+
expect(max_heap.pop).to be_nil # Popping an empty heap
136+
expect(max_heap.next).to be_nil # Getting next from an empty heap
137+
end
138+
139+
it "correctly updates size after popping" do
140+
expect(max_heap.size).to eq(books.size)
141+
max_heap.pop
142+
expect(max_heap.size).to eq(books.size - 1)
143+
end
144+
end
145+
146+
context "Edge cases" do
147+
it "handles an empty initialization" do
148+
heap = ::Containers::Heap.new([])
149+
expect(heap.size).to eq(0)
150+
expect(heap.empty?).to be true
151+
expect(heap.pop).to be_nil
152+
expect(heap.next).to be_nil
153+
end
154+
155+
it "handles initialization with one element" do
156+
heap = ::Containers::Heap.new([book_1965])
157+
expect(heap.size).to eq(1)
158+
expect(heap.next).to eq(book_1965)
159+
expect(heap.pop).to eq(book_1965)
160+
expect(heap.empty?).to be true
161+
end
162+
end
163+
164+
end
165+
4166
describe Containers::Heap do
5167
before(:each) do
6168
@heap = Containers::MaxHeap.new
7169
end
170+
171+
it "should run without error when given Objects and a non-ordering comparator" do
172+
# Create and store the initial distinct objects
173+
initial_objects = 10.times.map { Object.new }
174+
initial_object_ids = initial_objects.map(&:object_id) # Store IDs for comparison
175+
176+
min_heap = ::Containers::Heap.new(initial_objects) { |x, y| (x <=> y) == -1 }
177+
178+
expect(min_heap.size).to eq(10)
179+
180+
popped_elements = []
181+
expect {
182+
while val = min_heap.pop do
183+
popped_elements << val
184+
end
185+
}.not_to raise_error
186+
187+
expect(min_heap.empty?).to be true
188+
expect(popped_elements.size).to eq(10)
189+
190+
# Assert that exactly the same objects were returned, regardless of order.
191+
# Comparing by object_id is the most reliable way for distinct Objects.
192+
expect(popped_elements.map(&:object_id)).to contain_exactly(*initial_object_ids)
193+
end
8194

9195
it "should not let you merge with non-heaps" do
10196
expect { @heap.merge!(nil) }.to raise_error(ArgumentError)

0 commit comments

Comments
 (0)