├── .travis.yml ├── bin ├── setup └── console ├── Gemfile ├── lib ├── linked-list │ ├── version.rb │ ├── node.rb │ ├── conversions.rb │ └── list.rb └── linked-list.rb ├── Rakefile ├── .gitignore ├── test ├── test_helper.rb ├── node_test.rb ├── conversions_test.rb └── list_test.rb ├── LICENSE.txt ├── linked-list.gemspec ├── CHANGELOG.md └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.7 4 | cache: bundler 5 | env: 6 | CI: true 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in linked-list.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/linked-list/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Linked 4 | module List 5 | VERSION = '0.0.16' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/linked-list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'linked-list/conversions' 4 | require 'linked-list/node' 5 | require 'linked-list/list' 6 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'linked-list' 6 | 7 | require 'irb' 8 | IRB.start(__FILE__) 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << 'test' 6 | t.pattern = 'test/*_test.rb' 7 | end 8 | 9 | task default: 'test' 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['CI'] 2 | require 'coveralls' 3 | Coveralls.wear! 4 | end 5 | 6 | require 'linked-list' 7 | 8 | gem 'minitest' 9 | require 'minitest/autorun' 10 | require 'minitest/pride' 11 | 12 | class Minitest::Spec 13 | def create_node(*args) 14 | LinkedList::Node.new(*args) 15 | end 16 | 17 | def create_list(*args) 18 | LinkedList::List.new(*args) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/linked-list/node.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module LinkedList 4 | class Node 5 | attr_accessor :data, :next, :prev 6 | 7 | def initialize(data) 8 | @data = data 9 | @next = nil 10 | @prev = nil 11 | end 12 | 13 | # Conversion function, see +Conversions.Node+. 14 | # 15 | # == Returns: 16 | # self 17 | # 18 | def to_node 19 | self 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/node_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe LinkedList::Node do 4 | let(:node) { create_node('foo') } 5 | 6 | describe 'instantiation' do 7 | it 'assigns data' do 8 | assert_equal 'foo', node.data 9 | end 10 | 11 | it 'assigns nil to next' do 12 | assert_nil node.next 13 | end 14 | 15 | it 'assigns nil to prev' do 16 | assert_nil node.prev 17 | end 18 | end 19 | 20 | describe 'accessors' do 21 | it '#data' do 22 | node.data = 'bar' 23 | assert_equal 'bar', node.data 24 | end 25 | 26 | it '#next' do 27 | node.next = 'bar' 28 | assert_equal 'bar', node.next 29 | end 30 | 31 | it '#prev' do 32 | node.prev = 'xyz' 33 | assert_equal 'xyz', node.prev 34 | end 35 | end 36 | 37 | describe 'conversion' do 38 | it '#to_node returns self' do 39 | assert_equal node, node.to_node 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/conversions_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe LinkedList::Conversions do 4 | describe 'Node()' do 5 | it 'returns self if node is given' do 6 | node = create_node('foo') 7 | assert_equal LinkedList::Conversions.Node(node), node 8 | end 9 | 10 | it 'returns new node' do 11 | assert_instance_of LinkedList::Node, 12 | LinkedList::Conversions.Node('foo') 13 | end 14 | end 15 | 16 | describe 'List()' do 17 | it 'returns self if list is given' do 18 | list = create_list 19 | assert_equal LinkedList::Conversions.List(list), list 20 | end 21 | 22 | it 'returns list with nodes made from array' do 23 | list = LinkedList::Conversions.List([1, 2]) 24 | assert_equal [1, 2], [list.first, list.last] 25 | end 26 | 27 | it 'returns new list with one node' do 28 | list = LinkedList::Conversions.List('foo') 29 | assert_equal 'foo', list.first 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Yury Velikanau 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /linked-list.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'linked-list/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'linked-list' 8 | spec.version = Linked::List::VERSION 9 | spec.authors = ['Yury Velikanau'] 10 | spec.email = ['yury.velikanau@gmail.com'] 11 | spec.description = %q(Ruby implementation of Doubly Linked List, following some Ruby idioms.) 12 | spec.summary = %q(Ruby implementation of Doubly Linked List, following some Ruby idioms.) 13 | spec.homepage = 'https://github.com/spectator/linked-list' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '>= 2.0', '< 3.0' 22 | spec.add_development_dependency 'coveralls', '~> 0' 23 | spec.add_development_dependency 'm', '~> 1.5', '>= 1.5.0' 24 | spec.add_development_dependency 'minitest', '>= 5.0', '<= 6.0' 25 | spec.add_development_dependency 'rake', '>= 12.3.3' 26 | end 27 | -------------------------------------------------------------------------------- /lib/linked-list/conversions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module LinkedList 4 | module Conversions 5 | module_function 6 | 7 | # +Node()+ tries to convert its argument to +Node+ object by first calling 8 | # +#to_node+, if that is not availabe then it will instantiate a new +Node+ 9 | # object. 10 | # 11 | # == Returns: 12 | # New +Node+ object. 13 | # 14 | def Node(arg) 15 | if arg.respond_to?(:to_node) 16 | arg.to_node 17 | else 18 | Node.new(arg) 19 | end 20 | end 21 | 22 | # +List()+ tries to conver its argument to +List+ object by first calling 23 | # +#to_list+, if that is not availabe and its argument is an array (or can 24 | # be convertd into array with +#to_ary+) then it will instantiate a new 25 | # +List+ object making nodes from array elements. If none above applies, 26 | # then a new +List+ will be instantiated with one node holding argument 27 | # value. 28 | # 29 | # == Returns: 30 | # New +List+ object. 31 | # 32 | def List(arg) 33 | if arg.respond_to?(:to_list) 34 | arg.to_list 35 | elsif arg.respond_to?(:to_ary) 36 | arg.to_ary.each_with_object(List.new) { |n, l| l.push(Node(n)) } 37 | else 38 | List.new.push(Node(arg)) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.x / Unreleased 2 | 3 | # 0.0.16 / 2021-09-21 4 | 5 | ## Fixed 6 | 7 | - Fixed bug when reference to prev node was not set correctly аfter using `shift`. 8 | 9 | [Compare v0.0.15...v0.0.16](https://github.com/spectator/linked-list/compare/v0.0.15...v0.0.16) 10 | 11 | # 0.0.15 / 2020-05-26 12 | 13 | ## Fixed 14 | 15 | - Fixed bug when deleting the last node of the list. The @tail value was not updated if the @head value was updated. 16 | 17 | [Compare v0.0.14...v0.0.15](https://github.com/spectator/linked-list/compare/v0.0.14...v0.0.15) 18 | 19 | # 0.0.14 / 2020-04-17 20 | 21 | ## Fixed 22 | 23 | Forced minimal rake version in gemspec to skip rake versions with security issues. 24 | 25 | [Compare v0.0.13...v0.0.14](https://github.com/spectator/linked-list/compare/v0.0.13...v0.0.14) 26 | 27 | # 0.0.13 / 2018-11-27 28 | 29 | ## Fixed 30 | 31 | - Delete duplicate method definitions (nex3 in [#7](https://github.com/spectator/linked-list/pull/7)) 32 | 33 | ## Added 34 | 35 | - Allow `List#delete` to delete a `Node` in O(1) time (nex3 in [#6](https://github.com/spectator/linked-list/pull/6)) 36 | 37 | [Compare v0.0.12...v0.0.13](https://github.com/spectator/linked-list/compare/v0.0.12...v0.0.13) 38 | 39 | # 0.0.12 / 2018-09-04 40 | 41 | ## Added 42 | 43 | - Added `insert`, `insert_before` and `insert_after` methods (mpospelov in [#3](https://github.com/spectator/linked-list/pull/3)) 44 | - Added `reverse_each` and `reverse_each_node` methods (mpospelov in [#4](https://github.com/spectator/linked-list/pull/4)) 45 | 46 | [Compare v0.0.11...v0.0.12](https://github.com/spectator/linked-list/compare/v0.0.11...v0.0.12) 47 | 48 | # 0.0.11 / 2018-08-23 49 | 50 | ## Added 51 | 52 | - Added `delete` and `delete_all` methods (mpospelov in [#2](https://github.com/spectator/linked-list/pull/2)) 53 | 54 | [Compare v0.0.10...v0.0.11](https://github.com/spectator/linked-list/compare/v0.0.10...v0.0.11) 55 | 56 | # 0.0.10 / 2018-04-02 57 | 58 | ## Fixed 59 | 60 | - Fixed bug when `@tail.prev` was mistekenly set to `nil` instead of `@tail.next` when popping elements off the list (Sonna in [#1](https://github.com/spectator/linked-list/pull/1)) 61 | 62 | [Compare v0.0.9...v0.0.10](https://github.com/spectator/linked-list/compare/v0.0.9...v0.0.10) 63 | 64 | # 0.0.9 / 2013-12-11 65 | 66 | Initial release. 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Climate](https://codeclimate.com/github/spectator/linked-list.png)](https://codeclimate.com/github/spectator/linked-list) 2 | [![Build Status](https://secure.travis-ci.org/spectator/linked-list.png?branch=master)](http://travis-ci.org/spectator/linked-list) 3 | [![Gem Version](https://badge.fury.io/rb/linked-list.png)](http://badge.fury.io/rb/linked-list) 4 | [![Coverage Status](https://coveralls.io/repos/spectator/linked-list/badge.png)](https://coveralls.io/r/spectator/linked-list) 5 | 6 | # LinkedList 7 | 8 | Ruby implementation of Doubly Linked List, following some Ruby idioms. 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem 'linked-list' 16 | ``` 17 | 18 | And then execute: 19 | 20 | ```shell 21 | $ bundle 22 | ``` 23 | 24 | Or install it yourself as: 25 | 26 | ```shell 27 | $ gem install linked-list 28 | ``` 29 | 30 | ## Usage 31 | 32 | ```ruby 33 | object = Object.new # could be anything 34 | list = LinkedList::List.new 35 | 36 | list.push(object) 37 | list << object # same as `push` 38 | 39 | list.unshift(object) 40 | 41 | list.pop 42 | list.shift 43 | 44 | list.insert(object, before: object) 45 | list.insert(object, before: ->(n) { n == 'foo' }) 46 | 47 | list.insert(object, after: object) 48 | list.insert(object, after: ->(n) { n == 'foo' }) 49 | 50 | list.insert_before(object, node) 51 | list.insert_after(object, node) 52 | 53 | list.reverse 54 | list.reverse! 55 | 56 | list.delete(object) 57 | list.delete { |n| n == 'foo' } 58 | 59 | list.delete_all(object) 60 | list.delete_all { |n| n == 'foo' } 61 | 62 | list.each # Enumerator object 63 | list.each { |e| puts e } 64 | 65 | list.reverse_each # Enumerator object 66 | list.reverse_each { |e| puts e } 67 | 68 | list.reverse_each_node # Enumerator object 69 | list.reverse_each_node { |node| puts node.data } 70 | 71 | list.first # head of the list 72 | list.last # tail of the list 73 | 74 | list.length 75 | list.size # same as `length` 76 | 77 | list.to_a 78 | ``` 79 | 80 | Another way to instantiate `List` or `Node` is to use conversion functions. 81 | First, include `LinkedList::Conversions` module to your class 82 | 83 | ```ruby 84 | class Foo 85 | include LinkedList::Conversions 86 | end 87 | ``` 88 | 89 | Now anywhere in your class you can use the following methods 90 | 91 | ```ruby 92 | Node(object) # will return new `Node` object 93 | List(object) # will return new `List` object with one `Node` object 94 | List([object, object]) # will return new `List` object with two `Node` objects 95 | ``` 96 | 97 | Please see `LinkedList::List`, `LinkedList::Node`, and 98 | `LinkedList::Conversions` for details. 99 | 100 | ## Tests 101 | 102 | Run test with 103 | 104 | ```shell 105 | $ rake 106 | ``` 107 | 108 | ## Contributing 109 | 110 | 1. Fork it 111 | 2. Create your feature branch (`git checkout -b my-new-feature`) 112 | 3. Commit your changes (`git commit -am 'Add some feature'`) 113 | 4. Push to the branch (`git push origin my-new-feature`) 114 | 5. Create new Pull Request 115 | -------------------------------------------------------------------------------- /lib/linked-list/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module LinkedList 4 | class List 5 | include Conversions 6 | 7 | attr_reader :length 8 | alias_method :size, :length 9 | 10 | def initialize 11 | @head = nil 12 | @tail = nil 13 | @length = 0 14 | end 15 | 16 | # Returns the first element of the list or nil. 17 | # 18 | def first 19 | @head && @head.data 20 | end 21 | 22 | # Returns the last element of the list or nil. 23 | # 24 | def last 25 | @tail && @tail.data 26 | end 27 | 28 | # Pushes new nodes to the end of the list. 29 | # 30 | # == Parameters: 31 | # node:: Any object, including +Node+ objects. 32 | # 33 | # == Returns: 34 | # +self+ of +List+ object. 35 | # 36 | def push(node) 37 | node = Node(node) 38 | @head ||= node 39 | 40 | if @tail 41 | @tail.next = node 42 | node.prev = @tail 43 | end 44 | 45 | @tail = node 46 | 47 | @length += 1 48 | self 49 | end 50 | alias_method :<<, :push 51 | 52 | # Pushes new nodes on top of the list. 53 | # 54 | # == Parameters: 55 | # node:: Any object, including +Node+ objects. 56 | # 57 | # == Returns: 58 | # +self+ of +List+ object. 59 | # 60 | def unshift(node) 61 | node = Node(node) 62 | @tail ||= node 63 | 64 | node.next = @head 65 | @head.prev = node if @head 66 | @head = node 67 | 68 | @length += 1 69 | self 70 | end 71 | 72 | # Inserts after or before first matched node.data from the the list by passed block or value. 73 | # 74 | # == Returns: 75 | # Inserted node data 76 | # 77 | def insert(to_add, after: nil, before: nil) 78 | if after && before || !after && !before 79 | raise ArgumentError, 'either :after or :before keys should be passed' 80 | end 81 | matcher = after || before 82 | matcher_proc = if matcher.is_a?(Proc) 83 | __to_matcher(&matcher) 84 | else 85 | __to_matcher(matcher) 86 | end 87 | node = each_node.find(&matcher_proc) 88 | return unless node 89 | new_node = after ? insert_after_node(to_add, node) : insert_before_node(to_add, node) 90 | new_node.data 91 | end 92 | 93 | # Inserts data after first matched node.data. 94 | # 95 | # == Returns: 96 | # Inserted node 97 | # 98 | def insert_after_node(data, node) 99 | Node(data).tap do |new_node| 100 | new_node.prev = node 101 | new_node.next = node.next 102 | if node.next 103 | node.next.prev = new_node 104 | else 105 | @tail = new_node 106 | end 107 | node.next = new_node 108 | @length += 1 109 | end 110 | end 111 | 112 | # Inserts data before first matched node.data. 113 | # 114 | # == Returns: 115 | # Inserted node 116 | # 117 | def insert_before_node(data, node) 118 | Node(data).tap do |new_node| 119 | new_node.next = node 120 | new_node.prev = node.prev 121 | if node.prev 122 | node.prev.next = new_node 123 | else 124 | @head = new_node 125 | end 126 | node.prev = new_node 127 | @length += 1 128 | end 129 | end 130 | 131 | # Removes first matched node.data from the the list by passed block or value. 132 | # 133 | # If +val+ is a +Node+, removes that node from the list. Behavior is 134 | # undefined if +val+ is a +Node+ that's not a member of this list. 135 | # 136 | # == Returns: 137 | # Deleted node's data 138 | # 139 | def delete(val = nil, &block) 140 | if val.respond_to?(:to_node) 141 | node = val.to_node 142 | __unlink_node(node) 143 | return node.data 144 | end 145 | 146 | each_node.find(&__to_matcher(val, &block)).tap do |node_to_delete| 147 | return unless node_to_delete 148 | __unlink_node(node_to_delete) 149 | end.data 150 | end 151 | 152 | # Removes all matched data.data from the the list by passed block or value. 153 | # 154 | # == Returns: 155 | # Array of deleted nodes 156 | # 157 | def delete_all(val = nil, &block) 158 | each_node.select(&__to_matcher(val, &block)).each do |node_to_delete| 159 | next unless node_to_delete 160 | __unlink_node(node_to_delete) 161 | end.map(&:data) 162 | end 163 | 164 | # Removes data from the end of the list. 165 | # 166 | # == Returns: 167 | # Data stored in the node or nil. 168 | # 169 | def pop 170 | return nil unless @head 171 | 172 | tail = __pop 173 | @head = nil unless @tail 174 | 175 | @length -= 1 176 | tail.data 177 | end 178 | 179 | # Removes data from the beginning of the list. 180 | # 181 | # == Returns: 182 | # Data stored in the node or nil. 183 | # 184 | def shift 185 | return nil unless @head 186 | 187 | head = __shift 188 | @tail = nil unless @head 189 | 190 | @length -= 1 191 | head.data 192 | end 193 | 194 | # Reverse list of nodes and returns new instance of the list. 195 | # 196 | # == Returns: 197 | # New +List+ in reverse order. 198 | # 199 | def reverse 200 | List(to_a).reverse! 201 | end 202 | 203 | # Reverses list of nodes in place. 204 | # 205 | # == Returns: 206 | # +self+ in reverse order. 207 | # 208 | def reverse! 209 | return self unless @head 210 | 211 | __each do |curr_node| 212 | curr_node.prev, curr_node.next = curr_node.next, curr_node.prev 213 | end 214 | @head, @tail = @tail, @head 215 | 216 | self 217 | end 218 | 219 | # Iterates over nodes from top to bottom passing node data to the block if 220 | # given. If no block given, returns +Enumerator+. 221 | # 222 | # == Returns: 223 | # +Enumerator+ or yields data to the block stored in every node on the 224 | # list. 225 | # 226 | def each 227 | return to_enum(__callee__) unless block_given? 228 | __each { |node| yield(node.data) } 229 | end 230 | 231 | # Iterates over nodes from top to bottom passing node(LinkedList::Node instance) 232 | # to the block if given. If no block given, returns +Enumerator+. 233 | # 234 | # == Returns: 235 | # +Enumerator+ or yields list nodes to the block 236 | # 237 | def each_node 238 | return to_enum(__callee__) unless block_given? 239 | __each { |node| yield(node) } 240 | end 241 | 242 | 243 | # Iterates over nodes from bottom to top passing node data to the block if 244 | # given. If no block given, returns +Enumerator+. 245 | # 246 | # == Returns: 247 | # +Enumerator+ or yields data to the block stored in every node on the 248 | # list. 249 | # 250 | def reverse_each 251 | return to_enum(__callee__) unless block_given? 252 | __reverse_each { |node| yield(node.data) } 253 | end 254 | 255 | # Iterates over nodes from bottom to top passing node(LinkedList::Node instance) 256 | # to the block if given. If no block given, returns +Enumerator+. 257 | # 258 | # == Returns: 259 | # +Enumerator+ or yields list nodes to the block 260 | # 261 | def reverse_each_node 262 | return to_enum(__callee__) unless block_given? 263 | __reverse_each { |node| yield(node) } 264 | end 265 | 266 | # Converts list to array. 267 | # 268 | def to_a 269 | each.to_a 270 | end 271 | alias_method :to_ary, :to_a 272 | 273 | def inspect 274 | sprintf('#<%s:%#x %s>', self.class, self.__id__, to_a.inspect) 275 | end 276 | 277 | # Conversion function, see +Conversions.List+. 278 | # 279 | # == Returns: 280 | # +self+ 281 | # 282 | def to_list 283 | self 284 | end 285 | 286 | private 287 | 288 | def __unlink_node(node) 289 | @head = node.next if node.prev.nil? 290 | @tail = node.prev if node.next.nil? 291 | 292 | if node.prev.nil? 293 | node.next.prev = nil if node.next 294 | elsif node.next.nil? 295 | node.prev.next = nil if node.prev 296 | else 297 | node.prev.next, node.next.prev = node.next, node.prev 298 | end 299 | @length -= 1 300 | end 301 | 302 | def __to_matcher(val = nil, &block) 303 | raise ArgumentError, 'either value or block should be passed' if val && block_given? 304 | block = ->(e) { e == val } unless block_given? 305 | ->(node) { block.call(node.data) } 306 | end 307 | 308 | def __shift 309 | head = @head 310 | @head = @head.next 311 | @head.prev = nil if @head 312 | head 313 | end 314 | 315 | def __pop 316 | tail = @tail 317 | @tail = @tail.prev 318 | @tail.next = nil if @tail 319 | tail 320 | end 321 | 322 | def __reverse_each 323 | curr_node = @tail 324 | while(curr_node) 325 | yield curr_node 326 | curr_node = curr_node.prev 327 | end 328 | end 329 | 330 | def __each 331 | curr_node = @head 332 | while(curr_node) 333 | yield curr_node 334 | curr_node = curr_node.next 335 | end 336 | end 337 | end 338 | end 339 | -------------------------------------------------------------------------------- /test/list_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe LinkedList::List do 4 | let(:list) { create_list } 5 | let(:node_1) { create_node('foo') } 6 | let(:node_2) { create_node('bar') } 7 | let(:node_3) { create_node('baz') } 8 | 9 | describe 'instantiation' do 10 | it 'assigns first to nil' do 11 | assert_nil list.first 12 | end 13 | 14 | it 'assigns last to nil' do 15 | assert_nil list.last 16 | end 17 | 18 | it 'has zero length' do 19 | assert_equal 0, list.length 20 | end 21 | end 22 | 23 | describe '#push' do 24 | it 'last pushed node can be accessed with #last' do 25 | list.push(node_1) 26 | assert_equal node_1.data, list.last 27 | end 28 | 29 | it 'last pushed node data can be accessed with #first' do 30 | list.push(node_1) 31 | assert_equal node_1.data, list.first 32 | end 33 | 34 | it 'maintains first pushed node as first' do 35 | list.push(node_1) 36 | list.push(node_2) 37 | assert_equal node_1.data, list.first 38 | end 39 | 40 | it 'sets reference to the next node' do 41 | list.push(node_1) 42 | list.push(node_2) 43 | assert_equal node_1.next, node_2 44 | end 45 | 46 | it 'sets reference to the prev node' do 47 | list.push(node_1) 48 | list.push(node_2) 49 | assert_equal node_2.prev, node_1 50 | end 51 | 52 | it 'increases list length by 1' do 53 | list.push(node_1) 54 | assert_equal 1, list.length 55 | end 56 | 57 | it 'returns self' do 58 | assert_equal list.push(node_1), list 59 | end 60 | end 61 | 62 | describe '#unshift' do 63 | it 'last pushed node can be accessed with #last' do 64 | list.unshift(node_1) 65 | assert_equal node_1.data, list.last 66 | end 67 | 68 | it 'last pushed node can be accessed with #first' do 69 | list.unshift(node_1) 70 | assert_equal node_1.data, list.first 71 | end 72 | 73 | it 'maintains first pushed node as last' do 74 | list.unshift(node_1) 75 | list.unshift(node_2) 76 | assert_equal node_1.data, list.last 77 | end 78 | 79 | it 'sets reference to the next node' do 80 | list.unshift(node_1) 81 | list.unshift(node_2) 82 | assert_equal node_2.next, node_1 83 | end 84 | 85 | it 'sets reference to the prev node' do 86 | list.unshift(node_1) 87 | list.unshift(node_2) 88 | assert_equal node_1.prev, node_2 89 | end 90 | 91 | it 'increases list length by 1' do 92 | list.unshift(node_1) 93 | assert_equal 1, list.length 94 | end 95 | 96 | it 'returns self' do 97 | assert_equal list.unshift(node_1), list 98 | end 99 | end 100 | 101 | describe '#pop' do 102 | it 'returns nil if list is empty' do 103 | assert_nil list.pop 104 | end 105 | 106 | it 'returns node from the end of the list' do 107 | list.push(node_1) 108 | list.push(node_2) 109 | assert_equal node_2.data, list.pop 110 | end 111 | 112 | it 'sets #last to nil when all nodes are removed' do 113 | list.push(node_1) 114 | list.pop 115 | assert_nil list.last 116 | end 117 | 118 | it 'sets #first to nil when all nodes are removed' do 119 | list.push(node_1) 120 | list.pop 121 | assert_nil list.first 122 | end 123 | 124 | it 'reduces list length by 1' do 125 | list.push(node_1) 126 | list.pop 127 | assert_equal 0, list.length 128 | end 129 | 130 | it 'maintains list when multiple nodes are removed' do 131 | list.push(node_1) 132 | list.push(node_2) 133 | list.pop 134 | assert_equal [node_1.data], list.to_a 135 | end 136 | end 137 | 138 | describe '#insert' do 139 | it 'raises error if after and before are passed' do 140 | err = assert_raises ArgumentError do 141 | assert_nil list.insert(1, after: 'x', before: 'y') 142 | end 143 | assert_equal err.message, 'either :after or :before keys should be passed' 144 | end 145 | 146 | it 'raises error if no after nore before are passed' do 147 | err = assert_raises ArgumentError do 148 | assert_nil list.insert(1) 149 | end 150 | assert_equal err.message, 'either :after or :before keys should be passed' 151 | end 152 | 153 | describe 'after:' do 154 | describe 'by block' do 155 | it 'does not add value if insert after not found' do 156 | list.push('foo') 157 | assert_nil list.insert(1, after: ->(d) { d == 'foo1' }) 158 | assert_equal ['foo'], list.to_a 159 | end 160 | 161 | it 'inserts value after first matching node by block' do 162 | list.push('foo') 163 | list.push('bar') 164 | assert_equal 1, list.insert(1, after: ->(d) { d == 'foo' }) 165 | assert_equal ['foo', 1, 'bar'], list.to_a 166 | assert_equal 3, list.length 167 | end 168 | 169 | describe 'position edge cases' do 170 | before do 171 | list.push(0) 172 | list.push(1) 173 | list.push(2) 174 | end 175 | 176 | it 'inserts after in the middle' do 177 | list.insert('foo', after: ->(d) { d == 0 }) 178 | assert_equal [0, 'foo', 1, 2], list.to_a 179 | end 180 | 181 | it 'inserts after the tail' do 182 | list.insert('foo', after: ->(d) { d == 2 }) 183 | assert_equal [0, 1, 2, 'foo'], list.to_a 184 | assert_equal 'foo', list.last 185 | end 186 | end 187 | end 188 | 189 | describe 'by value' do 190 | it 'does not add value if insert after not found' do 191 | list.push('foo') 192 | assert_nil list.insert(1, after: 'foo1') 193 | assert_equal ['foo'], list.to_a 194 | end 195 | 196 | it 'inserts value after first matching node by block' do 197 | list.push('foo') 198 | list.push('bar') 199 | assert_equal 1, list.insert(1, after: 'foo') 200 | assert_equal ['foo', 1, 'bar'], list.to_a 201 | assert_equal 3, list.length 202 | end 203 | 204 | describe 'position edge cases' do 205 | before do 206 | list.push(0) 207 | list.push(1) 208 | list.push(2) 209 | end 210 | 211 | it 'inserts after in the middle' do 212 | list.insert('foo', after: 0) 213 | assert_equal [0, 'foo', 1, 2], list.to_a 214 | end 215 | 216 | it 'inserts after the tail' do 217 | list.insert('foo', after: 2) 218 | assert_equal [0, 1, 2, 'foo'], list.to_a 219 | assert_equal 'foo', list.last 220 | end 221 | end 222 | end 223 | end 224 | 225 | describe ':before' do 226 | describe 'by block' do 227 | it 'does not add value if insert before not found' do 228 | list.push('foo') 229 | assert_nil list.insert(1, before: ->(d) { d == 'foo1' }) 230 | assert_equal ['foo'], list.to_a 231 | end 232 | 233 | it 'inserts value before first matching node by block' do 234 | list.push('foo') 235 | list.push('bar') 236 | assert_equal 1, list.insert(1, before: ->(d) { d == 'foo' }) 237 | assert_equal [1, 'foo', 'bar'], list.to_a 238 | assert_equal 3, list.length 239 | end 240 | 241 | describe 'position edge cases' do 242 | before do 243 | list.push(0) 244 | list.push(1) 245 | list.push(2) 246 | end 247 | 248 | it 'inserts before head' do 249 | list.insert('foo', before: ->(d) { d == 0 }) 250 | assert_equal ['foo', 0, 1, 2], list.to_a 251 | assert_equal 'foo', list.first 252 | end 253 | 254 | it 'inserts before in the middle' do 255 | list.insert('foo', before: ->(d) { d == 2 }) 256 | assert_equal [0, 1, 'foo', 2], list.to_a 257 | end 258 | end 259 | end 260 | 261 | describe 'by value' do 262 | it 'does not add value if insert before not found' do 263 | list.push('foo') 264 | assert_nil list.insert(1, before: 'foo1') 265 | assert_equal ['foo'], list.to_a 266 | end 267 | 268 | it 'inserts value before first' do 269 | list.push('foo') 270 | list.push('bar') 271 | assert_equal 1, list.insert(1, before: 'foo') 272 | assert_equal [1, 'foo', 'bar'], list.to_a 273 | assert_equal 3, list.length 274 | end 275 | 276 | describe 'position edge cases' do 277 | before do 278 | list.push(0) 279 | list.push(1) 280 | list.push(2) 281 | end 282 | 283 | it 'inserts before head' do 284 | list.insert('foo', before: 0) 285 | assert_equal ['foo', 0, 1, 2], list.to_a 286 | assert_equal 'foo', list.first 287 | end 288 | 289 | it 'inserts before in the middle' do 290 | list.insert('foo', before: 2) 291 | assert_equal [0, 1, 'foo', 2], list.to_a 292 | end 293 | end 294 | end 295 | end 296 | end 297 | 298 | describe '#insert_after_node' do 299 | it 'inserts value after passed node' do 300 | list.push('foo') 301 | list.push('bar') 302 | node = list.each_node.find { |n| n.data == 'foo' } 303 | assert_equal 1, list.insert_after_node(1, node).data 304 | assert_equal ['foo', 1, 'bar'], list.to_a 305 | assert_equal 3, list.length 306 | end 307 | 308 | describe 'position edge cases' do 309 | before do 310 | list.push(0) 311 | list.push(1) 312 | list.push(2) 313 | end 314 | 315 | it 'inserts after in the middle' do 316 | node = list.each_node.find { |n| n.data == 0 } 317 | list.insert_after_node('foo', node) 318 | assert_equal [0, 'foo', 1, 2], list.to_a 319 | end 320 | 321 | it 'inserts after the tail' do 322 | node = list.each_node.find { |n| n.data == 2 } 323 | list.insert_after_node('foo', node) 324 | assert_equal [0, 1, 2, 'foo'], list.to_a 325 | assert_equal 'foo', list.last 326 | end 327 | end 328 | end 329 | 330 | describe '#insert_after_node' do 331 | it 'inserts value before first' do 332 | list.push('foo') 333 | list.push('bar') 334 | node = list.each_node.find { |n| n.data == 'foo' } 335 | assert_equal 1, list.insert_before_node(1, node).data 336 | assert_equal [1, 'foo', 'bar'], list.to_a 337 | assert_equal 3, list.length 338 | end 339 | 340 | describe 'position edge cases' do 341 | before do 342 | list.push(0) 343 | list.push(1) 344 | list.push(2) 345 | end 346 | 347 | it 'inserts before head' do 348 | node = list.each_node.find { |n| n.data == 0 } 349 | list.insert_before_node('foo', node) 350 | assert_equal ['foo', 0, 1, 2], list.to_a 351 | assert_equal 'foo', list.first 352 | end 353 | 354 | it 'inserts before in the middle' do 355 | node = list.each_node.find { |n| n.data == 2 } 356 | list.insert_before_node('foo', node) 357 | assert_equal [0, 1, 'foo', 2], list.to_a 358 | end 359 | end 360 | end 361 | 362 | describe '#delete' do 363 | it 'raises error if block and value are passed' do 364 | err = assert_raises ArgumentError do 365 | assert_nil list.delete('x') { |d| d == 'x' } 366 | end 367 | assert_equal err.message, 'either value or block should be passed' 368 | end 369 | 370 | describe 'by block' do 371 | it 'returns nil if list is empty' do 372 | assert_nil list.delete { |d| d == 'x' } 373 | end 374 | 375 | it 'deletes value in first matching node' do 376 | calls_count = 0 377 | list.push('foo') 378 | list.push('foo') 379 | list.push('bar') 380 | list.push('foo') 381 | list.delete { |d| calls_count += 1;d == 'bar' } 382 | assert_equal ['foo', 'foo', 'foo'], list.to_a 383 | assert_equal 3, calls_count 384 | end 385 | 386 | it 'returns deleted value' do 387 | list.push('foo') 388 | assert_equal 'foo', list.delete { |d| d == 'foo' } 389 | end 390 | 391 | it 'decreases length of list' do 392 | list.push('foo') 393 | list.push('bar') 394 | list.push('foo') 395 | list.delete { |d| d == 'foo' } 396 | assert_equal 2, list.length 397 | assert_equal ['bar', 'foo'], list.to_a 398 | end 399 | 400 | describe 'position edge cases' do 401 | before do 402 | list.push(0) 403 | list.push(1) 404 | list.push(2) 405 | end 406 | 407 | it 'deletes value from head' do 408 | list.delete { |d| d == 0 } 409 | assert_equal [1, 2], list.to_a 410 | assert_equal 1, list.first 411 | end 412 | 413 | it 'deletes value from middle' do 414 | list.delete { |d| d == 1 } 415 | assert_equal [0, 2], list.to_a 416 | end 417 | 418 | it 'deletes value from tail' do 419 | list.delete { |d| d == 2 } 420 | assert_equal [0, 1], list.to_a 421 | assert_equal 1, list.last 422 | end 423 | end 424 | end 425 | 426 | describe 'by data equality' do 427 | it 'returns nil if list is empty' do 428 | assert_nil list.delete('x') 429 | end 430 | 431 | it 'deletes value in first node' do 432 | list.push('foo') 433 | list.push('foo') 434 | list.push('bar') 435 | list.push('foo') 436 | list.delete('foo') 437 | assert_equal ['foo', 'bar', 'foo'], list.to_a 438 | end 439 | 440 | it 'returns deleted value' do 441 | list.push('foo') 442 | assert_equal 'foo', list.delete('foo') 443 | end 444 | 445 | it 'decreases length of list' do 446 | list.push('foo') 447 | list.push('bar') 448 | list.push('foo') 449 | list.delete('foo') 450 | assert_equal 2, list.length 451 | assert_equal ['bar', 'foo'], list.to_a 452 | end 453 | 454 | describe 'position edge cases' do 455 | before do 456 | list.push(0) 457 | list.push(1) 458 | list.push(2) 459 | end 460 | 461 | it 'deletes value from head' do 462 | list.delete(0) 463 | assert_equal [1, 2], list.to_a 464 | assert_equal 1, list.first 465 | end 466 | 467 | it 'deletes value from middle' do 468 | list.delete(1) 469 | assert_equal [0, 2], list.to_a 470 | end 471 | 472 | 473 | it 'deletes value from tail' do 474 | list.delete(2) 475 | assert_equal [0, 1], list.to_a 476 | assert_equal 1, list.last 477 | end 478 | end 479 | end 480 | 481 | describe 'by node' do 482 | it 'deletes first node' do 483 | list.push(node_1) 484 | list.push(node_2) 485 | list.push(node_3) 486 | list.delete(node_1) 487 | assert_equal ['bar', 'baz'], list.to_a 488 | end 489 | 490 | it 'returns deleted value' do 491 | list.push(node_1) 492 | list.delete(node_1) 493 | assert_equal node_1.data, list.delete(node_1) 494 | end 495 | 496 | it 'decreases length of list' do 497 | list.push('foo') 498 | list.push('bar') 499 | list.push('baz') 500 | list.delete('foo') 501 | assert_equal 2, list.length 502 | end 503 | 504 | describe 'position edge cases' do 505 | before do 506 | list.push(node_1) 507 | list.push(node_2) 508 | list.push(node_3) 509 | end 510 | 511 | it 'deletes value from head' do 512 | list.delete(node_1) 513 | assert_equal [node_2.data, node_3.data], list.to_a 514 | assert_equal node_2.data, list.first 515 | assert_equal node_3.data, list.last 516 | end 517 | 518 | it 'deletes value from middle' do 519 | list.delete(node_2) 520 | assert_equal [node_1.data, node_3.data], list.to_a 521 | assert_equal node_1.data, list.first 522 | assert_equal node_3.data, list.last 523 | end 524 | 525 | 526 | it 'deletes value from tail' do 527 | list.delete(node_3) 528 | assert_equal [node_1.data, node_2.data], list.to_a 529 | assert_equal node_1.data, list.first 530 | assert_equal node_2.data, list.last 531 | end 532 | end 533 | 534 | describe 'delete edge cases' do 535 | it 'resets original list state when deleting the last node of the list' do 536 | assert_nil list.first 537 | assert_nil list.last 538 | 539 | list.push(node_1) 540 | assert_equal node_1.data, list.first 541 | assert_equal node_1.data, list.last 542 | 543 | list.delete(node_1) 544 | assert_nil list.first 545 | assert_nil list.last 546 | end 547 | end 548 | end 549 | end 550 | 551 | describe '#delete_all' do 552 | it 'raises error if block and value are passed' do 553 | err = assert_raises ArgumentError do 554 | assert_nil list.delete_all('x') { |d| d == 'x' } 555 | end 556 | assert_equal err.message, 'either value or block should be passed' 557 | end 558 | 559 | describe 'by block' do 560 | it 'returns nil if list is empty' do 561 | assert_equal list.delete_all { |d| d == 'x' }, [] 562 | end 563 | 564 | it 'deletes value in first matching node' do 565 | list.push('foo') 566 | list.push('foo') 567 | list.push('bar') 568 | list.push('foo') 569 | list.delete_all { |d| d == 'foo' } 570 | assert_equal ['bar'], list.to_a 571 | end 572 | 573 | it 'returns deleted value' do 574 | list.push('foo') 575 | assert_equal ['foo'], list.delete_all { |d| d == 'foo' } 576 | end 577 | 578 | it 'decreases length of list' do 579 | list.push('foo') 580 | list.push('bar') 581 | list.push('foo') 582 | list.delete_all { |d| d == 'foo' } 583 | assert_equal 1, list.length 584 | assert_equal ['bar'], list.to_a 585 | end 586 | end 587 | 588 | describe 'by data equality' do 589 | it 'returns nil if list is empty' do 590 | assert_nil list.delete('x') 591 | end 592 | 593 | it 'deletes all matched values' do 594 | list.push('foo') 595 | list.push('foo') 596 | list.push('bar') 597 | list.push('foo') 598 | list.delete_all('foo') 599 | assert_equal ['bar'], list.to_a 600 | end 601 | 602 | it 'returns deleted value' do 603 | list.push('foo') 604 | assert_equal ['foo'], list.delete_all('foo') 605 | end 606 | 607 | it 'decreases length of list' do 608 | list.push('foo') 609 | list.push('bar') 610 | list.push('foo') 611 | list.delete_all('foo') 612 | assert_equal 1, list.length 613 | assert_equal ['bar'], list.to_a 614 | end 615 | end 616 | end 617 | 618 | describe '#shift' do 619 | it 'returns nil if list is empty' do 620 | assert_nil list.shift 621 | end 622 | 623 | it 'returns node from the top of the list' do 624 | list.push(node_1) 625 | list.push(node_2) 626 | assert_equal node_1.data, list.shift 627 | end 628 | 629 | it 'sets #last to nil when all nodes are removed' do 630 | list.push(node_1) 631 | list.shift 632 | assert_nil list.last 633 | end 634 | 635 | it 'sets #first to nil when all nodes are removed' do 636 | list.push(node_1) 637 | list.shift 638 | assert_nil list.first 639 | end 640 | 641 | it 'sets reference to the prev node' do 642 | list.push(node_1) 643 | list.push(node_2) 644 | assert_nil node_1.prev 645 | assert_equal node_2.prev, node_1 646 | 647 | list.shift 648 | assert_nil node_2.prev 649 | end 650 | 651 | it 'reduces list length by 1' do 652 | list.push(node_1) 653 | list.shift 654 | assert_equal 0, list.length 655 | end 656 | end 657 | 658 | describe '#reverse' do 659 | it 'returns new empty list when receiver list is empty' do 660 | refute_equal list, list.reverse 661 | end 662 | 663 | it 'returns new list in reverse order' do 664 | list.push(node_1) 665 | refute_equal list.reverse, list.reverse! 666 | end 667 | 668 | it 'reverses order of nodes' do 669 | list.push(node_1) 670 | list.push(node_2) 671 | new_list = list.reverse 672 | assert_equal %w(bar foo), [new_list.first, new_list.last] 673 | end 674 | end 675 | 676 | describe '#reverse!' do 677 | it 'returns self when list is empty' do 678 | assert_equal list, list.reverse! 679 | end 680 | 681 | it 'reverses order of nodes' do 682 | list.push(node_1) 683 | list.push(node_2) 684 | list.reverse! 685 | assert_equal [node_2.data, node_1.data], [list.first, list.last] 686 | end 687 | 688 | it 'returns same object' do 689 | list.push(node_1) 690 | assert_equal list, list.reverse! 691 | end 692 | end 693 | 694 | describe '#each_node' do 695 | it 'returns enumerator if no block given' do 696 | assert_instance_of Enumerator, list.each_node 697 | end 698 | 699 | it 'pass each node data to the block' do 700 | list.push(node_1) 701 | list.push(node_2) 702 | nodes = [] 703 | list.each_node { |e| nodes << e } 704 | assert_equal %w(foo bar), nodes.map(&:data) 705 | assert_equal true, nodes.all? { |n| n.is_a?(LinkedList::Node) } 706 | end 707 | end 708 | 709 | describe '#each' do 710 | it 'returns enumerator if no block given' do 711 | assert_instance_of Enumerator, list.each 712 | end 713 | 714 | it 'pass each node data to the block' do 715 | list.push(node_1) 716 | list.push(node_2) 717 | nodes = [] 718 | list.each { |e| nodes << e } 719 | assert_equal %w(foo bar), nodes 720 | end 721 | end 722 | 723 | describe '#each_node' do 724 | it 'returns enumerator if no block given' do 725 | assert_instance_of Enumerator, list.each 726 | end 727 | 728 | it 'pass each node data to the block' do 729 | list.push(node_1) 730 | list.push(node_2) 731 | nodes = [] 732 | list.each_node { |e| nodes << e } 733 | assert_equal %w(foo bar), nodes.map(&:data) 734 | end 735 | end 736 | 737 | describe '#reverse_each' do 738 | it 'returns enumerator if no block given' do 739 | assert_instance_of Enumerator, list.each 740 | end 741 | 742 | it 'pass each node data to the block' do 743 | list.push(node_1) 744 | list.push(node_2) 745 | nodes = [] 746 | list.reverse_each { |e| nodes << e } 747 | assert_equal %w(bar foo), nodes 748 | end 749 | end 750 | 751 | describe '#reverse_each_node' do 752 | it 'returns enumerator if no block given' do 753 | assert_instance_of Enumerator, list.each 754 | end 755 | 756 | it 'pass each node data to the block' do 757 | list.push(node_1) 758 | list.push(node_2) 759 | nodes = [] 760 | list.reverse_each_node { |e| nodes << e } 761 | assert_equal %w(bar foo), nodes.map(&:data) 762 | end 763 | end 764 | 765 | describe '#inspect' do 766 | it 'includes class name' do 767 | assert_match(/LinkedList::List/, list.inspect) 768 | end 769 | 770 | it 'includes object id' do 771 | assert_match(/#{list.object_id.to_s(16)}/, list.inspect) 772 | end 773 | 774 | it 'includes node values' do 775 | list.push(node_1) 776 | assert_match(/foo/, list.inspect) 777 | end 778 | end 779 | 780 | describe 'conversion' do 781 | it '#to_list returns self' do 782 | assert_equal list, list.to_list 783 | end 784 | end 785 | end 786 | --------------------------------------------------------------------------------