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 +4 -0
- data/Gemfile +4 -0
- data/README.rdoc +18 -0
- data/Rakefile +1 -0
- data/lib/numeric_hash/version.rb +3 -0
- data/lib/numeric_hash.rb +260 -0
- data/numeric_hash.gemspec +23 -0
- metadata +87 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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"
|
data/lib/numeric_hash.rb
ADDED
@@ -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
|
+
|