bai-money 0.5.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 +5 -0
- data/README.markdown +87 -0
- data/VERSION +1 -0
- data/lib/money.rb +4 -0
- data/lib/money/core_extensions.rb +136 -0
- data/lib/money/errors.rb +4 -0
- data/lib/money/money.rb +252 -0
- data/lib/money/symbols.rb +18 -0
- data/lib/money/variable_exchange_bank.rb +72 -0
- data/money.gemspec +53 -0
- data/rakefile +53 -0
- data/test/core_extensions_test.rb +74 -0
- data/test/money_test.rb +215 -0
- data/test/test_helper.rb +10 -0
- data/test/variable_exchange_bank_test.rb +47 -0
- metadata +71 -0
data/README.markdown
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
This library aids one in handling money and different currencies. Features:
|
|
2
|
+
|
|
3
|
+
* Provides a Money class which encapsulates all information about an certain amount of money, such as its value and its currency.
|
|
4
|
+
* Represents monetary values as integers, in cents. This avoids floating point rounding errors.
|
|
5
|
+
* Provides APIs for exchanging money from one currency to another.
|
|
6
|
+
* Has the ability to parse a money string into a Money object.
|
|
7
|
+
|
|
8
|
+
### Note on Patches/Pull Requests
|
|
9
|
+
|
|
10
|
+
* Fork the project.
|
|
11
|
+
* Make your feature addition or bug fix.
|
|
12
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
|
13
|
+
* Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
14
|
+
* Send me a pull request. Bonus points for topic branches.
|
|
15
|
+
|
|
16
|
+
### Usage
|
|
17
|
+
|
|
18
|
+
#### Synopsis
|
|
19
|
+
|
|
20
|
+
require 'money'
|
|
21
|
+
|
|
22
|
+
# 10.00 USD
|
|
23
|
+
money = Money.new(1000, "USD")
|
|
24
|
+
money.cents # => 1000
|
|
25
|
+
money.currency # => "USD"
|
|
26
|
+
|
|
27
|
+
Money.new(1000, "USD") == Money.new(1000, "USD") # => true
|
|
28
|
+
Money.new(1000, "USD") == Money.new(100, "USD") # => false
|
|
29
|
+
Money.new(1000, "USD") == Money.new(1000, "EUR") # => false
|
|
30
|
+
|
|
31
|
+
#### Currency Exchange
|
|
32
|
+
|
|
33
|
+
Exchanging money is performed through an exchange bank object. The default exchange bank object requires one to manually specify the exchange rate. Here’s an example of how it works:
|
|
34
|
+
|
|
35
|
+
Money.add_rate("USD", "CAD", 1.24515)
|
|
36
|
+
Money.add_rate("CAD", "USD", 0.803115)
|
|
37
|
+
|
|
38
|
+
Money.new(100, "USD").exchange_to("CAD") # => Money.new(124, "CAD")
|
|
39
|
+
Money.new(100, "CAD").exchange_to("USD") # => Money.new(80, "USD")
|
|
40
|
+
|
|
41
|
+
Comparison and arithmetic operations work as expected:
|
|
42
|
+
|
|
43
|
+
Money.new(1000, "USD") <=> Money.new(900, "USD") # => 1; 9.00 USD is smaller
|
|
44
|
+
Money.new(1000, "EUR") + Money.new(10, "EUR") == Money.new(1010, "EUR")
|
|
45
|
+
|
|
46
|
+
Money.add_rate("USD", "EUR", 0.5)
|
|
47
|
+
Money.new(1000, "EUR") + Money.new(1000, "USD") == Money.new(1500, "EUR")
|
|
48
|
+
|
|
49
|
+
There is nothing stopping you from creating bank objects which scrapes www.xe.com for the current rates or just returns rand(2):
|
|
50
|
+
|
|
51
|
+
Money.default_bank = ExchangeBankWhichScrapesXeDotCom.new
|
|
52
|
+
|
|
53
|
+
#### Ruby on Rails
|
|
54
|
+
|
|
55
|
+
Use the compose_of helper to let Active Record deal with embedding the money object in your models. The following example requires a cents and a currency field.
|
|
56
|
+
|
|
57
|
+
class ProductUnit < ActiveRecord::Base
|
|
58
|
+
belongs_to :product
|
|
59
|
+
composed_of :price, :class_name => "Money", :mapping => [%w(cents cents), %w(currency currency)]
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
validate :cents_not_zero
|
|
63
|
+
|
|
64
|
+
def cents_not_zero
|
|
65
|
+
errors.add("cents", "cannot be zero or less") unless cents > 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
validates_presence_of :sku, :currency
|
|
69
|
+
validates_uniqueness_of :sku
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#### Default Currency
|
|
73
|
+
|
|
74
|
+
By default Money defaults to USD as its currency. This can be overwritten using
|
|
75
|
+
|
|
76
|
+
Money.default_currency = "CAD"
|
|
77
|
+
|
|
78
|
+
If you use Rails, then initializer is a very good place to put this.
|
|
79
|
+
|
|
80
|
+
### Credits & License
|
|
81
|
+
|
|
82
|
+
Highly based on:
|
|
83
|
+
|
|
84
|
+
* http://github.com/FooBarWidget/money
|
|
85
|
+
* http://github.com/collectiveidea/money
|
|
86
|
+
|
|
87
|
+
This library inherits license of FooBarWidget-money.
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.5.0
|
data/lib/money.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
def calculate_cents(number)
|
|
32
|
+
# remove anything that's not a number, potential delimiter, or minus sign
|
|
33
|
+
num = number.gsub(/[^\d|\.|,|\'|\s|\-]/, '').strip
|
|
34
|
+
|
|
35
|
+
# set a boolean flag for if the number is negative or not
|
|
36
|
+
negative = num.split(//).first == "-"
|
|
37
|
+
|
|
38
|
+
# if negative, remove the minus sign from the number
|
|
39
|
+
num = num.gsub(/^-/, '') if negative
|
|
40
|
+
|
|
41
|
+
# gather all separators within the result number
|
|
42
|
+
used_separators = num.scan /[^\d]/
|
|
43
|
+
|
|
44
|
+
# determine the number of unique separators within the number
|
|
45
|
+
#
|
|
46
|
+
# e.g.
|
|
47
|
+
# $1,234,567.89 would return 2 (, and .)
|
|
48
|
+
# $125,00 would return 1
|
|
49
|
+
# $199 would return 0
|
|
50
|
+
# $1 234,567.89 would raise an error (separators are space, comma, and period)
|
|
51
|
+
case used_separators.uniq.length
|
|
52
|
+
# no separator or delimiter; major (dollars) is the number, and minor (cents) is 0
|
|
53
|
+
when 0 then major, minor = num, 0
|
|
54
|
+
|
|
55
|
+
# two separators, so we know the last item in this array is the
|
|
56
|
+
# major/minor delimiter and the rest are separators
|
|
57
|
+
when 2
|
|
58
|
+
separator, delimiter = used_separators.uniq
|
|
59
|
+
# remove all separators, split on the delimiter
|
|
60
|
+
major, minor = num.gsub(separator, '').split(delimiter)
|
|
61
|
+
min = 0 unless min
|
|
62
|
+
when 1
|
|
63
|
+
# we can't determine if the comma or period is supposed to be a separator or a delimiter
|
|
64
|
+
# e.g.
|
|
65
|
+
# 1,00 - comma is a delimiter
|
|
66
|
+
# 1.000 - period is a delimiter
|
|
67
|
+
# 1,000 - comma is a separator
|
|
68
|
+
# 1,000,000 - comma is a separator
|
|
69
|
+
# 10000,00 - comma is a delimiter
|
|
70
|
+
# 1000,000 - comma is a delimiter
|
|
71
|
+
|
|
72
|
+
# assign first separator for reusability
|
|
73
|
+
separator = used_separators.first
|
|
74
|
+
|
|
75
|
+
# separator is used as a separator when there are multiple instances, always
|
|
76
|
+
if num.scan(separator).length > 1 # multiple matches; treat as separator
|
|
77
|
+
major, minor = num.gsub(separator, ''), 0
|
|
78
|
+
else
|
|
79
|
+
# ex: 1,000 - 1.0000 - 10001.000
|
|
80
|
+
# split number into possible major (dollars) and minor (cents) values
|
|
81
|
+
possible_major, possible_minor = num.split(separator)
|
|
82
|
+
possible_major ||= "0"
|
|
83
|
+
possible_minor ||= "00"
|
|
84
|
+
|
|
85
|
+
# if the minor (cents) length isn't 3, assign major/minor from the possibles
|
|
86
|
+
# e.g.
|
|
87
|
+
# 1,00 => 1.00
|
|
88
|
+
# 1.0000 => 1.00
|
|
89
|
+
# 1.2 => 1.20
|
|
90
|
+
if possible_minor.length != 3 # delimiter
|
|
91
|
+
major, minor = possible_major, possible_minor
|
|
92
|
+
else
|
|
93
|
+
# minor length is three
|
|
94
|
+
# let's try to figure out intent of the delimiter
|
|
95
|
+
|
|
96
|
+
# the major length is greater than three, which means
|
|
97
|
+
# the comma or period is used as a delimiter
|
|
98
|
+
# e.g.
|
|
99
|
+
# 1000,000
|
|
100
|
+
# 100000,000
|
|
101
|
+
if possible_major.length > 3
|
|
102
|
+
major, minor = possible_major, possible_minor
|
|
103
|
+
else
|
|
104
|
+
# number is in format ###{sep}### or ##{sep}### or #{sep}###
|
|
105
|
+
# handle as , is sep, . is delimiter
|
|
106
|
+
if separator == '.'
|
|
107
|
+
major, minor = possible_major, possible_minor
|
|
108
|
+
else
|
|
109
|
+
major, minor = "#{possible_major}#{possible_minor}", 0
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
raise ArgumentError, "Invalid currency amount"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# build the string based on major/minor since separator/delimiters have been removed
|
|
119
|
+
# avoiding floating point arithmetic here to ensure accuracy
|
|
120
|
+
cents = (major.to_i * 100)
|
|
121
|
+
# add the minor number as well. this may have any number of digits,
|
|
122
|
+
# so we treat minor as a string and truncate or right-fill it with zeroes
|
|
123
|
+
# until it becomes a two-digit number string, which we add to cents.
|
|
124
|
+
minor = minor.to_s
|
|
125
|
+
truncated_minor = minor[0..1]
|
|
126
|
+
truncated_minor << "0" * (2 - truncated_minor.size) if truncated_minor.size < 2
|
|
127
|
+
cents += truncated_minor.to_i
|
|
128
|
+
# respect rounding rules
|
|
129
|
+
if minor.size >= 3 && minor[2..2].to_i >= 5
|
|
130
|
+
cents += 1
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# if negative, multiply by -1; otherwise, return positive cents
|
|
134
|
+
negative ? cents * -1 : cents
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/money/errors.rb
ADDED
data/lib/money/money.rb
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
require 'money/variable_exchange_bank'
|
|
2
|
+
|
|
3
|
+
# Represents an amount of money in a certain currency.
|
|
4
|
+
class Money
|
|
5
|
+
include Comparable
|
|
6
|
+
|
|
7
|
+
attr_reader :cents, :currency, :bank
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
# Each Money object is associated to a bank object, which is responsible
|
|
11
|
+
# for currency exchange. This property allows one to specify the default
|
|
12
|
+
# bank object.
|
|
13
|
+
#
|
|
14
|
+
# bank1 = MyBank.new
|
|
15
|
+
# bank2 = MyOtherBank.new
|
|
16
|
+
#
|
|
17
|
+
# Money.default_bank = bank1
|
|
18
|
+
# money1 = Money.new(10)
|
|
19
|
+
# money1.bank # => bank1
|
|
20
|
+
#
|
|
21
|
+
# Money.default_bank = bank2
|
|
22
|
+
# money2 = Money.new(10)
|
|
23
|
+
# money2.bank # => bank2
|
|
24
|
+
# money1.bank # => bank1
|
|
25
|
+
#
|
|
26
|
+
# The default value for this property is an instance if VariableExchangeBank.
|
|
27
|
+
# It allows one to specify custom exchange rates:
|
|
28
|
+
#
|
|
29
|
+
# Money.default_bank.add_rate("USD", "CAD", 1.24515)
|
|
30
|
+
# Money.default_bank.add_rate("CAD", "USD", 0.803115)
|
|
31
|
+
# Money.new(100, "USD").exchange_to("CAD") # => Money.new(124, "CAD")
|
|
32
|
+
# Money.new(100, "CAD").exchange_to("USD") # => Money.new(80, "USD")
|
|
33
|
+
attr_accessor :default_bank
|
|
34
|
+
|
|
35
|
+
# The default currency, which is used when <tt>Money.new</tt> is called
|
|
36
|
+
# without an explicit currency argument. The default value is "USD".
|
|
37
|
+
attr_accessor :default_currency
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
self.default_bank = VariableExchangeBank.instance
|
|
41
|
+
self.default_currency = "USD"
|
|
42
|
+
|
|
43
|
+
# Create a new money object with value 0.
|
|
44
|
+
def self.empty(currency = default_currency)
|
|
45
|
+
Money.new(0, currency)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.add_rate(from_currency, to_currency, rate)
|
|
49
|
+
Money.default_bank.add_rate(from_currency, to_currency, rate)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Creates a new money object.
|
|
53
|
+
# Money.new(100)
|
|
54
|
+
#
|
|
55
|
+
# Alternativly you can use the convinience methods like
|
|
56
|
+
# Money.ca_dollar and Money.us_dollar
|
|
57
|
+
def initialize(cents, currency = Money.default_currency, bank = Money.default_bank)
|
|
58
|
+
@cents = cents.round
|
|
59
|
+
@currency = currency
|
|
60
|
+
@bank = bank
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Do two money objects equal? Only works if both objects are of the same currency
|
|
64
|
+
def ==(other_money)
|
|
65
|
+
cents == other_money.cents && bank.same_currency?(currency, other_money.currency)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def <=>(other_money)
|
|
69
|
+
if bank.same_currency?(currency, other_money.currency)
|
|
70
|
+
cents <=> other_money.cents
|
|
71
|
+
else
|
|
72
|
+
cents <=> other_money.exchange_to(currency).cents
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def +(other_money)
|
|
77
|
+
if currency == other_money.currency
|
|
78
|
+
Money.new(cents + other_money.cents, other_money.currency)
|
|
79
|
+
else
|
|
80
|
+
Money.new(cents + other_money.exchange_to(currency).cents,currency)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def -(other_money)
|
|
85
|
+
if currency == other_money.currency
|
|
86
|
+
Money.new(cents - other_money.cents, other_money.currency)
|
|
87
|
+
else
|
|
88
|
+
Money.new(cents - other_money.exchange_to(currency).cents, currency)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get the cents value of the object
|
|
93
|
+
def cents
|
|
94
|
+
@cents
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Multiply money by fixnum
|
|
98
|
+
def *(fixnum)
|
|
99
|
+
Money.new(cents * fixnum, currency)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Divide money by fixnum
|
|
103
|
+
def /(fixnum)
|
|
104
|
+
Money.new(cents / fixnum, currency)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Test if the money amount is zero
|
|
108
|
+
def zero?
|
|
109
|
+
cents == 0
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Creates a formatted price string according to several rules. The following
|
|
113
|
+
# options are supported: :display_free, :with_currency, :no_cents, :symbol
|
|
114
|
+
# and :html.
|
|
115
|
+
#
|
|
116
|
+
# === +:display_free+
|
|
117
|
+
#
|
|
118
|
+
# Whether a zero amount of money should be formatted of "free" or as the
|
|
119
|
+
# supplied string.
|
|
120
|
+
#
|
|
121
|
+
# Money.us_dollar(0).format(:display_free => true) => "free"
|
|
122
|
+
# Money.us_dollar(0).format(:display_free => "gratis") => "gratis"
|
|
123
|
+
# Money.us_dollar(0).format => "$0.00"
|
|
124
|
+
#
|
|
125
|
+
# === +:with_currency+
|
|
126
|
+
#
|
|
127
|
+
# Whether the currency name should be appended to the result string.
|
|
128
|
+
#
|
|
129
|
+
# Money.ca_dollar(100).format => "$1.00"
|
|
130
|
+
# Money.ca_dollar(100).format(:with_currency => true) => "$1.00 CAD"
|
|
131
|
+
# Money.us_dollar(85).format(:with_currency => true) => "$0.85 USD"
|
|
132
|
+
#
|
|
133
|
+
# === +:no_cents+
|
|
134
|
+
#
|
|
135
|
+
# Whether cents should be omitted.
|
|
136
|
+
#
|
|
137
|
+
# Money.ca_dollar(100).format(:no_cents => true) => "$1"
|
|
138
|
+
# Money.ca_dollar(599).format(:no_cents => true) => "$5"
|
|
139
|
+
#
|
|
140
|
+
# Money.ca_dollar(570).format(:no_cents => true, :with_currency => true) => "$5 CAD"
|
|
141
|
+
# Money.ca_dollar(39000).format(:no_cents => true) => "$390"
|
|
142
|
+
#
|
|
143
|
+
# === +:symbol+
|
|
144
|
+
#
|
|
145
|
+
# Whether a money symbol should be prepended to the result string. The default is true.
|
|
146
|
+
# This method attempts to pick a symbol that's suitable for the given currency.
|
|
147
|
+
#
|
|
148
|
+
# Money.new(100, :currency => "USD") => "$1.00"
|
|
149
|
+
# Money.new(100, :currency => "GBP") => "£1.00"
|
|
150
|
+
# Money.new(100, :currency => "EUR") => "€1.00"
|
|
151
|
+
#
|
|
152
|
+
# # Same thing.
|
|
153
|
+
# Money.new(100, :currency => "USD").format(:symbol => true) => "$1.00"
|
|
154
|
+
# Money.new(100, :currency => "GBP").format(:symbol => true) => "£1.00"
|
|
155
|
+
# Money.new(100, :currency => "EUR").format(:symbol => true) => "€1.00"
|
|
156
|
+
#
|
|
157
|
+
# You can specify a false expression or an empty string to disable prepending
|
|
158
|
+
# a money symbol:
|
|
159
|
+
#
|
|
160
|
+
# Money.new(100, :currency => "USD").format(:symbol => false) => "1.00"
|
|
161
|
+
# Money.new(100, :currency => "GBP").format(:symbol => nil) => "1.00"
|
|
162
|
+
# Money.new(100, :currency => "EUR").format(:symbol => "") => "1.00"
|
|
163
|
+
#
|
|
164
|
+
#
|
|
165
|
+
# If the symbol for the given currency isn't known, then it will default
|
|
166
|
+
# to "$" as symbol:
|
|
167
|
+
#
|
|
168
|
+
# Money.new(100, :currency => "AWG").format(:symbol => true) => "$1.00"
|
|
169
|
+
#
|
|
170
|
+
# You can specify a string as value to enforce using a particular symbol:
|
|
171
|
+
#
|
|
172
|
+
# Money.new(100, :currency => "AWG").format(:symbol => "ƒ") => "ƒ1.00"
|
|
173
|
+
#
|
|
174
|
+
# === +:html+
|
|
175
|
+
#
|
|
176
|
+
# Whether the currency should be HTML-formatted. Only useful in combination with +:with_currency+.
|
|
177
|
+
#
|
|
178
|
+
# Money.ca_dollar(570).format(:html => true, :with_currency => true)
|
|
179
|
+
# => "$5.70 <span class=\"currency\">CAD</span>"
|
|
180
|
+
def format(*rules)
|
|
181
|
+
# support for old format parameters
|
|
182
|
+
rules = normalize_formatting_rules(rules)
|
|
183
|
+
|
|
184
|
+
if cents == 0
|
|
185
|
+
if rules[:display_free].respond_to?(:to_str)
|
|
186
|
+
return rules[:display_free]
|
|
187
|
+
elsif rules[:display_free]
|
|
188
|
+
return "free"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if rules.has_key?(:symbol)
|
|
193
|
+
if rules[:symbol] === true
|
|
194
|
+
symbol = SYMBOLS[currency[:currency]] || "$"
|
|
195
|
+
elsif rules[:symbol]
|
|
196
|
+
symbol = rules[:symbol]
|
|
197
|
+
else
|
|
198
|
+
symbol = ""
|
|
199
|
+
end
|
|
200
|
+
else
|
|
201
|
+
symbol = "$"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if rules[:no_cents]
|
|
205
|
+
formatted = sprintf("#{symbol}%d", cents.to_f / 100)
|
|
206
|
+
else
|
|
207
|
+
formatted = sprintf("#{symbol}%.2f", cents.to_f / 100)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Commify ("10000" => "10,000")
|
|
211
|
+
formatted.gsub!(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2')
|
|
212
|
+
|
|
213
|
+
if rules[:with_currency]
|
|
214
|
+
formatted << " "
|
|
215
|
+
formatted << '<span class="currency">' if rules[:html]
|
|
216
|
+
formatted << currency
|
|
217
|
+
formatted << '</span>' if rules[:html]
|
|
218
|
+
end
|
|
219
|
+
formatted
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Returns the amount of money as a string.
|
|
223
|
+
#
|
|
224
|
+
# Money.ca_dollar(100).to_s => "1.00"
|
|
225
|
+
def to_s
|
|
226
|
+
sprintf("%.2f", cents / 100.00)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Recieve the amount of this money object in another currency.
|
|
230
|
+
def exchange_to(other_currency)
|
|
231
|
+
Money.new(@bank.exchange(self.cents, currency, other_currency), other_currency)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Conversation to self
|
|
235
|
+
def to_money
|
|
236
|
+
self
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
def normalize_formatting_rules(rules)
|
|
241
|
+
if rules.size == 1
|
|
242
|
+
rules = rules.pop
|
|
243
|
+
rules = { rules => true } if rules.is_a?(Symbol)
|
|
244
|
+
else
|
|
245
|
+
rules = rules.inject({}) do |h,s|
|
|
246
|
+
h[s] = true
|
|
247
|
+
h
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
rules
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
# Add more from http://www.xe.com/symbols.php
|
|
4
|
+
Money::SYMBOLS = {
|
|
5
|
+
"GBP" => "£",
|
|
6
|
+
"JPY" => "¥",
|
|
7
|
+
"EUR" => "€",
|
|
8
|
+
"ZWD" => "Z$",
|
|
9
|
+
"CNY" => "¥",
|
|
10
|
+
"INR" => "₨",
|
|
11
|
+
"NPR" => "₨",
|
|
12
|
+
"SCR" => "₨",
|
|
13
|
+
"LKR" => "₨",
|
|
14
|
+
"SEK" => "kr",
|
|
15
|
+
"GHC" => "¢"
|
|
16
|
+
|
|
17
|
+
# Everything else defaults to $
|
|
18
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'money/errors'
|
|
3
|
+
|
|
4
|
+
# Class for aiding in exchanging money between different currencies.
|
|
5
|
+
# By default, the Money class uses an object of this class (accessible through
|
|
6
|
+
# Money#bank) for performing currency exchanges.
|
|
7
|
+
#
|
|
8
|
+
# By default, VariableExchangeBank has no knowledge about conversion rates.
|
|
9
|
+
# One must manually specify them with +add_rate+, after which one can perform
|
|
10
|
+
# exchanges with +exchange+. For example:
|
|
11
|
+
#
|
|
12
|
+
# bank = Money::VariableExchangeBank.new
|
|
13
|
+
# bank.add_rate("USD", "CAD", 1.24515)
|
|
14
|
+
# bank.add_rate("CAD", "USD", 0.803115)
|
|
15
|
+
#
|
|
16
|
+
# # Exchange 100 CAD to USD:
|
|
17
|
+
# bank.exchange(100_00, "CAD", "USD") # => 124
|
|
18
|
+
# # Exchange 100 USD to CAD:
|
|
19
|
+
# bank.exchange(100_00, "USD", "CAD") # => 80
|
|
20
|
+
class Money
|
|
21
|
+
class VariableExchangeBank
|
|
22
|
+
# Returns the singleton instance of VariableExchangeBank.
|
|
23
|
+
#
|
|
24
|
+
# By default, <tt>Money.default_bank</tt> returns the same object.
|
|
25
|
+
def self.instance
|
|
26
|
+
@@singleton
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@rates = {}
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Registers a conversion rate. +from+ and +to+ are both currency names.
|
|
35
|
+
def add_rate(from, to, rate)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@rates["#{from}_TO_#{to}".upcase] = rate
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Gets the rate for exchanging the currency named +from+ to the currency
|
|
42
|
+
# named +to+. Returns nil if the rate is unknown.
|
|
43
|
+
def get_rate(from, to)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@rates["#{from}_TO_#{to}".upcase]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Given two currency names, checks whether they're both the same currency.
|
|
50
|
+
#
|
|
51
|
+
# bank = VariableExchangeBank.new
|
|
52
|
+
# bank.same_currency?("usd", "USD") # => true
|
|
53
|
+
# bank.same_currency?("usd", "EUR") # => false
|
|
54
|
+
def same_currency?(currency1, currency2)
|
|
55
|
+
currency1.upcase == currency2.upcase
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Exchange the given amount of cents in +from_currency+ to +to_currency+.
|
|
59
|
+
# Returns the amount of cents in +to_currency+ as an integer, rounded down.
|
|
60
|
+
#
|
|
61
|
+
# If the conversion rate is unknown, then Money::UnknownRate will be raised.
|
|
62
|
+
def exchange(cents, from_currency, to_currency)
|
|
63
|
+
rate = get_rate(from_currency, to_currency)
|
|
64
|
+
if !rate
|
|
65
|
+
raise Money::UnknownRate, "No conversion rate known for '#{from_currency}' -> '#{to_currency}'"
|
|
66
|
+
end
|
|
67
|
+
(cents * rate).floor
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@@singleton = VariableExchangeBank.new
|
|
71
|
+
end
|
|
72
|
+
end
|
data/money.gemspec
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = %q{money}
|
|
5
|
+
s.version = "0.5.0"
|
|
6
|
+
|
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
8
|
+
s.authors = ["beawesomeinstead"]
|
|
9
|
+
s.date = %q{2009-08-04}
|
|
10
|
+
s.description = %q{Library for dealing with money and currency conversion.}
|
|
11
|
+
s.email = %q{beawesomeinstead@yahoo.com}
|
|
12
|
+
s.extra_rdoc_files = [
|
|
13
|
+
"README.markdown"
|
|
14
|
+
]
|
|
15
|
+
s.files = [
|
|
16
|
+
".gitignore",
|
|
17
|
+
"README.markdown",
|
|
18
|
+
"VERSION",
|
|
19
|
+
"lib/money.rb",
|
|
20
|
+
"lib/money/core_extensions.rb",
|
|
21
|
+
"lib/money/errors.rb",
|
|
22
|
+
"lib/money/money.rb",
|
|
23
|
+
"lib/money/symbols.rb",
|
|
24
|
+
"lib/money/variable_exchange_bank.rb",
|
|
25
|
+
"money.gemspec",
|
|
26
|
+
"rakefile",
|
|
27
|
+
"test/core_extensions_test.rb",
|
|
28
|
+
"test/money_test.rb",
|
|
29
|
+
"test/test_helper.rb",
|
|
30
|
+
"test/variable_exchange_bank_test.rb"
|
|
31
|
+
]
|
|
32
|
+
s.homepage = %q{http://github.com/bai/money}
|
|
33
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
|
34
|
+
s.require_paths = ["lib"]
|
|
35
|
+
s.rubygems_version = %q{1.3.5}
|
|
36
|
+
s.summary = %q{Library for dealing with money and currency conversion.}
|
|
37
|
+
s.test_files = [
|
|
38
|
+
"test/variable_exchange_bank_test.rb",
|
|
39
|
+
"test/money_test.rb",
|
|
40
|
+
"test/test_helper.rb",
|
|
41
|
+
"test/core_extensions_test.rb"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
if s.respond_to? :specification_version then
|
|
45
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
|
46
|
+
s.specification_version = 3
|
|
47
|
+
|
|
48
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
|
49
|
+
else
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
end
|
|
53
|
+
end
|
data/rakefile
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
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 = %Q{Library for dealing with money and currency conversion.}
|
|
9
|
+
gem.description = %Q{Library for dealing with money and currency conversion.}
|
|
10
|
+
gem.email = "beawesomeinstead@yahoo.com"
|
|
11
|
+
gem.homepage = "http://github.com/bai/money"
|
|
12
|
+
gem.authors = ["beawesomeinstead"]
|
|
13
|
+
end
|
|
14
|
+
rescue LoadError
|
|
15
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require 'rake/testtask'
|
|
19
|
+
Rake::TestTask.new(:test) do |test|
|
|
20
|
+
test.libs << 'lib' << 'test'
|
|
21
|
+
test.pattern = 'test/**/*_test.rb'
|
|
22
|
+
test.verbose = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
require 'rcov/rcovtask'
|
|
27
|
+
Rcov::RcovTask.new do |test|
|
|
28
|
+
test.libs << 'test'
|
|
29
|
+
test.pattern = 'test/**/*_test.rb'
|
|
30
|
+
test.verbose = true
|
|
31
|
+
end
|
|
32
|
+
rescue LoadError
|
|
33
|
+
task :rcov do
|
|
34
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
require 'rake/rdoctask'
|
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
|
40
|
+
if File.exist?('VERSION.yml')
|
|
41
|
+
config = YAML.load(File.read('VERSION.yml'))
|
|
42
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
|
43
|
+
elsif File.exist?('VERSION')
|
|
44
|
+
version = File.read('VERSION')
|
|
45
|
+
else
|
|
46
|
+
version = ""
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
50
|
+
rdoc.title = "money #{version}"
|
|
51
|
+
rdoc.rdoc_files.include('README*')
|
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
53
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
class CoreExtensionsTest < Test::Unit::TestCase
|
|
4
|
+
context "Money core extensions" do
|
|
5
|
+
should "transform Numberic to Money with Numberic#to_money" do
|
|
6
|
+
money = 1234.to_money
|
|
7
|
+
assert_equal 1234_00, money.cents
|
|
8
|
+
assert_equal Money.default_currency, money.currency
|
|
9
|
+
|
|
10
|
+
money = 100.37.to_money
|
|
11
|
+
assert_equal 100_37, money.cents
|
|
12
|
+
assert_equal Money.default_currency, money.currency
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
should "transform String to Money with String#to_money" do
|
|
16
|
+
assert_equal Money.new(20_15), "20.15".to_money
|
|
17
|
+
assert_equal Money.new(100_00), "100".to_money
|
|
18
|
+
assert_equal Money.new(100_37), "100.37".to_money
|
|
19
|
+
assert_equal Money.new(100_37), "100,37".to_money
|
|
20
|
+
assert_equal Money.new(100_000_00), "100 000".to_money
|
|
21
|
+
assert_equal Money.new(100_000_00), "100,000.00".to_money
|
|
22
|
+
assert_equal Money.new(1_000_00), "1,000".to_money
|
|
23
|
+
assert_equal Money.new(-1_000_00), "-1,000".to_money
|
|
24
|
+
assert_equal Money.new(1_000_50), "1,000.5".to_money
|
|
25
|
+
assert_equal Money.new(1_000_51), "1,000.51".to_money
|
|
26
|
+
assert_equal Money.new(1_000_51), "1,000.505".to_money
|
|
27
|
+
assert_equal Money.new(1_000_50), "1,000.504".to_money
|
|
28
|
+
assert_equal Money.new(1_000_00), "1,000.0000".to_money
|
|
29
|
+
assert_equal Money.new(1_000_50), "1,000.5000".to_money
|
|
30
|
+
assert_equal Money.new(1_000_51), "1,000.5099".to_money
|
|
31
|
+
assert_equal Money.new(1_55), "1.550".to_money
|
|
32
|
+
assert_equal Money.new(25_00), "25.".to_money
|
|
33
|
+
assert_equal Money.new(75), ".75".to_money
|
|
34
|
+
|
|
35
|
+
assert_equal Money.new(100_00, "USD"), "100 USD".to_money
|
|
36
|
+
assert_equal Money.new(-100_00, "USD"), "-100 USD".to_money
|
|
37
|
+
assert_equal Money.new(100_00, "EUR"), "100 EUR".to_money
|
|
38
|
+
assert_equal Money.new(100_37, "EUR"), "100.37 EUR".to_money
|
|
39
|
+
assert_equal Money.new(100_37, "EUR"), "100,37 EUR".to_money
|
|
40
|
+
assert_equal Money.new(100_000_00, "USD"), "100,000.00 USD".to_money
|
|
41
|
+
assert_equal Money.new(100_000_00, "EUR"), "100.000,00 EUR".to_money
|
|
42
|
+
assert_equal Money.new(1_000_00, "USD"), "1,000 USD".to_money
|
|
43
|
+
assert_equal Money.new(-1_000_00, "USD"), "-1,000 USD".to_money
|
|
44
|
+
assert_equal Money.new(1_000_55, "USD"), "1,000.5500 USD".to_money
|
|
45
|
+
assert_equal Money.new(-1_000_65, "USD"), "-1,000.6500 USD".to_money
|
|
46
|
+
assert_equal Money.new(1_55, "USD"), "1.550 USD".to_money
|
|
47
|
+
|
|
48
|
+
assert_equal Money.new(100_00, "USD"), "USD 100".to_money
|
|
49
|
+
assert_equal Money.new(100_00, "EUR"), "EUR 100".to_money
|
|
50
|
+
assert_equal Money.new(100_37, "EUR"), "EUR 100.37".to_money
|
|
51
|
+
assert_equal Money.new(-100_37, "CAD"), "CAD -100.37".to_money
|
|
52
|
+
assert_equal Money.new(100_37, "EUR"), "EUR 100,37".to_money
|
|
53
|
+
assert_equal Money.new(-100_37, "EUR"), "EUR -100,37".to_money
|
|
54
|
+
assert_equal Money.new(100_000_00, "USD"), "USD 100,000.00".to_money
|
|
55
|
+
assert_equal Money.new(100_000_00, "EUR"), "EUR 100.000,00".to_money
|
|
56
|
+
assert_equal Money.new(1_000_00, "USD"), "USD 1,000".to_money
|
|
57
|
+
assert_equal Money.new(-1_000_00, "USD"), "USD -1,000".to_money
|
|
58
|
+
assert_equal Money.new(1_000_90, "USD"), "USD 1,000.9000".to_money
|
|
59
|
+
assert_equal Money.new(-1_000_09, "USD"), "USD -1,000.090".to_money
|
|
60
|
+
assert_equal Money.new(1_55, "USD"), "USD 1.5500".to_money
|
|
61
|
+
|
|
62
|
+
assert_equal Money.new(100_00, "USD"), "$100 USD".to_money
|
|
63
|
+
assert_equal Money.new(1_194_59, "USD"), "$1,194.59 USD".to_money
|
|
64
|
+
assert_equal Money.new(-1_955_00, "USD"), "$-1,955 USD".to_money
|
|
65
|
+
assert_equal Money.new(1_194_59, "USD"), "$1,194.5900 USD".to_money
|
|
66
|
+
assert_equal Money.new(-1_955_00, "USD"), "$-1,955.000 USD".to_money
|
|
67
|
+
assert_equal Money.new(1_99, "USD"), "$1.99000 USD".to_money
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
should "ignore unrecognized data" do
|
|
71
|
+
assert_equal Money.new(2000_00), "hello 2000 world".to_money
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/test/money_test.rb
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
class MoneyTest < Test::Unit::TestCase
|
|
4
|
+
context "Money" do
|
|
5
|
+
should "be associated to the singleton instance of VariableExchangeBank by default" do
|
|
6
|
+
money_bank_object_id = Money.new(0).bank.object_id
|
|
7
|
+
bank_instance_object_id = Money::VariableExchangeBank.instance.object_id
|
|
8
|
+
assert_equal bank_instance_object_id, money_bank_object_id
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
should "round the given cents to an integer" do
|
|
12
|
+
assert_equal 1, Money.new(1.00, "USD").cents
|
|
13
|
+
assert_equal 1, Money.new(1.01, "USD").cents
|
|
14
|
+
assert_equal 2, Money.new(1.50, "USD").cents
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
should "have 0 cents of default currency when created with Money.empty" do
|
|
18
|
+
assert_equal Money.new(0), Money.empty
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
should "add new currency rates" do
|
|
22
|
+
Money.add_rate("EUR", "USD", 10)
|
|
23
|
+
|
|
24
|
+
assert_equal Money.new(100_00, "USD"), Money.new(10_00, "EUR").exchange_to("USD")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
should "return the amount of cents passed to the constructor with #cents" do
|
|
28
|
+
assert_equal 200_00, Money.new(200_00, "USD").cents
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
should "return the currency passed to the constructor with #currency" do
|
|
32
|
+
assert_equal "USD", Money.new(200_00, "USD").currency
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
should "return whether the amount is 0 with #zero?" do
|
|
36
|
+
assert_equal true, Money.new(0, "USD").zero?
|
|
37
|
+
assert_equal true, Money.new(0, "EUR").zero?
|
|
38
|
+
assert_equal false, Money.new(1, "USD").zero?
|
|
39
|
+
assert_equal false, Money.new(10, "YEN").zero?
|
|
40
|
+
assert_equal false, Money.new(-1, "EUR").zero?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
should "exchange the amount via its exchange bank and do it properly with #exchange_to" do
|
|
44
|
+
Money.add_rate("USD", "EUR", 10)
|
|
45
|
+
|
|
46
|
+
money = Money.new(100_00, "USD")
|
|
47
|
+
assert_equal Money.new(1000_00, "EUR"), money.exchange_to("EUR")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
should "return true if and only if their amount and currency are equal with #==" do
|
|
51
|
+
assert_equal Money.new(1_00, "USD"), Money.new(1_00, "USD")
|
|
52
|
+
assert_not_equal Money.new(1_00, "USD"), Money.new(1_00, "EUR")
|
|
53
|
+
assert_not_equal Money.new(1_00, "USD"), Money.new(2_00, "USD")
|
|
54
|
+
assert_not_equal Money.new(1_00, "USD"), Money.new(99_00, "EUR")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
should "multiply the money's amount by the multiplier while retaining the currency with #*" do
|
|
58
|
+
assert_equal Money.new(10_00, "USD"), Money.new(1_00, "USD") * 10
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
should "divide the money's amount by the divisor while retaining the currency with #/" do
|
|
62
|
+
assert_equal Money.new(1_00, "USD"), Money.new(10_00, "USD") / 10
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context "#format" do
|
|
66
|
+
should "return the monetary value as a string" do
|
|
67
|
+
assert_equal "$1.00", Money.new(100, "CAD").format
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
context "if the monetary value is 0" do
|
|
71
|
+
setup do
|
|
72
|
+
@money = Money.new(0, "USD")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
should "return 'free' when :display_free is true" do
|
|
76
|
+
assert_equal 'free', @money.format(:display_free => true)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
should "return '$0.00' when :display_free is false or not given" do
|
|
80
|
+
assert_equal '$0.00', @money.format
|
|
81
|
+
assert_equal '$0.00', @money.format(:display_free => false)
|
|
82
|
+
assert_equal '$0.00', @money.format(:display_free => nil)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
should "return the value specified by :display_free if it's a string-like object" do
|
|
86
|
+
assert_equal 'gratis', @money.format(:display_free => 'gratis')
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
should "work as documented when :with_currency option is true" do
|
|
91
|
+
assert_equal "$1.00 CAD", Money.new(100, "CAD").format(:with_currency => true)
|
|
92
|
+
assert_equal "$0.85 USD", Money.new(85, "USD").format(:with_currency => true)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
should "work as documented when :with_currency option is present" do
|
|
96
|
+
assert_equal "$1.00 CAD", Money.new(100, "CAD").format(:with_currency)
|
|
97
|
+
assert_equal "$0.85 USD", Money.new(85, "USD").format(:with_currency)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
should "work as documented when :no_cents option is true" do
|
|
101
|
+
assert_equal "$1", Money.new(100, "CAD").format(:no_cents => true)
|
|
102
|
+
assert_equal "$5", Money.new(599, "CAD").format(:no_cents => true)
|
|
103
|
+
assert_equal "$5 CAD", Money.new(570, "CAD").format(:no_cents => true, :with_currency => true)
|
|
104
|
+
assert_equal "$390", Money.new(39000, "CAD").format(:no_cents => true)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
should "work as documented when :no_cents option is present" do
|
|
108
|
+
assert_equal "$1", Money.new(100, "CAD").format(:no_cents)
|
|
109
|
+
assert_equal "$5", Money.new(599, "CAD").format(:no_cents)
|
|
110
|
+
assert_equal "$5 CAD", Money.new(570, "CAD").format(:no_cents, :with_currency)
|
|
111
|
+
assert_equal "$390", Money.new(39000, "CAD").format(:no_cents)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
should "use a given :symbol value as the money symbol" do
|
|
115
|
+
assert_equal "£1.00", Money.new(100, :currency => "GBP").format(:symbol => "£")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
should "return symbol based on the given currency code when :symbol option is true" do #!!!!!
|
|
119
|
+
one = Proc.new { |currency| Money.new(100, :currency => currency).format(:symbol => true) }
|
|
120
|
+
|
|
121
|
+
# Pounds
|
|
122
|
+
assert_equal "£1.00", one["GBP"]
|
|
123
|
+
|
|
124
|
+
# Dollars
|
|
125
|
+
assert_equal "$1.00", one["USD"]
|
|
126
|
+
assert_equal "$1.00", one["CAD"]
|
|
127
|
+
assert_equal "$1.00", one["AUD"]
|
|
128
|
+
assert_equal "$1.00", one["NZD"]
|
|
129
|
+
assert_equal "Z$1.00", one["ZWD"]
|
|
130
|
+
|
|
131
|
+
# Yen
|
|
132
|
+
assert_equal "¥1.00", one["JPY"]
|
|
133
|
+
assert_equal "¥1.00", one["CNY"]
|
|
134
|
+
|
|
135
|
+
# Euro
|
|
136
|
+
assert_equal "€1.00", one["EUR"]
|
|
137
|
+
|
|
138
|
+
# Rupees
|
|
139
|
+
assert_equal "₨1.00", one["INR"]
|
|
140
|
+
assert_equal "₨1.00", one["NPR"]
|
|
141
|
+
assert_equal "₨1.00", one["SCR"]
|
|
142
|
+
assert_equal "₨1.00", one["LKR"]
|
|
143
|
+
|
|
144
|
+
# Other
|
|
145
|
+
assert_equal "kr1.00", one["SEK"]
|
|
146
|
+
assert_equal "¢1.00", one["GHC"]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
should "return $ when currency code is not recognized and :symbol option is true" do
|
|
150
|
+
assert_equal "$1.00", Money.new(100, :currency => "XYZ").format(:symbol => true)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
should "return symbol based on the given currency code when :symbol option is some non-Boolean value that evaluates to true" do
|
|
154
|
+
assert_equal "£1.00", Money.new(100, :currency => "GBP").format(:symbol => true) #!!!!
|
|
155
|
+
assert_equal "€1.00", Money.new(100, :currency => "EUR").format(:symbol => true)
|
|
156
|
+
assert_equal "kr1.00", Money.new(100, :currency => "SEK").format(:symbol => true)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
should "return the amount without a symbol when :symbol option is "", nil or false" do
|
|
160
|
+
money = Money.new(100, :currency => "GBP")
|
|
161
|
+
assert_equal "1.00", money.format(:symbol => "")
|
|
162
|
+
assert_equal "1.00", money.format(:symbol => nil)
|
|
163
|
+
assert_equal "1.00", money.format(:symbol => false)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
should "work as documented when :html option is true" do
|
|
167
|
+
string = Money.new(570, "CAD").format(:html => true, :with_currency => true)
|
|
168
|
+
assert_equal "$5.70 <span class=\"currency\">CAD</span>", string
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
should "insert commas into the result if the amount is sufficiently large" do
|
|
172
|
+
assert_equal "$1,000,000,000.12", Money.new(1_000_000_000_12, "USD").format
|
|
173
|
+
assert_equal "$1,000,000,000", Money.new(1_000_000_000_12, "USD").format(:no_cents => true)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
context "Actions involving two Money objects" do
|
|
179
|
+
context "if the other Money object has the same currency" do
|
|
180
|
+
should "compare the two objects' amounts with #<=>" do
|
|
181
|
+
assert_operator 0, :>, Money.new(1_00, "USD") <=> Money.new(2_00, "USD")
|
|
182
|
+
assert_operator 0, :==, Money.new(1_00, "USD") <=> Money.new(1_00, "USD")
|
|
183
|
+
assert_operator 0, :<, Money.new(1_00, "USD") <=> Money.new(99, "USD")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
should "add the other object's amount to the current object's amount while retaining the currency with #+" do
|
|
187
|
+
assert_equal Money.new(10_90, "USD"), Money.new(10_00, "USD") + Money.new(90, "USD")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
should "substract the other object's amount from the current object's amount while retaining the currency with #-" do
|
|
191
|
+
assert_equal Money.new(9_10, "USD"), Money.new(10_00, "USD") - Money.new(90, "USD")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
context "if the other Money object has a different currency" do
|
|
196
|
+
setup do
|
|
197
|
+
Money.add_rate("EUR", "USD", 10)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
should "compare the two objects' amount after converting the other object's amount to its own currency with #<=>" do
|
|
201
|
+
assert_operator 0, :>, Money.new(100_00, "USD") <=> Money.new(11_00, "EUR")
|
|
202
|
+
assert_operator 0, :==, Money.new(100_00, "USD") <=> Money.new(10_00, "EUR")
|
|
203
|
+
assert_operator 0, :<, Money.new(100_00, "USD") <=> Money.new(5_00, "EUR")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
should "add the other object's amount, converted to this object's currency, to this object's amount while retaining its currency with #+" do
|
|
207
|
+
assert_equal Money.new(150_00, "USD"), Money.new(100_00, "USD") + Money.new(5_00, "EUR")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
should "substract the other object's amount, converted to this object's currency, to this object's amount while retaining its currency with #-" do
|
|
211
|
+
assert_equal Money.new(50_00, "USD"), Money.new(100_00, "USD") - Money.new(5_00, "EUR")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
class VariableExchangeBankTest < Test::Unit::TestCase
|
|
4
|
+
context "Money::VariableExchangeBank" do
|
|
5
|
+
setup do
|
|
6
|
+
@bank = Money::VariableExchangeBank.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
should "return the previously specified conversion rate" do
|
|
10
|
+
@bank.add_rate("USD", "EUR", 0.788332676)
|
|
11
|
+
@bank.add_rate("EUR", "YEN", 122.631477)
|
|
12
|
+
assert_equal 0.788332676, @bank.get_rate("USD", "EUR")
|
|
13
|
+
assert_equal 122.631477, @bank.get_rate("EUR", "YEN")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
should "treat currency names case-insensitively" do
|
|
17
|
+
@bank.add_rate("usd", "eur", 1)
|
|
18
|
+
assert_equal 1, @bank.get_rate("USD", "EUR")
|
|
19
|
+
assert @bank.same_currency?("USD", "usd")
|
|
20
|
+
assert !@bank.same_currency?("EUR", "usd")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
should "return nil if the conversion rate is unknown" do
|
|
24
|
+
assert_nil @bank.get_rate("American Pesos", "EUR")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
should "exchange money from one currency to another according to the specified conversion rates" do
|
|
28
|
+
@bank.add_rate("USD", "EUR", 0.5)
|
|
29
|
+
@bank.add_rate("EUR", "YEN", 10)
|
|
30
|
+
assert_equal 5_00, @bank.exchange(10_00, "USD", "EUR")
|
|
31
|
+
assert_equal 5000_00, @bank.exchange(500_00, "EUR", "YEN")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
should "round the exchanged result down" do
|
|
35
|
+
@bank.add_rate("USD", "EUR", 0.788332676)
|
|
36
|
+
@bank.add_rate("EUR", "YEN", 122.631477)
|
|
37
|
+
assert_equal 788, @bank.exchange(10_00, "USD", "EUR")
|
|
38
|
+
assert_equal 6131573, @bank.exchange(500_00, "EUR", "YEN")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
should "raise Money::UnknownRate upon conversion if the conversion rate is unknown" do
|
|
42
|
+
assert_raise Money::UnknownRate do
|
|
43
|
+
@bank.exchange(10, "USD", "EUR")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: bai-money
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- beawesomeinstead
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-08-04 00:00:00 -07:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: Library for dealing with money and currency conversion.
|
|
17
|
+
email: beawesomeinstead@yahoo.com
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files:
|
|
23
|
+
- README.markdown
|
|
24
|
+
files:
|
|
25
|
+
- .gitignore
|
|
26
|
+
- README.markdown
|
|
27
|
+
- VERSION
|
|
28
|
+
- lib/money.rb
|
|
29
|
+
- lib/money/core_extensions.rb
|
|
30
|
+
- lib/money/errors.rb
|
|
31
|
+
- lib/money/money.rb
|
|
32
|
+
- lib/money/symbols.rb
|
|
33
|
+
- lib/money/variable_exchange_bank.rb
|
|
34
|
+
- money.gemspec
|
|
35
|
+
- rakefile
|
|
36
|
+
- test/core_extensions_test.rb
|
|
37
|
+
- test/money_test.rb
|
|
38
|
+
- test/test_helper.rb
|
|
39
|
+
- test/variable_exchange_bank_test.rb
|
|
40
|
+
has_rdoc: false
|
|
41
|
+
homepage: http://github.com/bai/money
|
|
42
|
+
licenses:
|
|
43
|
+
post_install_message:
|
|
44
|
+
rdoc_options:
|
|
45
|
+
- --charset=UTF-8
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: "0"
|
|
53
|
+
version:
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: "0"
|
|
59
|
+
version:
|
|
60
|
+
requirements: []
|
|
61
|
+
|
|
62
|
+
rubyforge_project:
|
|
63
|
+
rubygems_version: 1.3.5
|
|
64
|
+
signing_key:
|
|
65
|
+
specification_version: 3
|
|
66
|
+
summary: Library for dealing with money and currency conversion.
|
|
67
|
+
test_files:
|
|
68
|
+
- test/variable_exchange_bank_test.rb
|
|
69
|
+
- test/money_test.rb
|
|
70
|
+
- test/test_helper.rb
|
|
71
|
+
- test/core_extensions_test.rb
|