timeprice 0.4.0 → 0.5.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +18 -2
  4. data/data/cpi/eu.json +406 -403
  5. data/data/cpi/jp.json +78 -75
  6. data/data/cpi/uk.json +513 -510
  7. data/data/cpi/us.json +488 -485
  8. data/data/cpi/vn.json +342 -339
  9. data/data/fx/usd/1999.json +23 -7
  10. data/data/fx/usd/2000.json +23 -7
  11. data/data/fx/usd/2001.json +23 -7
  12. data/data/fx/usd/2002.json +23 -7
  13. data/data/fx/usd/2003.json +23 -7
  14. data/data/fx/usd/2004.json +23 -7
  15. data/data/fx/usd/2005.json +23 -7
  16. data/data/fx/usd/2006.json +23 -7
  17. data/data/fx/usd/2007.json +23 -7
  18. data/data/fx/usd/2008.json +23 -7
  19. data/data/fx/usd/2009.json +23 -7
  20. data/data/fx/usd/2010.json +23 -7
  21. data/data/fx/usd/2011.json +23 -7
  22. data/data/fx/usd/2012.json +23 -7
  23. data/data/fx/usd/2013.json +23 -7
  24. data/data/fx/usd/2014.json +23 -7
  25. data/data/fx/usd/2015.json +23 -7
  26. data/data/fx/usd/2016.json +23 -7
  27. data/data/fx/usd/2017.json +23 -7
  28. data/data/fx/usd/2018.json +23 -7
  29. data/data/fx/usd/2019.json +23 -7
  30. data/data/fx/usd/2020.json +23 -7
  31. data/data/fx/usd/2021.json +23 -7
  32. data/data/fx/usd/2022.json +23 -7
  33. data/data/fx/usd/2023.json +23 -7
  34. data/data/fx/usd/2024.json +23 -7
  35. data/data/fx/usd/2025.json +24 -5
  36. data/data/fx/usd/2026.json +24 -5
  37. data/data/fx/usd/_annual.json +145 -0
  38. data/data/manifest.json +90 -0
  39. data/lib/timeprice/compare.rb +1 -1
  40. data/lib/timeprice/cpi_lookup.rb +2 -2
  41. data/lib/timeprice/data_loader.rb +42 -7
  42. data/lib/timeprice/errors.rb +4 -4
  43. data/lib/timeprice/exchange.rb +8 -8
  44. data/lib/timeprice/inflation.rb +2 -2
  45. data/lib/timeprice/sources/coverage.rb +27 -32
  46. data/lib/timeprice/supported.rb +39 -22
  47. data/lib/timeprice/version.rb +1 -1
  48. data/lib/timeprice.rb +2 -2
  49. metadata +3 -15
  50. data/data/fx/usd/1983.json +0 -11
  51. data/data/fx/usd/1986.json +0 -11
  52. data/data/fx/usd/1987.json +0 -11
  53. data/data/fx/usd/1988.json +0 -11
  54. data/data/fx/usd/1989.json +0 -11
  55. data/data/fx/usd/1990.json +0 -11
  56. data/data/fx/usd/1991.json +0 -11
  57. data/data/fx/usd/1992.json +0 -11
  58. data/data/fx/usd/1993.json +0 -11
  59. data/data/fx/usd/1994.json +0 -11
  60. data/data/fx/usd/1995.json +0 -11
  61. data/data/fx/usd/1996.json +0 -11
  62. data/data/fx/usd/1997.json +0 -11
  63. data/data/fx/usd/1998.json +0 -11
@@ -0,0 +1,90 @@
1
+ {
2
+ "countries": [
3
+ {
4
+ "code": "EU",
5
+ "cpi_file": "cpi/eu.json",
6
+ "currency": "EUR",
7
+ "granularities": [
8
+ "monthly",
9
+ "annual"
10
+ ]
11
+ },
12
+ {
13
+ "code": "JP",
14
+ "cpi_file": "cpi/jp.json",
15
+ "currency": "JPY",
16
+ "granularities": [
17
+ "annual"
18
+ ]
19
+ },
20
+ {
21
+ "code": "UK",
22
+ "cpi_file": "cpi/uk.json",
23
+ "currency": "GBP",
24
+ "granularities": [
25
+ "monthly",
26
+ "annual"
27
+ ]
28
+ },
29
+ {
30
+ "code": "US",
31
+ "cpi_file": "cpi/us.json",
32
+ "currency": "USD",
33
+ "granularities": [
34
+ "monthly",
35
+ "annual"
36
+ ]
37
+ },
38
+ {
39
+ "code": "VN",
40
+ "cpi_file": "cpi/vn.json",
41
+ "currency": "VND",
42
+ "granularities": [
43
+ "monthly",
44
+ "annual"
45
+ ]
46
+ }
47
+ ],
48
+ "fx": {
49
+ "annual_file": "fx/usd/_annual.json",
50
+ "base": "USD",
51
+ "currencies": [
52
+ "EUR",
53
+ "GBP",
54
+ "JPY",
55
+ "VND"
56
+ ],
57
+ "daily_years": [
58
+ 1999,
59
+ 2000,
60
+ 2001,
61
+ 2002,
62
+ 2003,
63
+ 2004,
64
+ 2005,
65
+ 2006,
66
+ 2007,
67
+ 2008,
68
+ 2009,
69
+ 2010,
70
+ 2011,
71
+ 2012,
72
+ 2013,
73
+ 2014,
74
+ 2015,
75
+ 2016,
76
+ 2017,
77
+ 2018,
78
+ 2019,
79
+ 2020,
80
+ 2021,
81
+ 2022,
82
+ 2023,
83
+ 2024,
84
+ 2025,
85
+ 2026
86
+ ]
87
+ },
88
+ "generated_at": "2026-05-11",
89
+ "schema_version": 3
90
+ }
@@ -38,7 +38,7 @@ module Timeprice
38
38
  # accepts a {Point} or a 2-tuple like `["USD", "2010"]` or `["USD", "2010-06"]`
39
39
  # @param to [Timeprice::Point, Array(String, String)] destination point
40
40
  # @return [CompareResult]
41
- # @raise [UnsupportedCurrency] if either currency is not in {Supported::CURRENCIES}
41
+ # @raise [UnsupportedCurrency] if either currency is not in {Supported.currencies}
42
42
  def run(amount:, from:, to:)
43
43
  from_point, to_point, to_country = resolve_points(from, to)
44
44
 
@@ -14,8 +14,8 @@ module Timeprice
14
14
  class CpiLookup
15
15
  def initialize(data)
16
16
  @data = data
17
- @monthly = data["monthly"] || {}
18
- @annual = data["annual"] || {}
17
+ @monthly = data.dig("series", "monthly") || {}
18
+ @annual = data.dig("series", "annual") || {}
19
19
  end
20
20
 
21
21
  # @param key [String] "YYYY" or "YYYY-MM"
@@ -2,19 +2,18 @@
2
2
 
3
3
  require "json"
4
4
  require_relative "errors"
5
- require_relative "supported"
6
5
 
7
6
  module Timeprice
8
7
  # Loads and caches the bundled JSON data files. Override the search root
9
8
  # by setting `TIMEPRICE_DATA_ROOT` in the environment or assigning
10
9
  # {DataLoader.data_root=}.
11
10
  module DataLoader
12
- SUPPORTED_SCHEMA_VERSION = 2
11
+ SUPPORTED_SCHEMA_VERSION = 3
13
12
 
14
13
  DEFAULT_DATA_ROOT = File.expand_path("../../data", __dir__)
15
14
 
16
15
  class << self
17
- # @return [String] absolute path to the directory containing `cpi/` and `fx/`
16
+ # @return [String] absolute path to the directory containing `cpi/`, `fx/`, `manifest.json`.
18
17
  def data_root
19
18
  ENV["TIMEPRICE_DATA_ROOT"] || @data_root || DEFAULT_DATA_ROOT
20
19
  end
@@ -32,19 +31,36 @@ module Timeprice
32
31
  def clear_cache!
33
32
  @cpi_cache = {}
34
33
  @fx_cache = {}
34
+ @manifest_cache = {}
35
+ @annual_fallback_cache = {}
36
+ end
37
+
38
+ # Load the top-level manifest describing the bundled dataset.
39
+ # @return [Hash]
40
+ # @raise [DataNotFound] if `manifest.json` is missing
41
+ def load_manifest
42
+ manifest_cache[data_root] ||= begin
43
+ path = File.join(data_root, "manifest.json")
44
+ unless File.exist?(path)
45
+ raise DataNotFound, "manifest.json missing (looked in #{path}). " \
46
+ "Check TIMEPRICE_DATA_ROOT or reinstall the gem."
47
+ end
48
+
49
+ parse_with_schema(path)
50
+ end
35
51
  end
36
52
 
37
53
  # Load the CPI series for a supported country.
38
54
  # @param country [String]
39
- # @return [Hash] parsed JSON with "monthly" / "annual" / metadata keys
40
- # @raise [UnsupportedCountry] if `country` is not in {Supported::COUNTRIES}
55
+ # @return [Hash] parsed JSON with "series" / "index" / "provenance" / "providers"
56
+ # @raise [UnsupportedCountry] if `country` is not in {Supported.countries}
41
57
  # @raise [DataNotFound] if the file is missing
42
58
  # @raise [UnsupportedSchemaVersion] if the file uses a future schema
43
59
  def load_cpi(country)
44
60
  key = country.to_s.downcase
45
61
  code = country.to_s.upcase
46
62
  cpi_cache[[data_root, key]] ||= begin
47
- raise UnsupportedCountry, code unless Supported::COUNTRIES.include?(code)
63
+ raise UnsupportedCountry, code unless Supported.country?(code)
48
64
 
49
65
  path = File.join(data_root, "cpi", "#{key}.json")
50
66
  unless File.exist?(path)
@@ -58,7 +74,7 @@ module Timeprice
58
74
 
59
75
  # Load the FX rates for a year.
60
76
  # @param year [Integer, String]
61
- # @return [Hash] parsed JSON with a "rates" map of date → currency → Float
77
+ # @return [Hash] parsed JSON with `rates` (and optional `annual`) blocks
62
78
  # @raise [DataNotFound] if the per-year file is missing
63
79
  def load_fx_year(year)
64
80
  key = year.to_i
@@ -70,6 +86,17 @@ module Timeprice
70
86
  end
71
87
  end
72
88
 
89
+ # Load the sparse historical FX annual-only fallback file, if present.
90
+ # Returns nil when no fallback file ships with this data root.
91
+ # @return [Hash, nil]
92
+ def load_fx_annual_fallback
93
+ return @annual_fallback_cache[data_root] if @annual_fallback_cache&.key?(data_root)
94
+
95
+ @annual_fallback_cache ||= {}
96
+ path = File.join(data_root, "fx", "usd", "_annual.json")
97
+ @annual_fallback_cache[data_root] = File.exist?(path) ? parse_with_schema(path) : nil
98
+ end
99
+
73
100
  private
74
101
 
75
102
  def cpi_cache
@@ -80,6 +107,10 @@ module Timeprice
80
107
  @fx_cache ||= {}
81
108
  end
82
109
 
110
+ def manifest_cache
111
+ @manifest_cache ||= {}
112
+ end
113
+
83
114
  def parse_with_schema(path)
84
115
  data = JSON.parse(File.read(path))
85
116
  version = data["schema_version"]
@@ -90,3 +121,7 @@ module Timeprice
90
121
  end
91
122
  end
92
123
  end
124
+
125
+ # Supported is loaded by the top-level entry point. Referenced lazily inside
126
+ # load_cpi to avoid a require cycle (Supported reads the manifest via DataLoader).
127
+ require_relative "supported" unless defined?(Timeprice::Supported)
@@ -7,23 +7,23 @@ module Timeprice
7
7
  # to handle anything the gem can throw at you.
8
8
  class Error < StandardError; end
9
9
 
10
- # Raised when a country code is not in {Supported::COUNTRIES}.
10
+ # Raised when a country code is not in {Supported.countries}.
11
11
  class UnsupportedCountry < Error
12
12
  attr_reader :country
13
13
 
14
14
  def initialize(country)
15
15
  @country = country
16
- super("Unsupported country: #{country.inspect} (supported: #{Supported::COUNTRIES.join(", ")})")
16
+ super("Unsupported country: #{country.inspect} (supported: #{Supported.countries.join(", ")})")
17
17
  end
18
18
  end
19
19
 
20
- # Raised when a currency code is not in {Supported::CURRENCIES}.
20
+ # Raised when a currency code is not in {Supported.currencies}.
21
21
  class UnsupportedCurrency < Error
22
22
  attr_reader :currency
23
23
 
24
24
  def initialize(currency)
25
25
  @currency = currency
26
- super("Unsupported currency: #{currency.inspect} (supported: #{Supported::CURRENCIES.join(", ")})")
26
+ super("Unsupported currency: #{currency.inspect} (supported: #{Supported.currencies.join(", ")})")
27
27
  end
28
28
  end
29
29
 
@@ -33,8 +33,8 @@ module Timeprice
33
33
  def convert(amount:, from:, to:, date:)
34
34
  from = from.to_s.upcase
35
35
  to = to.to_s.upcase
36
- raise UnsupportedCurrency, from unless Supported::CURRENCIES.include?(from)
37
- raise UnsupportedCurrency, to unless Supported::CURRENCIES.include?(to)
36
+ raise UnsupportedCurrency, from unless Supported.currency?(from)
37
+ raise UnsupportedCurrency, to unless Supported.currency?(to)
38
38
 
39
39
  d = parse_date(date)
40
40
 
@@ -82,7 +82,7 @@ module Timeprice
82
82
  end
83
83
 
84
84
  # Walk back up to MAX_FALLBACK_DAYS to find a daily rate; if none, fall
85
- # back to the year file's top-level `annual` block.
85
+ # back to data/fx/usd/_annual.json (the single source of annual FX truth).
86
86
  # Returns [rate, effective_date, granularity].
87
87
  def lookup_usd_base(currency, d)
88
88
  (0..MAX_FALLBACK_DAYS).each do |offset|
@@ -108,12 +108,12 @@ module Timeprice
108
108
  raise DataNotFound, "No FX rate for USD->#{currency} on or before #{d}"
109
109
  end
110
110
 
111
- # Consult the year file's top-level `annual` block. Returns Float or nil.
111
+ # Consult data/fx/usd/_annual.json. Returns Float or nil.
112
112
  def annual_fallback(currency, year)
113
- year_data = DataLoader.load_fx_year(year)
114
- year_data.dig("annual", currency)&.to_f
115
- rescue DataNotFound
116
- nil
113
+ fallback = DataLoader.load_fx_annual_fallback
114
+ return nil unless fallback
115
+
116
+ fallback.dig("annual", year.to_s, currency)&.to_f
117
117
  end
118
118
 
119
119
  def parse_date(date)
@@ -21,7 +21,7 @@ module Timeprice
21
21
  end
22
22
  end
23
23
 
24
- # CPI-based inflation adjustment for the {Supported::COUNTRIES} list.
24
+ # CPI-based inflation adjustment for the {Supported.countries} list.
25
25
  module Inflation
26
26
  module_function
27
27
 
@@ -32,7 +32,7 @@ module Timeprice
32
32
  # @param amount [Numeric]
33
33
  # @param from [String] source date ("YYYY" or "YYYY-MM")
34
34
  # @param to [String] target date ("YYYY" or "YYYY-MM")
35
- # @param country [String] country code (see {Supported::COUNTRIES})
35
+ # @param country [String] country code (see {Supported.countries})
36
36
  # @return [InflationResult]
37
37
  # @raise [UnsupportedCountry] if `country` is not supported
38
38
  # @raise [DataNotFound] if no CPI data covers the requested period
@@ -5,9 +5,10 @@ require_relative "../data_loader"
5
5
 
6
6
  module Timeprice
7
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.
8
+ # Computes coverage strings for bundled data sources at runtime by reading
9
+ # the structured `provenance` blocks in v3 data files. The Sources
10
+ # attribution registry stays a pure data table; Coverage is the only
11
+ # place that touches the filesystem.
11
12
  module Coverage
12
13
  module_function
13
14
 
@@ -25,46 +26,40 @@ module Timeprice
25
26
 
26
27
  def cpi(country)
27
28
  data = DataLoader.load_cpi(country)
28
- monthly = (data["monthly"] || {}).keys.sort
29
- annual = (data["annual"] || {}).keys.sort
29
+ monthly = data.dig("series", "monthly") || {}
30
+ annual = data.dig("series", "annual") || {}
30
31
  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?
32
+ parts << "monthly #{monthly.keys.min}..#{monthly.keys.max} (#{monthly.size})" if monthly.any?
33
+ parts << "annual #{annual.keys.min}..#{annual.keys.max} (#{annual.size})" if annual.any?
33
34
  parts.join(", ")
34
35
  end
35
36
 
36
37
  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}"
38
+ case id
39
+ when "fx_ecb" then ecb_summary
40
+ when "fx_vnd" then vnd_summary
41
+ else "n/a"
42
+ end
52
43
  end
53
44
 
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?
45
+ # Frankfurter (ECB) → daily EUR/GBP/JPY in per-year files. Range derived
46
+ # from the manifest's `daily_years` list.
47
+ def ecb_summary
48
+ years = DataLoader.load_manifest.dig("fx", "daily_years") || []
49
+ return "no ECB data" if years.empty?
57
50
 
58
- "USD↔EUR/GBP/JPY daily #{ecb_years.first}..#{ecb_years.last}"
51
+ "USD↔EUR/GBP/JPY daily #{years.first}..#{years.last}"
59
52
  end
60
53
 
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
54
+ # All annual FX (today only VND) lives in data/fx/usd/_annual.json.
55
+ def vnd_summary
56
+ fallback = DataLoader.load_fx_annual_fallback
57
+ years = (fallback&.dig("annual") || {})
58
+ .select { |_y, ccy_hash| ccy_hash.key?("VND") }
59
+ .keys.map(&:to_i).sort
60
+ return "no VND data" if years.empty?
65
61
 
66
- def fx_root
67
- File.join(DataLoader.data_root, "fx", "usd")
62
+ "USD↔VND #{years.first}..#{years.last}"
68
63
  end
69
64
  end
70
65
  end
@@ -1,34 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
+ # Supported country and currency codes, derived from `data/manifest.json`.
5
+ # Adding a country = drop a CPI file + regenerate the manifest. No code
6
+ # change required.
6
7
  #
7
8
  # 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.
9
+ # must read it from here.
10
10
  module Supported
11
- COUNTRIES = %w[US UK EU JP VN].freeze
12
- CURRENCIES = %w[USD GBP EUR JPY VND].freeze
11
+ # Currencies with no minor unit — formatted as whole numbers. This is
12
+ # ISO 4217 metadata, not bundled data, so it stays hardcoded.
13
+ ZERO_DECIMAL_CURRENCIES = %w[JPY VND].freeze
13
14
 
14
- COUNTRY_TO_CURRENCY = {
15
- "US" => "USD",
16
- "UK" => "GBP",
17
- "EU" => "EUR",
18
- "JP" => "JPY",
19
- "VN" => "VND",
20
- }.freeze
15
+ module_function
21
16
 
22
- CURRENCY_TO_COUNTRY = COUNTRY_TO_CURRENCY.invert.freeze
17
+ # @return [Array<String>] frozen list of supported country codes.
18
+ def countries
19
+ manifest_countries.map { |c| c["code"] }.freeze
20
+ end
23
21
 
24
- # Currencies with no minor unit formatted as whole numbers.
25
- ZERO_DECIMAL_CURRENCIES = %w[JPY VND].freeze
22
+ # @return [Array<String>] frozen list of supported currency codes
23
+ # (the FX base USD plus every currency the manifest declares).
24
+ def currencies
25
+ base = DataLoader.load_manifest.dig("fx", "base")
26
+ ([base] + DataLoader.load_manifest.dig("fx", "currencies")).uniq.freeze
27
+ end
26
28
 
27
- module_function
29
+ # @return [Hash{String=>String}] country code → currency code.
30
+ def country_to_currency
31
+ manifest_countries.to_h { |c| [c["code"], c["currency"]] }.freeze
32
+ end
33
+
34
+ # @return [Hash{String=>String}] currency code → country code.
35
+ def currency_to_country
36
+ country_to_currency.invert.freeze
37
+ end
28
38
 
29
39
  # ISO 4217 minor-unit count for a currency. Falls back to 2 for unknown
30
40
  # codes so callers can still render *some* value rather than crashing.
31
- #
32
41
  # @param currency [String]
33
42
  # @return [Integer]
34
43
  def decimals_for(currency)
@@ -38,25 +47,33 @@ module Timeprice
38
47
  # @param country [String]
39
48
  # @return [Boolean]
40
49
  def country?(country)
41
- COUNTRIES.include?(country.to_s.upcase)
50
+ countries.include?(country.to_s.upcase)
42
51
  end
43
52
 
44
53
  # @param currency [String]
45
54
  # @return [Boolean]
46
55
  def currency?(currency)
47
- CURRENCIES.include?(currency.to_s.upcase)
56
+ currencies.include?(currency.to_s.upcase)
48
57
  end
49
58
 
50
59
  # @param currency [String] ISO 4217 code (e.g. "USD")
51
60
  # @return [String, nil] country code, or nil if unsupported
52
61
  def country_for_currency(currency)
53
- CURRENCY_TO_COUNTRY[currency.to_s.upcase]
62
+ currency_to_country[currency.to_s.upcase]
54
63
  end
55
64
 
56
65
  # @param country [String] country code (e.g. "US")
57
66
  # @return [String, nil] currency code, or nil if unsupported
58
67
  def currency_for_country(country)
59
- COUNTRY_TO_CURRENCY[country.to_s.upcase]
68
+ country_to_currency[country.to_s.upcase]
69
+ end
70
+
71
+ class << self
72
+ private
73
+
74
+ def manifest_countries
75
+ DataLoader.load_manifest["countries"] || []
76
+ end
60
77
  end
61
78
  end
62
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Timeprice
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/timeprice.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "timeprice/version"
4
+ require_relative "timeprice/data_loader"
4
5
  require_relative "timeprice/supported"
5
6
  require_relative "timeprice/errors"
6
7
  require_relative "timeprice/point"
7
- require_relative "timeprice/data_loader"
8
8
  require_relative "timeprice/inflation"
9
9
  require_relative "timeprice/exchange"
10
10
  require_relative "timeprice/compare"
@@ -30,7 +30,7 @@ module Timeprice
30
30
  # @param amount [Numeric] the original amount
31
31
  # @param from [String] source date as "YYYY" or "YYYY-MM"
32
32
  # @param to [String] target date as "YYYY" or "YYYY-MM"
33
- # @param country [String] country code from {Supported::COUNTRIES}
33
+ # @param country [String] country code from {Supported.countries}
34
34
  # @return [InflationResult]
35
35
  # @raise [UnsupportedCountry] if `country` is not supported
36
36
  # @raise [DataNotFound] if no CPI point covers `from` or `to`
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -112,20 +112,6 @@ files:
112
112
  - data/cpi/uk.json
113
113
  - data/cpi/us.json
114
114
  - data/cpi/vn.json
115
- - data/fx/usd/1983.json
116
- - data/fx/usd/1986.json
117
- - data/fx/usd/1987.json
118
- - data/fx/usd/1988.json
119
- - data/fx/usd/1989.json
120
- - data/fx/usd/1990.json
121
- - data/fx/usd/1991.json
122
- - data/fx/usd/1992.json
123
- - data/fx/usd/1993.json
124
- - data/fx/usd/1994.json
125
- - data/fx/usd/1995.json
126
- - data/fx/usd/1996.json
127
- - data/fx/usd/1997.json
128
- - data/fx/usd/1998.json
129
115
  - data/fx/usd/1999.json
130
116
  - data/fx/usd/2000.json
131
117
  - data/fx/usd/2001.json
@@ -154,6 +140,8 @@ files:
154
140
  - data/fx/usd/2024.json
155
141
  - data/fx/usd/2025.json
156
142
  - data/fx/usd/2026.json
143
+ - data/fx/usd/_annual.json
144
+ - data/manifest.json
157
145
  - exe/timeprice
158
146
  - lib/timeprice.rb
159
147
  - lib/timeprice/cli.rb
@@ -1,11 +0,0 @@
1
- {
2
- "schema_version": 2,
3
- "base": "USD",
4
- "year": 1983,
5
- "source": "Frankfurter (ECB) for EUR/GBP/JPY (daily); World Bank PA.NUS.FCRF for VND (annual)",
6
- "updated_at": "2026-05-11",
7
- "rates": {},
8
- "annual": {
9
- "VND": 1.0
10
- }
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "schema_version": 2,
3
- "base": "USD",
4
- "year": 1986,
5
- "source": "Frankfurter (ECB) for EUR/GBP/JPY (daily); World Bank PA.NUS.FCRF for VND (annual)",
6
- "updated_at": "2026-05-11",
7
- "rates": {},
8
- "annual": {
9
- "VND": 22.94
10
- }
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "schema_version": 2,
3
- "base": "USD",
4
- "year": 1987,
5
- "source": "Frankfurter (ECB) for EUR/GBP/JPY (daily); World Bank PA.NUS.FCRF for VND (annual)",
6
- "updated_at": "2026-05-11",
7
- "rates": {},
8
- "annual": {
9
- "VND": 78.95
10
- }
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "schema_version": 2,
3
- "base": "USD",
4
- "year": 1988,
5
- "source": "Frankfurter (ECB) for EUR/GBP/JPY (daily); World Bank PA.NUS.FCRF for VND (annual)",
6
- "updated_at": "2026-05-11",
7
- "rates": {},
8
- "annual": {
9
- "VND": 611.65
10
- }
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "schema_version": 2,
3
- "base": "USD",
4
- "year": 1989,
5
- "source": "Frankfurter (ECB) for EUR/GBP/JPY (daily); World Bank PA.NUS.FCRF for VND (annual)",
6
- "updated_at": "2026-05-11",
7
- "rates": {},
8
- "annual": {
9
- "VND": 4501.69
10
- }
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "schema_version": 2,
3
- "base": "USD",
4
- "year": 1990,
5
- "source": "Frankfurter (ECB) for EUR/GBP/JPY (daily); World Bank PA.NUS.FCRF for VND (annual)",
6
- "updated_at": "2026-05-11",
7
- "rates": {},
8
- "annual": {
9
- "VND": 6537.6
10
- }
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "schema_version": 2,
3
- "base": "USD",
4
- "year": 1991,
5
- "source": "Frankfurter (ECB) for EUR/GBP/JPY (daily); World Bank PA.NUS.FCRF for VND (annual)",
6
- "updated_at": "2026-05-11",
7
- "rates": {},
8
- "annual": {
9
- "VND": 10121.89
10
- }
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "schema_version": 2,
3
- "base": "USD",
4
- "year": 1992,
5
- "source": "Frankfurter (ECB) for EUR/GBP/JPY (daily); World Bank PA.NUS.FCRF for VND (annual)",
6
- "updated_at": "2026-05-11",
7
- "rates": {},
8
- "annual": {
9
- "VND": 11202.19
10
- }
11
- }