latinum 1.4.2 → 1.7.1

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: 3571f7728673389092f0ad54528e038ed15f7b9a5943b909ba355c3e9c96ad54
4
- data.tar.gz: 4ab949df4e4666b2ed782a82efde0386186655ffe370ffbe028c8fc79d9f8dee
3
+ metadata.gz: 1d47aa25865b0ac4f8a71378e34f3ab3d2747cbcc53028aa39cf5db59ee7c283
4
+ data.tar.gz: 73244a39809482164e21800ac18a18831cd57c625e9a98d4036fc9a13e02ec71
5
5
  SHA512:
6
- metadata.gz: de2de3d69cadf33628b97c74d57a524f0148805804b351b7164080d0fa43b900cc0e385de33f92ca899275b240e93beb2c0517a51d194bd0115f2cdebd48ad8b
7
- data.tar.gz: ac41080a9e0f74116020e69850d57614f713934dd318f6664fec1b111a6cd3fd36103608e89f059bcbe80695fe1b10df55ea900b6aa1c3a35ab785890747f786
6
+ metadata.gz: af1494045eaecbf9ca00635bd7f9e02355554116d4227dc4fc8ef61326ab0be480b8891624352ad5f89639db482a0d7ed101e38ec47276523f93ab66d30059e1
7
+ data.tar.gz: f03166e50246ff382aa4423c3e1438d5fbfefb66f5da4c6cd043a40e7f64282e37eb1c27e424c0f290f9aa30f790746bf0d9813eec3b21818e5be65c3350f1e9
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,55 @@ 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 parse_named_resource(parts[1], parts[0])
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
+ name = symbol.last.to_s
134
+ elsif default_name
135
+ name = default_name
112
136
  else
113
- raise ArgumentError.new("Could not parse #{string}, could not determine currency!")
137
+ raise ArgumentError, "Could not parse #{string}, could not determine resource name!"
114
138
  end
139
+
140
+ return parse_named_resource(name, string)
115
141
  end
116
142
  end
117
143
 
144
+ private def parse_named_resource(name, value)
145
+ if formatter = @formatters[name]
146
+ return Resource.new(formatter.parse(value), name)
147
+ else
148
+ raise ArgumentError, "No formatter found for #{name}!"
149
+ end
150
+ end
151
+
152
+ # 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.
153
+ # @parameter resource [Resource] The resource to round.
154
+ # @returns [Resource] A copy of the resource with the amount rounded as per the formatter.
118
155
  def round(resource)
119
- formatter = @formatters[resource.name]
120
- raise ArgumentError.new("No formatter found for #{resource.name}") unless formatter
156
+ unless formatter = @formatters[resource.name]
157
+ raise ArgumentError.new("No formatter found for #{resource.name}")
158
+ end
121
159
 
122
- Latinum::Resource.new(formatter.round(resource.amount), resource.name)
160
+ return Resource.new(formatter.round(resource.amount), resource.name)
123
161
  end
124
162
 
125
163
  # 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
164
+ # @parameter resource [Resource] The resource to format.
165
+ def format(resource, *arguments, **options)
166
+ unless formatter = @formatters[resource.name]
167
+ raise ArgumentError.new("No formatter found for #{resource.name}")
168
+ end
129
169
 
130
- formatter.format(resource.amount, *args)
170
+ formatter.format(resource.amount, *arguments, **options)
131
171
  end
132
172
 
133
173
  # Convert the resource to an integral representation based on the currency's precision.
174
+ # @parameter resource [Resource] The resource to convert.
175
+ # @returns [Integer] The integer representation.
134
176
  def to_integral(resource)
135
177
  formatter = @formatters[resource.name]
136
178
 
@@ -138,10 +180,13 @@ module Latinum
138
180
  end
139
181
 
140
182
  # 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]
183
+ # @parameter amount [Integer] The integral resource amount.
184
+ # @parameter name [String] The resource name.
185
+ # @returns [Resource] The converted resource.
186
+ def from_integral(amount, name)
187
+ formatter = @formatters[name]
143
188
 
144
- Resource.new(formatter.from_integral(amount), resource_name)
189
+ Resource.new(formatter.from_integral(amount), name)
145
190
  end
146
191
  end
147
192
  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("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,15 +109,20 @@ module Latinum
94
109
  end
95
110
  end
96
111
 
112
+ # Whether the collection is empty.
113
+ # @returns [Boolean]
97
114
  def empty?
98
115
  @resources.empty?
99
116
  end
100
117
 
118
+ # Whether the collection contains the specified resource (may be zero).
119
+ # @returns [Boolean]
101
120
  def include?(key)
102
121
  @resources.include?(key)
103
122
  end
104
123
 
105
124
  # Generate a new collection but ignore zero values.
125
+ # @returns [Collection] A new collection.
106
126
  def compact
107
127
  collection = self.class.new
108
128
 
@@ -115,6 +135,9 @@ module Latinum
115
135
  return collection
116
136
  end
117
137
 
138
+ # A human readable representation of the collection.
139
+ # e.g. `"5.0 NZD; 10.0 USD"`
140
+ # @returns [String]
118
141
  def to_s
119
142
  @resources.map{|name, amount| "#{amount.to_s('F')} #{name}"}.join("; ")
120
143
  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,16 +68,20 @@ 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 => '€',
64
76
  :name => 'EUR',
65
77
  :description => 'Euro',
66
78
  :formatter => Formatters::DecimalCurrencyFormatter,
67
- #:delimeter => '.',
79
+ #:delimiter => '.',
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,20 @@ module Latinum
76
90
  :formatter => Formatters::DecimalCurrencyFormatter
77
91
  }
78
92
 
93
+ # @name Global[:BRL]
94
+ # @attribute [Hash] The Brazilian Real configuration.
95
+ Global[:BRL] = {
96
+ :precision => 2,
97
+ :symbol => 'R$',
98
+ :name => 'BRL',
99
+ :description => 'Brazilian Real',
100
+ :formatter => Formatters::DecimalCurrencyFormatter,
101
+ :delimiter => '.',
102
+ :separator => ','
103
+ }
104
+
105
+ # @name Global[:BTC]
106
+ # @attribute [Hash] The Bitcoin configuration.
79
107
  Global[:BTC] = {
80
108
  :precision => 8,
81
109
  :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,107 @@
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
+ # Parse a string into an amount.
32
+ # @returns [BigDecimal] The parsed amount.
33
+ def parse(string)
34
+ BigDecimal(string)
35
+ end
36
+
37
+ # Formats the amount using a general notation.
38
+ # e.g. "5.0 NZD".
39
+ # @returns [String] The formatted string.
32
40
  def format(amount)
33
41
  "#{amount.to_s('F')} #{@name}"
34
42
  end
35
43
 
44
+ # Converts the amount directly to an integer, truncating any decimal part.
45
+ # @parameter amount [BigDecimal] The amount to convert to an integral.
46
+ # @returns [Integer] The converted whole number integer.
36
47
  def to_integral(amount)
37
48
  amount.to_i
38
49
  end
39
50
 
51
+ # Converts the amount to a decimal.
52
+ # @parameter amount [Integer] The amount to convert to a decimal.
53
+ # @returns [BigDecimal] The converted amount.
40
54
  def from_integral(amount)
41
55
  amount.to_d
42
56
  end
43
57
  end
44
-
58
+
59
+ # Formats a currency using a standard decimal notation.
45
60
  class DecimalCurrencyFormatter
46
- def initialize(options = {})
61
+ def initialize(**options)
47
62
  @symbol = options[:symbol] || '$'
48
63
  @separator = options[:separator] || '.'
49
- @delimeter = options[:delimter] || ','
64
+ @delimiter = options[:delimiter] || ','
50
65
  @places = options[:precision] || 2
51
66
  @zero = options[:zero] || '0'
52
-
67
+
53
68
  @name = options[:name]
54
69
  end
55
-
70
+
56
71
  def round(amount)
57
72
  return amount.round(@places)
58
73
  end
59
-
60
- def format(amount, options = DEFAULT_OPTIONS)
74
+
75
+ # Parse a string into an amount using the configured separator and delimiter.
76
+ # @returns [BigDecimal] The parsed amount.
77
+ def parse(string)
78
+ BigDecimal(
79
+ string.gsub(/[^\-0-9#{@separator}]/, '').gsub(@separator, '.')
80
+ )
81
+ end
82
+
83
+ # Formats the amount using the configured symbol, separator, delimiter, and places.
84
+ # e.g. "$5,000.00 NZD". Rounds the amount to the specified number of decimal places.
85
+ # @returns [String] The formatted string.
86
+ def format(amount, places: @places, **options)
61
87
  # Round to the desired number of places. Truncation used to be the default.
62
- amount = amount.round(@places)
88
+ amount = amount.round(places).to_d
89
+
90
+ integral, fraction = amount.abs.to_s('F').split(/\./, 2)
63
91
 
64
- fix, frac = amount.abs.to_s('F').split(/\./, 2)
65
-
66
92
  # The sign of the number
67
93
  sign = '-' if amount < 0
68
-
94
+
69
95
  # Decimal places, e.g. the '.00' in '$10.00'
70
- frac = frac[0...@places].ljust(@places, @zero)
71
-
96
+ fraction = fraction[0...places].ljust(places, @zero)
97
+
72
98
  # 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
-
99
+ remainder = integral.size % 3
100
+ groups = integral[remainder..-1].scan(/.{3}/).to_a
101
+ groups.unshift(integral[0...remainder]) if remainder > 0
102
+
77
103
  symbol = options.fetch(:symbol, @symbol)
78
- value = "#{sign}#{symbol}#{groups.join(@delimeter)}"
79
-
104
+ value = "#{sign}#{symbol}#{groups.join(@delimiter)}"
105
+
80
106
  name = options.fetch(:name, @name)
81
107
  suffix = name ? " #{name}" : ''
82
-
83
- if @places > 0
84
- "#{value}#{@separator}#{frac}#{suffix}"
108
+
109
+ if places > 0
110
+ "#{value}#{@separator}#{fraction}#{suffix}"
85
111
  else
86
112
  "#{value}#{suffix}"
87
113
  end
88
114
  end
89
115
 
116
+ # Converts the amount directly to an integer, truncating any decimal part, taking into account the number of specified decimal places.
117
+ # @parameter amount [BigDecimal] The amount to convert to an integral.
118
+ # @returns [Integer] The converted whole number integer.
90
119
  def to_integral(amount)
91
120
  (amount * 10**@places).to_i
92
121
  end
93
122
 
123
+ # Converts the amount to a decimal, taking into account the number of specified decimal places.
124
+ # @parameter amount [Integer] The amount to convert to a decimal.
125
+ # @returns [BigDecimal] The converted amount.
94
126
  def from_integral(amount)
95
127
  (amount.to_d / 10**@places)
96
128
  end