├── .travis.yml ├── Rakefile ├── .gitignore ├── deep_enumerable.gemspec ├── LICENSE ├── README.md ├── lib └── deep_enumerable.rb └── test └── deep_enumerable_test.rb /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.libs.push "lib" 5 | t.test_files = FileList['test/*_test.rb'] 6 | t.verbose = true 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | doc/ 19 | 20 | # vim files 21 | *.swp 22 | 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /deep_enumerable.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'deep_enumerable' 3 | s.version = '0.1.4' 4 | s.date = '2015-12-18' 5 | s.summary = "A library for manipulating nested collections" 6 | s.description = "A library for manipulating nested collections. See https://github.com/dgopstein/deep_enumerable for details" 7 | s.email = 'dan@gopstein.com' 8 | s.files = ["lib/deep_enumerable.rb"] 9 | s.authors = ["Dan Gopstein"] 10 | s.homepage = 'https://github.com/dgopstein/deep_enumerable' 11 | s.license = 'MIT' 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 dgopstein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DeepEnumerable (α) [![Oh Noes!](https://travis-ci.org/dgopstein/deep_enumerable.png?branch=master)](https://travis-ci.org/dgopstein/deep_enumerable) [![Code Climate](https://codeclimate.com/github/dgopstein/deep_enumerable/badges/gpa.svg)](https://codeclimate.com/github/dgopstein/deep_enumerable) 2 | =============== 3 | 4 | A library for manipulating nested collections in Ruby 5 | 6 | ## Installation 7 | 8 | ```gem install deep_enumerable``` 9 | 10 | ## Usage 11 | 12 | ```require 'deep_enumerable'``` 13 | 14 | ## Docs 15 | http://www.rubydoc.info/gems/deep_enumerable/DeepEnumerable 16 | 17 | ## What is a nested collection? 18 | A nested collection is a data structure wrapped in another data structure! 19 | 20 | For example, a flat array might look like: `[1, 2, 3]` while a nested array might look like: `[1, [2, 3]]` 21 | 22 | Other collections can be nested as well, e.g. Hashes: `{:a => :b, :c => {:d => :e}}` 23 | 24 | Collections can even be nested inside collections of a different type, as in lists of hashes: `[{:name => 'alice'}, {:name => 'bob'}]`, or hashes of lists: `{:name => 'carol', :favorite_colors => [:yellow, :blue]}` 25 | 26 | ## What is DeepEnumerable? 27 | 28 | Ruby has excellent native support for a few common collections such as Array, Hash, Set and Range. At the heart of each of these collection libraries is the Enumerable module which provides dozens of general purpose methods (map, inject, select) implemented on top of each base class's `:each` method. Enumerable's methods make operating on traditional collections clear, concise and less error prone. Dealing with nested collections, however, is still relatively clunky. Consider a simple logging configuration: 29 | 30 | ```ruby 31 | >> conf_values = { 32 | :level => :error, 33 | :appender => { 34 | :file => '/var/log/error', 35 | :update_interval => :∞ 36 | } 37 | } 38 | ``` 39 | 40 | We might reasonably want to do some sanity checking on the types of the configuration. For instance, if `:update_interval` were not an integer we would like to know before trying to operate on that value. With vanilla ruby we would need to imperatively test every element, which is tedious and potentially error producing: 41 | 42 | ```ruby 43 | >> Symbol === conf_values[:level] 44 | => true 45 | >> String === conf_values[:appender][:file] 46 | => true 47 | >> Fixnum === conf_values[:appender][:update_interval] 48 | => false 49 | >> String === conf_values[:output][:format] 50 | NoMethodError: undefined method `[]' for nil:NilClass 51 | ``` 52 | 53 | Instead using a DeepEnumerable we can model our rules as data, and find errors in a single expression: 54 | 55 | ```ruby 56 | >> conf_types = { 57 | :level => Symbol, 58 | :appender => { 59 | :file => String, 60 | :update_interval => Fixnum 61 | }, 62 | :output => {:format => String} 63 | } 64 | 65 | >> conf_types.deep_outersect(conf_values, &:===) 66 | => {:appender=>{:update_interval=>[Fixnum, :∞]}, :output=>[{:format=>String}, nil]} 67 | 68 | ``` 69 | 70 | ## What else is DeepEnumerable? 71 | 72 | DeepEnumerable provides a few interesting methods on a couple different standard data structures. Here are some examples: 73 | 74 | Iterate and transform leaf nodes: 75 | ```ruby 76 | >> {a: {b: 1, c: {d: 2, e: 3}, f: 4}, g: 5}.deep_flat_map{|k,v| v*2} 77 | => [2, 4, 6, 8, 10] 78 | ``` 79 | 80 | Retrieve a nested element from a DeepEnumerable: 81 | 82 | ```ruby 83 | >> prefix_tree = {"a"=>{"a"=>"aardvark", "b"=>["abacus", "abaddon"], "c"=>"actuary"}} 84 | >> prefix_tree.deep_get("a"=>"b") 85 | => ["abacus", "abadon"] 86 | ``` 87 | 88 | ## What else could be a DeepEnumerable in the future? 89 | 90 | Right now DeepEnumerable ships with default implementations for Array's and Hash's. Like Enumerable, all of DeepEnumerable's methods are built on top of only a single iterator, `:shallow_keys`, which means if your data structure implements `:shallow_keys`, your data structure can simply include the DeepEnumerable module and get a mixin-ful of useful methods. If implementing your own `:shallow_keys` sounds scary, just look to the default implementations in Array and Hash - they're quite modest: 91 | 92 | Hash: 93 | ```ruby 94 | alias_method :shallow_keys, :keys 95 | ``` 96 | 97 | Array: 98 | ```ruby 99 | def shallow_keys 100 | (0...size).to_a 101 | end 102 | ``` 103 | 104 | ## Contributing 105 | 106 | Pull requests welcome. 107 | 108 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. 109 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. 110 | * Fork the project. 111 | * Start a feature/bugfix branch. 112 | * Commit and push until you are happy with your contribution. 113 | * Make sure to add tests for it. This is important so we don't break it in a future version unintentionally. 114 | * Submit a pull request 115 | -------------------------------------------------------------------------------- /lib/deep_enumerable.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # A set of general methods that can be applied to any conformant nested structure 3 | module DeepEnumerable 4 | ## 5 | # Subtracts the leaves of one DeepEnumerable from another. 6 | # 7 | # @return a result of the same structure as the primary DeepEnumerable. 8 | # 9 | # @example 10 | # >> alice = {name: "alice", age: 26} 11 | # >> bob = {name: "bob", age: 26} 12 | # >> alice.deep_diff(bob) 13 | # => {:name=>"alice"} 14 | # 15 | # >> bob = {friends: ["alice","carol"]} 16 | # >> carol = {friends: ["alice","bob"]} 17 | # >> bob.deep_diff(carol) 18 | # => {:friends=>"carol"} 19 | # 20 | def deep_diff(other, &block) 21 | shallow_keys.each_with_object(empty) do |key, res| 22 | s_val = (self[key] rescue nil) #TODO don't rely on rescue 23 | o_val = (other[key] rescue nil) 24 | 25 | comparator = block || :==.to_proc 26 | 27 | if s_val.respond_to?(:deep_diff) && o_val.respond_to?(:deep_diff) 28 | diff = s_val.deep_diff(o_val, &block) 29 | res[key] = diff if diff.any? 30 | elsif !comparator.call(s_val, o_val) 31 | res[key] = s_val 32 | end 33 | end 34 | end 35 | 36 | ## 37 | # Computes the complement of the intersection of two DeepEnumerables. 38 | # 39 | # @return The common structure of both arguments, with tuples containing differing values in the leaf nodes. 40 | # 41 | # @example 42 | # >> alice = {:name=>"alice", :age=>26} 43 | # >> bob = {:name=>"bob", :age=>26} 44 | # >> alice.deep_diff_symmetric(bob) 45 | # => {:name=>["alice", "bob"]} 46 | # 47 | # >> bob = {:friends=>["alice","carol"]} 48 | # >> carol = {:friends=>["alice","bob"]} 49 | # >> bob.deep_diff_symmetric(carol) 50 | # => {:friends=>{1=>["carol", "bob"]}} 51 | # 52 | def deep_diff_symmetric(other, &block) 53 | (shallow_keys + other.shallow_keys).each_with_object({}) do |key, res| 54 | s_val = (self[key] rescue nil) #TODO don't rely on rescue 55 | o_val = (other[key] rescue nil) 56 | 57 | comparator = block || :==.to_proc 58 | 59 | if s_val.respond_to?(:deep_diff_symmetric) && o_val.respond_to?(:deep_diff_symmetric) 60 | diff = s_val.deep_diff_symmetric(o_val, &block) 61 | res[key] = diff if diff.any? 62 | elsif !comparator.call(s_val, o_val) 63 | res[key] = [s_val, o_val] 64 | end 65 | end 66 | end 67 | alias_method :deep_outersect, :deep_diff_symmetric 68 | 69 | ## 70 | # Deeply copy a DeepEnumerable 71 | # 72 | # @return the same data structure at a different memory address 73 | def deep_dup 74 | deep_select{true} 75 | end 76 | 77 | ## 78 | # Iterate elements of a DeepEnumerable 79 | # 80 | # @example 81 | # >> {event: {id: 1, title: 'bowling'}}.deep_each.to_a 82 | # => [[{:event=>:id}, 1], [{:event=>:title}, "bowling"]] 83 | # 84 | # >> [:a, [:b, :c]].deep_each.to_a 85 | # => [[0, :a], [{1=>0}, :b], [{1=>1}, :c]] 86 | # 87 | # >> {events: [{title: 'movie'}, {title: 'dinner'}]}.deep_each.to_a 88 | # => [[{:events=>{0=>:title}}, "movie"], [{:events=>{1=>:title}}, "dinner"]] 89 | # 90 | # @return an iterator over each deep-key/value pair for every leaf 91 | def deep_each(&block) 92 | depth_first_map.each(&block) 93 | end 94 | 95 | ## 96 | # Concatenate all the results from the supplied code block together. 97 | # 98 | # @return an array with the results of running +block+ once for every leaf element in the original structure, all flattened together. 99 | # 100 | # @example 101 | # >> {a: {b: 1, c: {d: 2, e: 3}, f: 4}, g: 5}.deep_flat_map{|k,v| v*2} 102 | # => [2, 4, 6, 8, 10] 103 | # 104 | # >> {a: {b: 1, c: {d: 2, e: 3}, f: 4}, g: 5}.deep_flat_map{|k,v| [v, v*2]} 105 | # => [1, 2, 2, 4, 3, 6, 4, 8, 5, 10] 106 | def deep_flat_map(&block) 107 | deep_each.flat_map(&block) 108 | end 109 | 110 | ## 111 | # Retrieve a nested element from a DeepEnumerable 112 | # 113 | # @example 114 | # 115 | # >> prefix_tree = {"a"=>{"a"=>"aardvark", "b"=>["abacus", "abadon"], "c"=>"actuary"}} 116 | # 117 | # >> prefix_tree.deep_get("a") 118 | # => {"a"=>"aardvark", "b"=>["abacus", "abadon"], "c"=>"actuary"} 119 | # 120 | # >> prefix_tree.deep_get("a"=>"b") 121 | # => ["abacus", "abadon"] 122 | # 123 | # @return a DeepEnumerable representing the subtree specified by the query key 124 | # 125 | def deep_get(key) 126 | if DeepEnumerable::nested_key?(key) 127 | key_head, key_tail = DeepEnumerable::split_key(key) 128 | if self[key_head].respond_to?(:deep_get) 129 | self[key_head].deep_get(key_tail) 130 | else 131 | nil #SHOULD? raise an error 132 | end 133 | else 134 | self[key] 135 | end 136 | end 137 | 138 | ## 139 | # Fold over all leaf nodes 140 | # 141 | # @example 142 | # >> friends = [{name: 'alice', age: 26}, {name: 'bob', age: 26}] 143 | # >> friends.deep_inject(Hash.new{[]}) {|sum, (k, v)| sum[k.values.first] <<= v; sum} 144 | # => {:name=>["alice", "bob"], :age=>[26, 26]} 145 | # 146 | # @return The accumulation of the results of executing the provided block over every element in the DeepEnumerable 147 | def deep_inject(initial, &block) 148 | deep_each.inject(initial, &block) 149 | end 150 | 151 | ## 152 | # Describes the similarities between two DeepEnumerables. 153 | # 154 | # @example 155 | # >> alice = {:name=>"alice", :age=>26} 156 | # >> bob = {:name=>"bob", :age=>26} 157 | # >> alice.deep_intersect(bob) 158 | # => {:age=>26} 159 | # 160 | # >> bob = {:friends=>["alice","carol"]} 161 | # >> carol = {:friends=>["alice","bob"]} 162 | # >> bob.deep_intersect(carol) 163 | # => {:friends=>["alice"]} 164 | # 165 | # @return a result of the same structure as the primary DeepEnumerable. 166 | # 167 | def deep_intersect(other, &block) 168 | (shallow_keys + other.shallow_keys).each_with_object(empty) do |key, res| 169 | s_val = (self[key] rescue nil) #TODO don't rely on rescue 170 | o_val = (other[key] rescue nil) 171 | 172 | comparator = block || :==.to_proc 173 | 174 | if s_val.respond_to?(:deep_intersect) && o_val.respond_to?(:deep_intersect) 175 | int = s_val.deep_intersect(o_val, &block) 176 | res[key] = int if int.any? 177 | elsif comparator.call(s_val, o_val) 178 | res[key] = s_val 179 | end 180 | end 181 | end 182 | 183 | ## 184 | # Returns the result of running block on each leaf of this DeepEnumerable 185 | # 186 | # @example 187 | # >> h = {a: [1, 2]} 188 | # >> h.deep_map!{|k, v| [k, v]} 189 | # >> h 190 | # => {:a=>[[{:a=>0}, 1], [{:a=>1}, 2]]} 191 | # 192 | # @return The original structure updated by the result of the block 193 | def deep_map!(&block) 194 | if block_given? 195 | deep_each{|k,v| deep_set(k, block.call([k, v]))} 196 | self 197 | else 198 | deep_each 199 | end 200 | end 201 | 202 | ## 203 | # Create a new nested structure populated by the result of executing +block+ on the deep-keys and values of the original DeepEnumerable 204 | # 205 | # @example 206 | # >> {a: [1, 2]}.deep_map{|k, v| [k, v]} 207 | # => {:a=>[[{:a=>0}, 1], [{:a=>1}, 2]]} 208 | # 209 | # @return A copy of the input, updated by the result of the block 210 | def deep_map(&block) 211 | deep_dup.deep_map!(&block) 212 | end 213 | 214 | ## 215 | # Modifies this collection to use the result of +block+ as the values 216 | # 217 | # @example 218 | # >> h = {a: [1, 2]} 219 | # >> h.deep_map_values!{v| v*2} 220 | # >> h 221 | # => {:a=>[2, 4]} 222 | # 223 | # @return The original structure updated by the result of the block 224 | def deep_map_values!(&block) 225 | deep_map!{|_, v| block.call(v)} 226 | end 227 | 228 | ## 229 | # Creates a new nested structure populated by the result of executing +block+ on the values of the original DeepEnumerable 230 | # 231 | # @example 232 | # >> {a: [1, 2].deep_map_values{v| v*2} 233 | # => {:a=>[2, 4]} 234 | # 235 | # @return A copy of the input, updated by the result of the block 236 | def deep_map_values(&block) 237 | deep_dup.deep_map_values!(&block) 238 | end 239 | 240 | ## 241 | # Filter leaf nodes by the result of the given block 242 | # 243 | # @example 244 | # >> inventory = {fruit: {apples: 4, oranges: 7}} 245 | # 246 | # >> inventory.deep_reject{|k, v| v > 5} 247 | # => {:fruit=>{:apples=>4}} 248 | # 249 | # >> inventory.deep_reject(&:even?) 250 | # => {:fruit=>{:oranges=>7}} 251 | # 252 | # @return a copy of the input, filtered by the given predicate 253 | # 254 | def deep_reject(&block) 255 | new_block = 256 | case block.arity 257 | when 2 then ->(k,v){!block.call(k, v)} 258 | else ->(v){ !block.call(v)} 259 | end 260 | deep_select(&new_block) 261 | end 262 | 263 | ## 264 | # Filter leaf nodes by the result of the given block 265 | # 266 | # @example 267 | # >> inventory = {fruit: {apples: 4, oranges: 7}} 268 | # 269 | # >> inventory.deep_select{|k, v| v > 5} 270 | # => {:fruit=>{:oranges=>7}} 271 | # 272 | # >> inventory.deep_select(&:even?) 273 | # => {:fruit=>{:apples=>4}} 274 | # 275 | # @return a copy of the input, filtered by the given predicate 276 | def deep_select(&block) 277 | copy = self.select{false} # get an empty version of this shallow collection 278 | 279 | # insert/push a selected item into the copied enumerable 280 | accept = lambda do |k, v| 281 | # Don't insert elements at arbitrary positions in an array if appending is an option 282 | if copy.respond_to?('push') # jruby has a Hash#<< method 283 | copy.push(v) 284 | else 285 | copy[k] = v 286 | end 287 | end 288 | 289 | shallow_each do |k, v| 290 | if v.respond_to?(:deep_select) 291 | selected = v.deep_select(&block) 292 | accept.call(k, selected) 293 | else 294 | res = 295 | case block.arity 296 | when 2 then block.call(k, v) 297 | else block.call(v) 298 | end 299 | 300 | if res 301 | accept.call(k, (v.dup rescue v)) # FixNum's and Symbol's can't/shouldn't be dup'd 302 | end 303 | end 304 | end 305 | 306 | copy 307 | end 308 | 309 | ## 310 | # Update a DeepEnumerable, indexed by an Array or Hash. 311 | # 312 | # Intermediate values are created when necessary, with the same type as its parent. 313 | # 314 | # @example 315 | # >> [].deep_set({1 => 2}, 3) 316 | # => [nil, [nil, nil, 3]] 317 | # >> {}.deep_set([1, 2, 3], 4) 318 | # => {1=>{2=>{3=>4}}} 319 | # 320 | # @return (tentative) returns the object that's been modified. Warning: This behavior is subject to change. 321 | # 322 | def deep_set(key, val) 323 | if DeepEnumerable::nested_key?(key) 324 | key_head, key_tail = DeepEnumerable::split_key(key) 325 | 326 | if key_tail.nil? 327 | self[key_head] = val 328 | else 329 | if self[key_head].respond_to?(:deep_set) 330 | self[key_head].deep_set(key_tail, val) 331 | else 332 | self[key_head] = empty.deep_set(key_tail, val) 333 | end 334 | end 335 | elsif !key.nil? # don't index on nil 336 | self[key] = val 337 | end 338 | 339 | self #SHOULD? return val instead of self 340 | end 341 | 342 | ## 343 | # List the values stored at every leaf 344 | # 345 | # @example 346 | # >> prefix_tree = {"a"=>{"a"=>"aardvark", "b"=>["abacus", "abadon"], "c"=>"actuary"}} 347 | # >> prefix_tree.deep_values 348 | # => ["aardvark", "abacus", "abadon", "actuary"] 349 | # 350 | # @return a list of every leaf value 351 | def deep_values 352 | deep_flat_map{|_, v| v} 353 | end 354 | 355 | ## 356 | # Combine two DeepEnumerables into one, with the elements from each joined into tuples 357 | # 358 | # @example 359 | # >> inventory = {fruit: {apples: 4, oranges: 7}} 360 | # >> prices = {fruit: {apples: 0.79, oranges: 1.21}} 361 | # >> inventory.deep_zip(prices) 362 | # => {:fruit=>{:apples=>[4, 0.79], :oranges=>[7, 1.21]}} 363 | # 364 | # @return one data structure with elements from both arguments joined together 365 | # 366 | def deep_zip(other) 367 | (shallow_keys).inject(empty) do |res, key| 368 | s_val = self[key] 369 | o_val = (other[key] rescue nil) #TODO don't rely on rescue 370 | 371 | comparator = :==.to_proc 372 | 373 | if s_val.respond_to?(:deep_zip) && o_val.respond_to?(:deep_zip) 374 | diff = s_val.deep_zip(o_val) 375 | diff.empty? ? res : res.deep_set(key, diff) 376 | else 377 | res.deep_set(key, [s_val, o_val]) 378 | end 379 | end 380 | end 381 | 382 | ## 383 | # A copy of the DeepEnumerable containing no elements 384 | # 385 | # @example 386 | # >> inventory = {fruit: {apples: 4, oranges: 7}} 387 | # >> inventory.empty 388 | # => {} 389 | # 390 | # @return a new object of the same type as the original collection, only empty 391 | # 392 | def empty 393 | select{false} 394 | end 395 | 396 | # Provide a homogenous |k,v| iterator for Arrays/Hashes/DeepEnumerables 397 | #TODO test this 398 | def shallow_key_value_pairs 399 | shallow_keys.map{|k| [k, self[k]]} 400 | end 401 | 402 | ## 403 | # Replaces every top-level element with the result of the given block 404 | def shallow_map_keys!(&block) 405 | new_kvs = shallow_key_value_pairs.map do |k, v| 406 | new_key = 407 | if block.arity == 2 408 | block.call(k, v) 409 | else 410 | block.call(k) 411 | end 412 | 413 | self.delete(k) #TODO This is not defined on Enumerable! 414 | [new_key, v] 415 | end 416 | 417 | new_kvs.each do |k, v| 418 | self[k] = v 419 | end 420 | 421 | self 422 | end 423 | 424 | ## 425 | # Returns a new collection where every top-level element is replaced with the result of the given block 426 | def shallow_map_keys(&block) 427 | deep_dup.shallow_map_keys!(&block) 428 | end 429 | 430 | ## 431 | # Replaces every top-level element with the result of the given block 432 | def shallow_map_values!(&block) 433 | shallow_key_value_pairs.each do |k, v| 434 | self[k] = 435 | if block.arity == 2 436 | block.call(k, v) 437 | else 438 | block.call(v) 439 | end 440 | end 441 | 442 | self 443 | end 444 | 445 | ## 446 | # Returns a new collection where every top-level element is replaced with the result of the given block 447 | def shallow_map_values(&block) 448 | deep_dup.shallow_map_values!(&block) 449 | end 450 | 451 | ## 452 | # The primary iterator of a DeepEnumerable 453 | # If this method is implemented DeepEnumerable can construct every other method in terms of shallow_each. 454 | def shallow_each(&block) 455 | shallow_key_value_pairs.each(&block) 456 | end 457 | 458 | # This method is commented out because redefining '.to_a' on Array, for example, 459 | # seems like a terrible idea 460 | #def to_a 461 | # deep_each.to_a 462 | #end 463 | 464 | protected 465 | 466 | #def shallow_get(x) # this should technically be defined in Hash/Array individually 467 | # self[x] 468 | #end 469 | 470 | def depth_first_map(ancestry=[]) 471 | shallow_each.flat_map do |key, val| 472 | full_ancestry = ancestry + [key] 473 | full_key = DeepEnumerable::deep_key_from_array(full_ancestry) #TODO this is an n^2 operation 474 | 475 | if val.respond_to?(:depth_first_map, true) # Search protected methods as well 476 | val.depth_first_map(full_ancestry) 477 | else 478 | [[full_key, val]] 479 | end 480 | end 481 | end 482 | 483 | # Everything below should be a class method, but Ruby method visibility is a nightmare 484 | def self.deep_key_from_array(array) 485 | if array.size > 1 486 | {array.first => deep_key_from_array(array.drop(1))} 487 | else 488 | array.first 489 | end 490 | end 491 | 492 | # DeepEnumerable keys are represented as hashes, this function 493 | # converts them to arrays for convenience 494 | def self.deep_key_to_array(key) 495 | if DeepEnumerable::nested_key?(key) 496 | head, tail = split_key(key) 497 | [head] + deep_key_to_array(tail) 498 | elsif key.nil? 499 | [] 500 | else 501 | [key] 502 | end 503 | end 504 | 505 | def self.nested_key?(key) 506 | key.is_a?(Hash) || key.is_a?(Array) 507 | end 508 | 509 | # Disassembles a key into its head and tail elements 510 | # 511 | # @example 512 | # >> DeepEnumerable.split_key({a: {0 => :a}}) 513 | # => [:a, {0 => :a}] 514 | # >> DeepEnumerable.split_key([a: [0 => :a]]) 515 | # => [a:, [0, :a]] 516 | # 517 | def self.split_key(key) 518 | case key 519 | when Hash then 520 | key_head = key.keys.first 521 | key_tail = key[key_head] 522 | [key_head, key_tail] 523 | when Array then 524 | case key.size 525 | when 0 then [nil, nil] 526 | when 1 then [key[0], nil] 527 | else [key[0], key.drop(1)] 528 | end 529 | when nil then [nil, nil] 530 | else [key, nil] 531 | end 532 | end 533 | 534 | # Get the lowest-level key 535 | # 536 | # for example: {a: {b: :c}} goes to :c 537 | def self.leaf_key(key) 538 | nested_key?(key) ? leaf_key(split_key(key)[1]) : key 539 | end 540 | end 541 | 542 | ## 543 | # This class implements the necessary methods to qualify Hash as a DeepEnumerable 544 | class Hash 545 | include DeepEnumerable 546 | 547 | alias_method :shallow_keys, :keys 548 | end 549 | 550 | ## 551 | # This class implements the necessary methods to qualify Array as a DeepEnumerable 552 | class Array 553 | include DeepEnumerable 554 | 555 | def shallow_keys 556 | (0...size).to_a 557 | end 558 | end 559 | -------------------------------------------------------------------------------- /test/deep_enumerable_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'deep_enumerable.rb' 3 | require 'set' 4 | 5 | describe Hash do 6 | nested_hash_generator = lambda{{a: {b: 1, c: {d: 2, e: 3}, f: 4}, g: 5}} 7 | nested_hash = nested_hash_generator.call 8 | 9 | # TODO delete is not defined on Enumerable 10 | # it "should deep_delete_key" do 11 | # # delete one leaf of many 12 | # a = nested_hash_generator[] 13 | # a.deep_delete_key({a: {b: :e}}) 14 | # expected = {a: {b: 1, c: {d: 2}, f: 4}, g: 5} 15 | # assert_equal(expected, a) 16 | 17 | # # delete last element in parent collection 18 | # # delete non-leaf 19 | # # delete non-existent key 20 | # end 21 | 22 | it "should deep_diff" do 23 | a = {:a => {:b => :c}} 24 | b = {:a => {:b => :d}} 25 | a_diff = {:a => {:b => :c}} 26 | b_diff = {:a => {:b => :d}} 27 | assert_equal(a_diff, a.deep_diff(b), "swapped node a->b") 28 | assert_equal(b_diff, b.deep_diff(a), "swapped node b->a") 29 | 30 | a = {:a => {:b => :c}} 31 | b = {:a => :b} 32 | a_diff = {:a => {:b => :c}} 33 | b_diff = {:a => :b} 34 | assert_equal(a_diff, a.deep_diff(b), "different values a->b") 35 | assert_equal(b_diff, b.deep_diff(a), "different values b->a") 36 | 37 | a = {:a => {:b => :c}} 38 | b = {:a => :b, :c => :d} 39 | a_diff = {:a => {:b => :c}} 40 | b_diff = {:a => :b, :c => :d} 41 | assert_equal(a_diff, a.deep_diff(b), "new key a->b") 42 | assert_equal(b_diff, b.deep_diff(a), "new key b->a") 43 | 44 | a = [0, :b, 2] 45 | b = {1 => :b, 2 => 3, :c => :d} 46 | a_diff = [0, nil, 2] 47 | b_diff = {2 => 3, :c => :d} 48 | assert_equal(a_diff, a.deep_diff(b), "array vs hash") 49 | assert_equal(b_diff, b.deep_diff(a), "hash vs array") 50 | 51 | a = [{0 => :a}] 52 | b = {0 => [:a]} 53 | a_diff = [] 54 | b_diff = {} 55 | assert_equal(a_diff, a.deep_diff(b), "nested array vs hash") 56 | assert_equal(b_diff, b.deep_diff(a), "nested hash vs array") 57 | 58 | a = {:a => Hash} 59 | b = {:a => {1 => 2}} 60 | a_diff = {} 61 | b_diff = {:a => {1 => 2}} 62 | assert_equal(a_diff, a.deep_diff(b, &:===), "class equality with to_proc a->b") 63 | assert_equal(b_diff, b.deep_diff(a, &:===), "class equality with to_proc b->a") 64 | 65 | a = {:a => Array} 66 | b = {:a => {1 => 2}} 67 | a_diff = {:a => Array} 68 | b_diff = {:a => {1 => 2}} 69 | assert_equal(a_diff, a.deep_diff(b){|a,b| a === b}, "class inequality with block a->b") 70 | assert_equal(b_diff, b.deep_diff(a){|a,b| a === b}, "class inequality with block b->a") 71 | end 72 | 73 | it "should deep_diff_symmetric" do 74 | a = {:a => {:b => :c}} 75 | b = {:a => {:b => :d}} 76 | diff = {:a => {:b => [:c, :d]}} 77 | assert_equal(diff, a.deep_diff_symmetric(b), "swapped node") 78 | 79 | a = {:a => {:b => :c}} 80 | b = {:a => :b} 81 | diff = {:a => [{:b => :c}, :b]} 82 | assert_equal(diff, a.deep_diff_symmetric(b), "different values") 83 | 84 | a = {:a => {:b => :c}} 85 | b = {:a => :b, :c => :d} 86 | diff = {:a => [{:b => :c}, :b], :c => [nil, :d]} 87 | assert_equal(diff, a.deep_diff_symmetric(b), "new key") 88 | 89 | a = [0, :b, 2] 90 | b = {1 => :b, 2 => 3, :c => :d} 91 | a_diff = {0 => [0, nil], 2 => [2, 3], :c => [nil, :d]} 92 | b_diff = {0 => [nil, 0], 2 => [3, 2], :c => [:d, nil]} 93 | assert_equal(a_diff, a.deep_diff_symmetric(b), "array vs hash") 94 | assert_equal(b_diff, b.deep_diff_symmetric(a), "hash vs array") 95 | 96 | a = [{0 => :a}] 97 | b = {0 => [:a]} 98 | diff = {} 99 | assert_equal(diff, a.deep_diff_symmetric(b), "nested array vs hash") 100 | 101 | a = {:a => Hash} 102 | b = {:a => {1 => 2}} 103 | diff = {} 104 | assert_equal(diff, a.deep_diff_symmetric(b, &:===), "class equality with to_proc") 105 | 106 | a = {:a => Array} 107 | b = {:a => {1 => 2}} 108 | diff = {:a => [Array, {1 => 2}]} 109 | assert_equal(diff, a.deep_diff_symmetric(b){|a,b| a === b}, "class inequality with block") 110 | end 111 | 112 | it "should deep_dup" do 113 | test_deep_dup(nested_hash_generator) 114 | end 115 | 116 | it "should deep_each" do 117 | keys = [{:a=>:b}, {:a=>{:c=>:d}}, {:a=>{:c=>:e}}, {:a=>:f}, :g].to_set 118 | vals = (1..5).to_set 119 | 120 | test_deep_each(nested_hash, keys, vals) 121 | end 122 | 123 | it "should deep_flat_map" do 124 | keys = [{:a=>:b}, {:a=>{:c=>:d}}, {:a=>{:c=>:e}}, {:a=>:f}, :g].to_set 125 | vals = (1..5).to_set 126 | 127 | test_deep_flat_map(nested_hash, keys, vals) 128 | end 129 | 130 | it "should deep_get" do 131 | test_deep_get(nested_hash, {:a => :b}, 1) 132 | test_deep_get(nested_hash, {:a => :c}, {d: 2, e: 3}) 133 | end 134 | 135 | it "should deep_inject" do 136 | test_deep_inject(nested_hash) 137 | end 138 | 139 | it "should deep_intersect" do 140 | assert_equal({:age=>25}, 141 | {:name=>"alice", :age=>25}.deep_intersect( 142 | :name=>"bob", :age=>25)) 143 | end 144 | 145 | it "should deep_map" do 146 | test_deep_map(nested_hash) 147 | end 148 | 149 | it "should deep_map_values" do 150 | vals = {a: {b: Fixnum, c: {d: Fixnum, e: Fixnum}, f: Fixnum}, g: Fixnum} 151 | 152 | test_deep_map_values(nested_hash, vals) 153 | end 154 | 155 | it "should deep_reject" do 156 | expected = {:a=>{:c=>{:e=>3}}, :g=>5} 157 | assert_equal(expected, nested_hash.deep_reject{|k, v| DeepEnumerable::leaf_key(k).to_s.ord.even?}) 158 | 159 | expected = {:a=>{:b=>1, :c=>{:e=>3}}, :g=>5} 160 | assert_equal(expected, nested_hash.deep_reject(&:even?)) 161 | end 162 | 163 | it "should deep_select" do 164 | expected = {a: {c: {d: 2}, f: 4}} 165 | assert_equal(expected, nested_hash.deep_select(&:even?)) 166 | 167 | expected = {:a=>{:c=>{:e=>3}}, :g=>5} 168 | assert_equal(expected, nested_hash.deep_select{|k, v| DeepEnumerable::leaf_key(k).to_s.ord.odd?}) 169 | end 170 | 171 | it "should deep_set" do 172 | nested_hash2 = nested_hash_generator[] 173 | test_deep_set(nested_hash2, {:a => :c}) 174 | 175 | assert_equal({1=>{2=>3}}, {}.deep_set({1 => 2}, 3), 'create intermediate hashes with hash key') 176 | assert_equal({1=>{2=>3}}, {}.deep_set([1, 2], 3), 'create intermediate hashes with array key') 177 | 178 | assert_equal({1=>{2=>{3=>4}}}, {}.deep_set({1 => {2 => 3}}, 4), 'create deeper intermediate hashes with hash key') 179 | assert_equal({1=>{2=>{3=>4}}}, {}.deep_set([1, 2, 3], 4), 'create deeper intermediate hashes with array key') 180 | 181 | assert_equal({1=>{2=>{3=>4}}}, {1 => 2}.deep_set({1 => {2 => 3}}, 4), 'create deeper intermediate hashes with hash key and branch collision') 182 | assert_equal({1=>{2=>{3=>4}}}, {1 => {2 => 3}}.deep_set([1, 2, 3], 4), 'create deeper intermediate hashes with array key and leaf collision') 183 | assert_equal({1=>{2=>{3=>4}, 3 => 2}}, {1 => {3 => 2}}.deep_set([1, 2, 3], 4), 'create deeper intermediate hashes with array key and leaf collision') 184 | 185 | assert_equal({1=>2}, {}.deep_set([1], 2), 'set using a singular key') 186 | end 187 | 188 | it "should deep_values" do 189 | vals = [1, 2, 3, 4, 5] 190 | 191 | test_deep_values(nested_hash, vals) 192 | end 193 | 194 | it "should deep_zip" do 195 | test_deep_zip(nested_hash) {|x| x*2} 196 | 197 | a = {a: 1, c: 3} 198 | b = {b: 2, d: 4} 199 | c = {c: 1, d: 4} 200 | expected_ab = {a: [1, nil], c: [3, nil]} 201 | expected_ac = {a: [1, nil], c: [3, 1]} 202 | assert_equal(expected_ab, a.deep_zip(b)) 203 | assert_equal(expected_ac, a.deep_zip(c)) 204 | end 205 | 206 | it "should shallow_each" do 207 | expected = [[:a, {:b=>1, :c=>{:d=>2, :e=>3}, :f=>4}], [:g, 5]] 208 | assert_equal(expected, nested_hash.shallow_each.to_a) 209 | 210 | res = [] 211 | nested_hash.shallow_each{|k, v| res << [k, v]} 212 | assert_equal(expected, res) 213 | 214 | res = nested_hash.shallow_each{|k, v| nil} 215 | assert_equal(expected, res) 216 | end 217 | 218 | it "should shallow_map_keys" do 219 | upper_keys = {A: {b: 1, c: {d: 2, e: 3}, f: 4}, G: 5} 220 | class_keys = {Hash => {b: 1, c: {d: 2, e: 3}, f: 4}, Fixnum => 5} 221 | 222 | assert_equal(upper_keys, nested_hash.shallow_map_keys(&:upcase)) 223 | 224 | # test the two-arg version 225 | assert_equal(class_keys, nested_hash.shallow_map_keys{|_, v| v.class}) 226 | end 227 | 228 | it "should shallow_map_values" do 229 | vals = {a: Hash, g: Fixnum} 230 | 231 | test_shallow_map_values(nested_hash, vals) 232 | 233 | # test the two-arg version 234 | assert_equal(vals, nested_hash.shallow_map_values{|_, v| v.class}) 235 | assert_equal({a: Symbol, g: Symbol}, nested_hash.shallow_map_values{|k, _| k.class}) 236 | end 237 | end 238 | 239 | describe Array do 240 | nested_array_generator = lambda{[:a, [:b, [[:c], :d], :e]]} 241 | nested_array = nested_array_generator[] 242 | 243 | # TODO delete is not defined on Enumerable 244 | # it "should deep_delete_key" do 245 | # # delete one leaf of many 246 | # a = nested_array_generator[] 247 | # a.deep_delete_key({1 => 1}) 248 | # expected = [:a, [:b, :d], :e] 249 | # assert_equal(expected, a) 250 | 251 | # # delete last element in parent collection 252 | # # delete non-leaf 253 | # # delete non-existent key 254 | # end 255 | 256 | it "should deep_diff" do 257 | a = [{a: 1, b: 2}, {c: 3, d: 4}] 258 | b = [{a: 1, b: 2}, {c: 3, d: 5}] 259 | a_diff = [nil, {d: 4}] 260 | b_diff = [nil, {d: 5}] 261 | 262 | assert(a_diff, a.deep_diff(b)) 263 | assert(b_diff, b.deep_diff(a)) 264 | end 265 | 266 | it "should deep_diff_symmetric" do 267 | a = [{a: 1, b: 2}, {c: 3, d: 4}] 268 | b = [{a: 1, b: 2}, {c: 3, d: 5}] 269 | a_diff = {1 => {d: [4, 5]}} 270 | b_diff = {1 => {d: [5, 4]}} 271 | 272 | assert(a_diff, a.deep_diff_symmetric(b)) 273 | assert(b_diff, a.deep_diff_symmetric(a)) 274 | end 275 | 276 | it "should deep_dup" do 277 | test_deep_dup(nested_array_generator) 278 | end 279 | 280 | it "should deep_each" do 281 | keys = [0, {1 => 0}, {1 => {1 => {0 => 0}}}, {1 => {1 => 1}}, {1 => 2}].to_set 282 | vals = (:a..:e).to_set 283 | 284 | test_deep_each(nested_array, keys, vals) 285 | end 286 | 287 | it "should deep_flat_map" do 288 | keys = [0, {1 => 0}, {1 => {1 => {0 => 0}}}, {1 => {1 => 1}}, {1 => 2}].to_set 289 | vals = (:a..:e).to_set 290 | 291 | test_deep_flat_map(nested_array, keys, vals) 292 | end 293 | 294 | it "should deep_get" do 295 | test_deep_get(nested_array, {1 => 0}, :b) 296 | test_deep_get(nested_array, {1 => 1}, [[:c], :d]) 297 | end 298 | 299 | it "should deep_inject" do 300 | test_deep_inject(nested_array) 301 | end 302 | 303 | it "should deep_intersect" do 304 | bob = {:friends=>["alice","carol"]} 305 | carol = {:friends=>["alice","bob"]} 306 | assert_equal({:friends=>["alice"]}, bob.deep_intersect(carol)) 307 | end 308 | 309 | it "should deep_map" do 310 | test_deep_map(nested_array) 311 | end 312 | 313 | it "should deep_map_values" do 314 | vals = [Symbol, [Symbol, [[Symbol], Symbol], Symbol]] 315 | 316 | test_deep_map_values(nested_array, vals) 317 | end 318 | 319 | it "should deep_select" do 320 | expected = [[:b, [[], :d]]] 321 | assert_equal(expected, nested_array.deep_select{|sym| sym.to_s.ord.even?}) 322 | 323 | shallow_a = [2, 3, 4] 324 | assert_equal(shallow_a.select(&:even?), shallow_a.deep_select(&:even?)) 325 | assert_equal(shallow_a.select{|k,v| k.odd?}, shallow_a.deep_select{|k,v| k.odd?}) 326 | 327 | assert_equal([[2, 4], 6] , [1, shallow_a, 6].deep_select(&:even?)) 328 | end 329 | 330 | it "should deep_set" do 331 | nested_array2 = nested_array_generator[] 332 | test_deep_set(nested_array2, {1 => 1}) 333 | 334 | assert_equal([nil, [nil, nil, 3]], [].deep_set({1 => 2}, 3), 'create intermediate arrays') 335 | assert_equal([nil, [nil, nil, 3]], [].deep_set([1, 2], 3), 'create intermediate hashes with array key') 336 | 337 | assert_equal([nil, [nil, nil, [nil, nil, nil, 4]]], 338 | [nil, [nil, nil, 3]].deep_set({1 => {2 => 3}}, 4), 'create deeper intermediate hashes with hash key and branch collision') 339 | assert_equal([nil, [nil, nil, [nil, nil, nil, 4]]], 340 | [nil, [nil, nil, [nil, nil, nil, 4]]].deep_set([1, 2, 3], 4), 'create deeper intermediate hashes with array key and leaf collision') 341 | assert_equal([nil, [nil, nil, [nil, nil, nil, 4, 5]]], 342 | [nil, [nil, nil, [nil, nil, nil, 3, 5]]].deep_set([1, 2, 3], 4), 'create deeper intermediate hashes with array key and leaf collision') 343 | 344 | assert_equal([nil, 2], [].deep_set([1], 2), 'set using a singular key') 345 | end 346 | 347 | it "should deep_values" do 348 | vals = [:a, :b, :c, :d, :e] 349 | 350 | test_deep_values(nested_array, vals) 351 | end 352 | 353 | it "should deep_zip" do 354 | test_deep_zip(nested_array, &:upcase) 355 | 356 | a = [0, 1] 357 | b = [2] 358 | c = [3, 4, 5] 359 | expected_ab = [[0, 2], [1, nil]] 360 | expected_ac = [[0, 3], [1, 4]] 361 | assert_equal(expected_ab, a.deep_zip(b)) 362 | assert_equal(expected_ac, a.deep_zip(c)) 363 | end 364 | 365 | it "should shallow_each" do 366 | expected = [[0, :a], [1, [:b, [[:c], :d], :e]]] 367 | assert_equal(expected, nested_array.shallow_each.to_a) 368 | 369 | res = [] 370 | nested_array.shallow_each{|k, v| res << [k, v]} 371 | assert_equal(expected, res) 372 | 373 | res = nested_array.shallow_each{|k, v| nil} 374 | assert_equal(expected, res) 375 | end 376 | 377 | it "should shallow_map_keys" do 378 | array = [2, 3, 3, 1, 0, 5] 379 | every_other = [2, nil, 3, nil, 3, nil, 1, nil, 0, nil, 5] 380 | by_value = [0, 1, 2, 3, nil, 5] 381 | 382 | assert_equal(every_other, array.shallow_map_keys{|k| k*2}) 383 | assert_equal(by_value, array.shallow_map_keys{|_, v| v}) 384 | end 385 | 386 | it "should shallow_map_values" do 387 | vals = [Symbol, Array] 388 | 389 | test_shallow_map_values(nested_array, vals) 390 | 391 | # test the two-arg version 392 | assert_equal(vals, nested_array.shallow_map_values{|_, v| v.class}) 393 | assert_equal([Fixnum, Fixnum], nested_array.shallow_map_values{|k, _| k.class}) 394 | end 395 | end 396 | 397 | describe "Helper Functions" do 398 | it "should deep_key_to_array" do 399 | assert_equal(['a'], DeepEnumerable::deep_key_to_array('a')) 400 | assert_equal([:a], DeepEnumerable::deep_key_to_array(:a)) 401 | assert_equal([1], DeepEnumerable::deep_key_to_array(1)) 402 | 403 | assert_equal(['a'], DeepEnumerable::deep_key_to_array(['a'])) 404 | assert_equal([:a], DeepEnumerable::deep_key_to_array([:a])) 405 | assert_equal([1], DeepEnumerable::deep_key_to_array([1])) 406 | 407 | assert_equal(['b', 'a'], DeepEnumerable::deep_key_to_array({'b' => 'a'})) 408 | assert_equal([:b, :a], DeepEnumerable::deep_key_to_array({:b => :a})) 409 | assert_equal([2, 1], DeepEnumerable::deep_key_to_array({2 => 1})) 410 | 411 | assert_equal(['c', 'b', 'a'], DeepEnumerable::deep_key_to_array({'c' => {'b' => 'a'}})) 412 | assert_equal([:c, :b, :a], DeepEnumerable::deep_key_to_array({:c => {:b => :a}})) 413 | assert_equal([3, 2, 1], DeepEnumerable::deep_key_to_array({3 => {2 => 1}})) 414 | end 415 | 416 | it "should leaf_key" do 417 | assert_equal('a', DeepEnumerable::leaf_key('a')) 418 | assert_equal(:a, DeepEnumerable::leaf_key(:a)) 419 | assert_equal(1, DeepEnumerable::leaf_key(1)) 420 | 421 | assert_equal('a', DeepEnumerable::leaf_key({'b' => 'a'})) 422 | assert_equal(:a, DeepEnumerable::leaf_key({:b => :a})) 423 | assert_equal(1, DeepEnumerable::leaf_key({2 => 1})) 424 | 425 | assert_equal('a', DeepEnumerable::leaf_key({'c' => {'b' => 'a'}})) 426 | assert_equal(:a, DeepEnumerable::leaf_key({:c => {:b => :a}})) 427 | assert_equal(1, DeepEnumerable::leaf_key({3 => {2 => 1}})) 428 | end 429 | 430 | it "should split_key" do 431 | assert_equal([:a, {0 => :a}], DeepEnumerable::split_key({a: {0 => :a}})) 432 | assert_equal([0, :a], DeepEnumerable::split_key({0 => :a})) 433 | assert_equal([:a, [0, :a]], DeepEnumerable::split_key([:a, 0, :a])) 434 | assert_equal([0, [:a]], DeepEnumerable::split_key([0, :a])) 435 | assert_equal([:a, nil], DeepEnumerable::split_key([:a])) 436 | end 437 | end 438 | 439 | def test_deep_dup(de_generator) 440 | de = de_generator.call 441 | untouched = de_generator.call 442 | 443 | assert_equal(de, untouched, "An untouched deep_dup'd enumerable matches the original") 444 | 445 | mutated_copy = de.deep_dup 446 | mutated_copy.deep_each{|k,v| mutated_copy.deep_set(k, nil)} 447 | 448 | refute_equal(mutated_copy, de, "A deep_dup'd copy cannot effect the original") 449 | assert_equal(untouched, de, "An untouched deep_dup'd enumerable matches the original, even after other stuff is mutated") 450 | assert_equal(untouched.class, de.class, "A deep_dup'd copy should be the same class as the original") 451 | assert_equal(untouched.to_a, de.to_a, "A deep_dup'd copy should have the same elements as the original") 452 | end 453 | 454 | def test_deep_each(de, keys, vals) 455 | result_keys = Set.new 456 | result_vals = Set.new 457 | de.deep_each do |k,v| 458 | result_keys << k 459 | result_vals << v 460 | end 461 | assert_kind_of(Enumerator, de.deep_each, 'deep_each without a block returns on Enumerator') 462 | 463 | assert_equal(keys, result_keys, 'yields fully qualified keys') 464 | assert_equal(vals, result_vals, 'yields values') 465 | 466 | assert_equal(keys, de.deep_each.map(&:first).to_set, 'maps fully qualified keys') 467 | assert_equal(vals, de.deep_each.map(&:last).to_set, 'maps values') 468 | end 469 | 470 | def test_deep_flat_map(de, keys, vals) 471 | assert_equal(keys, de.deep_flat_map(&:first).to_set, 'maps fully qualified keys') 472 | assert_equal(vals, de.deep_flat_map(&:last).to_set, 'maps values') 473 | assert_equal(keys.zip(vals).flatten, de.deep_flat_map{|a, b| [a, b]}, 'maps keys and values') 474 | end 475 | 476 | def test_deep_get(de, key, val) 477 | first_key = key.keys.first 478 | assert_equal(de[first_key], de.deep_get(first_key), "deep_get gets shallow values (at a non-leaf)") 479 | assert_equal(val, de.deep_get(key), "deep_get gets nested values (at a leaf)") 480 | end 481 | 482 | def test_deep_inject(de) 483 | expected = "test: "+de.deep_values.join 484 | sum = de.deep_inject("test: ") {|str, (k, v)| str+v.to_s} 485 | assert_equal(expected, sum, 'injects all values into argument string') 486 | end 487 | 488 | def test_deep_map(de) 489 | assert_kind_of(Enumerator, de.deep_map, 'deep_map without a block returns on Enumerator') 490 | assert_equal(de.class, de.deep_map{|x| x}.class, 'deep_map_values preserves enumerable type') 491 | orig = de.deep_dup 492 | de.deep_map{|x, y| y.class} 493 | assert_equal(orig, de, "deep_map does not mutate the DeepEnumerable") 494 | 495 | mapped = de.deep_map{|k,v| v.class} 496 | all_the_same = true 497 | de.deep_each{|k,v| all_the_same &&= (v.class == mapped.deep_get(k))} 498 | assert(all_the_same, "deep_map maps over all the elements deep_each hits") 499 | end 500 | 501 | def test_deep_map_values(de, vals) 502 | mapped = de.deep_map_values(&:class) 503 | assert_equal(de.class, mapped.class, 'deep_map_values preserves enumerable type') 504 | assert_equal(vals, mapped) 505 | end 506 | 507 | def test_deep_set(de, key) 508 | dc = de.deep_dup 509 | dc.deep_set(key, 42) 510 | assert_equal(42, dc.deep_get(key), "deep_set sets deep values") 511 | 512 | dc.deep_set(key.keys.first, 43) 513 | assert_equal(43, dc.deep_get(key.keys.first), "deep_set sets shallow values") 514 | 515 | non_existant_key = {1 => {2 => 3}} 516 | dc.deep_set(non_existant_key, 44) 517 | assert_equal(44, dc.deep_get(non_existant_key)) 518 | 519 | singular_key = [7] 520 | dc.deep_set(singular_key, 45) 521 | assert_equal(45, dc.deep_get(singular_key[0])) 522 | end 523 | 524 | def test_deep_values(de, values) 525 | assert_equal(values, de.deep_values, 'returns leaf values') 526 | end 527 | 528 | def test_deep_zip(de, &block) 529 | h1 = de 530 | h2 = de.deep_map_values{|x| block.call(x)} 531 | expected = h1.deep_map_values{|x| [x, block.call(x)]} 532 | assert_equal(expected, h1.deep_zip(h2)) 533 | 534 | # throw out first item 535 | h1_2 = h1.reject{|k, v| k == h1.shallow_keys.last} 536 | h2_2 = h2.reject{|k, v| k == h2.shallow_keys.last} 537 | expected_1 = h1_2.deep_map_values{|v| [v, block.call(v)]} 538 | expected_2 = h1.deep_map_values{|v| [v, if v == h1[h1.shallow_keys.last] || v.nil? then nil else block.call(v) end]} 539 | assert_equal(expected_1, h1_2.deep_zip(h2)) 540 | assert_equal(expected_2, h1.deep_zip(h2_2)) 541 | end 542 | 543 | def test_shallow_map_values(de, vals) 544 | mapped = de.shallow_map_values(&:class) 545 | assert_equal(de.class, mapped.class, 'deep_map_values preserves enumerable type') 546 | assert_equal(vals, mapped) 547 | end 548 | --------------------------------------------------------------------------------