lare_round 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 34df43a119476abc7ae5444a36b0f0780037132e
4
+ data.tar.gz: 4080b4c67caaf8defc3f6cac1166cb79e899ed3a
5
+ SHA512:
6
+ metadata.gz: ad2b381a02883609ffd9bed1a70c398a4e9a80c680c7e7e0def0b8ad752c8c2620bc602753dc38b84db4a0ee450091346d2b36843c8cdc84a9953a9b82c8121e
7
+ data.tar.gz: 99372d477e87a7ee1150fe1358752bf853215ca77101ac8dfe83d3be60129e396fdd4d4f04997cc919135615e7a2056f4533222aebc4d9cd601c9f781b37f5ad
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,9 @@
1
+ rvm:
2
+
3
+ - "1.9.3"
4
+ - jruby-19mode # JRuby in 1.9 mode
5
+ - rbx-19mode
6
+ branches:
7
+ only:
8
+ - master
9
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in lare_round.gemspec
4
+ gemspec
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2013 Carsten Wirth
2
+
3
+
4
+
5
+ MIT License
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,122 @@
1
+ # LareRound
2
+
3
+ A collection of BigDecimal items e.g. invoice items can be rounded for displaying them in views. Rounding may apply a rounding error to the items such as the summed up rounded items will show deviation towards an invoice total with summed unrounded items. Which might cause confusion for customers and finance departments alike. Application of the largest remainder method can help to preserve the total sum for rounded parts thus eliminating this confusion.
4
+
5
+ ## Build status
6
+ [![Build Status](https://secure.travis-ci.org/jethroo/lare_round.png)](http://travis-ci.org/jethroo/lare_round)
7
+
8
+
9
+ ## Used in production
10
+
11
+ since 4th of september 2013 (still needs to be released as gem)
12
+
13
+ ## Example
14
+
15
+ say we have an array of 3 invoice items which are stored in the database and your invoice calculations are precise to the 4th position after the decimal point:
16
+
17
+ ```ruby
18
+ Array.new(3){BigDecimal.new('0.3334')}
19
+ # => [#<BigDecimal:c75a38,'0.3334E0',9(18)>, #<BigDecimal:c759c0,'0.3334E0',9(18)>, #<BigDecimal:c75920,'0.3334E0',9(18)>]
20
+ ```
21
+ say you have an invoice which is rendered as pdf which only needs to display the total you are fine, because you only
22
+ have to round once for displaying a customer friendly price:
23
+
24
+ ```ruby
25
+ Array.new(3){BigDecimal.new('0.3334')}.reduce(:+).round(2).to_f
26
+ # => 1.0
27
+ ```
28
+
29
+ But what if you need to dispay each item separately? Each item of the invoice has to be displayed in a customer friendly way.
30
+
31
+ Item | Price
32
+ --- | ---
33
+ item 1 | 0.3334
34
+ item 2 | 0.3334
35
+ item 3 | 0.3334
36
+ **Total** | **1.0002**
37
+
38
+
39
+ So the most likely aproach is to simply round each item by itself, so the customer isn't bothered with 34/10000 €-Cents. Simple at it is its not quite what you want:
40
+
41
+ ```ruby
42
+ Array.new(3){BigDecimal.new('0.3334')}.map{|i| i.round(2)}.reduce(:+).to_f
43
+ # => 0.99
44
+ ```
45
+
46
+ Item | Price
47
+ --- | ---
48
+ item 1 | 0.33
49
+ item 2 | 0.33
50
+ item 3 | 0.33
51
+ **Total** | **1.00**
52
+
53
+ Now you have the customer bothering about why there is a difference between the invoice total and the invoice items sum. Which may lead in mistrust ("Hey! These guys can't even do math. I'll keep my money.") or all kinds of related confusions.
54
+
55
+ This gem helps to distribute the rounding error amongst the items to preserve the total:
56
+ ```ruby
57
+ a = Array.new(3){BigDecimal.new('0.3334')}
58
+ # => [#<BigDecimal:887b6c8,'0.3334E0',9(18)>, #<BigDecimal:887b600,'0.3334E0',9(18)>, #<BigDecimal:887b4c0,'0.3334E0',9(18)>]
59
+ a = LareRound.round(a,2)
60
+ # => [#<BigDecimal:8867330,'0.34E0',9(36)>, #<BigDecimal:8867290,'0.33E0',9(36)>, #<BigDecimal:88671f0,'0.33E0',9(36)>]
61
+ a.reduce(:+).to_f
62
+ # => 1.0
63
+ ```
64
+
65
+ This is accomplish by utilizing the largest remainder method. Which checks for the items with the largest rounding error and increasing them iteratively as long as the sums do not match. Regarding the before mentioned expample each item
66
+ is rounded down to 0.33 and then the algorithm adds 0.01 to one item thus making the sums equal.
67
+
68
+ Item | Price
69
+ --- | ---
70
+ item 1 | 0.34
71
+ item 2 | 0.33
72
+ item 3 | 0.33
73
+ **Total** | **1.00**
74
+
75
+ LareRound supports *Array* and *Hash* as collection types. As usage of array was shown above here comes the hash example:
76
+
77
+ ```ruby
78
+ hash = Hash[(1..3).map.with_index{|x,i|[x,BigDecimal.new('0.3334')]}]
79
+ # => {1=>#<BigDecimal:26ac7d0,'0.3334E0',9(18)>, 2=>#<BigDecimal:26ac730,'0.3334E0',9(18)>, 3=>#<BigDecimal:26ac690,'0.3334E0',9(18)>}
80
+ LareRound.round(hash,2)
81
+ # => {1=>#<BigDecimal:26b9318,'0.34E0',9(36)>, 2=>#<BigDecimal:26b9250,'0.33E0',9(36)>, 3=>#<BigDecimal:26b91b0,'0.33E0',9(36)>}
82
+ LareRound.round(hash,2).values.reduce(:+).to_f
83
+ #=> 1.0
84
+ ```
85
+
86
+ ## Open Issues / Features
87
+
88
+ * support specific type of rounding behavior for single items such as always rounding up in the case of taxes
89
+
90
+ Item (unrounded)| Price (unrounded) | LareRound | Financial
91
+ --- | --- | --- | ---
92
+ item | 10.000 | 10.00 | 10.00
93
+ tax ( 8.23%) | 0.823 | 0.82 | 0.83
94
+ **Total** | **10.823** | **10.82** | **10.83**
95
+
96
+ * release as gem
97
+
98
+ ## Installation
99
+
100
+ Add this line to your application's Gemfile:
101
+
102
+ gem 'lare_round', :git => "git://github.com/jethroo/lare_round.git"
103
+
104
+ And then execute:
105
+
106
+ $ bundle
107
+
108
+ ## Contributing
109
+
110
+ 1. Fork it
111
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
112
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
113
+ 4. Push to the branch (`git push origin my-new-feature`)
114
+ 5. Create new Pull Request
115
+
116
+ ## License
117
+ Hereby released under MIT license.
118
+
119
+ ## Authors/Contributors
120
+
121
+ - [BlackLane GmbH](http://www.blacklane.com "Blacklane")
122
+ - [Carsten Wirth](http://github.com/jethroo)
@@ -0,0 +1,13 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'lib'
7
+ t.libs << 'test'
8
+ t.pattern = 'test/**/*_test.rb'
9
+ t.verbose = false
10
+ end
11
+
12
+
13
+ task :default => :test
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'lare_round/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "lare_round"
8
+ spec.version = LareRound::VERSION
9
+ spec.authors = ["Carsten Wirth\n\n"]
10
+ spec.email = ["carsten.wirth@blacklane.com\n"]
11
+ spec.description = %q{A collection of BigDecimal items e.g. invoice items can be rounded for displaying them in views. Rounding may apply a rounding error to the items such as the summed up rounded items will show deviation towards an invoice total with summed unrounded items. Which might cause confusion for customers and finance departments alike. Application of the largest remainder method can help to preserve the total sum for fractionated parts thus eliminating this confusion.
12
+ }
13
+ spec.summary = %q{gem for rounding BigDecimal items by preserving its sum}
14
+ spec.homepage = ""
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ end
@@ -0,0 +1,54 @@
1
+ require 'bigdecimal'
2
+
3
+ module LareRound
4
+
5
+ def self.round(values,precision)
6
+ raise LareRoundError.new("precision must not be nil") if precision.nil?
7
+ raise LareRoundError.new("precision must be a number") unless precision.is_a? Numeric
8
+ raise LareRoundError.new("precision must be greater or equal to 0") if precision < 0
9
+ raise LareRoundError.new("values must not be nil") if values.nil?
10
+ raise LareRoundError.new("values must not be empty") if values.empty?
11
+ if values.kind_of?(Array)
12
+ round_array_of_values(values,precision)
13
+ elsif values.kind_of?(Hash)
14
+ rounded_values = round_array_of_values(values.values,precision)
15
+ values.keys.each_with_index do |key,index|
16
+ values[key] = rounded_values[index]
17
+ end
18
+ return values
19
+ end
20
+ end
21
+
22
+ # StandardError for dealing with application level errors
23
+ class LareRoundError < StandardError
24
+
25
+ end
26
+
27
+ private
28
+ def self.round_array_of_values(array_of_values,precision)
29
+ raise LareRoundError.new("array_of_values must be an array") unless array_of_values.is_a? Array
30
+ number_of_invalid_values = array_of_values.map{|i| i.is_a? Numeric}.reject{|i| i == true}.size
31
+ raise LareRoundError.new("values contains not numeric values (#{number_of_invalid_values})") if number_of_invalid_values > 0
32
+ warn "values contains non decimal values, you might loose precision or even get wrong rounding results" if array_of_values.map{|i| i.is_a? BigDecimal}.reject{|i| i == true}.size > 0
33
+
34
+ #prevention of can't omit precision for a Rational
35
+ decimal_shift = BigDecimal.new (10 ** precision.to_i)
36
+ rounded_total = array_of_values.reduce(:+).round(precision) * decimal_shift
37
+ array_of_values = array_of_values.map{|v| ((v.is_a? BigDecimal) ? v : BigDecimal.new(v.to_s))}
38
+ unrounded_values = array_of_values.map{|v| v * decimal_shift }
39
+
40
+ # items needed to be rounded down if positiv:
41
+ # 0.7 + 0.7 + 0.7 = ( 2.1 ).round(0) = 2
42
+ # (0.7).round(0) + (0.7).round(0) + (0.7).round(0) = 1 + 1 + 1 = 3
43
+ # elsewise if negative
44
+ rounded_values = array_of_values.map{|v| v < 0 ? v.round(precision, BigDecimal::ROUND_UP) * decimal_shift : v.round(precision, BigDecimal::ROUND_DOWN) * decimal_shift }
45
+
46
+ while not rounded_values.reduce(:+) >= rounded_total
47
+ fractions = unrounded_values.zip(rounded_values).map { |x, y| x - y }
48
+ rounded_values[fractions.index(fractions.max)] += 1
49
+ end
50
+
51
+ return rounded_values.map{|v| v / decimal_shift }
52
+ end
53
+
54
+ end
@@ -0,0 +1,3 @@
1
+ module LareRound
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,116 @@
1
+ require_relative 'test_helper'
2
+ require 'bigdecimal'
3
+ require 'securerandom'
4
+
5
+ class LareRoundTest < MiniTest::Unit::TestCase
6
+
7
+ def test_lareRound_has_static_method_round
8
+ assert_equal(true,LareRound.respond_to?(:round))
9
+ end
10
+
11
+ (1..9).each do |digit|
12
+ (1..23).each do |items|
13
+ (0..10).each do |precision|
14
+
15
+ method_name = "test #{items} rounded items with last digit of #{digit} should sum up to rounded total of BigDecimal items with precision of #{precision} if passed as array".gsub(' ','_')
16
+ define_method method_name do
17
+ decimal = BigDecimal.new("0."+"3"*precision+"#{digit}")
18
+ arr = Array.new(items){decimal}
19
+ rounded_total = arr.reduce(:+).round(precision)
20
+ assert_equal(rounded_total,LareRound.round(arr,precision).reduce(:+).round(precision))
21
+ end
22
+
23
+ method_name = "test #{items} rounded items with last digit of #{digit} should sum up to rounded total of BigDecimal items with precision of #{precision} if passed as hash".gsub(' ','_')
24
+ define_method method_name do
25
+ decimal = BigDecimal.new("0."+"3"*precision+"#{digit}")
26
+ hash = Hash[(1..items).map.with_index{|x,i|[x,decimal]}]
27
+ rounded_total = hash.values.reduce(:+).round(precision)
28
+ assert_equal(rounded_total,LareRound.round(hash,precision).values.reduce(:+).round(precision))
29
+ end
30
+
31
+ method_name = "test #{items} rounded items with last digit of #{digit} and precision of #{precision} if passed as hash should not change order".gsub(' ','_')
32
+ define_method method_name do
33
+ decimal = BigDecimal.new("0."+"3"*precision+"#{digit}")
34
+ hash = Hash[(1..items).map.with_index{|x,i|[x,decimal+BigDecimal.new(i)]}]
35
+ rounded_hash = LareRound.round(hash.clone,precision)
36
+ hash.keys.each do |key|
37
+ assert( (((hash[key] - rounded_hash[key])*10**precision).abs < 1) )
38
+ end
39
+ end
40
+
41
+ method_name = "test #{items} rounded negative items with last digit of #{digit} should sum up to rounded total of BigDecimal items with precision of #{precision} if passed as array".gsub(' ','_')
42
+ define_method method_name do
43
+ decimal = BigDecimal.new("-0."+"3"*precision+"#{digit}")
44
+ arr = Array.new(items){decimal}
45
+ rounded_total = arr.reduce(:+).round(precision)
46
+ assert_equal(rounded_total,LareRound.round(arr,precision).reduce(:+).round(precision))
47
+ end
48
+
49
+ method_name = "test #{items} rounded mixed (+/-) items with last digit of #{digit} should sum up to rounded total of BigDecimal items with precision of #{precision} if passed as array".gsub(' ','_')
50
+ define_method method_name do
51
+ decimal = BigDecimal.new( (SecureRandom.random_number(100) % 2 == 0) ? "" : "-" + "0."+"3"*precision+"#{digit}")
52
+ arr = Array.new(items){decimal}
53
+ rounded_total = arr.reduce(:+).round(precision)
54
+ assert_equal(rounded_total,LareRound.round(arr,precision).reduce(:+).round(precision))
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+
61
+ def test_should_raise_if_precision_is_nil
62
+ decimal = BigDecimal.new("0.1234")
63
+ arr = Array.new(3){decimal}
64
+ exception = assert_raises(LareRound::LareRoundError){
65
+ LareRound.round(arr,nil)
66
+ }
67
+ assert_equal("precision must not be nil", exception.message)
68
+ end
69
+
70
+ def test_should_raise_if_precision_is_less_than_zero
71
+ decimal = BigDecimal.new("0.1234")
72
+ arr = Array.new(3){decimal}
73
+ exception = assert_raises(LareRound::LareRoundError){
74
+ LareRound.round(arr,-1)
75
+ }
76
+ assert_equal("precision must be greater or equal to 0", exception.message)
77
+ end
78
+
79
+ def test_should_raise_if_precision_is_not_a_number
80
+ decimal = BigDecimal.new("0.1234")
81
+ arr = Array.new(3){decimal}
82
+ exception = assert_raises(LareRound::LareRoundError){
83
+ LareRound.round(arr,"not_a_number")
84
+ }
85
+ assert_equal("precision must be a number", exception.message)
86
+ end
87
+
88
+ def test_should_raise_if_values_is_nil
89
+ exception = assert_raises(LareRound::LareRoundError){
90
+ LareRound.round(nil,2)
91
+ }
92
+ assert_equal("values must not be nil", exception.message)
93
+ end
94
+
95
+ def test_should_raise_if_values_is_empty
96
+ exception = assert_raises(LareRound::LareRoundError){
97
+ LareRound.round([],2)
98
+ }
99
+ assert_equal("values must not be empty", exception.message)
100
+ end
101
+
102
+ def test_should_raise_if_values_contains_invalid_values
103
+ exception = assert_raises(LareRound::LareRoundError){
104
+ LareRound.round([3.2, 1, "not_a_number", Exception.new, nil],2)
105
+ }
106
+ assert_equal("values contains not numeric values (3)", exception.message)
107
+ end
108
+
109
+ def test_should_warn_if_numbers_not_big_decimals
110
+ out, err = capture_io do
111
+ LareRound.round([1.2132, 12.21212, 323.23],2)
112
+ end
113
+ assert_match(/values contains non decimal values, you might loose precision or even get wrong rounding results/, err)
114
+ end
115
+
116
+ end
@@ -0,0 +1,8 @@
1
+ base_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
2
+ lib_dir = File.join(base_dir, "lib")
3
+ test_dir = File.join(base_dir, "test")
4
+
5
+ $LOAD_PATH.unshift(lib_dir)
6
+
7
+ require 'test/unit'
8
+ require 'lare_round'
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lare_round
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - |+
8
+ Carsten Wirth
9
+
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-10-20 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1.3'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rake
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ description: "A collection of BigDecimal items e.g. invoice items can be rounded for
44
+ displaying them in views. Rounding may apply a rounding error to the items such
45
+ as the summed up rounded items will show deviation towards an invoice total with
46
+ summed unrounded items. Which might cause confusion for customers and finance departments
47
+ alike. Application of the largest remainder method can help to preserve the total
48
+ sum for fractionated parts thus eliminating this confusion.\n "
49
+ email:
50
+ - |
51
+ carsten.wirth@blacklane.com
52
+ executables: []
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - ".gitignore"
57
+ - ".travis.yml"
58
+ - Gemfile
59
+ - LICENSE.txt
60
+ - README.md
61
+ - Rakefile
62
+ - lare_round.gemspec
63
+ - lib/lare_round.rb
64
+ - lib/lare_round/version.rb
65
+ - test/lare_round_test.rb
66
+ - test/test_helper.rb
67
+ homepage: ''
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 2.2.2
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: gem for rounding BigDecimal items by preserving its sum
91
+ test_files:
92
+ - test/lare_round_test.rb
93
+ - test/test_helper.rb