money 2.1.5 → 2.2.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,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
@@ -1,21 +1,21 @@
1
- Copyright (c) 2005 Tobias Lutke
2
- Copyright (c) 2008 Phusion
3
-
4
- Permission is hereby granted, free of charge, to any person obtaining
5
- a copy of this software and associated documentation files (the
6
- "Software"), to deal in the Software without restriction, including
7
- without limitation the rights to use, copy, modify, merge, publish,
8
- distribute, sublicense, and/or sell copies of the Software, and to
9
- permit persons to whom the Software is furnished to do so, subject to
10
- the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be
13
- included in all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ Copyright (c) 2005 Tobias Lutke
2
+ Copyright (c) 2008 Phusion
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,97 +1,97 @@
1
- = Introduction
2
-
3
- This library aids one in handling money and different currencies. Features:
4
-
5
- - Provides a Money class which encapsulates all information about an certain
6
- amount of money, such as its value and its currency.
7
- - Represents monetary values as integers, in cents. This avoids floating point
8
- rounding errors.
9
- - Provides APIs for exchanging money from one currency to another.
10
- - Has the ability to parse a money string into a Money object.
11
-
12
- Resources:
13
-
14
- - Website: http://money.rubyforge.org
15
- - RDoc API: http://money.rubyforge.org
16
- - Git repository: http://github.com/FooBarWidget/money/tree/master
17
-
18
- == Download
19
-
20
- Install stable releases with the following command:
21
-
22
- gem install money
23
-
24
- The development version (hosted on Github) can be installed with:
25
-
26
- gem sources -a http://gems.github.com
27
- gem install FooBarWidget-money
28
-
29
- == Usage
30
-
31
- === Synopsis
32
-
33
- require 'money'
34
-
35
- # 10.00 USD
36
- money = Money.new(1000, "USD")
37
- money.cents # => 1000
38
- money.currency # => "USD"
39
-
40
- Money.new(1000, "USD") == Money.new(1000, "USD") # => true
41
- Money.new(1000, "USD") == Money.new(100, "USD") # => false
42
- Money.new(1000, "USD") == Money.new(1000, "EUR") # => false
43
-
44
- === Currency Exchange
45
-
46
- Exchanging money is performed through an exchange bank object. The default
47
- exchange bank object requires one to manually specify the exchange rate. Here's
48
- an example of how it works:
49
-
50
- Money.add_rate("USD", "CAD", 1.24515)
51
- Money.add_rate("CAD", "USD", 0.803115)
52
-
53
- Money.us_dollar(100).exchange_to("CAD") # => Money.new(124, "CAD")
54
- Money.ca_dollar(100).exchange_to("USD") # => Money.new(80, "USD")
55
-
56
- Comparison and arithmetic operations work as expected:
57
-
58
- Money.new(1000, "USD") <=> Money.new(900, "USD") # => 1; 9.00 USD is smaller
59
- Money.new(1000, "EUR") + Money.new(10, "EUR") == Money.new(1010, "EUR")
60
-
61
- Money.add_rate("USD", "EUR", 0.5)
62
- Money.new(1000, "EUR") + Money.new(1000, "USD") == Money.new(1500, "EUR")
63
-
64
- There is nothing stopping you from creating bank objects which scrapes
65
- www.xe.com for the current rates or just returns <tt>rand(2)</tt>:
66
-
67
- Money.default_bank = ExchangeBankWhichScrapesXeDotCom.new
68
-
69
- === Ruby on Rails
70
-
71
- Use the +compose_of+ helper to let Active Record deal with embedding the money
72
- object in your models. The following example requires a +cents+ and a +currency+
73
- field.
74
-
75
- class ProductUnit < ActiveRecord::Base
76
- belongs_to :product
77
- composed_of :price, :class_name => "Money", :mapping => [%w(cents cents), %w(currency currency)]
78
-
79
- private
80
- validate :cents_not_zero
81
-
82
- def cents_not_zero
83
- errors.add("cents", "cannot be zero or less") unless cents > 0
84
- end
85
-
86
- validates_presence_of :sku, :currency
87
- validates_uniqueness_of :sku
88
- end
89
-
90
- === Default Currency
91
-
92
- By default Money defaults to USD as its currency. This can be overwritten using
93
-
94
- Money.default_currency = "CAD"
95
-
96
- If you use Rails, then environment.rb is a very good place to put this.
97
-
1
+ = Introduction
2
+
3
+ This library aids one in handling money and different currencies. Features:
4
+
5
+ - Provides a Money class which encapsulates all information about an certain
6
+ amount of money, such as its value and its currency.
7
+ - Represents monetary values as integers, in cents. This avoids floating point
8
+ rounding errors.
9
+ - Provides APIs for exchanging money from one currency to another.
10
+ - Has the ability to parse a money string into a Money object.
11
+
12
+ Resources:
13
+
14
+ - Website: http://money.rubyforge.org
15
+ - RDoc API: http://money.rubyforge.org
16
+ - Git repository: http://github.com/FooBarWidget/money
17
+
18
+ == Download
19
+
20
+ Install stable releases with the following command:
21
+
22
+ gem install money
23
+
24
+ The development version (hosted on Github) can be installed with:
25
+
26
+ git clone git://github.com/FooBarWidget/money.git
27
+ cd money
28
+ rake install
29
+
30
+ == Usage
31
+
32
+ === Synopsis
33
+
34
+ require 'money'
35
+
36
+ # 10.00 USD
37
+ money = Money.new(1000, "USD")
38
+ money.cents # => 1000
39
+ money.currency # => "USD"
40
+
41
+ Money.new(1000, "USD") == Money.new(1000, "USD") # => true
42
+ Money.new(1000, "USD") == Money.new(100, "USD") # => false
43
+ Money.new(1000, "USD") == Money.new(1000, "EUR") # => false
44
+
45
+ === Currency Exchange
46
+
47
+ Exchanging money is performed through an exchange bank object. The default
48
+ exchange bank object requires one to manually specify the exchange rate. Here's
49
+ an example of how it works:
50
+
51
+ Money.add_rate("USD", "CAD", 1.24515)
52
+ Money.add_rate("CAD", "USD", 0.803115)
53
+
54
+ Money.us_dollar(100).exchange_to("CAD") # => Money.new(124, "CAD")
55
+ Money.ca_dollar(100).exchange_to("USD") # => Money.new(80, "USD")
56
+
57
+ Comparison and arithmetic operations work as expected:
58
+
59
+ Money.new(1000, "USD") <=> Money.new(900, "USD") # => 1; 9.00 USD is smaller
60
+ Money.new(1000, "EUR") + Money.new(10, "EUR") == Money.new(1010, "EUR")
61
+
62
+ Money.add_rate("USD", "EUR", 0.5)
63
+ Money.new(1000, "EUR") + Money.new(1000, "USD") == Money.new(1500, "EUR")
64
+
65
+ There is nothing stopping you from creating bank objects which scrapes
66
+ www.xe.com for the current rates or just returns <tt>rand(2)</tt>:
67
+
68
+ Money.default_bank = ExchangeBankWhichScrapesXeDotCom.new
69
+
70
+ === Ruby on Rails
71
+
72
+ Use the +compose_of+ helper to let Active Record deal with embedding the money
73
+ object in your models. The following example requires a +cents+ and a +currency+
74
+ field.
75
+
76
+ class ProductUnit < ActiveRecord::Base
77
+ belongs_to :product
78
+ composed_of :price, :class_name => "Money", :mapping => [%w(cents cents), %w(currency currency)]
79
+
80
+ private
81
+ validate :cents_not_zero
82
+
83
+ def cents_not_zero
84
+ errors.add("cents", "cannot be zero or less") unless cents > 0
85
+ end
86
+
87
+ validates_presence_of :sku, :currency
88
+ validates_uniqueness_of :sku
89
+ end
90
+
91
+ === Default Currency
92
+
93
+ By default Money defaults to USD as its currency. This can be overwritten using
94
+
95
+ Money.default_currency = "CAD"
96
+
97
+ If you use Rails, then environment.rb is a very good place to put this.
data/Rakefile CHANGED
@@ -1,18 +1,44 @@
1
- desc "Build a gem"
2
- task :gem do
3
- sh "gem build money.gemspec"
4
- end
5
-
6
- task "Generate RDoc documentation"
7
- task :rdoc do
8
- sh "hanna README.rdoc lib -U"
9
- end
10
-
11
- task :upload => :rdoc do
12
- sh "scp -r doc/* rubyforge.org:/var/www/gforge-projects/money/"
13
- end
14
-
15
- desc "Run unit tests"
16
- task :test do
17
- ruby "-S spec -f s -c test/*_spec.rb"
18
- end
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "money"
8
+ gem.summary = "Money and currency exchange support library"
9
+ gem.description = "Money and currency exchange support library."
10
+ gem.email = "hongli@phusion.nl"
11
+ gem.homepage = "http://money.rubyforge.org/"
12
+ gem.authors = ["Tobias Luetke", "Hongli Lai", "Jeremy McNevin", "Shane Emmons"]
13
+ gem.rubyforge_project = "money"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ gem.add_development_dependency "hanna", ">= 0.1.12"
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ Jeweler::RubyforgeTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'test'
26
+ spec.spec_files = FileList['test/**/*_spec.rb']
27
+ spec.spec_opts << '--format specdoc'
28
+ end
29
+
30
+ task :spec => :check_dependencies
31
+
32
+ task :default => :spec
33
+
34
+ require 'hanna/rdoctask'
35
+ Rake::RDocTask.new do |rdoc|
36
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
37
+
38
+ rdoc.rdoc_dir = 'rdoc'
39
+ rdoc.main = 'README.rdoc'
40
+ rdoc.title = "money #{version}"
41
+ rdoc.rdoc_files.include('README.rdoc', 'LICENSE')
42
+ rdoc.rdoc_files.include('lib/**/*.rb')
43
+ rdoc.options << '-U'
44
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.2.0
@@ -1,26 +1,26 @@
1
- # Copyright (c) 2005 Tobias Luetke
2
- # Copyright (c) 2008 Phusion
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining
5
- # a copy of this software and associated documentation files (the
6
- # "Software"), to deal in the Software without restriction, including
7
- # without limitation the rights to use, copy, modify, merge, publish,
8
- # distribute, sublicense, and/or sell copies of the Software, and to
9
- # permit persons to whom the Software is furnished to do so, subject to
10
- # the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be
13
- # included in all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
-
23
- $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
24
- require 'money/money'
25
- require 'money/symbols'
26
- require 'money/core_extensions'
1
+ # Copyright (c) 2005 Tobias Luetke
2
+ # Copyright (c) 2008 Phusion
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
24
+ require 'money/money'
25
+ require 'money/defaults'
26
+ require 'money/core_extensions'
@@ -1,138 +1,138 @@
1
- class Numeric
2
- # Converts this numeric to a Money object in the default currency. It
3
- # multiplies the numeric value by 100 and treats that as cents.
4
- #
5
- # 100.to_money => #<Money @cents=10000>
6
- # 100.37.to_money => #<Money @cents=10037>
7
- def to_money
8
- Money.new(self * 100)
9
- end
10
- end
11
-
12
- class String
13
- # Parses the current string and converts it to a Money object.
14
- # Excess characters will be discarded.
15
- #
16
- # '100'.to_money # => #<Money @cents=10000>
17
- # '100.37'.to_money # => #<Money @cents=10037>
18
- # '100 USD'.to_money # => #<Money @cents=10000, @currency="USD">
19
- # 'USD 100'.to_money # => #<Money @cents=10000, @currency="USD">
20
- # '$100 USD'.to_money # => #<Money @cents=10000, @currency="USD">
21
- # 'hello 2000 world'.to_money # => #<Money @cents=200000 @currency="USD")>
22
- def to_money
23
- # Get the currency.
24
- matches = scan /([A-Z]{2,3})/
25
- currency = matches[0] ? matches[0][0] : Money.default_currency
26
- cents = calculate_cents(self)
27
- Money.new(cents, currency)
28
- end
29
-
30
- private
31
-
32
- def calculate_cents(number)
33
- # remove anything that's not a number, potential delimiter, or minus sign
34
- num = number.gsub(/[^\d|\.|,|\'|\s|\-]/, '').strip
35
-
36
- # set a boolean flag for if the number is negative or not
37
- negative = num.split(//).first == "-"
38
-
39
- # if negative, remove the minus sign from the number
40
- num = num.gsub(/^-/, '') if negative
41
-
42
- # gather all separators within the result number
43
- used_separators = num.scan /[^\d]/
44
-
45
- # determine the number of unique separators within the number
46
- #
47
- # e.g.
48
- # $1,234,567.89 would return 2 (, and .)
49
- # $125,00 would return 1
50
- # $199 would return 0
51
- # $1 234,567.89 would raise an error (separators are space, comma, and period)
52
- case used_separators.uniq.length
53
- # no separator or delimiter; major (dollars) is the number, and minor (cents) is 0
54
- when 0 then major, minor = num, 0
55
-
56
- # two separators, so we know the last item in this array is the
57
- # major/minor delimiter and the rest are separators
58
- when 2
59
- separator, delimiter = used_separators.uniq
60
- # remove all separators, split on the delimiter
61
- major, minor = num.gsub(separator, '').split(delimiter)
62
- min = 0 unless min
63
- when 1
64
- # we can't determine if the comma or period is supposed to be a separator or a delimiter
65
- # e.g.
66
- # 1,00 - comma is a delimiter
67
- # 1.000 - period is a delimiter
68
- # 1,000 - comma is a separator
69
- # 1,000,000 - comma is a separator
70
- # 10000,00 - comma is a delimiter
71
- # 1000,000 - comma is a delimiter
72
-
73
- # assign first separator for reusability
74
- separator = used_separators.first
75
-
76
- # separator is used as a separator when there are multiple instances, always
77
- if num.scan(separator).length > 1 # multiple matches; treat as separator
78
- major, minor = num.gsub(separator, ''), 0
79
- else
80
- # ex: 1,000 - 1.0000 - 10001.000
81
- # split number into possible major (dollars) and minor (cents) values
82
- possible_major, possible_minor = num.split(separator)
83
- possible_major ||= "0"
84
- possible_minor ||= "00"
85
-
86
- # if the minor (cents) length isn't 3, assign major/minor from the possibles
87
- # e.g.
88
- # 1,00 => 1.00
89
- # 1.0000 => 1.00
90
- # 1.2 => 1.20
91
- if possible_minor.length != 3 # delimiter
92
- major, minor = possible_major, possible_minor
93
- else
94
- # minor length is three
95
- # let's try to figure out intent of the delimiter
96
-
97
- # the major length is greater than three, which means
98
- # the comma or period is used as a delimiter
99
- # e.g.
100
- # 1000,000
101
- # 100000,000
102
- if possible_major.length > 3
103
- major, minor = possible_major, possible_minor
104
- else
105
- # number is in format ###{sep}### or ##{sep}### or #{sep}###
106
- # handle as , is sep, . is delimiter
107
- if separator == '.'
108
- major, minor = possible_major, possible_minor
109
- else
110
- major, minor = "#{possible_major}#{possible_minor}", 0
111
- end
112
- end
113
- end
114
- end
115
- else
116
- raise ArgumentError, "Invalid currency amount"
117
- end
118
-
119
- # build the string based on major/minor since separator/delimiters have been removed
120
- # avoiding floating point arithmetic here to ensure accuracy
121
- cents = (major.to_i * 100)
122
- # add the minor number as well. this may have any number of digits,
123
- # so we treat minor as a string and truncate or right-fill it with zeroes
124
- # until it becomes a two-digit number string, which we add to cents.
125
- minor = minor.to_s
126
- truncated_minor = minor[0..1]
127
- truncated_minor << "0" * (2 - truncated_minor.size) if truncated_minor.size < 2
128
- cents += truncated_minor.to_i
129
- # respect rounding rules
130
- if minor.size >= 3 && minor[2..2].to_i >= 5
131
- cents += 1
132
- end
133
-
134
- # if negative, multiply by -1; otherwise, return positive cents
135
- negative ? cents * -1 : cents
136
- end
137
-
138
- end
1
+ class Numeric
2
+ # Converts this numeric to a Money object in the default currency. It
3
+ # multiplies the numeric value by 100 and treats that as cents.
4
+ #
5
+ # 100.to_money => #<Money @cents=10000>
6
+ # 100.37.to_money => #<Money @cents=10037>
7
+ def to_money
8
+ Money.new(self * 100)
9
+ end
10
+ end
11
+
12
+ class String
13
+ # Parses the current string and converts it to a Money object.
14
+ # Excess characters will be discarded.
15
+ #
16
+ # '100'.to_money # => #<Money @cents=10000>
17
+ # '100.37'.to_money # => #<Money @cents=10037>
18
+ # '100 USD'.to_money # => #<Money @cents=10000, @currency="USD">
19
+ # 'USD 100'.to_money # => #<Money @cents=10000, @currency="USD">
20
+ # '$100 USD'.to_money # => #<Money @cents=10000, @currency="USD">
21
+ # 'hello 2000 world'.to_money # => #<Money @cents=200000 @currency="USD")>
22
+ def to_money
23
+ # Get the currency.
24
+ matches = scan /([A-Z]{2,3})/
25
+ currency = matches[0] ? matches[0][0] : Money.default_currency
26
+ cents = calculate_cents(self)
27
+ Money.new(cents, currency)
28
+ end
29
+
30
+ private
31
+
32
+ def calculate_cents(number)
33
+ # remove anything that's not a number, potential delimiter, or minus sign
34
+ num = number.gsub(/[^\d|\.|,|\'|\s|\-]/, '').strip
35
+
36
+ # set a boolean flag for if the number is negative or not
37
+ negative = num.split(//).first == "-"
38
+
39
+ # if negative, remove the minus sign from the number
40
+ num = num.gsub(/^-/, '') if negative
41
+
42
+ # gather all separators within the result number
43
+ used_separators = num.scan /[^\d]/
44
+
45
+ # determine the number of unique separators within the number
46
+ #
47
+ # e.g.
48
+ # $1,234,567.89 would return 2 (, and .)
49
+ # $125,00 would return 1
50
+ # $199 would return 0
51
+ # $1 234,567.89 would raise an error (separators are space, comma, and period)
52
+ case used_separators.uniq.length
53
+ # no separator or delimiter; major (dollars) is the number, and minor (cents) is 0
54
+ when 0 then major, minor = num, 0
55
+
56
+ # two separators, so we know the last item in this array is the
57
+ # major/minor delimiter and the rest are separators
58
+ when 2
59
+ separator, delimiter = used_separators.uniq
60
+ # remove all separators, split on the delimiter
61
+ major, minor = num.gsub(separator, '').split(delimiter)
62
+ min = 0 unless min
63
+ when 1
64
+ # we can't determine if the comma or period is supposed to be a separator or a delimiter
65
+ # e.g.
66
+ # 1,00 - comma is a delimiter
67
+ # 1.000 - period is a delimiter
68
+ # 1,000 - comma is a separator
69
+ # 1,000,000 - comma is a separator
70
+ # 10000,00 - comma is a delimiter
71
+ # 1000,000 - comma is a delimiter
72
+
73
+ # assign first separator for reusability
74
+ separator = used_separators.first
75
+
76
+ # separator is used as a separator when there are multiple instances, always
77
+ if num.scan(separator).length > 1 # multiple matches; treat as separator
78
+ major, minor = num.gsub(separator, ''), 0
79
+ else
80
+ # ex: 1,000 - 1.0000 - 10001.000
81
+ # split number into possible major (dollars) and minor (cents) values
82
+ possible_major, possible_minor = num.split(separator)
83
+ possible_major ||= "0"
84
+ possible_minor ||= "00"
85
+
86
+ # if the minor (cents) length isn't 3, assign major/minor from the possibles
87
+ # e.g.
88
+ # 1,00 => 1.00
89
+ # 1.0000 => 1.00
90
+ # 1.2 => 1.20
91
+ if possible_minor.length != 3 # delimiter
92
+ major, minor = possible_major, possible_minor
93
+ else
94
+ # minor length is three
95
+ # let's try to figure out intent of the delimiter
96
+
97
+ # the major length is greater than three, which means
98
+ # the comma or period is used as a delimiter
99
+ # e.g.
100
+ # 1000,000
101
+ # 100000,000
102
+ if possible_major.length > 3
103
+ major, minor = possible_major, possible_minor
104
+ else
105
+ # number is in format ###{sep}### or ##{sep}### or #{sep}###
106
+ # handle as , is sep, . is delimiter
107
+ if separator == '.'
108
+ major, minor = possible_major, possible_minor
109
+ else
110
+ major, minor = "#{possible_major}#{possible_minor}", 0
111
+ end
112
+ end
113
+ end
114
+ end
115
+ else
116
+ raise ArgumentError, "Invalid currency amount"
117
+ end
118
+
119
+ # build the string based on major/minor since separator/delimiters have been removed
120
+ # avoiding floating point arithmetic here to ensure accuracy
121
+ cents = (major.to_i * 100)
122
+ # add the minor number as well. this may have any number of digits,
123
+ # so we treat minor as a string and truncate or right-fill it with zeroes
124
+ # until it becomes a two-digit number string, which we add to cents.
125
+ minor = minor.to_s
126
+ truncated_minor = minor[0..1]
127
+ truncated_minor << "0" * (2 - truncated_minor.size) if truncated_minor.size < 2
128
+ cents += truncated_minor.to_i
129
+ # respect rounding rules
130
+ if minor.size >= 3 && minor[2..2].to_i >= 5
131
+ cents += 1
132
+ end
133
+
134
+ # if negative, multiply by -1; otherwise, return positive cents
135
+ negative ? cents * -1 : cents
136
+ end
137
+
138
+ end