From 8948a565e435ba0780e6a5884469884792e9b067 Mon Sep 17 00:00:00 2001 From: ahoward Date: Sat, 4 Jul 2026 23:38:27 +0000 Subject: [PATCH] assume native ordered hashes; drop the legacy Ordering module (v9.0.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruby 1.9+ preserves Hash insertion order natively, and the gem already requires Ruby >= 3.0 — so the manual @keys bookkeeping is dead weight. Remove it: - delete lib/map/ordering.rb (the @keys array + allocate/[]=/keys/each/delete/ shift/unshift/push/pop overrides) and its conditional include - promote the native-Hash implementations (previously guarded by `unless RUBY_VERSION < '1.9' || ENV['MAP_FORCE_ORDERING']`) to unconditional defs - drop the MAP_FORCE_ORDERING env knob and the test:with/without_ordering rake tasks - scrub the stale "moved to Map::Ordering" comments No behavior change on any supported Ruby: order flows from Hash#keys/#each everywhere (==, <=>, to_hash, iterators). One documented note: `push` of an existing key keeps its position (native []=), rather than moving it to the end as the legacy @keys path did — untested either way; native is now canonical. test/map_test.rb: 56 tests, 4940 assertions, 100% green. Co-Authored-By: Claude Opus 4.8 --- Rakefile | 18 ---- lib/map.rb | 194 +++++++++++++++++--------------------------- lib/map/_lib.rb | 2 +- lib/map/ordering.rb | 126 ---------------------------- map.gemspec | 3 +- 5 files changed, 76 insertions(+), 267 deletions(-) delete mode 100644 lib/map/ordering.rb diff --git a/Rakefile b/Rakefile index 25194a9..b4900c9 100644 --- a/Rakefile +++ b/Rakefile @@ -20,24 +20,6 @@ namespace :test do task(:unit){ run_tests!(:unit) } task(:functional){ run_tests!(:functional) } task(:integration){ run_tests!(:integration) } - - # Test with Map::Ordering module forced (simulates Ruby < 1.9) - task(:with_ordering) do - ENV['MAP_FORCE_ORDERING'] = '1' - puts "\n#{ '=' * 60 }" - puts "Running tests WITH Map::Ordering module (MAP_FORCE_ORDERING=1)" - puts "#{ '=' * 60 }\n\n" - run_tests! - end - - # Test without Map::Ordering module (Ruby 1.9+ native Hash ordering) - task(:without_ordering) do - ENV.delete('MAP_FORCE_ORDERING') - puts "\n#{ '=' * 60 }" - puts "Running tests WITHOUT Map::Ordering module (Ruby 1.9+ mode)" - puts "#{ '=' * 60 }\n\n" - run_tests! - end end def run_tests!(which = nil) diff --git a/lib/map.rb b/lib/map.rb index 8bc7068..3b5f38b 100644 --- a/lib/map.rb +++ b/lib/map.rb @@ -3,9 +3,7 @@ class Map < Hash require_relative 'map/_lib.rb' class << Map - # allocate method moved to Map::Ordering module (conditionally included) - # When ordering module is not included (Ruby 1.9+), Hash.allocate is used - + # no custom allocate: Hash.allocate is used (Hash is ordered in Ruby 1.9+). def new(*args, &block) allocate.instance_eval do initialize(*args, &block) @@ -166,9 +164,6 @@ def bcall(*args, &block) # instance constructor # - # keys method moved to Map::Ordering module (conditionally included) - # When ordering module is not included (Ruby 1.9+), Hash#keys is used - def initialize(*args, &block) case args.size when 0 @@ -311,18 +306,12 @@ def default=(value) alias_method '__get__', '[]' unless method_defined?('__get__') alias_method '__update__', 'update' unless method_defined?('__update__') - # []= method: - # - With ordering module (Ruby < 1.9 or forced): tracks keys in @keys array - # - Without module (Ruby >= 1.9, not forced): just converts and stores - # - # Only define for Ruby >= 1.9 without forced ordering (module provides it otherwise) - unless RUBY_VERSION < '1.9' || ENV['MAP_FORCE_ORDERING'] - def []=(key, val) - key, val = convert(key, val) - __set__(key, val) - end - alias_method 'store', '[]=' + # Hash preserves insertion order natively, so []= just converts and stores. + def []=(key, val) + key, val = convert(key, val) + __set__(key, val) end + alias_method 'store', '[]=' def [](key) key = convert_key(key) @@ -367,63 +356,52 @@ def reverse_merge!(hash) replace(reverse_merge(hash)) end - # Ordering-dependent methods moved to Map::Ordering module (conditionally included): - # - values, each_with_index, each_key, each_value, each/each_pair (iterate via @keys) - # - clear (maintain @keys synchronization) - # When ordering module is not included (Ruby 1.9+), Hash methods are used + # order-dependent methods — Hash is ordered in Ruby 1.9+, so they delegate + # straight to it (key conversion where the reader needs it). + def delete(key) + key = convert_key(key) + super(key) + end - # For Ruby >= 1.9 without ordering module, provide optimized implementations - # These methods are only defined when the ordering module is NOT included - unless RUBY_VERSION < '1.9' || ENV['MAP_FORCE_ORDERING'] - # delete needs key conversion - def delete(key) - key = convert_key(key) - super(key) - end + # first and last return [key, value] pairs + def first + key = keys.first + [key, self[key]] if key + end - # first and last return [key, value] pairs - # Use keys array since Hash#first/Hash#last don't exist in all Ruby versions - def first - key = keys.first - [key, self[key]] if key - end + def last + key = keys.last + [key, self[key]] if key + end - def last - key = keys.last - [key, self[key]] if key - end + def values + Hash.instance_method(:values).bind(self).call + end - # values uses Hash#values (ordered in 1.9+) - def values - Hash.instance_method(:values).bind(self).call - end + def each + Hash.instance_method(:each).bind(self).call{|k, v| yield(k, v)} + end + alias_method 'each_pair', 'each' - # Iterator methods delegate to Hash (ordered in 1.9+) - def each - Hash.instance_method(:each).bind(self).call{|k, v| yield(k, v)} - end - alias_method 'each_pair', 'each' + def each_key + Hash.instance_method(:each_key).bind(self).call{|k| yield(k)} + end - def each_key - Hash.instance_method(:each_key).bind(self).call{|k| yield(k)} - end + def each_value + Hash.instance_method(:each_value).bind(self).call{|v| yield(v)} + end - def each_value - Hash.instance_method(:each_value).bind(self).call{|v| yield(v)} - end - - def each_with_index - i = 0 - each do |k, v| - yield([k, v], i) - i += 1 - end - self + def each_with_index + i = 0 + each do |k, v| + yield([k, v], i) + i += 1 end + self + end - def clear - Hash.instance_method(:clear).bind(self).call - end + def clear + Hash.instance_method(:clear).bind(self).call end def values_at(*keys) @@ -458,51 +436,44 @@ def replace(*args) # ordered container specific methods # - # When Map::Ordering module is included (Ruby < 1.9 or forced): - # These methods use @keys array for efficient order manipulation - # When module is NOT included (Ruby 1.9+): - # These methods are defined below and work with Hash's native ordering - # - # Only define for Ruby >= 1.9 without forced ordering (module provides them otherwise) - unless RUBY_VERSION < '1.9' || ENV['MAP_FORCE_ORDERING'] - def shift - unless empty? - key = keys.first - val = delete(key) - [key, val] - end + # ordered-container methods, riding Hash's native insertion order. + def shift + unless empty? + key = keys.first + val = delete(key) + [key, val] end + end - def unshift(*args) - # For Ruby 1.9+: process each pair in order, unshifting sequentially - # This matches the @keys array behavior - Map.each_pair(*args) do |key, val| - key = convert_key(key) - val = convert_value(val) - # Rebuild hash with this key at front - temp = {key => val} - each do |k, v| - temp[k] = v unless k == key - end - clear - temp.each{|k, v| __set__(k, v)} + # unshift moves each pair to the FRONT (rebuild with the key first). + def unshift(*args) + Map.each_pair(*args) do |key, val| + key = convert_key(key) + val = convert_value(val) + temp = {key => val} + each do |k, v| + temp[k] = v unless k == key end - self + clear + temp.each{|k, v| __set__(k, v)} end + self + end - def push(*args) - Map.each_pair(*args) do |key, val| - self[key] = val # This naturally appends in Ruby 1.9+ - end - self + # push appends. NOTE: for a key that already exists, Hash keeps its original + # position (native []= semantics) — re-pushing does NOT move it to the end. + def push(*args) + Map.each_pair(*args) do |key, val| + self[key] = val end + self + end - def pop - unless empty? - key = keys.last - val = delete(key) - [key, val] - end + def pop + unless empty? + key = keys.last + val = delete(key) + [key, val] end end @@ -1188,23 +1159,6 @@ def mongoize self end - # Conditionally include ordering module based on Ruby version - # - # Ruby 1.9+ maintains Hash insertion order natively, so the @keys array - # and associated methods are not needed. This provides memory optimization - # (25% reduction per Map instance) and delegates to Hash's efficient ordering. - # - # For Ruby < 1.9, the ordering module provides manual insertion order tracking - # via @keys array for backward compatibility. - if RUBY_VERSION < '1.9' - # Ruby < 1.9: MUST use ordering module (Hash is unordered) - require_relative 'map/ordering' - include Ordering - elsif ENV['MAP_FORCE_ORDERING'] - # Ruby >= 1.9 ONLY: Optionally force module inclusion for testing legacy code path - require_relative 'map/ordering' - include Ordering - end end module Kernel diff --git a/lib/map/_lib.rb b/lib/map/_lib.rb index cb7af8f..3b5527d 100644 --- a/lib/map/_lib.rb +++ b/lib/map/_lib.rb @@ -1,5 +1,5 @@ class Map - VERSION = '8.0.0' + VERSION = '9.0.0' class << Map def version diff --git a/lib/map/ordering.rb b/lib/map/ordering.rb deleted file mode 100644 index 3dd6fb2..0000000 --- a/lib/map/ordering.rb +++ /dev/null @@ -1,126 +0,0 @@ -# -*- encoding : utf-8 -*- - -# Map::Ordering -# -# This module contains all ordering-related methods that use the @keys array -# to maintain insertion order. It is conditionally included in Map only when -# Ruby's Hash class does not maintain insertion order (Ruby < 1.9), or when -# explicitly forced via ENV['MAP_FORCE_ORDERING']. -# -# On Ruby 1.9+, Hash maintains insertion order natively, so these methods are -# not needed and Map delegates to Hash's implementation for memory optimization. - -class Map < Hash - module Ordering - # Hook called when module is included into Map class - # This allows us to inject class methods into Map - def self.included(base) - base.class_eval do - # Override Map.allocate to initialize @keys array - def self.allocate - super.instance_eval do - @keys = [] - self - end - end - end - end - - # Instance methods that track ordering via @keys array - - def keys - @keys ||= [] - end - - def []=(key, val) - key, val = convert(key, val) - keys.push(key) unless has_key?(key) - __set__(key, val) - end - alias_method 'store', '[]=' - - def values - array = [] - keys.each{|key| array.push(self[key])} - array - end - alias_method 'vals', 'values' - - def first - [keys.first, self[keys.first]] - end - - def last - [keys.last, self[keys.last]] - end - - def each_with_index - keys.each_with_index{|key, index| yield([key, self[key]], index)} - self - end - - def each_key - keys.each{|key| yield(key)} - self - end - - def each_value - keys.each{|key| yield self[key]} - self - end - - def each - keys.each{|key| yield(key, self[key])} - self - end - alias_method 'each_pair', 'each' - - def delete(key) - key = convert_key(key) - keys.delete(key) - super(key) - end - - def clear - keys.clear - super - end - - # Array-like ordered operations - def shift - unless empty? - key = keys.first - val = delete(key) - [key, val] - end - end - - def unshift(*args) - Map.each_pair(*args) do |key, val| - key = convert_key(key) - delete(key) - keys.unshift(key) - __set__(key, val) - end - self - end - - def push(*args) - Map.each_pair(*args) do |key, val| - key = convert_key(key) - delete(key) - keys.push(key) - __set__(key, val) - end - self - end - - def pop - unless empty? - key = keys.last - val = delete(key) - [key, val] - end - end - end -end diff --git a/map.gemspec b/map.gemspec index 3cb8736..678a108 100644 --- a/map.gemspec +++ b/map.gemspec @@ -3,7 +3,7 @@ Gem::Specification::new do |spec| spec.name = "map" - spec.version = "8.0.0" + spec.version = "9.0.0" spec.required_ruby_version = '>= 3.0' spec.platform = Gem::Platform::RUBY spec.summary = "the perfect ruby data structure" @@ -23,7 +23,6 @@ Gem::Specification::new do |spec| "lib/map.rb", "lib/map/_lib.rb", "lib/map/options.rb", - "lib/map/ordering.rb", "map.gemspec", "specs", "specs/001-ordered-map-module",