Class: Hash

Inherits:
Object
  • Object
show all
Defined in:
lib/everythingrb/hash.rb

Overview

Extensions to Ruby’s core Hash class

Provides:

  • #to_struct, #to_ostruct, #to_istruct: Convert hashes to different structures

  • #join_map: Combine filter_map and join operations

  • #deep_freeze: Recursively freeze hash and contents

  • #transform_values.with_key: Transform values with access to keys

  • #transform, #transform!: Transform keys and values

  • #value_where, #values_where: Find values based on conditions

  • #rename_key, #rename_keys: Rename hash keys while preserving order

  • ::new_nested_hash: Create automatically nesting hashes

Examples:

require "everythingrb/hash"
config = {server: {port: 443}}.to_ostruct
config.server.port  # => 443

Constant Summary collapse

EMPTY_STRUCT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

A minimal empty struct for Ruby 3.2+ compatibility

Ruby 3.2 enforces stricter argument handling for Struct. This means trying to create a Struct from an empty Hash will result in an ArgumentError being raised. This is trying to keep a consistent experience with that version and newer versions.

Returns:

  • (Struct)

    A struct with a single nil-valued field

Struct.new(:_).new(nil)

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.new_nested_hash(depth: nil) ⇒ Hash

Note:

While unlimited nesting is convenient, it can interfere with common Ruby patterns like ||= when initializing values at deep depths. Use the depth parameter to control this behavior.

Creates a new Hash that automatically initializes missing keys with nested hashes

This method creates a hash where any missing key access will automatically create another nested hash with the same behavior. You can control the nesting depth with the depth parameter.

Examples:

Unlimited nesting (default behavior)

users = Hash.new_nested_hash
users[:john][:role] = "admin"  # No need to initialize users[:john] first
users # => {john: {role: "admin"}}

Deep nesting without initialization

stats = Hash.new_nested_hash
stats[:server][:region][:us_east][:errors] = ["Error"]
stats # => {server: {region: {us_east: {errors: ["Error"]}}}}

Limited nesting depth

hash = Hash.new_nested_hash(depth: 1)
hash[:user][:name] = "Alice"  # Works fine - only one level of auto-creation

# This pattern works correctly with limited nesting:
(hash[:user][:roles] ||= []) << "admin"
hash # => {user: {name: "Alice", roles: ["admin"]}}

Parameters:

  • depth (Integer, nil) (defaults to: nil)

    The maximum nesting depth for automatic hash creation When nil (default), creates unlimited nesting depth When 0, behaves like a regular hash (returns nil for missing keys) When > 0, automatically creates hashes only up to the specified level

Returns:

  • (Hash)

    A hash that creates nested hashes for missing keys



71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/everythingrb/hash.rb', line 71

def self.new_nested_hash(depth: nil)
  new do |hash, key|
    next if depth == 0

    hash[key] =
      if depth.nil?
        new_nested_hash
      else
        new_nested_hash(depth: depth - 1)
      end
  end
end

Instance Method Details

#deep_freezeself

Note:

CAUTION: Be careful when freezing collections that contain class objects or singleton instances - this will freeze those classes/objects globally! Only use deep_freeze on pure data structures you want to make immutable.

Recursively freezes self and all of its values

Examples:

{ user: { name: "Alice", roles: ["admin"] } }.deep_freeze
# => Hash and all nested structures are now frozen

Returns:

  • (self)

    Returns the frozen hash



249
250
251
252
# File 'lib/everythingrb/hash.rb', line 249

def deep_freeze
  each_value { |v| v.respond_to?(:deep_freeze) ? v.deep_freeze : v.freeze }
  freeze
end

#join_map(join_with = "") {|key, value| ... } ⇒ String

Combines filter_map and join operations

Examples:

{ a: 1, b: nil, c: 2, d: nil, e: 3 }.join_map(", ") { |k, v| "#{k}-#{v}" if v }
# => "a-1, c-2, e-3"

Without a block

{ a: 1, b: nil, c: 2 }.join_map(" ")
# => "a 1 b c 2"

Parameters:

  • join_with (String) (defaults to: "")

    The delimiter to join elements with (defaults to empty string)

Yields:

  • (key, value)

    Block that filters and transforms hash entries

Yield Parameters:

  • key (Object)

    The current key

  • value (Object)

    The current value

Returns:

  • (String)

    Joined string of filtered and transformed entries



103
104
105
106
107
# File 'lib/everythingrb/hash.rb', line 103

def join_map(join_with = "", &block)
  block = ->(kv_pair) { kv_pair.compact } if block.nil?

  filter_map(&block).join(join_with)
end

#og_transform_valuesObject

Allows calling original method. See below



255
# File 'lib/everythingrb/hash.rb', line 255

alias_method :og_transform_values, :transform_values

#og_transform_values!Object

Allows calling original method. See below



258
# File 'lib/everythingrb/hash.rb', line 258

alias_method :og_transform_values!, :transform_values!

#rename_key(old_key, new_key) ⇒ Hash

Renames a key in the hash while preserving the original order of elements

Examples:

Renames a single key

{a: 1, b: 2, c: 3}.rename_key(:b, :middle)
# => {a: 1, middle: 2, c: 3}

Parameters:

  • old_key (Object)

    The key to rename

  • new_key (Object)

    The new key to use

Returns:

  • (Hash)

    A new hash with the key renamed



423
424
425
# File 'lib/everythingrb/hash.rb', line 423

def rename_key(old_key, new_key)
  rename_keys(old_key => new_key)
end

#rename_key!(old_key, new_key) ⇒ self

Renames a key in the hash in place while preserving the original order of elements

Examples:

Renames a key in place

hash = {a: 1, b: 2, c: 3}
hash.rename_key!(:b, :middle)
# => {a: 1, middle: 2, c: 3}

Parameters:

  • old_key (Object)

    The key to rename

  • new_key (Object)

    The new key to use

Returns:

  • (self)

    The modified hash



440
441
442
# File 'lib/everythingrb/hash.rb', line 440

def rename_key!(old_key, new_key)
  rename_keys!(old_key => new_key)
end

#rename_key_unordered(old_key, new_key) ⇒ Hash

Renames a key in the hash without preserving element order (faster)

This method is significantly faster than #rename_key but does not guarantee that the order of elements in the hash will be preserved.

Examples:

Rename a single key without preserving order

{a: 1, b: 2, c: 3}.rename_key_unordered(:b, :middle)
# => {a: 1, c: 3, middle: 2}  # Order may differ

Parameters:

  • old_key (Object)

    The key to rename

  • new_key (Object)

    The new key to use

Returns:

  • (Hash)

    A new hash with the key renamed



502
503
504
505
506
507
508
509
# File 'lib/everythingrb/hash.rb', line 502

def rename_key_unordered(old_key, new_key)
  # Fun thing I learned. For small hashes, using #except is 1.5x faster than using dup and delete.
  # But as the hash becomes larger, the performance improvements become diminished until they're roughly the same.
  # Neat!
  hash = except(old_key)
  hash[new_key] = self[old_key]
  hash
end

#rename_key_unordered!(old_key, new_key) ⇒ self

Renames a key in the hash in place without preserving element order (faster)

This method is significantly faster than #rename_key! but does not guarantee that the order of elements in the hash will be preserved.

Examples:

Rename a key in place without preserving order

hash = {a: 1, b: 2, c: 3}
hash.rename_key_unordered!(:b, :middle)
# => {a: 1, c: 3, middle: 2}  # Order may differ

Parameters:

  • old_key (Object)

    The key to rename

  • new_key (Object)

    The new key to use

Returns:

  • (self)

    The modified hash



527
528
529
530
# File 'lib/everythingrb/hash.rb', line 527

def rename_key_unordered!(old_key, new_key)
  self[new_key] = delete(old_key)
  self
end

#rename_keys(**keys) ⇒ Hash

Renames multiple keys in the hash while preserving the original order of elements

This method maintains the original order of all keys in the hash, renaming only the specified keys while keeping their positions unchanged.

Examples:

Renames multiple keys

{a: 1, b: 2, c: 3, d: 4}.rename_keys(a: :first, c: :third)
# => {first: 1, b: 2, third: 3, d: 4}

Parameters:

  • keys (Hash)

    A mapping of old_key => new_key pairs

Returns:

  • (Hash)

    A new hash with keys renamed



458
459
460
461
462
463
# File 'lib/everythingrb/hash.rb', line 458

def rename_keys(**keys)
  # I tried multiple different ways to rename the key while preserving the order, this was the fastest
  transform_keys do |key|
    keys.key?(key) ? keys[key] : key
  end
end

#rename_keys!(**keys) ⇒ self

Renames multiple keys in the hash in place while preserving the original order of elements

This method maintains the original order of all keys in the hash, renaming only the specified keys while keeping their positions unchanged.

Examples:

Rename multiple keys in place

hash = {a: 1, b: 2, c: 3, d: 4}
hash.rename_keys!(a: :first, c: :third)
# => {first: 1, b: 2, third: 3, d: 4}

Parameters:

  • keys (Hash)

    A mapping of old_key => new_key pairs

Returns:

  • (self)

    The modified hash



480
481
482
483
484
485
# File 'lib/everythingrb/hash.rb', line 480

def rename_keys!(**keys)
  # I tried multiple different ways to rename the key while preserving the order, this was the fastest
  transform_keys! do |key|
    keys.key?(key) ? keys[key] : key
  end
end

#to_deep_hHash

Recursively converts all values that respond to #to_h

Similar to #to_h but recursively traverses the Hash structure and calls #to_h on any object that responds to it. Useful for normalizing nested data structures and parsing nested JSON.

Examples:

Converting nested Data objects

user = { name: "Alice", metadata: Data.define(:source).new(source: "API") }
user.to_deep_h  # => {name: "Alice", metadata: {source: "API"}}

Parsing nested JSON strings

nested = { profile: '{"role":"admin"}' }
nested.to_deep_h  # => {profile: {role: "admin"}}

Mixed nested structures

data = {
  config: OpenStruct.new(api_key: "secret"),
  users: [
    Data.define(:name).new(name: "Bob"),
    {role: "admin"}
  ]
}
data.to_deep_h
# => {
#      config: {api_key: "secret"},
#      users: [{name: "Bob"}, {role: "admin"}]
#    }

Returns:

  • (Hash)

    A deeply converted hash with all nested objects converted



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/everythingrb/hash.rb', line 140

def to_deep_h
  transform_values do |value|
    case value
    when Hash
      value.to_deep_h
    when Array
      value.to_deep_h
    when String
      # If the string is not valid JSON, #to_deep_h will return `nil`
      value.to_deep_h || value
    else
      value.respond_to?(:to_h) ? value.to_h : value
    end
  end
end

#to_istructData

Converts hash to an immutable Data structure

Examples:

hash = { person: { name: "Bob", age: 30 } }
data = hash.to_istruct
data.person.name # => "Bob"
data.class # => Data

Returns:

  • (Data)

    An immutable Data object with the same structure



167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/everythingrb/hash.rb', line 167

def to_istruct
  recurse = lambda do |input|
    case input
    when Hash
      input.to_istruct
    when Array
      input.map(&recurse)
    else
      input
    end
  end

  Data.define(*keys.map(&:to_sym)).new(*values.map { |value| recurse.call(value) })
end

#to_ostructOpenStruct

Converts hash to an OpenStruct recursively

Examples:

hash = { config: { api_key: "secret" } }
config = hash.to_ostruct
config.config.api_key # => "secret"

Returns:

  • (OpenStruct)

    An OpenStruct with methods matching hash keys



221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/everythingrb/hash.rb', line 221

def to_ostruct
  recurse = lambda do |value|
    case value
    when Hash
      value.to_ostruct
    when Array
      value.map(&recurse)
    else
      value
    end
  end

  OpenStruct.new(**transform_values { |value| recurse.call(value) })
end

#to_structStruct

Converts hash to a Struct recursively

Examples:

hash = { user: { name: "Alice", roles: ["admin"] } }
struct = hash.to_struct
struct.user.name # => "Alice"
struct.class # => Struct

Returns:

  • (Struct)

    A struct with methods matching hash keys



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/everythingrb/hash.rb', line 193

def to_struct
  # For Ruby 3.2, it raises if you attempt to create a Struct with no keys
  return EMPTY_STRUCT if RUBY_VERSION.start_with?("3.2") && empty?

  recurse = lambda do |value|
    case value
    when Hash
      value.to_struct
    when Array
      value.map(&recurse)
    else
      value
    end
  end

  Struct.new(*keys.map(&:to_sym)).new(*values.map { |value| recurse.call(value) })
end

#transform {|key, value| ... } ⇒ Hash, Enumerator

Transforms keys and values to create a new hash

Examples:

Transform both keys and values

{a: 1, b: 2}.transform { |k, v| ["#{k}_key", v * 2] }
# => {a_key: 2, b_key: 4}

Yields:

  • (key, value)

    Block that returns a new key-value pair

Yield Parameters:

  • key (Object)

    The original key

  • value (Object)

    The original value

Yield Returns:

  • (Array)

    Two-element array containing the new key and value

Returns:

  • (Hash)

    A new hash with transformed keys and values

  • (Enumerator)

    If no block is given



332
333
334
335
336
# File 'lib/everythingrb/hash.rb', line 332

def transform(&block)
  return to_enum(:transform) if block.nil?

  to_h(&block)
end

#transform! {|key, value| ... } ⇒ self, Enumerator

Transforms keys and values in place

Examples:

Transform both keys and values in place

hash = {a: 1, b: 2}
hash.transform! { |k, v| ["#{k}_key", v * 2] }
# => {a_key: 2, b_key: 4}

Yields:

  • (key, value)

    Block that returns a new key-value pair

Yield Parameters:

  • key (Object)

    The original key

  • value (Object)

    The original value

Yield Returns:

  • (Array)

    Two-element array containing the new key and value

Returns:

  • (self)

    The transformed hash

  • (Enumerator)

    If no block is given



354
355
356
357
358
# File 'lib/everythingrb/hash.rb', line 354

def transform!(&block)
  return to_enum(:transform!) if block.nil?

  replace(transform(&block))
end

#transform_values {|value| ... } ⇒ Hash, Enumerator

Transforms hash values while allowing access to keys via the chainable with_key method

This method either performs a standard transform_values operation if a block is given, or returns an enumerator with a with_key method that passes both the key and value to the block.

Examples:

Standard transform_values

{a: 1, b: 2}.transform_values { |v| v * 2 }
# => {a: 2, b: 4}

Using with_key to access keys during transformation

{a: 1, b: 2}.transform_values.with_key { |v, k| "#{k}_#{v}" }
# => {a: "a_1", b: "b_2"}

Yields:

  • (value)

    Block to transform each value (standard behavior)

Yield Parameters:

  • value (Object)

    The value to transform

Yield Returns:

  • (Object)

    The transformed value

Returns:

  • (Hash, Enumerator)

    Result hash or Enumerator with with_key method



281
282
283
284
285
# File 'lib/everythingrb/hash.rb', line 281

def transform_values(&block)
  return transform_values_enumerator if block.nil?

  og_transform_values(&block)
end

#transform_values! {|value| ... } ⇒ self, Enumerator

Transforms hash values in place while allowing access to keys via the chainable with_key method

This method either performs a standard transform_values! operation if a block is given, or returns an enumerator with a with_key method that passes both the key and value to the block, updating the hash in place.

Examples:

Standard transform_values!

hash = {a: 1, b: 2}
hash.transform_values! { |v| v * 2 }
# => {a: 2, b: 4}

Using with_key to access keys during in-place transformation

hash = {a: 1, b: 2}
hash.transform_values!.with_key { |v, k| "#{k}_#{v}" }
# => {a: "a_1", b: "b_2"}

Yields:

  • (value)

    Block to transform each value (standard behavior)

Yield Parameters:

  • value (Object)

    The value to transform

Yield Returns:

  • (Object)

    The transformed value

Returns:

  • (self, Enumerator)

    Original hash with transformed values or Enumerator with with_key method



311
312
313
314
315
# File 'lib/everythingrb/hash.rb', line 311

def transform_values!(&block)
  return transform_values_bang_enumerator if block.nil?

  og_transform_values!(&block)
end

#value_where {|key, value| ... } ⇒ Object, ...

Returns the first value where the key-value pair satisfies the given condition

Examples:

Find first admin user by role

users = {
  alice: {name: "Alice", role: "admin"},
  bob: {name: "Bob", role: "user"},
  charlie: {name: "Charlie", role: "admin"}
}
users.value_where { |k, v| v[:role] == "admin" } # => {name: "Alice", role: "admin"}

Yields:

  • (key, value)

    Block that determines whether to include the value

Yield Parameters:

  • key (Object)

    The current key

  • value (Object)

    The current value

Yield Returns:

  • (Boolean)

    Whether to include this value

Returns:

  • (Object, nil)

    The first matching value or nil if none found

  • (Enumerator)

    If no block is given



379
380
381
382
383
# File 'lib/everythingrb/hash.rb', line 379

def value_where(&block)
  return to_enum(:value_where) if block.nil?

  find(&block)&.last
end

#values_where {|key, value| ... } ⇒ Array, Enumerator

Returns all values where the key-value pairs satisfy the given condition

Examples:

Find all admin users by role

users = {
  alice: {name: "Alice", role: "admin"},
  bob: {name: "Bob", role: "user"},
  charlie: {name: "Charlie", role: "admin"}
}
users.values_where { |k, v| v[:role] == "admin" }
# => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]

Yields:

  • (key, value)

    Block that determines whether to include the value

Yield Parameters:

  • key (Object)

    The current key

  • value (Object)

    The current value

Yield Returns:

  • (Boolean)

    Whether to include this value

Returns:

  • (Array)

    All matching values

  • (Enumerator)

    If no block is given



405
406
407
408
409
# File 'lib/everythingrb/hash.rb', line 405

def values_where(&block)
  return to_enum(:values_where) if block.nil?

  select(&block).values
end