Class: NumericHash

Inherits:
Hash
  • Object
show all
Defined in:
lib/numeric_hash.rb,
lib/numeric_hash/version.rb

Overview

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.

Constant Summary collapse

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.

0
BINARY_OPERATORS =
[:+, :-, :*, :/, :%, :**, :&, :|, :^, :div, :modulo, :quo, :fdiv, :remainder]
UNARY_OPERATORS =
[:+@, :-@, :~@, :abs, :ceil, :floor, :round, :truncate]
VERSION =
"0.5.2"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(initial_contents = nil, initial_value = DEFAULT_INITIAL_VALUE) ⇒ NumericHash

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 } }


28
29
30
31
32
33
34
# 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

Class Method Details

.sum(array) ⇒ Object

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([])       # => { }


431
432
433
# File 'lib/numeric_hash.rb', line 431

def sum(array)
  array.empty? ? self.new : array.sum
end

Instance Method Details

#apply_array!(array, initial_value = DEFAULT_INITIAL_VALUE) ⇒ Object



36
37
38
# 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) ⇒ Object



40
41
42
43
44
# 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(&block) ⇒ Object Also known as: map_numeric

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" } }


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# 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

#collect_numeric!(&block) ⇒ Object Also known as: map_numeric!



217
218
219
220
221
222
223
224
225
226
227
228
229
230
# 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

#compressObject

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 }


62
63
64
# File 'lib/numeric_hash.rb', line 62

def compress
  map_values { |value| convert_to_numeric(value) }
end

#compress!Object



66
67
68
# File 'lib/numeric_hash.rb', line 66

def compress!
  map_values! { |value| convert_to_numeric(value) }
end

#deep_merge(other_hash, match_structure = false) ⇒ Object

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

Raises:

  • (ArgumentError)


292
293
294
295
296
297
298
299
300
301
302
303
304
# 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) ⇒ Object

Raises:

  • (ArgumentError)


306
307
308
309
310
311
312
313
314
315
316
317
318
# 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_negativesObject

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 }


121
122
123
124
# 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

#maxObject

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



109
110
111
# File 'lib/numeric_hash.rb', line 109

def max
  compressed_key_values_sorted.last
end

#minObject

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



103
104
105
# File 'lib/numeric_hash.rb', line 103

def min
  compressed_key_values_sorted.first
end

#normalize(magnitude = 1.0) ⇒ Object

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 }


77
78
79
80
# 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) ⇒ Object



82
83
84
85
# 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(&block) ⇒ Object

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 }


241
242
243
244
245
246
247
248
249
250
251
# 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!(&block) ⇒ Object



253
254
255
256
257
258
259
260
261
262
# 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_numericObject

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 } }


271
272
273
# File 'lib/numeric_hash.rb', line 271

def select_numeric
  reject_numeric { |value| !yield(value) }
end

#select_numeric!Object



275
276
277
# File 'lib/numeric_hash.rb', line 275

def select_numeric!
  reject_numeric! { |value| !yield(value) }
end

#strip_zeroObject

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}


134
135
136
137
138
# 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) ⇒ Object



97
98
99
# File 'lib/numeric_hash.rb', line 97

def to_amount(amount)
  normalize(amount)
end

#to_hashObject

Converts the NumericHash into a regular Hash.



322
323
324
325
326
# 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_percentObject



93
94
95
# File 'lib/numeric_hash.rb', line 93

def to_percent
  normalize(100.0)
end

#to_ratioObject

Shortcuts to normalize the hash to various totals.



89
90
91
# File 'lib/numeric_hash.rb', line 89

def to_ratio
  normalize(1.0)
end

#totalObject

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


53
54
55
# File 'lib/numeric_hash.rb', line 53

def total
  values.map { |value| convert_to_numeric(value) }.sum
end