timeprice 0.1.0 → 0.1.2
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 +24 -0
- data/lib/timeprice/cli.rb +58 -23
- data/lib/timeprice/compare.rb +3 -3
- data/lib/timeprice/data_loader.rb +11 -4
- data/lib/timeprice/errors.rb +5 -2
- data/lib/timeprice/exchange.rb +11 -1
- data/lib/timeprice/inflation.rb +23 -7
- data/lib/timeprice/sources.rb +12 -9
- data/lib/timeprice/version.rb +1 -1
- metadata +44 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0ebaf1340f0abefa6b4dcae31f7d2f2d22021155d24ebc258f15b0495f7a480
|
|
4
|
+
data.tar.gz: 6068762094c6008c72f4dd7becb10e1e8cb0d0c17b668234e8e0dff84c494cc2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa08a556599384d4ae292064b2342bfdc6976eac3fea784afb7426d623fdd0c8ee43001135bd2f7c3a065e735db8dd6d347459144e44ee702f30505ee23c3c1f
|
|
7
|
+
data.tar.gz: 9fd49be55c8790b4a1adfd9350fdd2e6eed82cdcbf8e41c2dd3b1e1514e5b81f9ffa71dc0a9b530738ac2f2d6fa22e969c240bd81dac043e4a4b8e23b21b6924
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.2] - 2026-05-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- RuboCop with `rubocop-rake` + `rubocop-rspec`, wired into Rake (`rake default` runs spec + rubocop) and CI (separate `RuboCop` job alongside `RSpec`).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `DataLoader.load_cpi` now distinguishes between "country isn't supported" (`UnsupportedCountry`) and "data file is missing on disk" (`DataNotFound` with the path the loader looked at). Previously both surfaced as `UnsupportedCountry`, masking install / `TIMEPRICE_DATA_ROOT` misconfigurations.
|
|
15
|
+
- `Timeprice.exchange` now rejects invalid calendar dates (e.g. `2021-02-29`) with `ArgumentError` instead of leaking a `Date::Error`. Honors the public error contract.
|
|
16
|
+
- Trimmed `ZERO_DECIMAL_CURRENCIES` to currencies actually supported by the gem (JPY, VND). Removed aspirational entries (KRW, IDR, HUF, CLP).
|
|
17
|
+
- Inline source comments now reference README sections (`README.md "Compare semantics"`) instead of `PLAN.md` (which is intentionally not shipped in the gem).
|
|
18
|
+
- `CONTRIBUTING.md` updated to match the single-Ruby CI; both `rspec` and `rubocop` must be green.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- CLI output formatting: currency-aware decimals (no `.0000` on JPY/VND), magnitude-aware FX rate precision (no `91.180000` for a 91.18 rate).
|
|
24
|
+
- `granularity` is omitted from human output when it's `monthly` (the happy path); surfaced only when the result used annual data.
|
|
25
|
+
- Error messages hint at supported values: `Unsupported country: "FR" (supported: US, UK, EU, JP, VN)`; out-of-range CPI errors include the actual coverage range.
|
|
26
|
+
- Validate FX currencies against the supported list up front instead of failing with a generic "no FX rate" message.
|
|
27
|
+
- Tightened CLI command descriptions so `timeprice help` fits in a standard terminal.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Hide Thor's built-in `tree` command — it was leaking into `timeprice help` as an internal-looking debug command.
|
|
31
|
+
|
|
8
32
|
## [0.1.0] - 2026-05-11
|
|
9
33
|
|
|
10
34
|
### Added
|
data/lib/timeprice/cli.rb
CHANGED
|
@@ -6,15 +6,23 @@ require_relative "../timeprice"
|
|
|
6
6
|
|
|
7
7
|
module Timeprice
|
|
8
8
|
class CLI < Thor
|
|
9
|
+
# Thor 1.5 ships a built-in `tree` command on every subclass. Strip it
|
|
10
|
+
# from this subclass — it's an internal debugging aid that leaks into
|
|
11
|
+
# our help output. all_commands inherits from the base Thor class, so
|
|
12
|
+
# filter it on read.
|
|
13
|
+
def self.all_commands
|
|
14
|
+
super.except("tree")
|
|
15
|
+
end
|
|
16
|
+
|
|
9
17
|
class_option :json, type: :boolean, default: false, desc: "Output result as JSON"
|
|
10
18
|
|
|
11
19
|
def self.exit_on_failure?
|
|
12
20
|
true
|
|
13
21
|
end
|
|
14
22
|
|
|
15
|
-
desc "inflation AMOUNT", "
|
|
16
|
-
method_option :from,
|
|
17
|
-
method_option :to,
|
|
23
|
+
desc "inflation AMOUNT", "Inflation-adjust an amount between two dates"
|
|
24
|
+
method_option :from, type: :string, required: true, desc: "Source date (YYYY or YYYY-MM)"
|
|
25
|
+
method_option :to, type: :string, required: true, desc: "Target date (YYYY or YYYY-MM)"
|
|
18
26
|
method_option :country, type: :string, required: true, desc: "Country code (US, UK, EU, JP, VN)"
|
|
19
27
|
def inflation(amount)
|
|
20
28
|
with_error_handling do
|
|
@@ -28,7 +36,7 @@ module Timeprice
|
|
|
28
36
|
end
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
desc "fx AMOUNT
|
|
39
|
+
desc "fx AMOUNT FROM TO", "Convert an amount between currencies on a date"
|
|
32
40
|
method_option :date, type: :string, required: true, desc: "Date (YYYY-MM-DD)"
|
|
33
41
|
def fx(amount, from_currency, to_currency)
|
|
34
42
|
with_error_handling do
|
|
@@ -58,14 +66,14 @@ module Timeprice
|
|
|
58
66
|
end
|
|
59
67
|
end
|
|
60
68
|
|
|
61
|
-
desc "sources", "List bundled data sources
|
|
69
|
+
desc "sources", "List bundled data sources and coverage"
|
|
62
70
|
def sources
|
|
63
71
|
list = Timeprice::Sources.list
|
|
64
72
|
if options[:json]
|
|
65
73
|
say JSON.generate(list)
|
|
66
74
|
else
|
|
67
75
|
list.each do |s|
|
|
68
|
-
say
|
|
76
|
+
say s[:name].to_s
|
|
69
77
|
say " id: #{s[:id]}"
|
|
70
78
|
say " license: #{s[:license]}"
|
|
71
79
|
say " license_url: #{s[:license_url]}"
|
|
@@ -86,21 +94,22 @@ module Timeprice
|
|
|
86
94
|
end
|
|
87
95
|
|
|
88
96
|
no_commands do
|
|
97
|
+
# Currencies with no minor unit — render whole numbers, no decimals.
|
|
98
|
+
ZERO_DECIMAL_CURRENCIES = %w[JPY VND].freeze
|
|
99
|
+
|
|
89
100
|
def with_error_handling
|
|
90
101
|
yield
|
|
91
102
|
rescue Timeprice::Error => e
|
|
92
103
|
warn "Error: #{e.message}"
|
|
93
104
|
exit 1
|
|
94
105
|
rescue ArgumentError => e
|
|
95
|
-
# Bad numeric/date format from Float() or library parsers — treat as user error.
|
|
96
106
|
warn "Error: #{e.message}"
|
|
97
107
|
exit 1
|
|
98
108
|
end
|
|
99
109
|
|
|
100
|
-
# Accepts "1995 USD" or "USD 1995" — order-agnostic.
|
|
101
|
-
# Returns [currency, year_string] tuple matching Timeprice.compare's API.
|
|
102
110
|
def parse_compare_token(token, label:)
|
|
103
111
|
raise ArgumentError, "#{label} is required" if token.nil? || token.strip.empty?
|
|
112
|
+
|
|
104
113
|
parts = token.strip.split(/\s+/)
|
|
105
114
|
unless parts.size == 2
|
|
106
115
|
raise ArgumentError,
|
|
@@ -115,15 +124,39 @@ module Timeprice
|
|
|
115
124
|
[currency.upcase, year]
|
|
116
125
|
end
|
|
117
126
|
|
|
127
|
+
def fmt_money(amount, currency)
|
|
128
|
+
decimals = ZERO_DECIMAL_CURRENCIES.include?(currency.to_s.upcase) ? 0 : 2
|
|
129
|
+
format("%.#{decimals}f", amount)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def fmt_rate(rate)
|
|
133
|
+
abs = rate.to_f.abs
|
|
134
|
+
decimals = if abs >= 1000 then 0
|
|
135
|
+
elsif abs >= 100 then 2
|
|
136
|
+
elsif abs >= 10 then 3
|
|
137
|
+
else 4
|
|
138
|
+
end
|
|
139
|
+
format("%.#{decimals}f", rate)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Granularity is loud noise on the happy path. Only surface it when the
|
|
143
|
+
# answer actually used annual data — that's where users want a heads-up.
|
|
144
|
+
def granularity_suffix(granularity)
|
|
145
|
+
return "" if granularity == :monthly
|
|
146
|
+
|
|
147
|
+
" (granularity: #{granularity})"
|
|
148
|
+
end
|
|
149
|
+
|
|
118
150
|
def emit_inflation(result)
|
|
119
151
|
if options[:json]
|
|
120
152
|
say JSON.generate(result.to_h)
|
|
121
153
|
else
|
|
154
|
+
ccy = result.country_currency_label
|
|
122
155
|
say format(
|
|
123
|
-
"
|
|
124
|
-
result.original_amount, result.
|
|
125
|
-
result.
|
|
126
|
-
result.
|
|
156
|
+
"%s %s in %s is %s %s in %s [%s]%s",
|
|
157
|
+
fmt_money(result.original_amount, ccy), ccy, result.from,
|
|
158
|
+
fmt_money(result.amount, ccy), ccy, result.to,
|
|
159
|
+
result.country, granularity_suffix(result.granularity)
|
|
127
160
|
)
|
|
128
161
|
end
|
|
129
162
|
end
|
|
@@ -133,12 +166,12 @@ module Timeprice
|
|
|
133
166
|
say JSON.generate(result.to_h)
|
|
134
167
|
else
|
|
135
168
|
line = format(
|
|
136
|
-
"
|
|
137
|
-
result.original_amount, result.from, result.date,
|
|
138
|
-
result.amount, result.to, result.rate
|
|
169
|
+
"%s %s on %s = %s %s (rate: %s)",
|
|
170
|
+
fmt_money(result.original_amount, result.from), result.from, result.date,
|
|
171
|
+
fmt_money(result.amount, result.to), result.to, fmt_rate(result.rate)
|
|
139
172
|
)
|
|
140
173
|
if result.effective_date && result.effective_date != result.date
|
|
141
|
-
line += "
|
|
174
|
+
line += " [effective: #{result.effective_date}, fallback]"
|
|
142
175
|
end
|
|
143
176
|
say line
|
|
144
177
|
end
|
|
@@ -149,14 +182,16 @@ module Timeprice
|
|
|
149
182
|
say JSON.generate(result.to_h)
|
|
150
183
|
else
|
|
151
184
|
say format(
|
|
152
|
-
"
|
|
153
|
-
result.original_amount, result.from_currency, result.from_date,
|
|
154
|
-
result.amount, result.to_currency, result.to_date
|
|
185
|
+
"%s %s in %s -> %s %s in %s",
|
|
186
|
+
fmt_money(result.original_amount, result.from_currency), result.from_currency, result.from_date,
|
|
187
|
+
fmt_money(result.amount, result.to_currency), result.to_currency, result.to_date
|
|
155
188
|
)
|
|
156
189
|
say format(
|
|
157
|
-
" steps:
|
|
158
|
-
result.
|
|
159
|
-
result.
|
|
190
|
+
" steps: %s %s -> %s %s (fx %s on %s), then inflate in %s x%.4f%s",
|
|
191
|
+
fmt_money(result.original_amount, result.from_currency), result.from_currency,
|
|
192
|
+
fmt_money(result.converted_amount, result.to_currency), result.to_currency,
|
|
193
|
+
fmt_rate(result.fx_rate), result.from_date,
|
|
194
|
+
result.country, result.cpi_ratio, granularity_suffix(result.granularity)
|
|
160
195
|
)
|
|
161
196
|
end
|
|
162
197
|
end
|
data/lib/timeprice/compare.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Timeprice
|
|
|
16
16
|
# Compare combines FX and inflation across two (currency, date) points.
|
|
17
17
|
#
|
|
18
18
|
# CONVENTION (critical): convert at SOURCE date first, then inflate in
|
|
19
|
-
# destination currency. See
|
|
19
|
+
# destination currency. See README.md "Compare semantics" section.
|
|
20
20
|
#
|
|
21
21
|
# This preserves purchasing-power equivalence in the destination economy.
|
|
22
22
|
# The naive alternative (inflate in source currency first, then convert at
|
|
@@ -32,7 +32,7 @@ module Timeprice
|
|
|
32
32
|
"GBP" => "UK",
|
|
33
33
|
"EUR" => "EU",
|
|
34
34
|
"JPY" => "JP",
|
|
35
|
-
"VND" => "VN"
|
|
35
|
+
"VND" => "VN",
|
|
36
36
|
}.freeze
|
|
37
37
|
|
|
38
38
|
module_function
|
|
@@ -78,7 +78,7 @@ module Timeprice
|
|
|
78
78
|
to_date: to_date.to_s,
|
|
79
79
|
country: to_country,
|
|
80
80
|
fx_rate: fx_result.rate,
|
|
81
|
-
cpi_ratio: infl.to_index.to_f / infl.from_index
|
|
81
|
+
cpi_ratio: infl.to_index.to_f / infl.from_index,
|
|
82
82
|
converted_amount: converted,
|
|
83
83
|
granularity: infl.granularity
|
|
84
84
|
)
|
|
@@ -27,9 +27,16 @@ module Timeprice
|
|
|
27
27
|
def load_cpi(country)
|
|
28
28
|
@cpi_cache ||= {}
|
|
29
29
|
key = country.to_s.downcase
|
|
30
|
+
code = country.to_s.upcase
|
|
30
31
|
@cpi_cache[[data_root, key]] ||= begin
|
|
32
|
+
raise UnsupportedCountry, code unless SUPPORTED_COUNTRIES.include?(code)
|
|
33
|
+
|
|
31
34
|
path = File.join(data_root, "cpi", "#{key}.json")
|
|
32
|
-
|
|
35
|
+
unless File.exist?(path)
|
|
36
|
+
raise DataNotFound, "CPI data file missing for #{code} (looked in #{path}). " \
|
|
37
|
+
"Check TIMEPRICE_DATA_ROOT or reinstall the gem."
|
|
38
|
+
end
|
|
39
|
+
|
|
33
40
|
parse_with_schema(path)
|
|
34
41
|
end
|
|
35
42
|
end
|
|
@@ -40,6 +47,7 @@ module Timeprice
|
|
|
40
47
|
@fx_cache[[data_root, key]] ||= begin
|
|
41
48
|
path = File.join(data_root, "fx", "usd", "#{key}.json")
|
|
42
49
|
raise DataNotFound, "No FX data for year #{key}" unless File.exist?(path)
|
|
50
|
+
|
|
43
51
|
parse_with_schema(path)
|
|
44
52
|
end
|
|
45
53
|
end
|
|
@@ -49,9 +57,8 @@ module Timeprice
|
|
|
49
57
|
def parse_with_schema(path)
|
|
50
58
|
data = JSON.parse(File.read(path))
|
|
51
59
|
version = data["schema_version"]
|
|
52
|
-
unless version == SUPPORTED_SCHEMA_VERSION
|
|
53
|
-
|
|
54
|
-
end
|
|
60
|
+
raise UnsupportedSchemaVersion.new(version, path) unless version == SUPPORTED_SCHEMA_VERSION
|
|
61
|
+
|
|
55
62
|
data
|
|
56
63
|
end
|
|
57
64
|
end
|
data/lib/timeprice/errors.rb
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
module Timeprice
|
|
4
4
|
class Error < StandardError; end
|
|
5
5
|
|
|
6
|
+
SUPPORTED_COUNTRIES = %w[US UK EU JP VN].freeze
|
|
7
|
+
SUPPORTED_CURRENCIES = %w[USD GBP EUR JPY VND].freeze
|
|
8
|
+
|
|
6
9
|
class UnsupportedCountry < Error
|
|
7
10
|
attr_reader :country
|
|
8
11
|
|
|
9
12
|
def initialize(country)
|
|
10
13
|
@country = country
|
|
11
|
-
super("Unsupported country: #{country.inspect}")
|
|
14
|
+
super("Unsupported country: #{country.inspect} (supported: #{SUPPORTED_COUNTRIES.join(", ")})")
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
|
|
@@ -17,7 +20,7 @@ module Timeprice
|
|
|
17
20
|
|
|
18
21
|
def initialize(currency)
|
|
19
22
|
@currency = currency
|
|
20
|
-
super("Unsupported currency: #{currency.inspect}")
|
|
23
|
+
super("Unsupported currency: #{currency.inspect} (supported: #{SUPPORTED_CURRENCIES.join(", ")})")
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
|
data/lib/timeprice/exchange.rb
CHANGED
|
@@ -20,6 +20,9 @@ module Timeprice
|
|
|
20
20
|
def convert(amount:, from:, to:, date:)
|
|
21
21
|
from = from.to_s.upcase
|
|
22
22
|
to = to.to_s.upcase
|
|
23
|
+
raise UnsupportedCurrency, from unless SUPPORTED_CURRENCIES.include?(from)
|
|
24
|
+
raise UnsupportedCurrency, to unless SUPPORTED_CURRENCIES.include?(to)
|
|
25
|
+
|
|
23
26
|
d = parse_date(date)
|
|
24
27
|
|
|
25
28
|
rate, eff_date = resolve_rate(from, to, d)
|
|
@@ -75,8 +78,10 @@ module Timeprice
|
|
|
75
78
|
end
|
|
76
79
|
rates_for_day = year_data.dig("rates", candidate.to_s)
|
|
77
80
|
next unless rates_for_day
|
|
81
|
+
|
|
78
82
|
rate = rates_for_day[currency]
|
|
79
83
|
next unless rate
|
|
84
|
+
|
|
80
85
|
return [rate.to_f, candidate]
|
|
81
86
|
end
|
|
82
87
|
raise DataNotFound, "No FX rate for USD->#{currency} on or before #{d}"
|
|
@@ -89,7 +94,12 @@ module Timeprice
|
|
|
89
94
|
unless date.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
90
95
|
raise ArgumentError, "Invalid date format: #{date.inspect} (use YYYY-MM-DD)"
|
|
91
96
|
end
|
|
92
|
-
|
|
97
|
+
|
|
98
|
+
begin
|
|
99
|
+
Date.parse(date)
|
|
100
|
+
rescue Date::Error
|
|
101
|
+
raise ArgumentError, "Invalid date: #{date.inspect} is not a real calendar date"
|
|
102
|
+
end
|
|
93
103
|
else
|
|
94
104
|
raise ArgumentError, "Invalid date: #{date.inspect}"
|
|
95
105
|
end
|
data/lib/timeprice/inflation.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Timeprice
|
|
|
27
27
|
from_index, from_gran = lookup_index(data, from)
|
|
28
28
|
to_index, to_gran = lookup_index(data, to)
|
|
29
29
|
|
|
30
|
-
ratio = to_index.to_f / from_index
|
|
30
|
+
ratio = to_index.to_f / from_index
|
|
31
31
|
InflationResult.new(
|
|
32
32
|
amount: amount.to_f * ratio,
|
|
33
33
|
original_amount: amount.to_f,
|
|
@@ -58,18 +58,18 @@ module Timeprice
|
|
|
58
58
|
[monthly[key], :monthly]
|
|
59
59
|
else
|
|
60
60
|
year = key[0, 4]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
61
|
+
raise DataNotFound, missing_cpi_message(key, data, monthly, annual) unless annual.key?(year)
|
|
62
|
+
|
|
63
|
+
[annual[year], :annual]
|
|
64
|
+
|
|
66
65
|
end
|
|
67
66
|
when /\A\d{4}\z/
|
|
68
67
|
if annual.key?(key)
|
|
69
68
|
[annual[key], :annual]
|
|
70
69
|
else
|
|
71
70
|
months = monthly.select { |k, _| k.start_with?("#{key}-") }
|
|
72
|
-
raise DataNotFound,
|
|
71
|
+
raise DataNotFound, missing_cpi_message(key, data, monthly, annual) if months.empty?
|
|
72
|
+
|
|
73
73
|
avg = months.values.sum.to_f / months.size
|
|
74
74
|
[avg, :annual_from_monthly_avg]
|
|
75
75
|
end
|
|
@@ -78,11 +78,27 @@ module Timeprice
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
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
|
+
|
|
81
96
|
# If either end fell back to annual_from_monthly_avg, propagate that label;
|
|
82
97
|
# else if either is annual, propagate :annual; else :monthly.
|
|
83
98
|
def merge_granularity(a, b)
|
|
84
99
|
return :annual_from_monthly_avg if a == :annual_from_monthly_avg || b == :annual_from_monthly_avg
|
|
85
100
|
return :annual if a == :annual || b == :annual
|
|
101
|
+
|
|
86
102
|
:monthly
|
|
87
103
|
end
|
|
88
104
|
end
|
data/lib/timeprice/sources.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Timeprice
|
|
|
15
15
|
name: "U.S. Bureau of Labor Statistics — CPI-U (series CUUR0000SA0)",
|
|
16
16
|
license: "U.S. Government work — public domain",
|
|
17
17
|
license_url: "https://www.bls.gov/bls/linksite.htm",
|
|
18
|
-
attribution: "Data: U.S. Bureau of Labor Statistics"
|
|
18
|
+
attribution: "Data: U.S. Bureau of Labor Statistics",
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
id: "uk_cpi",
|
|
@@ -24,7 +24,7 @@ module Timeprice
|
|
|
24
24
|
name: "UK Office for National Statistics — CPI all-items (series D7BT)",
|
|
25
25
|
license: "Open Government Licence v3.0",
|
|
26
26
|
license_url: "https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/",
|
|
27
|
-
attribution: "Contains public sector information licensed under the Open Government Licence v3.0"
|
|
27
|
+
attribution: "Contains public sector information licensed under the Open Government Licence v3.0",
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
id: "eu_hicp",
|
|
@@ -33,7 +33,7 @@ module Timeprice
|
|
|
33
33
|
name: "Eurostat — HICP prc_hicp_midx (Euro area, all items)",
|
|
34
34
|
license: "Eurostat reuse policy (free reuse with attribution)",
|
|
35
35
|
license_url: "https://ec.europa.eu/eurostat/about-us/policies/copyright",
|
|
36
|
-
attribution: "Source: Eurostat"
|
|
36
|
+
attribution: "Source: Eurostat",
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
id: "jp_cpi",
|
|
@@ -42,7 +42,7 @@ module Timeprice
|
|
|
42
42
|
name: "World Bank — FP.CPI.TOTL (annual, JP fallback)",
|
|
43
43
|
license: "CC BY 4.0",
|
|
44
44
|
license_url: "https://datacatalog.worldbank.org/public-licenses#cc-by",
|
|
45
|
-
attribution: "Source: World Bank, FP.CPI.TOTL"
|
|
45
|
+
attribution: "Source: World Bank, FP.CPI.TOTL",
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
id: "vn_cpi",
|
|
@@ -51,7 +51,7 @@ module Timeprice
|
|
|
51
51
|
name: "World Bank — FP.CPI.TOTL (annual)",
|
|
52
52
|
license: "CC BY 4.0",
|
|
53
53
|
license_url: "https://datacatalog.worldbank.org/public-licenses#cc-by",
|
|
54
|
-
attribution: "Source: World Bank, FP.CPI.TOTL"
|
|
54
|
+
attribution: "Source: World Bank, FP.CPI.TOTL",
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
id: "fx_ecb",
|
|
@@ -60,7 +60,7 @@ module Timeprice
|
|
|
60
60
|
name: "European Central Bank reference rates (via Frankfurter)",
|
|
61
61
|
license: "ECB reference rates — free reuse",
|
|
62
62
|
license_url: "https://www.ecb.europa.eu/services/disclaimer/html/index.en.html",
|
|
63
|
-
attribution: "FX data: European Central Bank reference rates via Frankfurter"
|
|
63
|
+
attribution: "FX data: European Central Bank reference rates via Frankfurter",
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
id: "fx_vnd",
|
|
@@ -69,8 +69,8 @@ module Timeprice
|
|
|
69
69
|
name: "World Bank — PA.NUS.FCRF (VND annual average, broadcast daily)",
|
|
70
70
|
license: "CC BY 4.0",
|
|
71
71
|
license_url: "https://datacatalog.worldbank.org/public-licenses#cc-by",
|
|
72
|
-
attribution: "VND FX: World Bank, PA.NUS.FCRF"
|
|
73
|
-
}
|
|
72
|
+
attribution: "VND FX: World Bank, PA.NUS.FCRF",
|
|
73
|
+
},
|
|
74
74
|
].freeze
|
|
75
75
|
|
|
76
76
|
module_function
|
|
@@ -105,6 +105,7 @@ module Timeprice
|
|
|
105
105
|
root = File.join(DataLoader.data_root, "fx", "usd")
|
|
106
106
|
years = Dir[File.join(root, "*.json")].map { |f| File.basename(f, ".json").to_i }.sort
|
|
107
107
|
return "no data" if years.empty?
|
|
108
|
+
|
|
108
109
|
case id
|
|
109
110
|
when "fx_vnd"
|
|
110
111
|
# VND broadcast-from-annual covers earlier years too.
|
|
@@ -113,14 +114,16 @@ module Timeprice
|
|
|
113
114
|
d["rates"].any? { |_, v| v.key?("VND") }
|
|
114
115
|
end
|
|
115
116
|
return "no VND data" if with_vnd.empty?
|
|
117
|
+
|
|
116
118
|
"USD↔VND #{with_vnd.first}..#{with_vnd.last}"
|
|
117
119
|
else
|
|
118
120
|
# ECB pairs (EUR/GBP/JPY) start 1999
|
|
119
121
|
ecb_years = years.select do |y|
|
|
120
122
|
d = JSON.parse(File.read(File.join(root, "#{y}.json")))
|
|
121
|
-
d["rates"].any? { |_, v|
|
|
123
|
+
d["rates"].any? { |_, v| v.keys.intersect?(%w[EUR GBP JPY]) }
|
|
122
124
|
end
|
|
123
125
|
return "no ECB data" if ecb_years.empty?
|
|
126
|
+
|
|
124
127
|
"USD↔EUR/GBP/JPY daily #{ecb_years.first}..#{ecb_years.last}"
|
|
125
128
|
end
|
|
126
129
|
end
|
data/lib/timeprice/version.rb
CHANGED
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.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -51,6 +51,48 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '3.13'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.69'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.69'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop-rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.6'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.6'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop-rspec
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.3'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.3'
|
|
54
96
|
description: Offline historical inflation & FX for Ruby - bundled data, no API keys,
|
|
55
97
|
monthly auto-refresh.
|
|
56
98
|
email:
|
|
@@ -130,6 +172,7 @@ metadata:
|
|
|
130
172
|
bug_tracker_uri: https://github.com/patrick204nqh/timeprice/issues
|
|
131
173
|
changelog_uri: https://github.com/patrick204nqh/timeprice/blob/main/CHANGELOG.md
|
|
132
174
|
github_repo: patrick204nqh/timeprice
|
|
175
|
+
rubygems_mfa_required: 'true'
|
|
133
176
|
rdoc_options: []
|
|
134
177
|
require_paths:
|
|
135
178
|
- lib
|