exchange 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/exchange.rb CHANGED
@@ -5,7 +5,7 @@ require 'exchange/base'
5
5
  require 'exchange/configurable'
6
6
  require 'exchange/gem_loader'
7
7
  require 'exchange/helper'
8
- require 'exchange/iso_4217'
8
+ require 'exchange/iso'
9
9
  require 'exchange/money'
10
10
  require 'exchange/external_api'
11
11
  require 'exchange/cache'
data/lib/exchange/base.rb CHANGED
@@ -2,7 +2,7 @@ module Exchange
2
2
 
3
3
  # The current version of the exchange gem
4
4
  #
5
- VERSION = '0.11.0'
5
+ VERSION = '0.12.0'
6
6
 
7
7
  # The root installation path of the gem
8
8
  # @version 0.5
@@ -8,8 +8,33 @@ module Exchange
8
8
  #
9
9
  class Configuration < Exchange::Configurable
10
10
  attr_accessor :expire, :host, :port, :path
11
+
12
+ class << self
13
+
14
+ def wipe_client_before_setting *setters
15
+
16
+ setters.each do |setter|
17
+ define_method :"#{setter}_with_client_wipe=" do |data|
18
+ wipe_subclass_client!
19
+ send(:"#{setter}_without_client_wipe=", data)
20
+ end
21
+ alias_method :"#{setter}_without_client_wipe=", :"#{setter}="
22
+ alias_method :"#{setter}=", :"#{setter}_with_client_wipe="
23
+ end
24
+
25
+ end
26
+
27
+ end
11
28
 
12
29
  def_delegators :instance, :expire, :expire=, :host, :host=, :port, :port=, :path, :path=
30
+ wipe_client_before_setting :host, :port
31
+
32
+ # Overrides the parent class method to wipe the client before setting
33
+ #
34
+ def set hash
35
+ wipe_subclass_client!
36
+ super
37
+ end
13
38
 
14
39
  def parent_module
15
40
  Cache
@@ -18,7 +43,13 @@ module Exchange
18
43
  def key
19
44
  :cache
20
45
  end
21
-
46
+
47
+ private
48
+
49
+ def wipe_subclass_client!
50
+ subclass.wipe_client! if subclass && subclass.respond_to?(:wipe_client!)
51
+ end
52
+
22
53
  end
23
54
  end
24
55
  end
@@ -14,6 +14,8 @@ module Exchange
14
14
  #
15
15
  class Memcached < Base
16
16
 
17
+ def_delegators :instance, :client, :wipe_client!
18
+
17
19
  # instantiates a memcached client and memoizes it in a class variable.
18
20
  # Use this client to access memcached data. For further explanation of use visit the memcached gem documentation
19
21
  # @example
@@ -25,6 +27,12 @@ module Exchange
25
27
  @client ||= Dalli::Client.new("#{config.host}:#{config.port}")
26
28
  end
27
29
 
30
+ # Wipe the client instance variable
31
+ #
32
+ def wipe_client!
33
+ @client = nil
34
+ end
35
+
28
36
  # returns either cached data from the memcached client or calls the block and caches it in memcached.
29
37
  # This method has to be the same in all the cache classes in order for the configuration binding to work
30
38
  # @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
@@ -12,7 +12,9 @@ module Exchange
12
12
  # c.cache_port = 'Your redis port (an Integer)'
13
13
  # end
14
14
  class Redis < Base
15
-
15
+
16
+ def_delegators :instance, :client, :wipe_client!
17
+
16
18
  # instantiates a redis client and memoizes it in a class variable.
17
19
  # Use this client to access redis data. For further explanation of use visit the redis gem documentation
18
20
  # @example
@@ -24,6 +26,12 @@ module Exchange
24
26
  @client ||= ::Redis.new(:host => config.host, :port => config.port)
25
27
  end
26
28
 
29
+ # Wipe the client instance variable
30
+ #
31
+ def wipe_client!
32
+ @client = nil
33
+ end
34
+
27
35
  # returns either cached data from the redis client or calls the block and caches it in redis.
28
36
  # This method has to be the same in all the cache classes in order for the configuration binding to work
29
37
  # @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
@@ -12,12 +12,15 @@ module Exchange
12
12
  def_delegators :instance, :subclass, :subclass=, :set
13
13
 
14
14
  def subclass_with_constantize
15
- self.subclass = parent_module.const_get camelize(self.subclass_without_constantize) unless self.subclass_without_constantize.is_a?(Class)
15
+ self.subclass = parent_module.const_get camelize(self.subclass_without_constantize) unless !self.subclass_without_constantize || self.subclass_without_constantize.is_a?(Class)
16
16
  subclass_without_constantize
17
17
  end
18
18
  alias_method :subclass_without_constantize, :subclass
19
19
  alias_method :subclass, :subclass_with_constantize
20
20
 
21
+ # Set a configuration via a hash of options
22
+ # @params [Hash] hash The hash of options to set the configuration to
23
+ #
21
24
  def set hash
22
25
  hash.each_pair do |k,v|
23
26
  self.send(:"#{k}=", v)
@@ -26,6 +29,8 @@ module Exchange
26
29
  self
27
30
  end
28
31
 
32
+ # Reset the configuration to a set of defaults
33
+ #
29
34
  def reset
30
35
  set Exchange::Configuration::DEFAULTS[key]
31
36
  end
@@ -19,7 +19,7 @@ module Exchange
19
19
  def self.prevent_errors_with_exchange_for base, meth
20
20
  base.send(:define_method, :"#{meth}without_errors", lambda { |other|
21
21
  if other.is_a?(Exchange::Money)
22
- BigDecimal.new(self.to_s).send(meth, other).to_f
22
+ BigDecimal.new(self.to_s).send(meth, other.value).to_f
23
23
  else
24
24
  send(:"#{meth}with_errors", other)
25
25
  end
@@ -29,11 +29,7 @@ module Exchange
29
29
  # @version 0.10
30
30
  #
31
31
  def in currency, options={}
32
- if ISO4217.currencies.include? currency
33
- Money.new(self, currency, options)
34
- else
35
- raise Exchange::NoCurrencyError.new("#{currency} is not a currency")
36
- end
32
+ Money.new(self, currency, options)
37
33
  end
38
34
  end
39
35
  end
@@ -10,7 +10,7 @@ module Exchange
10
10
  # @since 0.3
11
11
  # @author Beat Richartz
12
12
  #
13
- class ISO4217
13
+ class ISO
14
14
  include Singleton
15
15
  extend SingleForwardable
16
16
 
@@ -24,38 +24,34 @@ module Exchange
24
24
  self.class_eval <<-EOV
25
25
  def #{op}(amount, currency, precision=nil)
26
26
  minor = definitions[currency][:minor_unit]
27
- (amount.is_a?(BigDecimal) ? amount : BigDecimal.new(amount.to_s, minor)).#{op}(precision || minor)
27
+ (amount.is_a?(BigDecimal) ? amount : BigDecimal.new(amount.to_s, precision_for(amount, currency))).#{op}(precision || minor)
28
28
  end
29
29
  EOV
30
30
  end
31
31
 
32
32
  end
33
33
 
34
- # The ISO 4217 that have to be loaded. Nothing much to say here. Just use this method to get to the definitions
34
+ # The ISO 4217 that have to be loaded. Use this method to get to the definitions
35
35
  # They are static, so they can be stored in a class variable without many worries
36
- # @return [Hash] The iso427 Definitions with the currency code as keys
36
+ # @return [Hash] The iso4217 Definitions with the currency code as keys
37
37
  #
38
38
  def definitions
39
- return @definitions if @definitions
40
- loaded = YAML.load_file(File.join(ROOT_PATH, 'iso4217.yml'))
41
- @definitions = {}
42
-
43
- loaded.each_pair do |k,v|
44
- v.keys.each do |key|
45
- v[key.to_sym] = v.delete(key)
46
- end
47
-
48
- @definitions[k.downcase.to_sym] = v
49
- end
50
-
51
- @definitions
39
+ @definitions ||= symbolize_keys(YAML.load_file(File.join(ROOT_PATH, 'iso4217.yml')))
40
+ end
41
+
42
+ # A map of country abbreviations to currency codes. Makes an instantiation of currency codes via a country code
43
+ # possible
44
+ # @return [Hash] The ISO3166 (1 and 2) country codes matched to a currency
45
+ #
46
+ def country_map
47
+ @country_map ||= symbolize_keys(YAML.load_file(File.join(ROOT_PATH, 'iso4217_country_map.yml')))
52
48
  end
53
49
 
54
50
  # All currencies defined by ISO 4217 as an array of symbols for inclusion testing
55
51
  # @return [Array] An Array of currency symbols
56
52
  #
57
53
  def currencies
58
- @currencies ||= definitions.keys.map(&:to_s).sort.map(&:to_sym)
54
+ @currencies ||= definitions.keys.sort_by(&:to_s)
59
55
  end
60
56
 
61
57
  # Check if a currency is defined by ISO 4217 standards
@@ -63,7 +59,15 @@ module Exchange
63
59
  # @return [Boolean] true if the symbol matches a currency, false if not
64
60
  #
65
61
  def defines? currency
66
- currencies.include? currency
62
+ currencies.include?(country_map[currency] ? country_map[currency] : currency)
63
+ end
64
+
65
+ # Asserts a given argument is a currency. Tries to match with a country code if the argument is not a currency
66
+ # @param [Symbol, String] arg The argument to assert
67
+ # @return [Symbol] The matching currency as a symbol
68
+ #
69
+ def assert_currency! arg
70
+ defines?(arg) ? (country_map[arg] || arg) : raise(Exchange::NoCurrencyError.new("#{arg} is not a currency nor a country code matchable to a currency"))
67
71
  end
68
72
 
69
73
  # Use this to instantiate a currency amount. For one, it is important that we use BigDecimal here so nothing gets lost because
@@ -72,10 +76,15 @@ module Exchange
72
76
  # @param [String, Symbol] currency The currency you want to instantiate the money in
73
77
  # @return [BigDecimal] The instantiated currency
74
78
  # @example instantiate a currency from a string
75
- # Exchange::ISO4217.instantiate("4523", "usd") #=> #<Bigdecimal 4523.00>
79
+ # Exchange::ISO.instantiate("4523", "usd") #=> #<Bigdecimal 4523.00>
80
+ # @note Reinstantiation is not needed in case the amount is already a big decimal. In this case, the maximum precision is already given.
76
81
  #
77
82
  def instantiate(amount, currency)
78
- BigDecimal.new(amount.to_s, definitions[currency][:minor_unit])
83
+ if amount.is_a?(BigDecimal)
84
+ amount
85
+ else
86
+ BigDecimal.new(amount.to_s, precision_for(amount, currency))
87
+ end
79
88
  end
80
89
 
81
90
  # Converts the currency to a string in ISO 4217 standardized format, either with or without the currency. This leaves you
@@ -86,17 +95,18 @@ module Exchange
86
95
  # @option opts [Boolean] :amount_only Whether you want to have the currency in the string or not
87
96
  # @return [String] The formatted string
88
97
  # @example Convert a currency to a string
89
- # Exchange::ISO4217.stringify(49.567, :usd) #=> "USD 49.57"
98
+ # Exchange::ISO.stringify(49.567, :usd) #=> "USD 49.57"
90
99
  # @example Convert a currency without minor to a string
91
- # Exchange::ISO4217.stringif(45, :jpy) #=> "JPY 45"
100
+ # Exchange::ISO.stringif(45, :jpy) #=> "JPY 45"
92
101
  # @example Convert a currency with a three decimal minor to a string
93
- # Exchange::ISO4217.stringif(34.34, :omr) #=> "OMR 34.340"
102
+ # Exchange::ISO.stringif(34.34, :omr) #=> "OMR 34.340"
94
103
  # @example Convert a currency to a string without the currency
95
- # Exchange::ISO4217.stringif(34.34, :omr, :amount_only => true) #=> "34.340"
104
+ # Exchange::ISO.stringif(34.34, :omr, :amount_only => true) #=> "34.340"
96
105
  #
97
106
  def stringify(amount, currency, opts={})
98
107
  format = "%.#{definitions[currency][:minor_unit]}f"
99
- "#{currency.to_s.upcase + ' ' unless opts[:amount_only]}#{format % amount}"
108
+ pre = [opts[:amount_only] && '', opts[:symbol] && (definitions[currency][:symbol] || currency.to_s.upcase), currency.to_s.upcase + ' '].detect{|a| a.is_a?(String)}
109
+ "#{pre}#{format % amount}"
100
110
  end
101
111
 
102
112
  # Use this to round a currency amount. This allows us to round exactly to the number of minors the currency has in the
@@ -104,7 +114,7 @@ module Exchange
104
114
  # @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to round
105
115
  # @param [String, Symbol] currency The currency you want to round the money in
106
116
  # @example Round a currency with 2 minors
107
- # Exchange::ISO4217.round("4523.456", "usd") #=> #<Bigdecimal 4523.46>
117
+ # Exchange::ISO.round("4523.456", "usd") #=> #<Bigdecimal 4523.46>
108
118
 
109
119
  install_operation :round
110
120
 
@@ -113,7 +123,7 @@ module Exchange
113
123
  # @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to ceil
114
124
  # @param [String, Symbol] currency The currency you want to ceil the money in
115
125
  # @example Ceil a currency with 2 minors
116
- # Exchange::ISO4217.ceil("4523.456", "usd") #=> #<Bigdecimal 4523.46>
126
+ # Exchange::ISO.ceil("4523.456", "usd") #=> #<Bigdecimal 4523.46>
117
127
 
118
128
  install_operation :ceil
119
129
 
@@ -122,13 +132,44 @@ module Exchange
122
132
  # @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to floor
123
133
  # @param [String, Symbol] currency The currency you want to floor the money in
124
134
  # @example Floor a currency with 2 minors
125
- # Exchange::ISO4217.floor("4523.456", "usd") #=> #<Bigdecimal 4523.46>
135
+ # Exchange::ISO.floor("4523.456", "usd") #=> #<Bigdecimal 4523.46>
126
136
 
127
137
  install_operation :floor
128
138
 
129
139
  # Forwards the assure_time method to the instance using singleforwardable
130
140
  #
131
- def_delegators :instance, :definitions, :instantiate, :stringify, :round, :ceil, :floor, :currencies, :defines?
141
+ def_delegators :instance, :definitions, :instantiate, :stringify, :round, :ceil, :floor, :currencies, :country_map, :defines?, :assert_currency!
142
+
143
+ private
144
+
145
+ # symbolizes keys and returns a new hash
146
+ #
147
+ def symbolize_keys hsh
148
+ new_hsh = Hash.new
149
+
150
+ hsh.each_pair do |k,v|
151
+ if v.is_a?(Hash)
152
+ v.keys.each do |key|
153
+ v[key.to_sym] = v.delete(key)
154
+ end
155
+ end
156
+
157
+ new_hsh[k.downcase.to_sym] = v
158
+ end
159
+
160
+ new_hsh
161
+ end
162
+
163
+ # get a precision for a specified amount and a specified currency
164
+ # @params [Float, Integer] amount The amount to get the precision for
165
+ # @params [Symbol] currency the currency to get the precision for
166
+ #
167
+ def precision_for amount, currency
168
+ defined_minor_precision = definitions[currency][:minor_unit]
169
+ given_major_precision, given_minor_precision = amount.to_s.match(/^-?(\d*)\.?(\d*)$/).to_a[1..2].map(&:size)
170
+
171
+ given_major_precision + [defined_minor_precision, given_minor_precision].max
172
+ end
132
173
 
133
174
  end
134
175
  end
@@ -48,14 +48,16 @@ module Exchange
48
48
  # Exchange::Money.new(40, :usd).to(:eur, :at => Time.gm(2012,9,1))
49
49
  # #=> #<Exchange::Money @number=37.0 @currency=:usd @time=#<Time> @from=#<Exchange::Money @number=40.0 @currency=:usd>>
50
50
  #
51
- def initialize value, currency_arg=nil, opts={}, &block
51
+ def initialize value, currency_arg=nil, opts={}, &block
52
+ currency_arg = ISO.assert_currency!(currency_arg) if currency_arg
53
+
52
54
  @from = opts[:from]
53
55
  @api = Exchange.configuration.api.subclass
54
56
 
55
57
  yield(self) if block_given?
56
58
 
57
59
  self.time = Helper.assure_time(time || opts[:at], :default => :now)
58
- self.value = ISO4217.instantiate(value, currency || currency_arg)
60
+ self.value = ISO.instantiate(value, currency || currency_arg)
59
61
  self.currency = currency || currency_arg
60
62
  end
61
63
 
@@ -81,6 +83,8 @@ module Exchange
81
83
  # Exchange::Money.new(40,:nok).to(:sek, :at => Time.gm(2012,2,2))
82
84
  #
83
85
  def to other, options={}
86
+ other = ISO.assert_currency!(other)
87
+
84
88
  if api_supports_currency?(other)
85
89
  opts = { :at => time, :from => self }.merge(options)
86
90
  Money.new(api.new.convert(value, currency, other, opts), other, opts)
@@ -88,6 +92,7 @@ module Exchange
88
92
  raise_no_rate_error(other)
89
93
  end
90
94
  end
95
+ alias :in :to
91
96
 
92
97
  class << self
93
98
 
@@ -98,7 +103,7 @@ module Exchange
98
103
  #
99
104
  def install_operation op
100
105
  define_method op do |*precision|
101
- Exchange::Money.new(ISO4217.send(op, self.value, self.currency, precision.first), currency, :at => time, :from => self)
106
+ Exchange::Money.new(ISO.send(op, self.value, self.currency, precision.first), currency, :at => time, :from => self)
102
107
  end
103
108
  end
104
109
 
@@ -110,7 +115,7 @@ module Exchange
110
115
  self.class_eval <<-EOV
111
116
  def #{op}(other)
112
117
  test_for_currency_mix_error(other)
113
- new_value = value #{op} (other.kind_of?(Money) ? other.to(self.currency, :at => other.time) : BigDecimal.new(other.to_s))
118
+ new_value = value #{op} (other.kind_of?(Money) ? other.to(self.currency, :at => other.time).value : BigDecimal.new(other.to_s))
114
119
  Exchange::Money.new(new_value, currency, :at => time, :from => self)
115
120
  end
116
121
  EOV
@@ -292,12 +297,12 @@ module Exchange
292
297
  # @example Convert a currency with a three decimal minor to a string
293
298
  # Exchange::Money.new(34.34, :omr).to_s #=> "OMR 34.340"
294
299
  # @example Convert a currency to a string without the currency
295
- # Exchange::ISO4217.stringif(34.34, :omr).to_s(:iso) #=> "34.340"
300
+ # Exchange::ISO.stringif(34.34, :omr).to_s(:iso) #=> "34.340"
296
301
  #
297
302
  def to_s format=:currency
298
303
  [
299
- format == :currency && ISO4217.stringify(value, currency),
300
- format == :amount && ISO4217.stringify(value, currency, :amount_only => true)
304
+ format == :currency && ISO.stringify(value, currency),
305
+ format == :amount && ISO.stringify(value, currency, :amount_only => true)
301
306
  ].detect{|l| l.is_a?(String) }
302
307
  end
303
308
 
@@ -77,13 +77,13 @@ module Exchange
77
77
  att = send(attribute)
78
78
  attribute_setter = :"#{attribute}_without_exchange_typecasting="
79
79
 
80
- if !data.respond_to?(:currency)
81
- send(attribute_setter, data)
80
+ send(attribute_setter, if !data.respond_to?(:currency)
81
+ data
82
82
  elsif att.currency == data.currency
83
- send(attribute_setter, data.value)
83
+ data.value
84
84
  elsif att.currency != data.currency
85
- send(attribute_setter, data.to(att.currency).value)
86
- end
85
+ data.to(att.currency).value
86
+ end)
87
87
  end
88
88
  exchange_typecasting_alias_method_chain attribute, '='
89
89
  end
@@ -39,6 +39,44 @@ describe "Exchange::Cache::Configuration" do
39
39
  end
40
40
  end
41
41
 
42
+ describe "setting and wiping the client" do
43
+ context "with a wipeable client" do
44
+ before(:each) do
45
+ subject.subclass = :memcached
46
+ subject.host = '127.0.0.1'
47
+ subject.port = 11211
48
+ end
49
+ after(:each) do
50
+ subject.subclass = :no_cache
51
+ end
52
+ it "should do so for the host" do
53
+ subject.subclass.client.should_not be_nil
54
+ subject.subclass.instance.instance_variable_get("@client").should_not be_nil
55
+ subject.host = 'new'
56
+ subject.subclass.instance.instance_variable_get("@client").should be_nil
57
+ end
58
+ it "should do so for the port" do
59
+ subject.subclass.client.should_not be_nil
60
+ subject.subclass.instance.instance_variable_get("@client").should_not be_nil
61
+ subject.port = 112
62
+ subject.subclass.instance.instance_variable_get("@client").should be_nil
63
+ end
64
+ end
65
+ context "without a wipeable client" do
66
+ before(:each) do
67
+ subject.subclass = :memory
68
+ end
69
+ it "should not fail for the host" do
70
+ subject.host = 'new'
71
+ subject.host.should == 'new'
72
+ end
73
+ it "should not fail for the port" do
74
+ subject.port = 11
75
+ subject.port.should == 11
76
+ end
77
+ end
78
+ end
79
+
42
80
  describe "reset" do
43
81
  before(:each) do
44
82
  subject.set :subclass => :no_cache, :expire => :daily, :host => 'localhost', :port => 112211, :path => "PATH"