percentage 1.0.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.
@@ -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: