latinum 1.5.0 → 1.6.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
2
  SHA256:
3
- metadata.gz: 8884a3ef10ea2656d2c0016be0a46808783d66f6b6245cfadf3df03e5352e452
4
- data.tar.gz: 6ee07cd171635e5cc879b2dada4d52d2a0e31911090c125feb239093bc8850c2
3
+ metadata.gz: dc3529356aeebbfb020336961b359bd2a78f3473504bd8d5e81e5c19fb709f5a
4
+ data.tar.gz: bc57d00b4a80bf91a059709a973941a017e3c63324936942956ae5962c499640
5
5
  SHA512:
6
- metadata.gz: c38564220688bd0bb5e920bf3159bfedfef3d044e5d0073d0cbb294617c3fdb5f676f2da315a5e17363e10dc406775f156614bc18a3cbebaaa58ce899fe7b880
7
- data.tar.gz: cd68db69fc29c633611f3b96c7af99b29fcf1283db4ad6a861712f713863de4d2339dec789b3e1551e51b069282fea3ea4ca8a6178918bddab7d1a9b953739ca
6
+ metadata.gz: 134e6a9444aac017ce38d3239fbec37d11f7407324b386aa6ac3d56b06ccc9f4af2b9b7df6295382d4f33c3c7d171fdc4351da748d9ce2b8cd7993d2b01b77b2
7
+ data.tar.gz: 8b4b674b1ba7a8d3170c190674eecb7efb741a2814f3d0a55751794baa2f974cf098c2798b85560c1b75b56f0423005b0e7a5e2aa2a8a3d2d057cd4c0adab463
@@ -23,19 +23,31 @@
23
23
  require 'latinum/resource'
24
24
 
25
25
  module Latinum
26
+ # A basic exchange rate for a named resource.
26
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.
27
31
  def initialize(input, output, factor)
28
32
  @input = input
29
33
  @output = output
30
34
  @factor = factor.to_d
31
35
  end
32
36
 
37
+ # The name of the input resource.
38
+ # @attribute [String]
33
39
  attr :input
40
+
41
+ # The name of the output resource.
42
+ # @attribute [String]
34
43
  attr :output
44
+
45
+ # The rate of exchange.
46
+ # @attribute [String]
35
47
  attr :factor
36
48
  end
37
49
 
38
- # 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.
39
51
  class Bank
40
52
  # Imports all given currencies.
41
53
  def initialize(*imports)
@@ -62,7 +74,7 @@ module Latinum
62
74
  @currencies[name] = config
63
75
 
64
76
  # Create a formatter:
65
- @formatters[name] = config[:formatter].new(config)
77
+ @formatters[name] = config[:formatter].new(**config)
66
78
 
67
79
  if config[:symbol]
68
80
  symbols = (@symbols[config[:symbol]] ||= [])
@@ -73,16 +85,23 @@ module Latinum
73
85
  end
74
86
 
75
87
  # Look up a currency by name.
76
- def [] name
88
+ def [](name)
77
89
  @currencies[name]
78
90
  end
79
91
 
80
92
  attr :rates
93
+
94
+ # A map of all recognised symbols ordered by priority.
95
+ # @attribute [Hash(String, Tuple(Integer, Name))]
81
96
  attr :symbols
97
+
98
+ # The supported currents and assocaited formatting details.
99
+ # @attribute [Hash(String, Hash)]
82
100
  attr :currencies
83
101
 
84
102
  # Add an exchange rate to the bank.
85
- def << rate
103
+ # @parameter rate [ExchangeRate] The exchange rate to add.
104
+ def <<(rate)
86
105
  @rates << rate
87
106
 
88
107
  @exchange[rate.input] ||= {}
@@ -91,12 +110,13 @@ module Latinum
91
110
 
92
111
  # Exchange one resource for another using internally specified rates.
93
112
  def exchange(resource, for_name)
94
- rate = @exchange[resource.name][for_name] rescue nil
95
- 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
96
116
 
97
117
  config = self[for_name]
98
118
 
99
- resource.exchange(rate.factor, for_name, config[:precision])
119
+ return resource.exchange(rate.factor, for_name, config[:precision])
100
120
  end
101
121
 
102
122
  # Parse a string according to the loaded currencies.
@@ -104,37 +124,45 @@ module Latinum
104
124
  parts = string.strip.split(/\s+/, 2)
105
125
 
106
126
  if parts.size == 2
107
- Resource.new(parts[0].gsub(/[^\-\.0-9]/, ''), parts[1])
127
+ return Resource.new(parts[0].gsub(/[^\-\.0-9]/, ''), parts[1])
108
128
  else
109
129
  # Lookup the named symbol, e.g. '$', and get the highest priority name:
110
130
  symbol = @symbols.fetch(string.gsub(/[\-\.,0-9]/, ''), []).last
111
131
 
112
132
  if symbol
113
- Resource.new(string.gsub(/[^\-\.0-9]/, ''), symbol.last.to_s)
133
+ return Resource.new(string.gsub(/[^\-\.0-9]/, ''), symbol.last.to_s)
114
134
  elsif default_name
115
- Resource.new(string.gsub(/[^\-\.0-9]/, ''), default_name.to_s)
135
+ return Resource.new(string.gsub(/[^\-\.0-9]/, ''), default_name.to_s)
116
136
  else
117
137
  raise ArgumentError.new("Could not parse #{string}, could not determine currency!")
118
138
  end
119
139
  end
120
140
  end
121
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.
122
145
  def round(resource)
123
- formatter = @formatters[resource.name]
124
- 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
125
149
 
126
- Latinum::Resource.new(formatter.round(resource.amount), resource.name)
150
+ return Resource.new(formatter.round(resource.amount), resource.name)
127
151
  end
128
152
 
129
153
  # Format a resource as a string according to the loaded currencies.
130
- def format(resource, *args)
131
- formatter = @formatters[resource.name]
132
- 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
133
159
 
134
- formatter.format(resource.amount, *args)
160
+ formatter.format(resource.amount, *arguments, **options)
135
161
  end
136
162
 
137
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.
138
166
  def to_integral(resource)
139
167
  formatter = @formatters[resource.name]
140
168
 
@@ -142,10 +170,13 @@ module Latinum
142
170
  end
143
171
 
144
172
  # Convert the resource from an integral representation based on the currency's precision.
145
- def from_integral(amount, resource_name)
146
- 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]
147
178
 
148
- Resource.new(formatter.from_integral(amount), resource_name)
179
+ Resource.new(formatter.from_integral(amount), name)
149
180
  end
150
181
  end
151
182
  end
@@ -24,29 +24,34 @@ require 'bigdecimal'
24
24
  require 'set'
25
25
 
26
26
  module Latinum
27
- # 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.
28
28
  class Collection
29
29
  include Enumerable
30
30
 
31
31
  # Initialize the collection with a given set of resource names.
32
32
  def initialize(names = Set.new)
33
33
  @names = names
34
- @resources = Hash.new {|hash, key| @names << key; BigDecimal("0")}
34
+ @resources = Hash.new {|hash, key| @names << key; BigDecimal(0)}
35
35
  end
36
36
 
37
- # 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]
38
40
  attr :names
39
41
 
40
- # 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)]
41
44
  attr :resources
42
45
 
43
46
  # Add a resource into the totals.
44
- def add resource
47
+ # @parameter resource [Resource] The resource to add.
48
+ def add(resource)
45
49
  @resources[resource.name] += resource.amount
46
50
  end
47
51
 
48
52
  # Add a resource, an array of resources, or another collection into this one.
49
- def << object
53
+ # @parameter object [Resource | Array(Resource) | Collection] The resource(s) to add.
54
+ def <<(object)
50
55
  case object
51
56
  when Resource
52
57
  add(object)
@@ -67,7 +72,8 @@ module Latinum
67
72
  self << -other
68
73
  end
69
74
 
70
- # 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.
71
77
  def -@
72
78
  collection = self.class.new
73
79
 
@@ -78,16 +84,23 @@ module Latinum
78
84
  return collection
79
85
  end
80
86
 
81
- # Get a `Resource` for the given name:
87
+ # @returns [Resource | Nil] A resource for the specified name.
82
88
  def [] key
83
- Resource.new(@resources[key], key)
89
+ if amount = @resources[key]
90
+ Resource.new(@resources[key], key)
91
+ end
84
92
  end
85
93
 
86
- # 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.
87
97
  def []= key, amount
88
98
  @resources[key] = amount
89
99
  end
90
100
 
101
+ # Iterates over all the resources.
102
+ # @yields {|resource| ...} The resources if a block is given.
103
+ # @parameter resource [Resource]
91
104
  def each
92
105
  return to_enum(:each) unless block_given?
93
106
 
@@ -96,15 +109,20 @@ module Latinum
96
109
  end
97
110
  end
98
111
 
112
+ # Whether the collection is empty.
113
+ # @returns [Boolean]
99
114
  def empty?
100
115
  @resources.empty?
101
116
  end
102
117
 
118
+ # Whether the collection contains the specified resource (may be zero).
119
+ # @returns [Boolean]
103
120
  def include?(key)
104
121
  @resources.include?(key)
105
122
  end
106
123
 
107
124
  # Generate a new collection but ignore zero values.
125
+ # @returns [Collection] A new collection.
108
126
  def compact
109
127
  collection = self.class.new
110
128
 
@@ -117,6 +135,9 @@ module Latinum
117
135
  return collection
118
136
  end
119
137
 
138
+ # A human readable representation of the collection.
139
+ # e.g. `"5.0 NZD; 10.0 USD"`
140
+ # @returns [String]
120
141
  def to_s
121
142
  @resources.map{|name, amount| "#{amount.to_s('F')} #{name}"}.join("; ")
122
143
  end
@@ -28,6 +28,8 @@ module Latinum
28
28
  module Currencies
29
29
  Global = {}
30
30
 
31
+ # @name Global[:NZD]
32
+ # @attribute [Hash] The New Zealand Dollar configuration.
31
33
  Global[:NZD] = {
32
34
  :precision => 2,
33
35
  :symbol => '$',
@@ -36,6 +38,8 @@ module Latinum
36
38
  :formatter => Formatters::DecimalCurrencyFormatter,
37
39
  }
38
40
 
41
+ # @name Global[:GBP]
42
+ # @attribute [Hash] The Great British Pound configuration.
39
43
  Global[:GBP] = {
40
44
  :precision => 2,
41
45
  :symbol => '£',
@@ -44,6 +48,8 @@ module Latinum
44
48
  :formatter => Formatters::DecimalCurrencyFormatter,
45
49
  }
46
50
 
51
+ # @name Global[:AUD]
52
+ # @attribute [Hash] The Australian Dollar configuration.
47
53
  Global[:AUD] = {
48
54
  :precision => 2,
49
55
  :symbol => '$',
@@ -52,6 +58,8 @@ module Latinum
52
58
  :formatter => Formatters::DecimalCurrencyFormatter,
53
59
  }
54
60
 
61
+ # @name Global[:USD]
62
+ # @attribute [Hash] The United States Dollar configuration.
55
63
  Global[:USD] = {
56
64
  :precision => 2,
57
65
  :symbol => '$',
@@ -60,6 +68,8 @@ module Latinum
60
68
  :formatter => Formatters::DecimalCurrencyFormatter,
61
69
  }
62
70
 
71
+ # @name Global[:EUR]
72
+ # @attribute [Hash] The Euro configuration.
63
73
  Global[:EUR] = {
64
74
  :precision => 2,
65
75
  :symbol => '€',
@@ -70,6 +80,8 @@ module Latinum
70
80
  #:separator => ','
71
81
  }
72
82
 
83
+ # @name Global[:JPY]
84
+ # @attribute [Hash] The Japanese Yen configuration.
73
85
  Global[:JPY] = {
74
86
  :precision => 0,
75
87
  :symbol => '¥',
@@ -78,6 +90,8 @@ module Latinum
78
90
  :formatter => Formatters::DecimalCurrencyFormatter
79
91
  }
80
92
 
93
+ # @name Global[:BTC]
94
+ # @attribute [Hash] The Bitcoin configuration.
81
95
  Global[:BTC] = {
82
96
  :precision => 8,
83
97
  :symbol => 'B⃦',
@@ -20,23 +20,11 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
- require 'latinum/resource'
24
-
25
- RSpec.describe Latinum::Resource do
26
- it "should be comparable to numeric values" do
27
- resource = Latinum::Resource.load("10 NZD")
28
-
29
- expect(resource).to be < 20
30
- expect(resource).to be > 5
31
- expect(resource).to be == 10
32
- end
33
-
34
- it "should compare with nil" do
35
- a = Latinum::Resource.load("10 NZD")
36
-
37
- expect{a <=> nil}.to_not raise_exception
38
- expect{a == nil}.to_not raise_exception
39
- expect(a <=> nil).to be == nil
40
- 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
41
29
  end
42
30
  end
@@ -22,76 +22,93 @@
22
22
 
23
23
  module Latinum
24
24
  module Formatters
25
- DEFAULT_OPTIONS = {
26
- }
27
-
25
+ # Formats a currency using a standard decimal notation.
28
26
  class PlainFormatter
29
27
  def initialize(name:)
30
28
  @name = name
31
29
  end
32
30
 
31
+ # Formats the amount using a general notation.
32
+ # e.g. "5.0 NZD".
33
+ # @returns [String] The formatted string.
33
34
  def format(amount)
34
35
  "#{amount.to_s('F')} #{@name}"
35
36
  end
36
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.
37
41
  def to_integral(amount)
38
42
  amount.to_i
39
43
  end
40
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.
41
48
  def from_integral(amount)
42
49
  amount.to_d
43
50
  end
44
51
  end
45
-
52
+
53
+ # Formats a currency using a standard decimal notation.
46
54
  class DecimalCurrencyFormatter
47
- def initialize(options = {})
55
+ def initialize(**options)
48
56
  @symbol = options[:symbol] || '$'
49
57
  @separator = options[:separator] || '.'
50
58
  @delimeter = options[:delimter] || ','
51
59
  @places = options[:precision] || 2
52
60
  @zero = options[:zero] || '0'
53
-
61
+
54
62
  @name = options[:name]
55
63
  end
56
-
64
+
57
65
  def round(amount)
58
66
  return amount.round(@places)
59
67
  end
60
-
61
- 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, **options)
62
73
  # Round to the desired number of places. Truncation used to be the default.
63
74
  amount = amount.round(@places)
64
75
 
65
- fix, frac = amount.abs.to_s('F').split(/\./, 2)
66
-
76
+ integral, fraction = amount.abs.to_s('F').split(/\./, 2)
77
+
67
78
  # The sign of the number
68
79
  sign = '-' if amount < 0
69
-
80
+
70
81
  # Decimal places, e.g. the '.00' in '$10.00'
71
- frac = frac[0...@places].ljust(@places, @zero)
72
-
82
+ fraction = fraction[0...@places].ljust(@places, @zero)
83
+
73
84
  # Grouping, e.g. the ',' in '$10,000.00'
74
- remainder = fix.size % 3
75
- groups = fix[remainder..-1].scan(/.{3}/).to_a
76
- groups.unshift(fix[0...remainder]) if remainder > 0
77
-
85
+ remainder = integral.size % 3
86
+ groups = integral[remainder..-1].scan(/.{3}/).to_a
87
+ groups.unshift(integral[0...remainder]) if remainder > 0
88
+
78
89
  symbol = options.fetch(:symbol, @symbol)
79
90
  value = "#{sign}#{symbol}#{groups.join(@delimeter)}"
80
-
91
+
81
92
  name = options.fetch(:name, @name)
82
93
  suffix = name ? " #{name}" : ''
83
-
94
+
84
95
  if @places > 0
85
- "#{value}#{@separator}#{frac}#{suffix}"
96
+ "#{value}#{@separator}#{fraction}#{suffix}"
86
97
  else
87
98
  "#{value}#{suffix}"
88
99
  end
89
100
  end
90
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.
91
105
  def to_integral(amount)
92
106
  (amount * 10**@places).to_i
93
107
  end
94
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.
95
112
  def from_integral(amount)
96
113
  (amount.to_d / 10**@places)
97
114
  end