latinum 1.4.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0e6854676447a6101dc405923a573827249be07f
4
- data.tar.gz: 261060fbc7e72d20a1174a10a0b2b054348c2f5b
2
+ SHA256:
3
+ metadata.gz: 9c055e958ac724d131bdbb261f18f6f8b6e6f475a77d633710077a0722c19ffc
4
+ data.tar.gz: 258a4c2c4233bc9e030a9e5cec7b685c0f7563bb012427809a6c6901067f69f1
5
5
  SHA512:
6
- metadata.gz: 8fee9777e500e212d28a1798777cd21c531b1cbfe646894604ef3fcf04e2892768285bed3657ab5c6c598d40779ecd0851a66452818483c7ec7f4c0436378fd9
7
- data.tar.gz: a598a4d6499b15526418b68173e6cb373ac7d1a83e43b24621a84eee9366b38290c742c9fc3151d50d60de6f013f9f80e2e69b5819216dcebb42dbab0225616c
6
+ metadata.gz: cd104cf3d2664d37fde830088298544795148a782ff3ad37f9265acfbbac0d1e159d5aba27cbb8dfbac1679f25771fb0a26e902835f3b3417c465defa117d3ea
7
+ data.tar.gz: 6891feee50ec8cb81c7d6f99857ad555d8ad283a1d3729efdf4ed8f92204a3699beae4ef457e06d6e905d7890f5cca6464beb4f92c552da0853682ba3e671da5
data/lib/latinum.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
data/lib/latinum/bank.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -21,19 +23,31 @@
21
23
  require 'latinum/resource'
22
24
 
23
25
  module Latinum
26
+ # A basic exchange rate for a named resource.
24
27
  class ExchangeRate
28
+ # @parameter input [String] The name of the input resource.
29
+ # @parameter output [String] The name of the output resource.
30
+ # @parameter factor [Numeric] The rate of exchange.
25
31
  def initialize(input, output, factor)
26
32
  @input = input
27
33
  @output = output
28
34
  @factor = factor.to_d
29
35
  end
30
36
 
37
+ # The name of the input resource.
38
+ # @attribute [String]
31
39
  attr :input
40
+
41
+ # The name of the output resource.
42
+ # @attribute [String]
32
43
  attr :output
44
+
45
+ # The rate of exchange.
46
+ # @attribute [String]
33
47
  attr :factor
34
48
  end
35
49
 
36
- # A bank defines exchange rates and formatting rules for resources. It is a centralised location for resource related state.
50
+ # A bank defines exchange rates and formatting rules for resources. It is a centralised location for resource formatting and metadata.
37
51
  class Bank
38
52
  # Imports all given currencies.
39
53
  def initialize(*imports)
@@ -60,7 +74,7 @@ module Latinum
60
74
  @currencies[name] = config
61
75
 
62
76
  # Create a formatter:
63
- @formatters[name] = config[:formatter].new(config)
77
+ @formatters[name] = config[:formatter].new(**config)
64
78
 
65
79
  if config[:symbol]
66
80
  symbols = (@symbols[config[:symbol]] ||= [])
@@ -71,16 +85,23 @@ module Latinum
71
85
  end
72
86
 
73
87
  # Look up a currency by name.
74
- def [] name
88
+ def [](name)
75
89
  @currencies[name]
76
90
  end
77
91
 
78
92
  attr :rates
93
+
94
+ # A map of all recognised symbols ordered by priority.
95
+ # @attribute [Hash(String, Tuple(Integer, Name))]
79
96
  attr :symbols
97
+
98
+ # The supported currents and assocaited formatting details.
99
+ # @attribute [Hash(String, Hash)]
80
100
  attr :currencies
81
101
 
82
102
  # Add an exchange rate to the bank.
83
- def << rate
103
+ # @parameter rate [ExchangeRate] The exchange rate to add.
104
+ def <<(rate)
84
105
  @rates << rate
85
106
 
86
107
  @exchange[rate.input] ||= {}
@@ -89,12 +110,13 @@ module Latinum
89
110
 
90
111
  # Exchange one resource for another using internally specified rates.
91
112
  def exchange(resource, for_name)
92
- rate = @exchange[resource.name][for_name] rescue nil
93
- raise ArgumentError.new("Rate #{rate} unavailable") if rate == nil
113
+ unless rate = @exchange.dig(resource.name, for_name)
114
+ raise ArgumentError.new("Rate #{rate} unavailable")
115
+ end
94
116
 
95
117
  config = self[for_name]
96
118
 
97
- resource.exchange(rate.factor, for_name, config[:precision])
119
+ return resource.exchange(rate.factor, for_name, config[:precision])
98
120
  end
99
121
 
100
122
  # Parse a string according to the loaded currencies.
@@ -102,35 +124,45 @@ module Latinum
102
124
  parts = string.strip.split(/\s+/, 2)
103
125
 
104
126
  if parts.size == 2
105
- Resource.new(parts[0].gsub(/[^\.0-9]/, ''), parts[1])
127
+ return Resource.new(parts[0].gsub(/[^\-\.0-9]/, ''), parts[1])
106
128
  else
107
129
  # Lookup the named symbol, e.g. '$', and get the highest priority name:
108
- symbol = @symbols.fetch(string.gsub(/[\-\.,0-9]/, ''), []).last || default_name
130
+ symbol = @symbols.fetch(string.gsub(/[\-\.,0-9]/, ''), []).last
109
131
 
110
132
  if symbol
111
- Resource.new(string.gsub(/[^\.0-9]/, ''), symbol.last.to_s)
133
+ return Resource.new(string.gsub(/[^\-\.0-9]/, ''), symbol.last.to_s)
134
+ elsif default_name
135
+ return Resource.new(string.gsub(/[^\-\.0-9]/, ''), default_name.to_s)
112
136
  else
113
137
  raise ArgumentError.new("Could not parse #{string}, could not determine currency!")
114
138
  end
115
139
  end
116
140
  end
117
141
 
142
+ # Rounds the specified resource to the maximum precision as specified by the formatter. Whe computing things like tax, you often get fractional amounts which are unpayable because they are smaller than the minimum discrete unit of the currency. This method helps to round a currency to a payable amount.
143
+ # @parameter resource [Resource] The resource to round.
144
+ # @returns [Resource] A copy of the resource with the amount rounded as per the formatter.
118
145
  def round(resource)
119
- formatter = @formatters[resource.name]
120
- raise ArgumentError.new("No formatter found for #{resource.name}") unless formatter
146
+ unless formatter = @formatters[resource.name]
147
+ raise ArgumentError.new("No formatter found for #{resource.name}")
148
+ end
121
149
 
122
- Latinum::Resource.new(formatter.round(resource.amount), resource.name)
150
+ return Resource.new(formatter.round(resource.amount), resource.name)
123
151
  end
124
152
 
125
153
  # Format a resource as a string according to the loaded currencies.
126
- def format(resource, *args)
127
- formatter = @formatters[resource.name]
128
- raise ArgumentError.new("No formatter found for #{resource.name}") unless formatter
154
+ # @parameter resource [Resource] The resource to format.
155
+ def format(resource, *arguments, **options)
156
+ unless formatter = @formatters[resource.name]
157
+ raise ArgumentError.new("No formatter found for #{resource.name}")
158
+ end
129
159
 
130
- formatter.format(resource.amount, *args)
160
+ formatter.format(resource.amount, *arguments, **options)
131
161
  end
132
162
 
133
163
  # Convert the resource to an integral representation based on the currency's precision.
164
+ # @parameter resource [Resource] The resource to convert.
165
+ # @returns [Integer] The integer representation.
134
166
  def to_integral(resource)
135
167
  formatter = @formatters[resource.name]
136
168
 
@@ -138,10 +170,13 @@ module Latinum
138
170
  end
139
171
 
140
172
  # Convert the resource from an integral representation based on the currency's precision.
141
- def from_integral(amount, resource_name)
142
- formatter = @formatters[resource_name]
173
+ # @parameter amount [Integer] The integral resource amount.
174
+ # @parameter name [String] The resource name.
175
+ # @returns [Resource] The converted resource.
176
+ def from_integral(amount, name)
177
+ formatter = @formatters[name]
143
178
 
144
- Resource.new(formatter.from_integral(amount), resource_name)
179
+ Resource.new(formatter.from_integral(amount), name)
145
180
  end
146
181
  end
147
182
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,29 +24,34 @@ require 'bigdecimal'
22
24
  require 'set'
23
25
 
24
26
  module Latinum
25
- # Aggregates a set of resources, typically used for summing values.
27
+ # Aggregates a set of resources, typically used for summing values to compute a total.
26
28
  class Collection
27
29
  include Enumerable
28
30
 
29
31
  # Initialize the collection with a given set of resource names.
30
32
  def initialize(names = Set.new)
31
33
  @names = names
32
- @resources = Hash.new {|hash, key| @names << key; BigDecimal.new("0")}
34
+ @resources = Hash.new {|hash, key| @names << key; BigDecimal(0)}
33
35
  end
34
36
 
35
- # All resource names which have been added to the collection, e.g. `['NZD', 'USD']`.
37
+ # All resource names which have been added to the collection.
38
+ # e.g. `['NZD', 'USD']`.
39
+ # @attribute [Set]
36
40
  attr :names
37
41
 
38
- # A map of `name` => `total` for all added resources. Totals are stored as BigDecimal instances.
42
+ # Keeps track of all added resources.
43
+ # @attribute [Hash(String, BigDecimal)]
39
44
  attr :resources
40
45
 
41
46
  # Add a resource into the totals.
42
- def add resource
47
+ # @parameter resource [Resource] The resource to add.
48
+ def add(resource)
43
49
  @resources[resource.name] += resource.amount
44
50
  end
45
51
 
46
52
  # Add a resource, an array of resources, or another collection into this one.
47
- def << object
53
+ # @parameter object [Resource | Array(Resource) | Collection] The resource(s) to add.
54
+ def <<(object)
48
55
  case object
49
56
  when Resource
50
57
  add(object)
@@ -65,7 +72,8 @@ module Latinum
65
72
  self << -other
66
73
  end
67
74
 
68
- # Allow negation of all values within the collection:
75
+ # Allow negation of all values within the collection.
76
+ # @returns [Collection] A new collection with the inverted values.
69
77
  def -@
70
78
  collection = self.class.new
71
79
 
@@ -76,16 +84,23 @@ module Latinum
76
84
  return collection
77
85
  end
78
86
 
79
- # Get a `Resource` for the given name:
87
+ # @returns [Resource | Nil] A resource for the specified name.
80
88
  def [] key
81
- Resource.new(@resources[key], key)
89
+ if amount = @resources[key]
90
+ Resource.new(@resources[key], key)
91
+ end
82
92
  end
83
93
 
84
- # Set a `BigDecimal` value for the given name:
94
+ # Set the amount for the specified resource name.
95
+ # @parameter key [String] The resource name.
96
+ # @parameter value [BigDecimal] The resource amount.
85
97
  def []= key, amount
86
98
  @resources[key] = amount
87
99
  end
88
100
 
101
+ # Iterates over all the resources.
102
+ # @yields {|resource| ...} The resources if a block is given.
103
+ # @parameter resource [Resource]
89
104
  def each
90
105
  return to_enum(:each) unless block_given?
91
106
 
@@ -94,7 +109,20 @@ module Latinum
94
109
  end
95
110
  end
96
111
 
112
+ # Whether the collection is empty.
113
+ # @returns [Boolean]
114
+ def empty?
115
+ @resources.empty?
116
+ end
117
+
118
+ # Whether the collection contains the specified resource (may be zero).
119
+ # @returns [Boolean]
120
+ def include?(key)
121
+ @resources.include?(key)
122
+ end
123
+
97
124
  # Generate a new collection but ignore zero values.
125
+ # @returns [Collection] A new collection.
98
126
  def compact
99
127
  collection = self.class.new
100
128
 
@@ -106,5 +134,12 @@ module Latinum
106
134
 
107
135
  return collection
108
136
  end
137
+
138
+ # A human readable representation of the collection.
139
+ # e.g. `"5.0 NZD; 10.0 USD"`
140
+ # @returns [String]
141
+ def to_s
142
+ @resources.map{|name, amount| "#{amount.to_s('F')} #{name}"}.join("; ")
143
+ end
109
144
  end
110
145
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # encoding: UTF-8
2
4
  #
3
5
  # Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
@@ -26,6 +28,8 @@ module Latinum
26
28
  module Currencies
27
29
  Global = {}
28
30
 
31
+ # @name Global[:NZD]
32
+ # @attribute [Hash] The New Zealand Dollar configuration.
29
33
  Global[:NZD] = {
30
34
  :precision => 2,
31
35
  :symbol => '$',
@@ -34,6 +38,8 @@ module Latinum
34
38
  :formatter => Formatters::DecimalCurrencyFormatter,
35
39
  }
36
40
 
41
+ # @name Global[:GBP]
42
+ # @attribute [Hash] The Great British Pound configuration.
37
43
  Global[:GBP] = {
38
44
  :precision => 2,
39
45
  :symbol => '£',
@@ -42,6 +48,8 @@ module Latinum
42
48
  :formatter => Formatters::DecimalCurrencyFormatter,
43
49
  }
44
50
 
51
+ # @name Global[:AUD]
52
+ # @attribute [Hash] The Australian Dollar configuration.
45
53
  Global[:AUD] = {
46
54
  :precision => 2,
47
55
  :symbol => '$',
@@ -50,6 +58,8 @@ module Latinum
50
58
  :formatter => Formatters::DecimalCurrencyFormatter,
51
59
  }
52
60
 
61
+ # @name Global[:USD]
62
+ # @attribute [Hash] The United States Dollar configuration.
53
63
  Global[:USD] = {
54
64
  :precision => 2,
55
65
  :symbol => '$',
@@ -58,6 +68,8 @@ module Latinum
58
68
  :formatter => Formatters::DecimalCurrencyFormatter,
59
69
  }
60
70
 
71
+ # @name Global[:EUR]
72
+ # @attribute [Hash] The Euro configuration.
61
73
  Global[:EUR] = {
62
74
  :precision => 2,
63
75
  :symbol => '€',
@@ -68,6 +80,8 @@ module Latinum
68
80
  #:separator => ','
69
81
  }
70
82
 
83
+ # @name Global[:JPY]
84
+ # @attribute [Hash] The Japanese Yen configuration.
71
85
  Global[:JPY] = {
72
86
  :precision => 0,
73
87
  :symbol => '¥',
@@ -76,6 +90,8 @@ module Latinum
76
90
  :formatter => Formatters::DecimalCurrencyFormatter
77
91
  }
78
92
 
93
+ # @name Global[:BTC]
94
+ # @attribute [Hash] The Bitcoin configuration.
79
95
  Global[:BTC] = {
80
96
  :precision => 8,
81
97
  :symbol => 'B⃦',
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -18,23 +20,11 @@
18
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
21
  # THE SOFTWARE.
20
22
 
21
- require 'latinum/resource'
22
-
23
- RSpec.describe Latinum::Resource do
24
- it "should be comparable to numeric values" do
25
- resource = Latinum::Resource.load("10 NZD")
26
-
27
- expect(resource).to be < 20
28
- expect(resource).to be > 5
29
- expect(resource).to be == 10
30
- end
31
-
32
- it "should compare with nil" do
33
- a = Latinum::Resource.load("10 NZD")
34
-
35
- expect{a <=> nil}.to_not raise_exception
36
- expect{a == nil}.to_not raise_exception
37
- expect(a <=> nil).to be == nil
38
- expect(a == nil).to be == false
23
+ module Latinum
24
+ # Represents an error when trying to perform arithmetic on differently named resources.
25
+ class DifferentResourceNameError < ArgumentError
26
+ def initialize
27
+ super "Cannot operate on different currencies!"
28
+ end
39
29
  end
40
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -20,77 +22,93 @@
20
22
 
21
23
  module Latinum
22
24
  module Formatters
23
- DEFAULT_OPTIONS = {
24
- :format => :full
25
- }
26
-
25
+ # Formats a currency using a standard decimal notation.
27
26
  class PlainFormatter
28
27
  def initialize(name:)
29
28
  @name = name
30
29
  end
31
30
 
31
+ # Formats the amount using a general notation.
32
+ # e.g. "5.0 NZD".
33
+ # @returns [String] The formatted string.
32
34
  def format(amount)
33
35
  "#{amount.to_s('F')} #{@name}"
34
36
  end
35
37
 
38
+ # Converts the amount directly to an integer, truncating any decimal part.
39
+ # @parameter amount [BigDecimal] The amount to convert to an integral.
40
+ # @returns [Integer] The converted whole number integer.
36
41
  def to_integral(amount)
37
42
  amount.to_i
38
43
  end
39
44
 
45
+ # Converts the amount to a decimal.
46
+ # @parameter amount [Integer] The amount to convert to a decimal.
47
+ # @returns [BigDecimal] The converted amount.
40
48
  def from_integral(amount)
41
49
  amount.to_d
42
50
  end
43
51
  end
44
-
52
+
53
+ # Formats a currency using a standard decimal notation.
45
54
  class DecimalCurrencyFormatter
46
- def initialize(options = {})
55
+ def initialize(**options)
47
56
  @symbol = options[:symbol] || '$'
48
57
  @separator = options[:separator] || '.'
49
- @delimeter = options[:delimter] || ','
58
+ @delimeter = options[:delimeter] || ','
50
59
  @places = options[:precision] || 2
51
60
  @zero = options[:zero] || '0'
52
-
61
+
53
62
  @name = options[:name]
54
63
  end
55
-
64
+
56
65
  def round(amount)
57
66
  return amount.round(@places)
58
67
  end
59
-
60
- def format(amount, options = DEFAULT_OPTIONS)
68
+
69
+ # Formats the amount using the configured symbol, separator, delimeter, and places.
70
+ # e.g. "$5,000.00 NZD". Rounds the amount to the specified number of decimal places.
71
+ # @returns [String] The formatted string.
72
+ def format(amount, places: @places, **options)
61
73
  # Round to the desired number of places. Truncation used to be the default.
62
- amount = amount.round(@places)
74
+ amount = amount.round(places).to_d
75
+
76
+ integral, fraction = amount.abs.to_s('F').split(/\./, 2)
63
77
 
64
- fix, frac = amount.abs.to_s('F').split(/\./, 2)
65
-
66
78
  # The sign of the number
67
79
  sign = '-' if amount < 0
68
-
80
+
69
81
  # Decimal places, e.g. the '.00' in '$10.00'
70
- frac = frac[0...@places].ljust(@places, @zero)
71
-
82
+ fraction = fraction[0...places].ljust(places, @zero)
83
+
72
84
  # Grouping, e.g. the ',' in '$10,000.00'
73
- remainder = fix.size % 3
74
- groups = fix[remainder..-1].scan(/.{3}/).to_a
75
- groups.unshift(fix[0...remainder]) if remainder > 0
76
-
85
+ remainder = integral.size % 3
86
+ groups = integral[remainder..-1].scan(/.{3}/).to_a
87
+ groups.unshift(integral[0...remainder]) if remainder > 0
88
+
77
89
  symbol = options.fetch(:symbol, @symbol)
78
90
  value = "#{sign}#{symbol}#{groups.join(@delimeter)}"
79
-
91
+
80
92
  name = options.fetch(:name, @name)
81
93
  suffix = name ? " #{name}" : ''
82
-
83
- if @places > 0
84
- "#{value}#{@separator}#{frac}#{suffix}"
94
+
95
+ if places > 0
96
+ "#{value}#{@separator}#{fraction}#{suffix}"
85
97
  else
86
98
  "#{value}#{suffix}"
87
99
  end
88
100
  end
89
101
 
102
+ # Converts the amount directly to an integer, truncating any decimal part, taking into account the number of specified decimal places.
103
+ # @parameter amount [BigDecimal] The amount to convert to an integral.
104
+ # @returns [Integer] The converted whole number integer.
90
105
  def to_integral(amount)
91
106
  (amount * 10**@places).to_i
92
107
  end
93
108
 
109
+ # Converts the amount to a decimal, taking into account the number of specified decimal places.
110
+ # @parameter amount [Integer] The amount to convert to a decimal.
111
+ # @returns [BigDecimal] The converted amount.
94
112
  def from_integral(amount)
95
113
  (amount.to_d / 10**@places)
96
114
  end