class NumericHash

Defines a hash whose values are Numeric or additional nested NumericHashes.

Common arithmetic methods available on Numeric can be called on NumericHash to affect all values within the NumericHash at once.

Constants

BINARY_OPERATORS
DEFAULT_INITIAL_VALUE

Default initial value for hash values when an initial value is unspecified. Integer 0 is used instead of Float 0.0 because it can automatically be converted into a Float when necessary during operations with other Floats.

UNARY_OPERATORS
VERSION

Public Class Methods

new(initial_contents = nil, initial_value = DEFAULT_INITIAL_VALUE) click to toggle source

Initialize the NumericHash with an array of initial keys or hash of initial key-value pairs (whose values could also be arrays or hashes). An optional initial value for initial keys can be specified as well.

NumericHash.new                                     # => { }
NumericHash.new([:a, :b])                           # => { :a => 0, :b => 0 }
NumericHash.new([:c, :d], 1.0)                      # => { :c => 1.0, :d => 1.0 }
NumericHash.new(:e => 2, :f => 3.0)                 # => { :e => 2, :f => 3.0 }
NumericHash.new({ :g => 4, :h => [:i, :j] }, 5.0)   # => { :g => 4, :h => { :i => 5.0, :j => 5.0 } }
# File lib/numeric_hash.rb, line 28
def initialize(initial_contents = nil, initial_value = DEFAULT_INITIAL_VALUE)
  case initial_contents
    when ::Array  then apply_array!(initial_contents, initial_value)
    when ::Hash   then apply_hash!(initial_contents, initial_value)
    else raise ArgumentError.new("invalid initial data: #{initial_contents.inspect}") if initial_contents
  end
end

Protected Class Methods

sum(array) click to toggle source

Sums an array of NumericHashes, taking into account empty arrays.

@array        # => [ { :a => 1.0, :b => 2 }, { :a => 3, :c => 4 } ]
sum(@array)   # => { :a => 4.0, :b => 2, :c => 4 }
sum([])       # => { }
# File lib/numeric_hash.rb, line 431
def sum(array)
  array.empty? ? self.new : array.sum
end

Public Instance Methods

apply_array!(array, initial_value = DEFAULT_INITIAL_VALUE) click to toggle source
# File lib/numeric_hash.rb, line 36
def apply_array!(array, initial_value = DEFAULT_INITIAL_VALUE)
  array.each { |key| self[key] = initial_value }
end
apply_hash!(hash, initial_value = DEFAULT_INITIAL_VALUE) click to toggle source
# File lib/numeric_hash.rb, line 40
def apply_hash!(hash, initial_value = DEFAULT_INITIAL_VALUE)
  hash.each do |key, value|
    self[key] = (value.is_a?(::Array) || value.is_a?(::Hash)) ? NumericHash.new(value, initial_value) : convert_to_numeric(value)
  end
end
collect_numeric() { |value| ... } click to toggle source

Maps each numeric value using the specified block.

@hash                                     # => { :a => 1, :b => { :c => 2, :d => 3 } }
@hash.map_numeric { |value| "X" * value } # => { :a => "X", :b => { :c => "XX", :d => "XXX" } }
@hash.collect_numeric(&:to_s)             # => { :a => "1", :b => { :c => "2", :d => "3" } }
# File lib/numeric_hash.rb, line 194
def collect_numeric(&block)
  # First attempt to map into a NumericHash.
  map_values do |value|
    if value.is_a?(NumericHash)
      result = value.collect_numeric(&block)
    else
      result = yield(value)

      # If the mapped value not Numeric, abort so that we try again by
      # mapping into a regular Hash.
      raise TypeError.new("result is not Numeric: #{result.inspect}") unless result.is_a?(Numeric)
    end
    result
  end
rescue TypeError
  # At least one of the values mapped into a non-Numeric result; map into a
  # regular Hash instead.
  map_to_hash do |key, value|
    [key, value.is_a?(NumericHash) ? value.collect_numeric(&block) : yield(value) ]
  end
end
Also aliased as: map_numeric
collect_numeric!() { |value| ... } click to toggle source
# File lib/numeric_hash.rb, line 217
def collect_numeric!(&block)
  map_values! do |value|
    if value.is_a?(NumericHash)
      result = value.collect_numeric!(&block)
    else
      result = yield(value)

      # If the mapped value not Numeric, abort since we can't change a
      # NumericHash into a regular Hash.
      raise TypeError.new("result is not Numeric: #{result.inspect}") unless result.is_a?(Numeric)
    end
    result
  end
end
Also aliased as: map_numeric!
compress() click to toggle source

Compress the hash to its top level values, totaling all nested values.

@hash           # => { :a => 1, :b => { :c => 2.0, d: => 3 } }
@hash.compress  # => { :a => 1, :b => 5.0 }
# File lib/numeric_hash.rb, line 62
def compress
  map_values { |value| convert_to_numeric(value) }
end
compress!() click to toggle source
# File lib/numeric_hash.rb, line 66
def compress!
  map_values! { |value| convert_to_numeric(value) }
end
deep_merge(other_hash, match_structure = false) click to toggle source

Performs a merge with another hash while recursively merging any nested hashes. If true is specified as a second argument, the merge will ensure that the key structure of the other hash is a subset of the structure of the hash.

@hash1                            # => { :a => 1, :b => { :c => 2 } }
@hash2                            # => { :b => 3 }
@hash3                            # => { :d => 4 }
@hash1.deep_merge(@hash2)         # => { :a => 1, :b => 3 }
@hash1.deep_merge(@hash2, true)   # raises TypeError
@hash1.deep_merge(@hash3)         # => { :a => 1, :b => { :c => 2 }, :d => 4 }
@hash1.deep_merge(@hash3, true)   # raises TypeError
# File lib/numeric_hash.rb, line 292
def deep_merge(other_hash, match_structure = false)
  raise ArgumentError.new('hash must be specified') unless other_hash.is_a?(::Hash)
  raise TypeError.new('structure of specified hash is incompatible') if match_structure && !compatible_structure?(other_hash)

  other_hash.inject(self.copy) do |hash, (key, value)|
    hash[key] = if hash[key].is_a?(NumericHash) && value.is_a?(::Hash)
                  hash[key].deep_merge(value, match_structure)
                else
                  sanitize_numeric_hash_value(value)
                end
    hash
  end
end
deep_merge!(other_hash, match_structure = false) click to toggle source
# File lib/numeric_hash.rb, line 306
def deep_merge!(other_hash, match_structure = false)
  raise ArgumentError.new('hash not specified') unless other_hash.is_a?(::Hash)
  raise TypeError.new('structure of specified hash is incompatible') if match_structure && !compatible_structure?(other_hash)

  other_hash.each do |key, value|
    if self[key].is_a?(NumericHash) && value.is_a?(::Hash)
      self[key].deep_merge!(value, match_structure)
    else
      self[key] = sanitize_numeric_hash_value(value)
    end
  end
  self
end
ignore_negatives() click to toggle source

DEPRECATED This method is deprecated. Consider using map_numeric to perform the same function.

Set all negative values in the hash to zero.

@hash                   # => { :a => -0.6, :b => 1.2, :c => 0.4 }
@hash.ignore_negatives  # => { :a => 0.0, :b => 1.2, :a => 0.4 }
# File lib/numeric_hash.rb, line 121
def ignore_negatives
  warn "DEPRECATION WARNING: This method is deprecated. Consider using #map_numeric to perform the same function. Called from: #{caller.first}"
  convert_negatives_to_zero(self)
end
map_numeric(&block)
Alias for: collect_numeric
map_numeric!(&block)
Alias for: collect_numeric!
max() click to toggle source

Returns the key-value pair with the largest compressed value in the hash.

# File lib/numeric_hash.rb, line 109
def max
  compressed_key_values_sorted.last
end
min() click to toggle source

Returns the key-value pair with the smallest compressed value in the hash.

# File lib/numeric_hash.rb, line 103
def min
  compressed_key_values_sorted.first
end
normalize(magnitude = 1.0) click to toggle source

Normalize the total of all hash values to the specified magnitude. If no magnitude is specified, the hash is normalized to 1.0.

@hash                 # => { :a => 1, :b => 2, :c => 3, :d => 4 }
@hash.normalize       # => { :a => 0.1, :b => 0.2, :c => 0.3, :d => 0.4 }
@hash.normalize(120)  # => { :a => 12.0, :b => 24.0, :c => 36.0, :d => 48.0 }
# File lib/numeric_hash.rb, line 77
def normalize(magnitude = 1.0)
  norm_factor = normalization_factor(magnitude)
  map_values { |value| value * norm_factor }
end
normalize!(magnitude = 1.0) click to toggle source
# File lib/numeric_hash.rb, line 82
def normalize!(magnitude = 1.0)
  norm_factor = normalization_factor(magnitude)
  map_values! { |value| value * norm_factor }
end
reject_numeric() { |value| ... } click to toggle source

Rejects each numeric value for which the specified block evaluates to true. Any nested hashes that become empty during this procedure are also rejected.

@hash                                       # => { :a => 1, :b => 0.0, :c => { :d => 0, :e => -2 }, :f => { :g => 0.0 } }
@hash.reject_numeric(&:zero?)               # => { :a => 1, :c => { :e => -2 } }
@hash.reject_numeric { |value| value <= 0 } # => { :a => 1 }
# File lib/numeric_hash.rb, line 241
def reject_numeric(&block)
  inject_into_empty do |hash, (key, value)|
    if value.is_a?(NumericHash)
      rejected = value.reject_numeric(&block)
      hash[key] = rejected unless rejected.empty?
    elsif !yield(value)
      hash[key] = value
    end
    hash
  end
end
reject_numeric!() { |value| ... } click to toggle source
# File lib/numeric_hash.rb, line 253
def reject_numeric!(&block)
  reject_values! do |value|
    if value.is_a?(NumericHash)
      value.reject_values!(&block)
      value.empty?
    else
      yield(value)
    end
  end
end
select_numeric() { |value| ... } click to toggle source

Selects each numeric value for which the specified block evaluates to true. Any nested hashes with no selected values will not be included.

@hash                                       # => { :a => 1, :b => 0.0, :c => { :d => 0, :e => -2 }, :f => { :g => 0.0 } }
@hash.select_numeric(&:zero?)               # => { :b => 0.0, :c => { :d => 0 }, :f => { :g => 0.0 } }
@hash.select_numeric { |value| value <= 0 } # => { :b => 0.0, :c => { :d => 0, :e => -2 }, :f => { :g => 0.0 } }
# File lib/numeric_hash.rb, line 271
def select_numeric
  reject_numeric { |value| !yield(value) }
end
select_numeric!() { |value| ... } click to toggle source
# File lib/numeric_hash.rb, line 275
def select_numeric!
  reject_numeric! { |value| !yield(value) }
end
strip_zero() click to toggle source

DEPRECATED This method is deprecated. Consider using compress with reject_numeric to perform the same function.

Strips out any zero valued asset classes.

@hash             # => {:a => 0.0, :b => 0.0, :c => 0.8, :d => 0.15, :e => 0.05, :f => 0.0, :g => 0.0, :h => 0.0, :i => 0.0}
@hash.strip_zero  # => {:c => 0.8, :e => 0.05, :d => 0.15}
# File lib/numeric_hash.rb, line 134
def strip_zero
  warn "DEPRECATION WARNING: This method is deprecated. Consider using #compress with #reject_numeric to perform the same function. Called from: #{caller.first}"
  # TODO: Previous version of the code only retained values > 0.0, so the refactored code below retains this behavior; verify whether this is still desired.
  compress.select_values! { |value| value > 0.0 }
end
to_amount(amount) click to toggle source
# File lib/numeric_hash.rb, line 97
def to_amount(amount)
  normalize(amount)
end
to_hash() click to toggle source

Converts the NumericHash into a regular Hash.

# File lib/numeric_hash.rb, line 322
def to_hash
  map_to_hash do |key, value|
    [key, value.is_a?(NumericHash) ? value.to_hash : value]
  end
end
to_percent() click to toggle source
# File lib/numeric_hash.rb, line 93
def to_percent
  normalize(100.0)
end
to_ratio() click to toggle source

Shortcuts to normalize the hash to various totals.

# File lib/numeric_hash.rb, line 89
def to_ratio
  normalize(1.0)
end
total() click to toggle source

Total all values in the hash.

@hash1        # => { :a => 1.0, :b => 2 }
@hash2        # => { :c => 3, :d => { :e => 4, :f => 5} }
@hash1.total  # => 3.0
@hash2.total  # => 12
# File lib/numeric_hash.rb, line 53
def total
  values.map { |value| convert_to_numeric(value) }.sum
end

Protected Instance Methods

apply_operator_to_values(operator, value1, value2) click to toggle source

Helper method for applying an operator to two values of types Numeric and/or NumericHash.

# File lib/numeric_hash.rb, line 369
def apply_operator_to_values(operator, value1, value2)
  if value1.is_a?(NumericHash)
    # First value is a NumericHash; directly apply the second value to it.
    value1.__send__(operator, value2)
  else
    # First value is (or can be converted into) a Numeric
    value1 = convert_to_numeric(value1)
    if value2.is_a?(NumericHash)
      # Second value is a NumericHash; each of its Numeric values should be
      # applied to the first value.
      value2.map_numeric { |value2_sub_value| value1.__send__(operator, value2_sub_value) }
    else
      # Second value also is (or can be converted into) a Numeric; apply the
      # two values directly.
      value1.__send__(operator, convert_to_numeric(value2))
    end
  end
end
compatible_structure?(other_hash) click to toggle source

Helper method that determines whether the structure of the specified hash is a subset of the structure of the hash.

# File lib/numeric_hash.rb, line 397
def compatible_structure?(other_hash)
  other_hash.all? do |key, value|
    self.has_key?(key) && (
      (!value.is_a?(::Hash) && !self[key].is_a?(NumericHash)) ||
      (value.is_a?(::Hash) && self[key].is_a?(NumericHash) && self[key].compatible_structure?(value))
    )
  end
end
compressed_key_values_sorted() click to toggle source

Helper method for sorting the compressed version of the hash.

# File lib/numeric_hash.rb, line 390
def compressed_key_values_sorted
  compress.sort_by { |key, value| value }
end
convert_negatives_to_zero(value) click to toggle source

Helper method for converting negative values to zero.

# File lib/numeric_hash.rb, line 332
def convert_negatives_to_zero(value)
  if value.is_a?(NumericHash)
    # Map this method call over all values in the hash.
    value.map_values(&method(__method__))
  else
    value = convert_to_numeric(value)
    value < 0.0 ? 0.0 : value
  end
end
convert_to_numeric(value) click to toggle source

Helper method for converting a specified value to a Numeric.

# File lib/numeric_hash.rb, line 344
def convert_to_numeric(value)
  case
    when value.is_a?(NumericHash)   then value.total
    when value.is_a?(Numeric)       then value
    when value.nil?                 then DEFAULT_INITIAL_VALUE
    when value.respond_to?(:to_f)   then value.to_f
    when value.respond_to?(:to_i)   then value.to_i
    when value.respond_to?(:to_int) then value.to_int
    else raise TypeError.new("cannot convert to Numeric: #{value.inspect}")
  end
end
normalization_factor(magnitude) click to toggle source

Helper method for calculating the normalization factor that needs to be applied to each value for a given magnitude.

# File lib/numeric_hash.rb, line 417
def normalization_factor(magnitude)
  norm_factor = magnitude / total.to_f
  norm_factor = 0.0 unless norm_factor.finite?  # If total was zero, the normalization factor will not be finite; set it to zero in this case.
  norm_factor
end
reconcile_traits_with!(hash) click to toggle source

Helper method for reconciling traits from another hash when a binary operation is performed with that hash.

# File lib/numeric_hash.rb, line 409
def reconcile_traits_with!(hash)
  # There are no traits to reconcile in the base NumericHash.
  self
end
sanitize_numeric_hash_value(value) click to toggle source

Helper method for sanitizing a value to be placed into a NumericHash.

# File lib/numeric_hash.rb, line 358
def sanitize_numeric_hash_value(value)
  case value
    when NumericHash  then value
    when Hash         then NumericHash.new(value)
    else convert_to_numeric(value)
  end
end