{"id":15405686,"url":"https://github.com/zverok/idempotent_enumerable","last_synced_at":"2026-02-25T18:33:04.195Z","repository":{"id":56877253,"uuid":"106841174","full_name":"zverok/idempotent_enumerable","owner":"zverok","description":"IdempotentEnumerable is like Enumerable but preserves original collection class","archived":false,"fork":false,"pushed_at":"2017-10-16T16:20:59.000Z","size":27,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-29T05:51:17.236Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zverok.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-13T15:23:58.000Z","updated_at":"2019-07-15T23:14:10.000Z","dependencies_parsed_at":"2022-08-20T22:00:35.082Z","dependency_job_id":null,"html_url":"https://github.com/zverok/idempotent_enumerable","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fidempotent_enumerable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fidempotent_enumerable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fidempotent_enumerable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fidempotent_enumerable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zverok","download_url":"https://codeload.github.com/zverok/idempotent_enumerable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249299696,"owners_count":21246881,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-01T16:18:15.163Z","updated_at":"2026-02-25T18:32:59.171Z","avatar_url":"https://github.com/zverok.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# IdempotentEnumerable\n\n[![Gem Version](https://badge.fury.io/rb/idempotent_enumerable.svg)](http://badge.fury.io/rb/idempotent_enumerable)\n[![Build Status](https://travis-ci.org/zverok/idempotent_enumerable.svg?branch=master)](https://travis-ci.org/zverok/idempotent_enumerable)\n[![Coverage Status](https://coveralls.io/repos/zverok/idempotent_enumerable/badge.svg?branch=master)](https://coveralls.io/r/zverok/idempotent_enumerable?branch=master)\n\n\n`IdempotentEnumerable` is like Ruby core's `Enumerable` but tries to preserve the class of the\ncollection it included in, where reasonable.\n\n## Features/Showcase\n\n```ruby\nrequire 'set'\n\ns = Set.new(1..5)\n# =\u003e #\u003cSet: {1, 2, 3, 4, 5}\u003e\ns.reject(\u0026:odd?)\n# =\u003e [2, 4] -- FFFUUUU\n\nrequire 'idempotent_enumerable'\nSet.include IdempotentEnumerable\n\ns.reject(\u0026:odd?)\n# =\u003e #\u003cSet: {2, 4}\u003e -- Nice!\n```\n\n`IdempotentEnumerable` relies on fact your `each` method returns an instance of `Enumerator` (or\nother `Enumerable` object) when called without block. Which, honestly, it should do anyways.\n\nTo construct back an instance of original class, `IdempotentEnumerable` relies on the fact\n`OriginalClass.new(array)` call will work. But, if your class provides another way for construction\nfrom array, you can still use the module:\n\n```ruby\nh = {a: 1, b: 2, c: 3}\n# =\u003e {:a=\u003e1, :b=\u003e2, :c=\u003e3}\nh.first(2)\n# =\u003e [[:a, 1], [:b, 2]]\n\nHash.include IdempotentEnumerable\n\n# To make hash from array of pairs, one should use `Hash[array]` notation.\nHash.idempotent_enumerable.constructor = :[]\n\nh.first(2)\n# =\u003e {:a=\u003e1, :b=\u003e2}\n```\n\n`IdempotentEnumerable` also supports complicated collections, with `each` accepting additional\narguments, out of the box ([daru](https://github.com/SciRuby/daru) used as an example):\n\n```ruby\nrequire 'daru'\n\nDaru::DataFrame.include IdempotentEnumerable\n\ndf = Daru::DataFrame.new([[1,2,3], [4,5,6], [7,8,9]])\n# #\u003cDaru::DataFrame(3x3)\u003e\n#        0   1   2\n#    0   1   4   7\n#    1   2   5   8\n#    2   3   6   9\n\n# :column argument would be passed to DataFrame#each, so we are selecting columns\ndf.select(:column) { |col| col.sum \u003e 6 }\n# #\u003cDaru::DataFrame(3x2)\u003e\n#        0   1\n#    0   4   7\n#    1   5   8\n#    2   6   9\n```\n\n## Reasons\n\n`IdempotentEnumerable` can be used as:\n\n* soft patch to existing Ruby collections (like `Set` or `Hash`);\n* custom reimplementations of generic collections (some `FasterArray`);\n* custom specialized collection, like [Nokogiri::XML::NodeSet](http://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/NodeSet),\n  which quacks like `Array`, but also provides XML/CSS navigation methods. Unfortunately, if you'll\n  do something like `doc.search('a').reject { |a| a.text.include?('Google') }`, you'll receive regular\n  `Array` that haven't any useful `#at`/`#search` methods anymore.\n\n## Installation and usage\n\n`gem install idempotent_enumerable` or `gem 'idempotent_enumerable'` in your `Gemfile`.\n\nThen follow examples in this README.\n\n## List of methods redefined\n\n### Methods that return single collection\n\n* [drop](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-drop);\n* [drop_while](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-drop_while);\n* [first](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-first) (when used with argument);\n* [grep](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-grep);\n* [grep_v](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-grep_v) (RUBY_VERSION \u003e= 2.3);\n* [max](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-max) (when used with argument, RUBY_VERSION \u003e= 2.2);\n* [max_by](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-max_by) (when used with argument, RUBY_VERSION \u003e= 2.2);\n* [min](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-min) (when used with argument, RUBY_VERSION \u003e= 2.2);\n* [min_by](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-min_by) (when used with argument, RUBY_VERSION \u003e= 2.2);\n* [reduce](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-reduce);\n* [reject](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-reject);\n* [select](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-select);\n* [sort](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-sort);\n* [sort_by](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-sort_by);\n* [take](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-take);\n* [take_while](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-take_while);\n* [uniq](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-uniq)  (RUBY_VERSION \u003e= 2.4).\n\n### Methods that return (or emit) several collections\n\nFor methods like [partition](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-partition) that\nsomehow split an enumerable sequence into several, `IdempotentEnumerable` preserves the type of\n**internal** sequence. E.g.:\n\n```ruby\nSet.include IdempotentEnumerable\nset = Set.new(1..5)\nset.partition(\u0026:odd?)\n# =\u003e [#\u003cSet: {1, 3, 5}\u003e, #\u003cSet: {2, 4}\u003e]\nset.each_slice(3).to_a\n# =\u003e [#\u003cSet: {1, 2, 3}\u003e, #\u003cSet: {4, 5}\u003e]\n```\n\n* [chunk](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-chunk);\n* [chunk_while](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-chunk_while) (RUBY_VERSION \u003e= 2.3);\n* [each_cons](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-each_cons);\n* [each_slice](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-each_slice);\n* [group_by](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-group_by) (returns hash with\n  keys being group keys and values being original collection type);\n* [partition](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-partition);\n* [slice_after](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-slice_after) (RUBY_VERSION \u003e= 2.2);\n* [slice_before](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-slice_before);\n* [slice_when](https://ruby-doc.org/core-2.4.2/Enumerable.html#method-i-slice_when) (RUBY_VERSION \u003e= 2.2).\n\n### Optionally redefined methods\n\nGenerally speaking, `map` and `flat_map` can return collection of anything, probably not coercible\nto original collection type, so they are **not** redefined by default.\n\nBut they can be redefined with optional `idempotent_enumerable.redefine_map!` call:\n\n```ruby\nSet.include IdempotentEnumerable\nset = Set.new(1..5)\nset.map(\u0026:to_s)\n# =\u003e [\"1\", \"2\", \"3\", \"4\", \"5\"]\nSet.idempotent_enumerable.redefine_map!\nset.map(\u0026:to_s)\n# =\u003e #\u003cSet: {\"1\", \"2\", \"3\", \"4\", \"5\"}\u003e\n```\n\n`redefine_map!` has two options:\n* `only:` (by default `[:map, :flat_map]`) to specify that you want to redefine only one of those\n  methods;\n* `all:` to specify which condition all elements of produced collection should satisfy to coerce.\n\nExample of the latter:\n\n```ruby\nHash.include IdempotentEnumerable\nHash.idempotent_enumerable.constructor = :[]\n# only convert back to hash if `map` have returned array of pairs\nHash.idempotent_enumerable.redefine_map! all: -\u003e(e) { e.is_a?(Array) \u0026\u0026 e.count == 2 }\n{a: 1, b: 2}.map(\u0026:join)\n# =\u003e [\"a1\", \"b2\"]  -- no coercion\n{a: 1, b: 2}.map { |k, v| [k.to_s, v.to_s] }\n# =\u003e {\"a\"=\u003e\"1\", \"b\"=\u003e\"2\"} -- coercion\n```\n\n## Performance penalty\n\n...is, of course, present, yet not that awful (depends on your standards).\n\n```ruby\nrequire 'benchmark/ips'\n\nset1 = Set.new((1..100))\n\nclass SetI \u003c Set\n  include IdempotentEnumerable\nend\nset2 = SetI.new((1..100))\n\nBenchmark.ips do |x|\n  x.report('Enumerable') { set1.reject(\u0026:odd?) }\n  x.report('IdempotentEnumerable') { set2.reject(\u0026:odd?) }\n\n  x.compare!\nend\n```\n\nOutput:\n\n```\nWarming up --------------------------------------\n          Enumerable    10.681k i/100ms\nIdempotentEnumerable     4.035k i/100ms\nCalculating -------------------------------------\n          Enumerable    112.134k (± 3.5%) i/s -    566.093k in   5.055148s\nIdempotentEnumerable     42.197k (± 4.1%) i/s -    213.855k in   5.078339s\n\nComparison:\n          Enumerable:   112134.2 i/s\nIdempotentEnumerable:    42196.6 i/s - 2.66x  slower\n```\n\n## Author\n\n[Victor Shepelev](http://zverok.github.io/)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Fidempotent_enumerable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzverok%2Fidempotent_enumerable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Fidempotent_enumerable/lists"}