numeric_hash 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+