|
1 | 1 | $: << File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib') |
2 | 2 | require 'algorithms' |
3 | 3 |
|
| 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 | + |
4 | 166 | describe Containers::Heap do |
5 | 167 | before(:each) do |
6 | 168 | @heap = Containers::MaxHeap.new |
7 | 169 | 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 |
8 | 194 |
|
9 | 195 | it "should not let you merge with non-heaps" do |
10 | 196 | expect { @heap.merge!(nil) }.to raise_error(ArgumentError) |
|
0 commit comments