timeprice 0.1.2 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/README.md +98 -4
- data/lib/timeprice/cli/formatting.rb +34 -0
- data/lib/timeprice/cli/presenters/compare.rb +44 -0
- data/lib/timeprice/cli/presenters/exchange.rb +45 -0
- data/lib/timeprice/cli/presenters/inflation.rb +36 -0
- data/lib/timeprice/cli/presenters/sources.rb +65 -0
- data/lib/timeprice/cli.rb +83 -118
- data/lib/timeprice/compare.rb +30 -40
- data/lib/timeprice/cpi_lookup.rb +62 -0
- data/lib/timeprice/data_loader.rb +31 -5
- data/lib/timeprice/errors.rb +12 -5
- data/lib/timeprice/exchange.rb +15 -3
- data/lib/timeprice/inflation.rb +31 -55
- data/lib/timeprice/point.rb +62 -0
- data/lib/timeprice/sources/coverage.rb +71 -0
- data/lib/timeprice/sources.rb +3 -49
- data/lib/timeprice/supported.rb +62 -0
- data/lib/timeprice/version.rb +1 -1
- data/lib/timeprice.rb +40 -0
- metadata +10 -1
data/lib/timeprice/inflation.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "errors"
|
|
4
4
|
require_relative "data_loader"
|
|
5
|
+
require_relative "cpi_lookup"
|
|
5
6
|
|
|
6
7
|
module Timeprice
|
|
7
8
|
# Value object returned by Inflation.adjust.
|
|
@@ -14,85 +15,60 @@ module Timeprice
|
|
|
14
15
|
InflationResult = Data.define(
|
|
15
16
|
:amount, :original_amount, :from, :to, :country,
|
|
16
17
|
:from_index, :to_index, :granularity
|
|
17
|
-
)
|
|
18
|
+
) do
|
|
19
|
+
# The country's primary currency (e.g. "USD" for "US"). Falls back to the
|
|
20
|
+
# uppercased country code if the country isn't in the supported map —
|
|
21
|
+
# callers can still render *some* unit rather than crashing.
|
|
22
|
+
def country_currency_label
|
|
23
|
+
require_relative "supported"
|
|
24
|
+
Supported.currency_for_country(country) || country.to_s.upcase
|
|
25
|
+
end
|
|
26
|
+
end
|
|
18
27
|
|
|
28
|
+
# CPI-based inflation adjustment for the {Supported::COUNTRIES} list.
|
|
19
29
|
module Inflation
|
|
20
30
|
module_function
|
|
21
31
|
|
|
22
32
|
# Adjust `amount` from date `from` to date `to` using country CPI.
|
|
23
33
|
#
|
|
24
34
|
# Dates accept "YYYY" or "YYYY-MM".
|
|
35
|
+
#
|
|
36
|
+
# @param amount [Numeric]
|
|
37
|
+
# @param from [String] source date ("YYYY" or "YYYY-MM")
|
|
38
|
+
# @param to [String] target date ("YYYY" or "YYYY-MM")
|
|
39
|
+
# @param country [String] country code (see {Supported::COUNTRIES})
|
|
40
|
+
# @return [InflationResult]
|
|
41
|
+
# @raise [UnsupportedCountry] if `country` is not supported
|
|
42
|
+
# @raise [DataNotFound] if no CPI data covers the requested period
|
|
25
43
|
def adjust(amount:, from:, to:, country:)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
lookup = CpiLookup.new(DataLoader.load_cpi(country))
|
|
45
|
+
from_point = lookup.at(from)
|
|
46
|
+
to_point = lookup.at(to)
|
|
29
47
|
|
|
30
|
-
ratio =
|
|
48
|
+
ratio = to_point.value.to_f / from_point.value
|
|
31
49
|
InflationResult.new(
|
|
32
50
|
amount: amount.to_f * ratio,
|
|
33
51
|
original_amount: amount.to_f,
|
|
34
52
|
from: from,
|
|
35
53
|
to: to,
|
|
36
54
|
country: country.to_s.upcase,
|
|
37
|
-
from_index:
|
|
38
|
-
to_index:
|
|
39
|
-
granularity: merge_granularity(
|
|
55
|
+
from_index: from_point.value,
|
|
56
|
+
to_index: to_point.value,
|
|
57
|
+
granularity: merge_granularity(from_point.granularity, to_point.granularity)
|
|
40
58
|
)
|
|
41
59
|
end
|
|
42
60
|
|
|
43
61
|
# Inflation rate as decimal (e.g. 0.42 = 42%).
|
|
62
|
+
#
|
|
63
|
+
# @param from [String]
|
|
64
|
+
# @param to [String]
|
|
65
|
+
# @param country [String]
|
|
66
|
+
# @return [Float] decimal rate (positive means inflation, negative deflation)
|
|
44
67
|
def rate(from:, to:, country:)
|
|
45
68
|
result = adjust(amount: 1.0, from: from, to: to, country: country)
|
|
46
69
|
result.amount - 1.0
|
|
47
70
|
end
|
|
48
71
|
|
|
49
|
-
# Returns [index_value, granularity_symbol]
|
|
50
|
-
def lookup_index(data, key)
|
|
51
|
-
key = key.to_s
|
|
52
|
-
monthly = data["monthly"] || {}
|
|
53
|
-
annual = data["annual"] || {}
|
|
54
|
-
|
|
55
|
-
case key
|
|
56
|
-
when /\A\d{4}-\d{2}\z/
|
|
57
|
-
if monthly.key?(key)
|
|
58
|
-
[monthly[key], :monthly]
|
|
59
|
-
else
|
|
60
|
-
year = key[0, 4]
|
|
61
|
-
raise DataNotFound, missing_cpi_message(key, data, monthly, annual) unless annual.key?(year)
|
|
62
|
-
|
|
63
|
-
[annual[year], :annual]
|
|
64
|
-
|
|
65
|
-
end
|
|
66
|
-
when /\A\d{4}\z/
|
|
67
|
-
if annual.key?(key)
|
|
68
|
-
[annual[key], :annual]
|
|
69
|
-
else
|
|
70
|
-
months = monthly.select { |k, _| k.start_with?("#{key}-") }
|
|
71
|
-
raise DataNotFound, missing_cpi_message(key, data, monthly, annual) if months.empty?
|
|
72
|
-
|
|
73
|
-
avg = months.values.sum.to_f / months.size
|
|
74
|
-
[avg, :annual_from_monthly_avg]
|
|
75
|
-
end
|
|
76
|
-
else
|
|
77
|
-
raise ArgumentError, "Invalid date format: #{key.inspect} (use YYYY or YYYY-MM)"
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def missing_cpi_message(key, data, monthly, annual)
|
|
82
|
-
country = data["country"]
|
|
83
|
-
ranges = []
|
|
84
|
-
if monthly.any?
|
|
85
|
-
ks = monthly.keys.sort
|
|
86
|
-
ranges << "monthly #{ks.first}..#{ks.last}"
|
|
87
|
-
end
|
|
88
|
-
if annual.any?
|
|
89
|
-
ks = annual.keys.sort
|
|
90
|
-
ranges << "annual #{ks.first}..#{ks.last}"
|
|
91
|
-
end
|
|
92
|
-
hint = ranges.empty? ? "" : " (supported: #{ranges.join(", ")})"
|
|
93
|
-
"No CPI data for #{key.inspect} in #{country}#{hint}"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
72
|
# If either end fell back to annual_from_monthly_avg, propagate that label;
|
|
97
73
|
# else if either is annual, propagate :annual; else :monthly.
|
|
98
74
|
def merge_granularity(a, b)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Timeprice
|
|
4
|
+
# A (currency, date) pair used as input to {Timeprice.compare}.
|
|
5
|
+
#
|
|
6
|
+
# The library accepts either a Point or a 2-element array. Arrays may be
|
|
7
|
+
# ordered either way (`["USD", "2010"]` or `["2010", "USD"]`) — the year
|
|
8
|
+
# and currency are detected by shape. This mirrors what the CLI already
|
|
9
|
+
# tolerates and removes the only "which slot is which?" footgun.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Timeprice::Point.new(currency: "USD", date: "2010")
|
|
13
|
+
# Timeprice::Point.coerce(["USD", "2010"])
|
|
14
|
+
# Timeprice::Point.coerce(["2010", "USD"])
|
|
15
|
+
Point = Data.define(:currency, :date) do
|
|
16
|
+
# Coerce input into a Point. Accepts:
|
|
17
|
+
# - {Point} (returned as-is)
|
|
18
|
+
# - 2-element Array of [currency, date] in either order
|
|
19
|
+
#
|
|
20
|
+
# @param input [Point, Array]
|
|
21
|
+
# @return [Point]
|
|
22
|
+
# @raise [ArgumentError] if shape can't be recognised
|
|
23
|
+
def self.coerce(input)
|
|
24
|
+
case input
|
|
25
|
+
in Point
|
|
26
|
+
input
|
|
27
|
+
in [_, _]
|
|
28
|
+
a, b = input.map(&:to_s)
|
|
29
|
+
currency = [a, b].find { |s| s.match?(/\A[A-Za-z]{3}\z/) }
|
|
30
|
+
date = [a, b].find { |s| s.match?(/\A\d{4}(-\d{2}(-\d{2})?)?\z/) }
|
|
31
|
+
raise ArgumentError, malformed_pair_message(input) if currency.nil? || date.nil?
|
|
32
|
+
|
|
33
|
+
new(currency: currency.upcase, date: date)
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError, "Expected Timeprice::Point or [currency, date] tuple, got #{input.inspect}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.malformed_pair_message(input)
|
|
40
|
+
"Could not detect currency + date in #{input.inspect} " \
|
|
41
|
+
"(expected a 3-letter currency and a YYYY[-MM[-DD]] date)"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Resolve `date` to a full YYYY-MM-DD for FX lookup.
|
|
45
|
+
#
|
|
46
|
+
# Coarser grains anchor to a representative day:
|
|
47
|
+
# - "YYYY" → mid-year (YYYY-06-30)
|
|
48
|
+
# - "YYYY-MM" → mid-month (YYYY-MM-15)
|
|
49
|
+
# - "YYYY-MM-DD" → passes through
|
|
50
|
+
#
|
|
51
|
+
# @return [String]
|
|
52
|
+
# @raise [ArgumentError] if `date` doesn't match any supported shape
|
|
53
|
+
def fx_anchor_date
|
|
54
|
+
case date.to_s
|
|
55
|
+
when /\A\d{4}\z/ then "#{date}-06-30"
|
|
56
|
+
when /\A\d{4}-\d{2}\z/ then "#{date}-15"
|
|
57
|
+
when /\A\d{4}-\d{2}-\d{2}\z/ then date.to_s
|
|
58
|
+
else raise ArgumentError, "Invalid date for Point: #{date.inspect}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../data_loader"
|
|
5
|
+
|
|
6
|
+
module Timeprice
|
|
7
|
+
module Sources
|
|
8
|
+
# Computes coverage strings for bundled data sources at runtime. All
|
|
9
|
+
# filesystem reads happen here so the Sources attribution registry stays
|
|
10
|
+
# a pure data table.
|
|
11
|
+
module Coverage
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param src [Hash] one entry from Sources::ATTRIBUTIONS
|
|
15
|
+
# @return [String]
|
|
16
|
+
def for(src)
|
|
17
|
+
case src[:kind]
|
|
18
|
+
when "cpi" then cpi(src[:country])
|
|
19
|
+
when "fx" then fx(src[:id])
|
|
20
|
+
else "n/a"
|
|
21
|
+
end
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
"(coverage unavailable: #{e.message})"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cpi(country)
|
|
27
|
+
data = DataLoader.load_cpi(country)
|
|
28
|
+
monthly = (data["monthly"] || {}).keys.sort
|
|
29
|
+
annual = (data["annual"] || {}).keys.sort
|
|
30
|
+
parts = []
|
|
31
|
+
parts << "monthly #{monthly.first}..#{monthly.last} (#{monthly.size})" unless monthly.empty?
|
|
32
|
+
parts << "annual #{annual.first}..#{annual.last} (#{annual.size})" unless annual.empty?
|
|
33
|
+
parts.join(", ")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fx(id)
|
|
37
|
+
years = fx_years
|
|
38
|
+
return "no data" if years.empty?
|
|
39
|
+
|
|
40
|
+
id == "fx_vnd" ? vnd_summary(years) : ecb_summary(years)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fx_years
|
|
44
|
+
Dir[File.join(fx_root, "*.json")].map { |f| File.basename(f, ".json").to_i }.sort
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def vnd_summary(years)
|
|
48
|
+
with_vnd = years.select { |y| year_has_currency?(y, %w[VND]) }
|
|
49
|
+
return "no VND data" if with_vnd.empty?
|
|
50
|
+
|
|
51
|
+
"USD↔VND #{with_vnd.first}..#{with_vnd.last}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ecb_summary(years)
|
|
55
|
+
ecb_years = years.select { |y| year_has_currency?(y, %w[EUR GBP JPY]) }
|
|
56
|
+
return "no ECB data" if ecb_years.empty?
|
|
57
|
+
|
|
58
|
+
"USD↔EUR/GBP/JPY daily #{ecb_years.first}..#{ecb_years.last}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def year_has_currency?(year, codes)
|
|
62
|
+
rates = JSON.parse(File.read(File.join(fx_root, "#{year}.json")))["rates"]
|
|
63
|
+
rates.any? { |_, v| v.keys.intersect?(codes) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def fx_root
|
|
67
|
+
File.join(DataLoader.data_root, "fx", "usd")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/timeprice/sources.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "sources/coverage"
|
|
4
4
|
|
|
5
5
|
module Timeprice
|
|
6
6
|
# Enumerate bundled data sources with license/attribution and the actual
|
|
@@ -77,55 +77,9 @@ module Timeprice
|
|
|
77
77
|
|
|
78
78
|
# Returns an array of hashes with :id, :kind, :name, :license, :license_url,
|
|
79
79
|
# :attribution, :coverage (string like "1990-01 to 2026-03 (monthly+annual)").
|
|
80
|
+
# @return [Array<Hash>]
|
|
80
81
|
def list
|
|
81
|
-
ATTRIBUTIONS.map { |s| s.merge(coverage:
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def coverage_for(src)
|
|
85
|
-
case src[:kind]
|
|
86
|
-
when "cpi" then cpi_coverage(src[:country])
|
|
87
|
-
when "fx" then fx_coverage(src[:id])
|
|
88
|
-
else "n/a"
|
|
89
|
-
end
|
|
90
|
-
rescue StandardError => e
|
|
91
|
-
"(coverage unavailable: #{e.message})"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def cpi_coverage(country)
|
|
95
|
-
data = DataLoader.load_cpi(country)
|
|
96
|
-
monthly = (data["monthly"] || {}).keys.sort
|
|
97
|
-
annual = (data["annual"] || {}).keys.sort
|
|
98
|
-
parts = []
|
|
99
|
-
parts << "monthly #{monthly.first}..#{monthly.last} (#{monthly.size})" unless monthly.empty?
|
|
100
|
-
parts << "annual #{annual.first}..#{annual.last} (#{annual.size})" unless annual.empty?
|
|
101
|
-
parts.join(", ")
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def fx_coverage(id)
|
|
105
|
-
root = File.join(DataLoader.data_root, "fx", "usd")
|
|
106
|
-
years = Dir[File.join(root, "*.json")].map { |f| File.basename(f, ".json").to_i }.sort
|
|
107
|
-
return "no data" if years.empty?
|
|
108
|
-
|
|
109
|
-
case id
|
|
110
|
-
when "fx_vnd"
|
|
111
|
-
# VND broadcast-from-annual covers earlier years too.
|
|
112
|
-
with_vnd = years.select do |y|
|
|
113
|
-
d = JSON.parse(File.read(File.join(root, "#{y}.json")))
|
|
114
|
-
d["rates"].any? { |_, v| v.key?("VND") }
|
|
115
|
-
end
|
|
116
|
-
return "no VND data" if with_vnd.empty?
|
|
117
|
-
|
|
118
|
-
"USD↔VND #{with_vnd.first}..#{with_vnd.last}"
|
|
119
|
-
else
|
|
120
|
-
# ECB pairs (EUR/GBP/JPY) start 1999
|
|
121
|
-
ecb_years = years.select do |y|
|
|
122
|
-
d = JSON.parse(File.read(File.join(root, "#{y}.json")))
|
|
123
|
-
d["rates"].any? { |_, v| v.keys.intersect?(%w[EUR GBP JPY]) }
|
|
124
|
-
end
|
|
125
|
-
return "no ECB data" if ecb_years.empty?
|
|
126
|
-
|
|
127
|
-
"USD↔EUR/GBP/JPY daily #{ecb_years.first}..#{ecb_years.last}"
|
|
128
|
-
end
|
|
82
|
+
ATTRIBUTIONS.map { |s| s.merge(coverage: Coverage.for(s)) }
|
|
129
83
|
end
|
|
130
84
|
end
|
|
131
85
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Timeprice
|
|
4
|
+
# Canonical lists of supported country and currency codes, plus the
|
|
5
|
+
# bidirectional currency↔country map used by `Compare` and CLI output.
|
|
6
|
+
#
|
|
7
|
+
# Everything that needs to know "which currency pairs with which CPI series"
|
|
8
|
+
# must read it from here — duplicating the map elsewhere has bitten us before
|
|
9
|
+
# when a new country was added in one place and forgotten in the other.
|
|
10
|
+
module Supported
|
|
11
|
+
COUNTRIES = %w[US UK EU JP VN].freeze
|
|
12
|
+
CURRENCIES = %w[USD GBP EUR JPY VND].freeze
|
|
13
|
+
|
|
14
|
+
COUNTRY_TO_CURRENCY = {
|
|
15
|
+
"US" => "USD",
|
|
16
|
+
"UK" => "GBP",
|
|
17
|
+
"EU" => "EUR",
|
|
18
|
+
"JP" => "JPY",
|
|
19
|
+
"VN" => "VND",
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
CURRENCY_TO_COUNTRY = COUNTRY_TO_CURRENCY.invert.freeze
|
|
23
|
+
|
|
24
|
+
# Currencies with no minor unit — formatted as whole numbers.
|
|
25
|
+
ZERO_DECIMAL_CURRENCIES = %w[JPY VND].freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# ISO 4217 minor-unit count for a currency. Falls back to 2 for unknown
|
|
30
|
+
# codes so callers can still render *some* value rather than crashing.
|
|
31
|
+
#
|
|
32
|
+
# @param currency [String]
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
def decimals_for(currency)
|
|
35
|
+
ZERO_DECIMAL_CURRENCIES.include?(currency.to_s.upcase) ? 0 : 2
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param country [String]
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def country?(country)
|
|
41
|
+
COUNTRIES.include?(country.to_s.upcase)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param currency [String]
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def currency?(currency)
|
|
47
|
+
CURRENCIES.include?(currency.to_s.upcase)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param currency [String] ISO 4217 code (e.g. "USD")
|
|
51
|
+
# @return [String, nil] country code, or nil if unsupported
|
|
52
|
+
def country_for_currency(currency)
|
|
53
|
+
CURRENCY_TO_COUNTRY[currency.to_s.upcase]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param country [String] country code (e.g. "US")
|
|
57
|
+
# @return [String, nil] currency code, or nil if unsupported
|
|
58
|
+
def currency_for_country(country)
|
|
59
|
+
COUNTRY_TO_CURRENCY[country.to_s.upcase]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/timeprice/version.rb
CHANGED
data/lib/timeprice.rb
CHANGED
|
@@ -1,24 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "timeprice/version"
|
|
4
|
+
require_relative "timeprice/supported"
|
|
4
5
|
require_relative "timeprice/errors"
|
|
6
|
+
require_relative "timeprice/point"
|
|
5
7
|
require_relative "timeprice/data_loader"
|
|
6
8
|
require_relative "timeprice/inflation"
|
|
7
9
|
require_relative "timeprice/exchange"
|
|
8
10
|
require_relative "timeprice/compare"
|
|
9
11
|
require_relative "timeprice/sources"
|
|
10
12
|
|
|
13
|
+
# Offline historical inflation & FX for Ruby.
|
|
14
|
+
#
|
|
15
|
+
# Top-level module functions wrap the three core operations: inflation
|
|
16
|
+
# adjustment, currency exchange, and a combined "compare" that does both
|
|
17
|
+
# in the right order. Each returns an immutable `Data.define` value object.
|
|
18
|
+
#
|
|
19
|
+
# @example Inflation
|
|
20
|
+
# Timeprice.inflation(amount: 100, from: "1990-01", to: "2024-01", country: "US")
|
|
21
|
+
# @example FX
|
|
22
|
+
# Timeprice.exchange(amount: 100, from: "USD", to: "JPY", date: "2010-06-15")
|
|
23
|
+
# @example Compare
|
|
24
|
+
# Timeprice.compare(amount: 100, from: ["USD", "2010"], to: ["VND", "2024"])
|
|
11
25
|
module Timeprice
|
|
12
26
|
module_function
|
|
13
27
|
|
|
28
|
+
# Inflation-adjust an amount between two dates using a country's CPI.
|
|
29
|
+
#
|
|
30
|
+
# @param amount [Numeric] the original amount
|
|
31
|
+
# @param from [String] source date as "YYYY" or "YYYY-MM"
|
|
32
|
+
# @param to [String] target date as "YYYY" or "YYYY-MM"
|
|
33
|
+
# @param country [String] country code from {Supported::COUNTRIES}
|
|
34
|
+
# @return [InflationResult]
|
|
35
|
+
# @raise [UnsupportedCountry] if `country` is not supported
|
|
36
|
+
# @raise [DataNotFound] if no CPI point covers `from` or `to`
|
|
14
37
|
def inflation(amount:, from:, to:, country:)
|
|
15
38
|
Inflation.adjust(amount: amount, from: from, to: to, country: country)
|
|
16
39
|
end
|
|
17
40
|
|
|
41
|
+
# Convert an amount between currencies on a specific date.
|
|
42
|
+
#
|
|
43
|
+
# @param amount [Numeric] the original amount
|
|
44
|
+
# @param from [String] source currency (ISO 4217)
|
|
45
|
+
# @param to [String] destination currency (ISO 4217)
|
|
46
|
+
# @param date [String] date as "YYYY-MM-DD"
|
|
47
|
+
# @return [ExchangeResult]
|
|
48
|
+
# @raise [UnsupportedCurrency] if either currency is not supported
|
|
49
|
+
# @raise [DataNotFound] if no FX point exists within the fallback window
|
|
18
50
|
def exchange(amount:, from:, to:, date:)
|
|
19
51
|
Exchange.convert(amount: amount, from: from, to: to, date: date)
|
|
20
52
|
end
|
|
21
53
|
|
|
54
|
+
# Compare an amount across two (currency, date) points: convert at the
|
|
55
|
+
# source date, then inflate in the destination currency. See README.md
|
|
56
|
+
# "Compare semantics" for why this order is correct.
|
|
57
|
+
#
|
|
58
|
+
# @param amount [Numeric]
|
|
59
|
+
# @param from [Point, Array(String, String)] source point
|
|
60
|
+
# @param to [Point, Array(String, String)] destination point
|
|
61
|
+
# @return [CompareResult]
|
|
22
62
|
def compare(amount:, from:, to:)
|
|
23
63
|
Compare.run(amount: amount, from: from, to: to)
|
|
24
64
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: timeprice
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -157,12 +157,21 @@ files:
|
|
|
157
157
|
- exe/timeprice
|
|
158
158
|
- lib/timeprice.rb
|
|
159
159
|
- lib/timeprice/cli.rb
|
|
160
|
+
- lib/timeprice/cli/formatting.rb
|
|
161
|
+
- lib/timeprice/cli/presenters/compare.rb
|
|
162
|
+
- lib/timeprice/cli/presenters/exchange.rb
|
|
163
|
+
- lib/timeprice/cli/presenters/inflation.rb
|
|
164
|
+
- lib/timeprice/cli/presenters/sources.rb
|
|
160
165
|
- lib/timeprice/compare.rb
|
|
166
|
+
- lib/timeprice/cpi_lookup.rb
|
|
161
167
|
- lib/timeprice/data_loader.rb
|
|
162
168
|
- lib/timeprice/errors.rb
|
|
163
169
|
- lib/timeprice/exchange.rb
|
|
164
170
|
- lib/timeprice/inflation.rb
|
|
171
|
+
- lib/timeprice/point.rb
|
|
165
172
|
- lib/timeprice/sources.rb
|
|
173
|
+
- lib/timeprice/sources/coverage.rb
|
|
174
|
+
- lib/timeprice/supported.rb
|
|
166
175
|
- lib/timeprice/version.rb
|
|
167
176
|
homepage: https://github.com/patrick204nqh/timeprice
|
|
168
177
|
licenses:
|