numeric_hash 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in numeric_hash.gemspec
4
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,18 @@
1
+ = NumericHash
2
+
3
+ Defines a hash whose values are +Numeric+ or additional nested +NumericHash+es.
4
+
5
+ Common arithmetic methods available on +Numeric+ can be called on +NumericHash+
6
+ to affect all values within the +NumericHash+ at once.
7
+
8
+ == Examples
9
+
10
+ hash1 = NumericHash.new(:a => -1.0, :b => 2) # => { :a => -1.6, :b => 2 }
11
+ hash2 = NumericHash.new(:a => 3, :c => 4) # => { :a => 3, :c => 4 }
12
+ hash1 + hash2 # => { :a => 1.4, :b => 2, :c => 4 }
13
+ hash1 * 5 # => { :a => -8.0, :b => 10 }
14
+ -hash1 # => { :a => 1.6, :b => -2 }
15
+ hash1.round # => { :a => -2, :b => 2 }
16
+
17
+ Author:: Clyde Law (mailto:claw@alum.mit.edu)
18
+ License:: Released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ module NumericHash
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,260 @@
1
+ require "numeric_hash/version"
2
+
3
+ # Defines a hash whose values are Numeric or additional nested NumericHashes.
4
+ #
5
+ # Common arithmetic methods available on Numeric can be called on NumericHash
6
+ # to affect all values within the NumericHash at once.
7
+ #
8
+ class NumericHash < Hash
9
+
10
+ # Default initial value for hash values when an initial value is unspecified.
11
+ # Integer 0 is used instead of Float 0.0 because it can automatically be
12
+ # converted into a Float when necessary during operations with other Floats.
13
+ DEFAULT_INITIAL_VALUE = 0
14
+
15
+ BINARY_OPERATORS = [:+, :-, :*, :/, :%, :**, :&, :|, :^, :div, :modulo, :quo, :fdiv, :remainder]
16
+ UNARY_OPERATORS = [:+@, :-@, :~@, :abs, :ceil, :floor, :round, :truncate]
17
+
18
+ # Initialize the NumericHash with an array of initial keys or hash of initial
19
+ # key-value pairs (whose values could also be arrays or hashes). An optional
20
+ # initial value for initial keys can be specified as well.
21
+ #
22
+ # NumericHash.new # => { }
23
+ # NumericHash.new([:a, :b]) # => { :a => 0, :b => 0 }
24
+ # NumericHash.new([:c, :d], 1.0) # => { :c => 1.0, :d => 1.0 }
25
+ # NumericHash.new(:e => 2, :f => 3.0) # => { :e => 2, :f => 3.0 }
26
+ # NumericHash.new({ :g => 4, :h => [:i, :j] }, 5.0) # => { :g => 4, :h => { :i => 5.0, :j => 5.0 } }
27
+ #
28
+ def initialize(initial_contents = nil, initial_value = DEFAULT_INITIAL_VALUE)
29
+ case initial_contents
30
+ when Array then apply_array!(initial_contents, initial_value)
31
+ when Hash then apply_hash!(initial_contents, initial_value)
32
+ else raise ArgumentError.new("invalid initial data: #{initial_contents.inspect}") if initial_contents
33
+ end
34
+ end
35
+
36
+ def apply_array!(array, initial_value = DEFAULT_INITIAL_VALUE)
37
+ array.each { |key| self[key] = initial_value }
38
+ end
39
+
40
+ def apply_hash!(hash, initial_value = DEFAULT_INITIAL_VALUE)
41
+ hash.each do |key, value|
42
+ self[key] = (value.is_a?(Array) || value.is_a?(Hash)) ? NumericHash.new(value, initial_value) : convert_to_numeric(value)
43
+ end
44
+ end
45
+
46
+ # Total all values in the hash.
47
+ #
48
+ # @hash1 # => { :a => 1.0, :b => 2 }
49
+ # @hash2 # => { :c => 3, :d => { :e => 4, :f => 5} }
50
+ # @hash1.total # => 3.0
51
+ # @hash2.total # => 12
52
+ #
53
+ def total
54
+ values.map { |value| convert_to_numeric(value) }.sum
55
+ end
56
+
57
+ # Compress the hash to its top level values, totaling all nested values.
58
+ #
59
+ # @hash # => { :a => 1, :b => { :c => 2.0, d: => 3 } }
60
+ # @hash.compress # => { :a => 1, :b => 5.0 }
61
+ #
62
+ def compress
63
+ map_values { |value| convert_to_numeric(value) }
64
+ end
65
+
66
+ def compress!
67
+ map_values! { |value| convert_to_numeric(value) }
68
+ end
69
+
70
+ # Normalize the total of all hash values to the specified magnitude. If no
71
+ # magnitude is specified, the hash is normalized to 1.0.
72
+ #
73
+ # @hash # => { :a => 1, :b => 2, :c => 3, :d => 4 }
74
+ # @hash.normalize # => { :a => 0.1, :b => 0.2, :c => 0.3, :d => 0.4 }
75
+ # @hash.normalize(120) # => { :a => 12.0, :b => 24.0, :c => 36.0, :d => 48.0 }
76
+ #
77
+ def normalize(magnitude = 1.0)
78
+ norm_factor = magnitude / total.to_f
79
+ 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.
80
+ map_values { |value| value * norm_factor }
81
+ end
82
+
83
+ # Shortcuts to normalize the hash to various totals.
84
+ #
85
+ def to_ratio
86
+ normalize(1.0)
87
+ end
88
+
89
+ def to_percent
90
+ normalize(100.0)
91
+ end
92
+
93
+ def to_amount(amount)
94
+ normalize(amount)
95
+ end
96
+
97
+ # Returns the key-value pair with the smallest compressed value in the hash.
98
+ #
99
+ def min
100
+ compressed_key_values_sorted.first
101
+ end
102
+
103
+ # Returns the key-value pair with the largest compressed value in the hash.
104
+ #
105
+ def max
106
+ compressed_key_values_sorted.last
107
+ end
108
+
109
+ # Set all negative values in the hash to zero.
110
+ #
111
+ # @hash # => { :a => -0.6, :b => 1.2, :c => 0.4 }
112
+ # @hash.ignore_negatives # => { :a => 0.0, :b => 1.2, :a => 0.4 }
113
+ #
114
+ def ignore_negatives
115
+ convert_negatives_to_zero(self)
116
+ end
117
+
118
+ # Strips out any zero valued asset classes.
119
+ #
120
+ # @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}
121
+ # @hash.strip_zero # => {:c => 0.8, :e => 0.05, :d => 0.15}
122
+ #
123
+ def strip_zero
124
+ # 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.
125
+ compress.select_values! { |value| value > 0.0 }
126
+ end
127
+
128
+ # Define arithmetic operators that apply a Numeric or another NumericHash to
129
+ # the hash. A Numeric argument is applied to each value in the hash.
130
+ # Hash values of a NumericHash argument are applied to each corresponding
131
+ # value in the hash. In the case of no such corresponding value, the
132
+ # hash value of the argument is applied to DEFAULT_INITIAL_VALUE.
133
+ #
134
+ # @hash1 # => { :a => 1.0, :b => 2 }
135
+ # @hash2 # => { :a => 3, :c => 4 }
136
+ # @hash1 + @hash2 # => { :a => 4.0, :b => 2, :c => 4 }
137
+ # @hash1 * 5 # => { :a => 5.0, :b => 10 }
138
+ #
139
+ BINARY_OPERATORS.each do |operator|
140
+ define_method(operator) do |arg|
141
+ if arg.is_a?(NumericHash)
142
+ # Copy the hash into a new initial hash that will be used to return the
143
+ # result and reconcile its traits with those of the argument.
144
+ initial = self.copy.reconcile_traits_with!(arg)
145
+
146
+ # Apply the argument to the initial hash.
147
+ arg.inject(initial) do |hash, (arg_key, arg_value)|
148
+ hash[arg_key] = apply_operator_to_values(operator, hash[arg_key], arg_value)
149
+ hash
150
+ end
151
+ else
152
+ map_values { |value| value.__send__(operator, convert_to_numeric(arg)) }
153
+ end
154
+ end
155
+ end
156
+
157
+ # Define unary operators that apply to each value in the hash.
158
+ #
159
+ # @hash # => { :a => 1.0, :b => -2.5 }
160
+ # -@hash # => { :a => -1.0, :b => 2.5 }
161
+ # @hash.round # => { :a => 1, :b => -3 }
162
+ #
163
+ UNARY_OPERATORS.each do |operator|
164
+ define_method(operator) { map_values(&operator) }
165
+ end
166
+
167
+ # Define conversion methods that convert each value in the hash.
168
+ #
169
+ # @hash # => { :a => 1.0, :b => 2 }
170
+ # @hash.map_to_i # => { :a => 1, :b => 2 }
171
+ #
172
+ [:to_f, :to_i, :to_int].each do |convert_method|
173
+ define_method("map_#{convert_method}".to_sym) { map_values(&convert_method) }
174
+ end
175
+
176
+ protected
177
+
178
+ # Helper method for converting negative values to zero.
179
+ #
180
+ def convert_negatives_to_zero(value)
181
+ if value.is_a?(NumericHash)
182
+ # Map this method call over all values in the hash.
183
+ value.map_values(&method(__method__))
184
+ else
185
+ value = convert_to_numeric(value)
186
+ value < 0.0 ? 0.0 : value
187
+ end
188
+ end
189
+
190
+ # Helper method for converting a specified value to a Numeric.
191
+ #
192
+ def convert_to_numeric(value)
193
+ if value.is_a?(NumericHash)
194
+ value.total
195
+ elsif value.is_a?(Numeric)
196
+ value
197
+ elsif value.nil?
198
+ DEFAULT_INITIAL_VALUE
199
+ elsif value.respond_to?(:to_f)
200
+ value.to_f
201
+ elsif value.respond_to?(:to_i)
202
+ value.to_i
203
+ elsif value.respond_to?(:to_int)
204
+ value.to_int
205
+ else
206
+ raise ArgumentError.new("cannot convert to Numeric: #{value.inspect}")
207
+ end
208
+ end
209
+
210
+ # Helper method for applying an operator to two values of types Numeric
211
+ # and/or NumericHash.
212
+ #
213
+ def apply_operator_to_values(operator, value1, value2)
214
+ if value1.is_a?(NumericHash)
215
+ # First value is a NumericHash; directly apply the second value to it.
216
+ value1.__send__(operator, value2)
217
+ else
218
+ # First value is (or can be converted into) a Numeric
219
+ value1 = convert_to_numeric(value1)
220
+ if value2.is_a?(NumericHash)
221
+ # Second value is a NumericHash; each of its hash values should be
222
+ # applied to the first value.
223
+ value2.map_values { |value2_sub_value| value1.__send__(operator, value2_sub_value) }
224
+ else
225
+ # Second value also is (or can be converted into) a Numeric; apply the
226
+ # two values directly.
227
+ value1.__send__(operator, convert_to_numeric(value2))
228
+ end
229
+ end
230
+ end
231
+
232
+ # Helper method for sorting the compressed version of the hash.
233
+ #
234
+ def compressed_key_values_sorted
235
+ compress.sort_by { |key, value| value }
236
+ end
237
+
238
+ # Helper method for reconciling traits from another hash when a binary
239
+ # operation is performed with that hash.
240
+ #
241
+ def reconcile_traits_with!(hash)
242
+ # There are no traits to reconcile in the base NumericHash.
243
+ self
244
+ end
245
+
246
+ class << self
247
+
248
+ # Sums an array of NumericHashes, taking into account empty arrays.
249
+ #
250
+ # @array # => [ { :a => 1.0, :b => 2 }, { :a => 3, :c => 4 } ]
251
+ # sum(@array) # => { :a => 4.0, :b => 2, :c => 4 }
252
+ # sum([]) # => { }
253
+ #
254
+ def sum(array)
255
+ array.empty? ? self.new : array.sum
256
+ end
257
+
258
+ end
259
+
260
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "numeric_hash/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "numeric_hash"
7
+ s.version = NumericHash::VERSION
8
+ s.authors = ["Clyde Law"]
9
+ s.email = ["clyde@alum.mit.edu"]
10
+ s.homepage = %q{http://github.com/Umofomia/numeric_hash}
11
+ s.summary = %q{Defines a hash whose values are Numeric or additional nested NumericHashes.}
12
+ s.description = %q{Defines a hash whose values are Numeric or additional nested NumericHashes.}
13
+ s.license = 'MIT'
14
+
15
+ s.add_dependency('enumerate_hash_values')
16
+
17
+ s.rubyforge_project = "numeric_hash"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: numeric_hash
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Clyde Law
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-01-30 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: enumerate_hash_values
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: Defines a hash whose values are Numeric or additional nested NumericHashes.
36
+ email:
37
+ - clyde@alum.mit.edu
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files: []
43
+
44
+ files:
45
+ - .gitignore
46
+ - Gemfile
47
+ - README.rdoc
48
+ - Rakefile
49
+ - lib/numeric_hash.rb
50
+ - lib/numeric_hash/version.rb
51
+ - numeric_hash.gemspec
52
+ has_rdoc: true
53
+ homepage: http://github.com/Umofomia/numeric_hash
54
+ licenses:
55
+ - MIT
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ hash: 3
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 3
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ requirements: []
80
+
81
+ rubyforge_project: numeric_hash
82
+ rubygems_version: 1.6.2
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: Defines a hash whose values are Numeric or additional nested NumericHashes.
86
+ test_files: []
87
+