exchange 0.11.0 → 0.12.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/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"