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 +4 -4
- data/lib/latinum/bank.rb +51 -20
- data/lib/latinum/collection.rb +31 -10
- data/lib/latinum/currencies/global.rb +14 -0
- data/{spec/latinum/comparison_spec.rb → lib/latinum/error.rb} +6 -18
- data/lib/latinum/formatters.rb +38 -21
- data/lib/latinum/resource.rb +66 -35
- data/lib/latinum/version.rb +1 -1
- metadata +24 -44
- data/.rspec +0 -3
- data/.travis.yml +0 -20
- data/Gemfile +0 -5
- data/README.md +0 -276
- data/Rakefile +0 -8
- data/latinum.gemspec +0 -22
- data/spec/latinum/bank_spec.rb +0 -92
- data/spec/latinum/collection_spec.rb +0 -142
- data/spec/latinum/formatters_spec.rb +0 -95
- data/spec/latinum/integrals_spec.rb +0 -47
- data/spec/latinum/resource_spec.rb +0 -132
- data/spec/spec_helper.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc3529356aeebbfb020336961b359bd2a78f3473504bd8d5e81e5c19fb709f5a
|
4
|
+
data.tar.gz: bc57d00b4a80bf91a059709a973941a017e3c63324936942956ae5962c499640
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 134e6a9444aac017ce38d3239fbec37d11f7407324b386aa6ac3d56b06ccc9f4af2b9b7df6295382d4f33c3c7d171fdc4351da748d9ce2b8cd7993d2b01b77b2
|
7
|
+
data.tar.gz: 8b4b674b1ba7a8d3170c190674eecb7efb741a2814f3d0a55751794baa2f974cf098c2798b85560c1b75b56f0423005b0e7a5e2aa2a8a3d2d057cd4c0adab463
|
data/lib/latinum/bank.rb
CHANGED
@@ -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
|
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 []
|
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
|
-
|
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
|
95
|
-
|
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
|
-
|
146
|
+
unless formatter = @formatters[resource.name]
|
147
|
+
raise ArgumentError.new("No formatter found for #{resource.name}")
|
148
|
+
end
|
125
149
|
|
126
|
-
|
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
|
-
|
131
|
-
|
132
|
-
|
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, *
|
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
|
-
|
146
|
-
|
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),
|
179
|
+
Resource.new(formatter.from_integral(amount), name)
|
149
180
|
end
|
150
181
|
end
|
151
182
|
end
|
data/lib/latinum/collection.rb
CHANGED
@@ -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(
|
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
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
#
|
87
|
+
# @returns [Resource | Nil] A resource for the specified name.
|
82
88
|
def [] key
|
83
|
-
|
89
|
+
if amount = @resources[key]
|
90
|
+
Resource.new(@resources[key], key)
|
91
|
+
end
|
84
92
|
end
|
85
93
|
|
86
|
-
# Set
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
data/lib/latinum/formatters.rb
CHANGED
@@ -22,76 +22,93 @@
|
|
22
22
|
|
23
23
|
module Latinum
|
24
24
|
module Formatters
|
25
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
82
|
+
fraction = fraction[0...@places].ljust(@places, @zero)
|
83
|
+
|
73
84
|
# Grouping, e.g. the ',' in '$10,000.00'
|
74
|
-
remainder =
|
75
|
-
groups =
|
76
|
-
groups.unshift(
|
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}#{
|
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
|