percentage 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ Percentage
2
+ ==========
3
+
4
+
5
+ A little library for working with percentages.
6
+
7
+
8
+ Feature Tour
9
+ ------------
10
+
11
+ The `Percentage` method converts Integer/BigDecimal/Rational objects
12
+ to percentage objects with values that you would expect:
13
+
14
+ ```ruby
15
+ Percentage(50) # => 50%
16
+
17
+ Percentage(BigDecimal('17.5')) # => 17.5%
18
+
19
+ Percentage(Rational(25, 2)) # => 12.5%
20
+ ```
21
+
22
+ Percentage objects can also be constructed directly, but in this case
23
+ BigDecimal/Rational values are treated as fractions, for example:
24
+
25
+ ```ruby
26
+ Percentage.new(50) # => 50%
27
+
28
+ Percentage.new(BigDecimal('0.175')) # => 17.5%
29
+
30
+ Percentage.new(Rational(1, 8)) # => 12.5%
31
+ ```
32
+
33
+ As with other numerics, percentage objects are conceptually immutable.
34
+ Common numeric functionality like `to_i`, `to_f`, `to_s`, `to_r`, `zero?`,
35
+ and equality/comparison methods are defined.
36
+
37
+ Percentages can be added together:
38
+
39
+ ```ruby
40
+ Percentage(10) + Percentage(20) # => 30%
41
+ ```
42
+
43
+ They can also be "scaled up" using the `scale` method:
44
+
45
+ ```ruby
46
+ Percentage(10).scale(5) # => 50%
47
+ ```
48
+
49
+ Multiplication is then defined using the fractional value of the percentage.
50
+ BigDecimal objects can't be coerced into rational objects, so the order of
51
+ the multiplication will matter in some cases. For example:
52
+
53
+ ```ruby
54
+ Percentage(50) * 10 # => (5/1)
55
+
56
+ Percentage(50) * Percentage(10) # => 5.0%
57
+
58
+ BigDecimal('150.00') * Percentage(50) # => BigDecimal('75.00')
59
+
60
+ Percentage(50) * BigDecimal('150.00') # raises TypeError
61
+ ```
62
+
63
+
64
+ Bonus Extras
65
+ ------------
66
+
67
+ Some shortcut methods are defined on Integer/BigDecimal for convenience:
68
+
69
+ ```ruby
70
+ 50.percent # => 50%
71
+
72
+ 50.percent_of(BigDecimal(150)) # => BigDecimal('75.00')
73
+
74
+ 10.percent_of(100) # => (10/1)
75
+
76
+ 5.as_percentage_of(10) # => 50.0%
77
+ ```
78
+
79
+ And there's also a class method for calculating the percentage
80
+ change between two values:
81
+
82
+ ```ruby
83
+ Percentage.change(2, 3) # => 50.0%
84
+ ```
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :spec
4
+
5
+ Rake::TestTask.new(:spec) do |t|
6
+ t.test_files = FileList['spec/*_spec.rb']
7
+ end
@@ -0,0 +1,132 @@
1
+ require 'rational'
2
+ require 'bigdecimal'
3
+
4
+ class Percentage
5
+ attr_reader :value
6
+
7
+ include Comparable
8
+
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ def to_i
14
+ (fractional_value * 100).to_i
15
+ end
16
+
17
+ def to_f
18
+ (fractional_value * 100).to_f
19
+ end
20
+
21
+ def to_s
22
+ "#{string_value}%"
23
+ end
24
+
25
+ def to_r
26
+ fractional_value.to_r
27
+ end
28
+
29
+ def zero?
30
+ @value.zero?
31
+ end
32
+
33
+ def <=>(object)
34
+ if self.class === object
35
+ fractional_value <=> object.fractional_value
36
+ elsif Numeric === object
37
+ fractional_value <=> object
38
+ end
39
+ end
40
+
41
+ def eql?(object)
42
+ object.instance_of?(self.class) && @value.eql?(object.value)
43
+ end
44
+
45
+ def hash
46
+ @value.hash
47
+ end
48
+
49
+ def +(object)
50
+ if self.class === object
51
+ if @value.integer? ^ object.value.integer?
52
+ self.class.new(fractional_value + object.fractional_value)
53
+ else
54
+ self.class.new(@value + object.value)
55
+ end
56
+ else
57
+ raise TypeError, "cannot add #{object.class} to #{self.class}"
58
+ end
59
+ end
60
+
61
+ def *(object)
62
+ case object
63
+ when self.class
64
+ self.class.new(fractional_value.to_r * object.fractional_value)
65
+ else
66
+ fractional_value.coerce(object).inject(&:*)
67
+ end
68
+ end
69
+
70
+ def coerce(other)
71
+ case other
72
+ when Numeric
73
+ return fractional_value, other
74
+ else
75
+ raise TypeError, "#{self.class} can't be coerced into #{other.class}"
76
+ end
77
+ end
78
+
79
+ def truncate(n)
80
+ self.class.new(fractional_value.truncate(n))
81
+ end
82
+
83
+ def scale(n)
84
+ self.class.new(@value * n)
85
+ end
86
+
87
+ protected
88
+
89
+ def fractional_value
90
+ @fractional_value ||= @value.integer? ? Rational(@value, 100) : @value
91
+ end
92
+
93
+ private
94
+
95
+ def string_value
96
+ if @value.integer?
97
+ @value.to_s
98
+ elsif BigDecimal === @value
99
+ (@value * 100).to_s('F')
100
+ else
101
+ BigDecimal(@value * 100, _precision=10).to_s('F')
102
+ end
103
+ end
104
+ end
105
+
106
+ def Percentage(object)
107
+ Percentage.new(object.integer? ? object : object / 100)
108
+ end
109
+
110
+ def Percentage.change(a, b)
111
+ Percentage.new((b - a).to_r / a)
112
+ end
113
+
114
+ class BigDecimal
115
+ def as_percentage_of(n)
116
+ Percentage.new(self / n)
117
+ end
118
+ end
119
+
120
+ class Integer
121
+ def percent
122
+ Percentage.new(self)
123
+ end
124
+
125
+ def percent_of(n)
126
+ n * Percentage.new(self)
127
+ end
128
+
129
+ def as_percentage_of(n)
130
+ Percentage.new(Rational(self, n))
131
+ end
132
+ end
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'percentage'
3
+ s.version = '1.0.0'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ['Tim Craft']
6
+ s.email = ['mail@timcraft.com']
7
+ s.homepage = 'http://github.com/timcraft/percentage'
8
+ s.description = 'A little library for working with percentages'
9
+ s.summary = 'See description'
10
+ s.files = Dir.glob('{lib,spec}/**/*') + %w(README.md Rakefile.rb percentage.gemspec)
11
+ s.require_path = 'lib'
12
+ end
@@ -0,0 +1,352 @@
1
+ require 'minitest/autorun'
2
+
3
+ require_relative '../lib/percentage'
4
+
5
+ describe 'Percentage object' do
6
+ it 'is comparable' do
7
+ percentage = Percentage.new(Rational(1, 8))
8
+
9
+ (Comparable === percentage).must_equal(true)
10
+ end
11
+
12
+ it 'is comparable to other objects of the same class' do
13
+ (Percentage.new(Rational(1, 8)) > Percentage.new(Rational(1, 10))).must_equal(true)
14
+ end
15
+
16
+ it 'is comparable to other numeric objects' do
17
+ (Percentage.new(Rational(1, 8)) > Rational(1, 10)).must_equal(true)
18
+ end
19
+
20
+ it 'can be used as a hash key' do
21
+ hash = Hash.new(0)
22
+
23
+ 3.times { hash[Percentage.new(Rational(1, 10))] += 1 }
24
+
25
+ hash[Percentage.new(Rational(1, 10))].must_equal(3)
26
+ end
27
+ end
28
+
29
+ describe 'Percentage object initialized with an integer value' do
30
+ before do
31
+ @percentage = Percentage.new(10)
32
+ end
33
+
34
+ describe 'value method' do
35
+ it 'returns the value passed to the constructor' do
36
+ @percentage.value.must_equal(10)
37
+ end
38
+ end
39
+
40
+ describe 'to_i method' do
41
+ it 'returns the integer value of the percentage' do
42
+ @percentage.to_i.must_equal(10)
43
+ end
44
+ end
45
+
46
+ describe 'to_f method' do
47
+ it 'returns the float value of the percentage' do
48
+ @percentage.to_f.must_be_close_to(10.0)
49
+ end
50
+ end
51
+
52
+ describe 'to_s method' do
53
+ it 'returns the integer value of the percentage suffixed with the percent symbol' do
54
+ @percentage.to_s.must_equal('10%')
55
+ end
56
+ end
57
+
58
+ describe 'to_r method' do
59
+ it 'returns the rational value of the percentage' do
60
+ @percentage.to_r.must_equal(Rational(1, 10))
61
+ end
62
+ end
63
+
64
+ describe 'zero query method' do
65
+ it 'returns true if the percentage has a zero value' do
66
+ Percentage.new(0).zero?.must_equal(true)
67
+ end
68
+
69
+ it 'returns false otherwise' do
70
+ @percentage.zero?.must_equal(false)
71
+ end
72
+ end
73
+
74
+ describe 'truncate method' do
75
+ it 'returns a percentage object with a truncated rational value' do
76
+ percentage = @percentage.truncate(1)
77
+ percentage.must_be_instance_of(Percentage)
78
+ percentage.value.must_equal(Rational(1, 10))
79
+ end
80
+ end
81
+
82
+ describe 'scale method' do
83
+ it 'returns a percentage object with the value of the percentage multiplied by the integer argument' do
84
+ percentage = @percentage.scale(2)
85
+ percentage.must_be_instance_of(Percentage)
86
+ percentage.value.must_equal(20)
87
+ end
88
+ end
89
+ end
90
+
91
+ describe 'Percentage object initialized with a rational value' do
92
+ before do
93
+ @percentage = Percentage.new(Rational(1, 8))
94
+ end
95
+
96
+ describe 'value method' do
97
+ it 'returns the value passed to the constructor' do
98
+ @percentage.value.must_equal(Rational(1, 8))
99
+ end
100
+ end
101
+
102
+ describe 'to_i method' do
103
+ it 'returns the integer value of the percentage' do
104
+ @percentage.to_i.must_equal(12)
105
+ end
106
+ end
107
+
108
+ describe 'to_f method' do
109
+ it 'returns the float value of the percentage' do
110
+ @percentage.to_f.must_be_close_to(12.5)
111
+ end
112
+ end
113
+
114
+ describe 'to_s method' do
115
+ it 'returns the decimal value of the percentage suffixed with the percent symbol' do
116
+ @percentage.to_s.must_equal('12.5%')
117
+ end
118
+ end
119
+
120
+ describe 'to_r method' do
121
+ it 'returns the rational value of the percentage' do
122
+ @percentage.to_r.must_equal(Rational(1, 8))
123
+ end
124
+ end
125
+
126
+ describe 'zero query method' do
127
+ it 'returns true if the percentage has a zero value' do
128
+ Percentage.new(Rational(0)).zero?.must_equal(true)
129
+ end
130
+
131
+ it 'returns false otherwise' do
132
+ @percentage.zero?.must_equal(false)
133
+ end
134
+ end
135
+
136
+ describe 'truncate method' do
137
+ it 'returns a percentage object with a truncated rational value' do
138
+ percentage = @percentage.truncate(1)
139
+ percentage.must_be_instance_of(Percentage)
140
+ percentage.value.must_equal(Rational(1, 10))
141
+ end
142
+ end
143
+
144
+ describe 'scale method' do
145
+ it 'returns a percentage object with the value of the percentage multiplied by the integer argument' do
146
+ percentage = @percentage.scale(2)
147
+ percentage.must_be_instance_of(Percentage)
148
+ percentage.value.must_equal(Rational(1, 4))
149
+ end
150
+ end
151
+ end
152
+
153
+ describe 'Percentage object initialized with a decimal value' do
154
+ before do
155
+ @percentage = Percentage.new(BigDecimal('0.175'))
156
+ end
157
+
158
+ describe 'value method' do
159
+ it 'returns the value passed to the constructor' do
160
+ @percentage.value.must_equal(BigDecimal('0.175'))
161
+ end
162
+ end
163
+
164
+ describe 'to_i method' do
165
+ it 'returns the integer value of the percentage' do
166
+ @percentage.to_i.must_equal(17)
167
+ end
168
+ end
169
+
170
+ describe 'to_f method' do
171
+ it 'returns the float value of the percentage' do
172
+ @percentage.to_f.must_be_close_to(17.5)
173
+ end
174
+ end
175
+
176
+ describe 'to_s method' do
177
+ it 'returns the decimal value of the percentage suffixed with the percent symbol' do
178
+ @percentage.to_s.must_equal('17.5%')
179
+ end
180
+ end
181
+
182
+ describe 'to_r method' do
183
+ it 'returns the rational value of the percentage' do
184
+ @percentage.to_r.must_equal(Rational(175, 1000))
185
+ end
186
+ end
187
+
188
+ describe 'zero query method' do
189
+ it 'returns true if the percentage has a zero value' do
190
+ Percentage.new(BigDecimal(0)).zero?.must_equal(true)
191
+ end
192
+
193
+ it 'returns false otherwise' do
194
+ @percentage.zero?.must_equal(false)
195
+ end
196
+ end
197
+
198
+ describe 'truncate method' do
199
+ it 'returns a percentage object with a truncated decimal value' do
200
+ percentage = @percentage.truncate(1)
201
+ percentage.must_be_instance_of(Percentage)
202
+ percentage.value.must_equal(BigDecimal('0.1'))
203
+ end
204
+ end
205
+
206
+ describe 'scale method' do
207
+ it 'returns a percentage object with the value of the percentage multiplied by the integer argument' do
208
+ percentage = @percentage.scale(2)
209
+ percentage.must_be_instance_of(Percentage)
210
+ percentage.value.must_equal(BigDecimal('0.35'))
211
+ end
212
+ end
213
+ end
214
+
215
+ describe 'Addition of percentage objects' do
216
+ it 'returns a percentage object with the value of the two percentages added together' do
217
+ percentage = Percentage.new(10) + Percentage.new(10)
218
+ percentage.must_be_instance_of(Percentage)
219
+ percentage.value.must_equal(20)
220
+
221
+ percentage = Percentage.new(Rational(1, 8)) + Percentage.new(10)
222
+ percentage.must_be_instance_of(Percentage)
223
+ percentage.value.must_equal(Rational(9, 40))
224
+
225
+ percentage = Percentage.new(BigDecimal('0.175')) + Percentage.new(BigDecimal('0.025'))
226
+ percentage.must_be_instance_of(Percentage)
227
+ percentage.value.must_equal(BigDecimal('0.2'))
228
+ end
229
+ end
230
+
231
+ describe 'Addition of percentage object with another type of object' do
232
+ it 'raises an exception' do
233
+ proc { Percentage.new(10) + 5 }.must_raise(TypeError)
234
+ end
235
+ end
236
+
237
+ describe 'Multiplication of percentage objects' do
238
+ it 'returns a percentage object with the fractional value of the two percentages multiplied together' do
239
+ percentage = Percentage.new(10) * Percentage.new(10)
240
+ percentage.must_be_instance_of(Percentage)
241
+ percentage.value.must_equal(Rational(1, 100))
242
+
243
+ percentage = Percentage.new(Rational(1, 8)) * Percentage.new(10)
244
+ percentage.must_be_instance_of(Percentage)
245
+ percentage.value.must_equal(Rational(1, 80))
246
+
247
+ percentage = Percentage.new(BigDecimal('0.175')) * Percentage.new(10)
248
+ percentage.must_be_instance_of(Percentage)
249
+ percentage.value.must_equal(Rational(7, 400))
250
+ end
251
+ end
252
+
253
+ describe 'Multiplication of a decimal object with a percentage object' do
254
+ it 'returns a decimal object with the value of the decimal multiplied by the fractional value of the percentage' do
255
+ percentage, decimal = Percentage.new(BigDecimal('0.175')), BigDecimal('99.00')
256
+
257
+ (decimal * percentage).must_equal(BigDecimal('17.325'))
258
+ (percentage * decimal).must_equal(BigDecimal('17.325'))
259
+ end
260
+ end
261
+
262
+ describe 'Percentage object equality' do
263
+ describe 'double equals method' do
264
+ it 'returns true for percentage objects with the same fractional value' do
265
+ (Percentage.new(50) == Percentage.new(50)).must_equal(true)
266
+ (Percentage.new(50) == Percentage.new(Rational(1, 2))).must_equal(true)
267
+ (Percentage.new(50) == Percentage.new(BigDecimal('0.5'))).must_equal(true)
268
+ end
269
+
270
+ it 'returns false otherwise' do
271
+ (Percentage.new(50) == Percentage.new(100)).must_equal(false)
272
+ end
273
+ end
274
+
275
+ describe 'eql query method' do
276
+ it 'returns true for percentage objects with exactly the same fractional value' do
277
+ (Percentage.new(50).eql? Percentage.new(50)).must_equal(true)
278
+ end
279
+
280
+ it 'returns false otherwise' do
281
+ (Percentage.new(50).eql? Percentage.new(Rational(1, 2))).must_equal(false)
282
+ (Percentage.new(50).eql? Percentage.new(BigDecimal('0.5'))).must_equal(false)
283
+ (Percentage.new(50).eql? Percentage.new(100)).must_equal(false)
284
+ end
285
+ end
286
+ end
287
+
288
+ describe 'Percentage method' do
289
+ describe 'when called with an integer argument' do
290
+ it 'returns a percentage object with the integer value' do
291
+ percentage = Percentage(10)
292
+ percentage.must_be_instance_of(Percentage)
293
+ percentage.value.must_equal(10)
294
+ end
295
+ end
296
+
297
+ describe 'when called with a decimal argument' do
298
+ it 'returns a percentage object with the value of the argument divided by 100' do
299
+ percentage = Percentage(BigDecimal('17.5'))
300
+ percentage.must_be_instance_of(Percentage)
301
+ percentage.value.must_equal(BigDecimal('0.175'))
302
+ end
303
+ end
304
+
305
+ describe 'when called with a rational argument' do
306
+ it 'returns a percentage object with the value of the argument divided by 100' do
307
+ percentage = Percentage(Rational(100, 3))
308
+ percentage.must_be_instance_of(Percentage)
309
+ percentage.value.must_equal(Rational(1, 3))
310
+ end
311
+ end
312
+ end
313
+
314
+ describe 'Percentage change method' do
315
+ it 'returns the difference between the arguments as a percentage of the first argument' do
316
+ percentage = Percentage.change(2, 3)
317
+ percentage.must_be_instance_of(Percentage)
318
+ percentage.must_equal(Percentage.new(50))
319
+ end
320
+ end
321
+
322
+ describe 'BigDecimal as_percentage_of method' do
323
+ it 'returns a percentage object with the value of the decimal divided by the argument' do
324
+ percentage = BigDecimal('50.00').as_percentage_of(BigDecimal('100.00'))
325
+ percentage.must_be_instance_of(Percentage)
326
+ percentage.value.must_equal(BigDecimal('0.5'))
327
+ end
328
+ end
329
+
330
+ describe 'Integer percent method' do
331
+ it 'returns a percentage object with the value of the integer' do
332
+ percentage = 10.percent
333
+ percentage.must_be_instance_of(Percentage)
334
+ percentage.value.must_equal(10)
335
+ end
336
+ end
337
+
338
+ describe 'Integer percent_of method' do
339
+ it 'returns the value of the receiver as a percentage multiplied by the argument' do
340
+ 10.percent_of(100).must_equal(10)
341
+ 10.percent_of(BigDecimal(15)).must_equal(BigDecimal('1.5'))
342
+ 10.percent_of(Rational(150, 2)).must_equal(Rational(15, 2))
343
+ end
344
+ end
345
+
346
+ describe 'Integer as_percentage_of method' do
347
+ it 'returns a percentage object with the value of the integer divided by the argument' do
348
+ percentage = 10.as_percentage_of(20)
349
+ percentage.must_be_instance_of(Percentage)
350
+ percentage.value.must_equal(Rational(1, 2))
351
+ end
352
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: percentage
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Craft
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-29 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A little library for working with percentages
15
+ email:
16
+ - mail@timcraft.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/percentage.rb
22
+ - spec/percentage_spec.rb
23
+ - README.md
24
+ - Rakefile.rb
25
+ - percentage.gemspec
26
+ homepage: http://github.com/timcraft/percentage
27
+ licenses: []
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.24
47
+ signing_key:
48
+ specification_version: 3
49
+ summary: See description
50
+ test_files: []
51
+ has_rdoc: