timeprice 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0ebaf1340f0abefa6b4dcae31f7d2f2d22021155d24ebc258f15b0495f7a480
4
- data.tar.gz: 6068762094c6008c72f4dd7becb10e1e8cb0d0c17b668234e8e0dff84c494cc2
3
+ metadata.gz: 160a9501bc1004df45cd71202f3ae07711068adade2118cfe4605a2e92d92f6f
4
+ data.tar.gz: 68ea05103245f2bb1fb02d701fd5db6e1bb814a2b47d20f6eb9011d94a57b269
5
5
  SHA512:
6
- metadata.gz: fa08a556599384d4ae292064b2342bfdc6976eac3fea784afb7426d623fdd0c8ee43001135bd2f7c3a065e735db8dd6d347459144e44ee702f30505ee23c3c1f
7
- data.tar.gz: 9fd49be55c8790b4a1adfd9350fdd2e6eed82cdcbf8e41c2dd3b1e1514e5b81f9ffa71dc0a9b530738ac2f2d6fa22e969c240bd81dac043e4a4b8e23b21b6924
6
+ metadata.gz: fdddfd9cbcf4cefad6ee73134eb8ef3e5ad5b863aa8d5a78fa789b8890cefec0690cf3c1022f1b596c2bdeaf6f9a8f1122d8ff6ef9adfb7dd393e0aa9346afb6
7
+ data.tar.gz: 4342d02e30f4058f6e7a98eedab2fd0fa54b14031f061bf4d17b093d02f6a1e5cd16ea1f994e7d868cc893fe29119bdae1a398724665f14502c9b135ee1396b0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.0] - 2026-05-11
9
+
10
+ ### Added
11
+ - `Timeprice::Point` value object for compare inputs; `Point.coerce` accepts `Point` instances or 2-tuples in either `[currency, date]` or `[date, currency]` order.
12
+ - `Timeprice::Supported` module — canonical home for `COUNTRIES`, `CURRENCIES`, and the bidirectional currency↔country map. Replaces the duplicated maps in `Compare` and the CLI's `InflationResult` monkey-patch.
13
+ - `Sources::Base` class extracted from the CPI fetchers; `BLS`, `ONS`, and `Eurostat` now subclass it and implement only `fetch` returning `[monthly, annual]`. The drift-check, rebase, merge, write, and summary-log flow is shared.
14
+ - Per-fetcher GitHub Actions `::warning file=…,title=…::` annotations in `scripts/update_data.rb`, so individual fetcher failures show up on the workflow run with a link to the responsible source file.
15
+ - README "Using from Rails / Rake" section covering service objects, Sidekiq, Rake tasks, and `TIMEPRICE_DATA_ROOT`.
16
+ - YARD documentation on the public API (`Timeprice.{inflation,exchange,compare}`, `Inflation`, `Exchange`, `Compare`, `DataLoader`, `Sources`, error classes, `Supported`, `Point`).
17
+
18
+ ### Changed
19
+ - `SUPPORTED_COUNTRIES` / `SUPPORTED_CURRENCIES` are now thin aliases for `Supported::COUNTRIES` / `Supported::CURRENCIES`; existing consumers keep working unchanged.
20
+ - `Compare::CURRENCY_TO_COUNTRY` is now an alias for `Supported::CURRENCY_TO_COUNTRY`.
21
+
8
22
  ## [0.1.2] - 2026-05-11
9
23
 
10
24
  ### Added
data/README.md CHANGED
@@ -155,6 +155,91 @@ inflated = Timeprice.inflation(amount: 100, from: "2010", to: "2024", country: "
155
155
  converted = Timeprice.exchange(amount: inflated, from: "USD", to: "VND", date: "2024-06-30").amount
156
156
  ```
157
157
 
158
+ ## Using from Rails / Rake
159
+
160
+ `timeprice` is a plain Ruby library — no Railtie, no engine, no autoload magic. It works
161
+ the same way as `BigDecimal` or `JSON`: require it once, call the module functions.
162
+
163
+ ### In a Rails app
164
+
165
+ Add the gem to your `Gemfile`:
166
+
167
+ ```ruby
168
+ gem "timeprice", "~> 0.1"
169
+ ```
170
+
171
+ Then call it directly from controllers, jobs, presenters, or service objects. The library
172
+ is thread-safe (data files are loaded once and cached as frozen hashes), so it's safe to
173
+ call from threaded servers (Puma) and Sidekiq workers:
174
+
175
+ ```ruby
176
+ # app/services/historical_price.rb
177
+ class HistoricalPrice
178
+ def self.in_today_dollars(amount, year)
179
+ Timeprice.inflation(
180
+ amount: amount,
181
+ from: year.to_s,
182
+ to: Date.current.strftime("%Y-%m"),
183
+ country: "US"
184
+ ).amount
185
+ end
186
+ end
187
+ ```
188
+
189
+ Errors all inherit from `Timeprice::Error`, so a single rescue covers everything:
190
+
191
+ ```ruby
192
+ rescue Timeprice::Error => e
193
+ Rails.logger.warn("timeprice lookup failed: #{e.message}")
194
+ nil
195
+ end
196
+ ```
197
+
198
+ Result objects respond to `#to_h`, so they serialize cleanly in JSON APIs:
199
+
200
+ ```ruby
201
+ def show
202
+ render json: Timeprice.exchange(amount: 100, from: "USD", to: "EUR", date: params[:date]).to_h
203
+ end
204
+ ```
205
+
206
+ ### In a Rake task
207
+
208
+ ```ruby
209
+ # lib/tasks/inflation.rake
210
+ require "timeprice"
211
+
212
+ namespace :inflation do
213
+ desc "Print 1990→today inflation for the supported countries"
214
+ task :report do
215
+ today = Date.today.strftime("%Y-%m")
216
+ %w[US UK EU JP VN].each do |c|
217
+ r = Timeprice.inflation(amount: 100, from: "1990", to: today, country: c)
218
+ puts "#{c}: 100 in 1990 → #{r.amount.round(2)} in #{today} (#{r.granularity})"
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ ### Configuring the data root
225
+
226
+ By default the gem reads from its bundled `data/` directory. To point at a different
227
+ checkout (useful for testing a new data refresh before releasing it), set
228
+ `TIMEPRICE_DATA_ROOT`:
229
+
230
+ ```bash
231
+ TIMEPRICE_DATA_ROOT=/path/to/timeprice/data bundle exec rake inflation:report
232
+ ```
233
+
234
+ Or programmatically:
235
+
236
+ ```ruby
237
+ Timeprice::DataLoader.data_root = "/path/to/timeprice/data"
238
+ ```
239
+
240
+ Reassigning `data_root` clears the in-memory cache, so it's safe to call between requests
241
+ in development.
242
+
158
243
  ## Data sources & attribution
159
244
 
160
245
  `timeprice` redistributes data from several public sources. Each is governed by its own
data/lib/timeprice/cli.rb CHANGED
@@ -203,12 +203,8 @@ end
203
203
  # bloating the value object — the result doesn't carry currency, only country.
204
204
  module Timeprice
205
205
  class InflationResult
206
- COUNTRY_TO_CURRENCY = {
207
- "US" => "USD", "UK" => "GBP", "EU" => "EUR", "JP" => "JPY", "VN" => "VND"
208
- }.freeze
209
-
210
206
  def country_currency_label
211
- COUNTRY_TO_CURRENCY[country.to_s.upcase] || country.to_s.upcase
207
+ Supported.currency_for_country(country) || country.to_s.upcase
212
208
  end
213
209
  end
214
210
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "errors"
4
+ require_relative "supported"
5
+ require_relative "point"
4
6
  require_relative "inflation"
5
7
  require_relative "exchange"
6
8
 
@@ -26,29 +28,22 @@ module Timeprice
26
28
  # If a future refactor flips the order, the regression test in
27
29
  # spec/timeprice/compare_spec.rb will fail.
28
30
  module Compare
29
- # Map ISO currency → CPI country code.
30
- CURRENCY_TO_COUNTRY = {
31
- "USD" => "US",
32
- "GBP" => "UK",
33
- "EUR" => "EU",
34
- "JPY" => "JP",
35
- "VND" => "VN",
36
- }.freeze
31
+ # Map ISO currency → CPI country code. Kept as a back-compat alias;
32
+ # the canonical map lives in {Supported::CURRENCY_TO_COUNTRY}.
33
+ CURRENCY_TO_COUNTRY = Supported::CURRENCY_TO_COUNTRY
37
34
 
38
35
  module_function
39
36
 
40
- # amount: Numeric
41
- # from: [currency, date_or_year] e.g. ["USD", "2010"] or ["USD", "2010-06"]
42
- # to: [currency, date_or_year]
37
+ # Compare an amount across two (currency, date) points.
38
+ #
39
+ # @param amount [Numeric]
40
+ # @param from [Timeprice::Point, Array(String, String)] source point;
41
+ # accepts a {Point} or a 2-tuple like `["USD", "2010"]` or `["USD", "2010-06"]`
42
+ # @param to [Timeprice::Point, Array(String, String)] destination point
43
+ # @return [CompareResult]
44
+ # @raise [UnsupportedCurrency] if either currency is not in {Supported::CURRENCIES}
43
45
  def run(amount:, from:, to:)
44
- from_currency, from_date = from
45
- to_currency, to_date = to
46
- from_currency = from_currency.to_s.upcase
47
- to_currency = to_currency.to_s.upcase
48
-
49
- to_country = CURRENCY_TO_COUNTRY[to_currency] ||
50
- (raise UnsupportedCurrency, to_currency)
51
- CURRENCY_TO_COUNTRY[from_currency] || (raise UnsupportedCurrency, from_currency)
46
+ from_currency, from_date, to_currency, to_date, to_country = resolve_points(from, to)
52
47
 
53
48
  # Step 1: convert at source date into destination currency.
54
49
  fx_date = normalize_fx_date(from_date)
@@ -84,6 +79,19 @@ module Timeprice
84
79
  )
85
80
  end
86
81
 
82
+ # Coerce both points and resolve to_country. Returns a 5-element tuple.
83
+ def resolve_points(from, to)
84
+ from_point = Point.coerce(from)
85
+ to_point = Point.coerce(to)
86
+ from_currency = from_point.currency
87
+ to_currency = to_point.currency
88
+ to_country = Supported.country_for_currency(to_currency) ||
89
+ (raise UnsupportedCurrency, to_currency)
90
+ Supported.country_for_currency(from_currency) || (raise UnsupportedCurrency, from_currency)
91
+
92
+ [from_currency, from_point.date, to_currency, to_point.date, to_country]
93
+ end
94
+
87
95
  # If the user gave a year like "2010", anchor FX to mid-year (2010-06-30).
88
96
  # If they gave "YYYY-MM", anchor to the 15th. Full dates pass through.
89
97
  def normalize_fx_date(date)
@@ -4,26 +4,41 @@ require "json"
4
4
  require_relative "errors"
5
5
 
6
6
  module Timeprice
7
+ # Loads and caches the bundled JSON data files. Override the search root
8
+ # by setting `TIMEPRICE_DATA_ROOT` in the environment or assigning
9
+ # {DataLoader.data_root=}.
7
10
  module DataLoader
8
11
  SUPPORTED_SCHEMA_VERSION = 1
9
12
 
10
13
  DEFAULT_DATA_ROOT = File.expand_path("../../data", __dir__)
11
14
 
12
15
  class << self
16
+ # @return [String] absolute path to the directory containing `cpi/` and `fx/`
13
17
  def data_root
14
18
  ENV["TIMEPRICE_DATA_ROOT"] || @data_root || DEFAULT_DATA_ROOT
15
19
  end
16
20
 
21
+ # Override the data root and clear caches. Mostly useful in tests.
22
+ # @param path [String]
23
+ # @return [void]
17
24
  def data_root=(path)
18
25
  @data_root = path
19
26
  clear_cache!
20
27
  end
21
28
 
29
+ # Drop in-memory caches of parsed data files.
30
+ # @return [void]
22
31
  def clear_cache!
23
32
  @cpi_cache = {}
24
33
  @fx_cache = {}
25
34
  end
26
35
 
36
+ # Load the CPI series for a supported country.
37
+ # @param country [String]
38
+ # @return [Hash] parsed JSON with "monthly" / "annual" / metadata keys
39
+ # @raise [UnsupportedCountry] if `country` is not in {Supported::COUNTRIES}
40
+ # @raise [DataNotFound] if the file is missing
41
+ # @raise [UnsupportedSchemaVersion] if the file uses a future schema
27
42
  def load_cpi(country)
28
43
  @cpi_cache ||= {}
29
44
  key = country.to_s.downcase
@@ -41,6 +56,10 @@ module Timeprice
41
56
  end
42
57
  end
43
58
 
59
+ # Load the FX rates for a year.
60
+ # @param year [Integer, String]
61
+ # @return [Hash] parsed JSON with a "rates" map of date → currency → Float
62
+ # @raise [DataNotFound] if the per-year file is missing
44
63
  def load_fx_year(year)
45
64
  @fx_cache ||= {}
46
65
  key = year.to_i
@@ -1,29 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "supported"
4
+
3
5
  module Timeprice
6
+ # Base class for every error this library raises. Catch `Timeprice::Error`
7
+ # to handle anything the gem can throw at you.
4
8
  class Error < StandardError; end
5
9
 
6
- SUPPORTED_COUNTRIES = %w[US UK EU JP VN].freeze
7
- SUPPORTED_CURRENCIES = %w[USD GBP EUR JPY VND].freeze
8
-
10
+ # Raised when a country code is not in {Supported::COUNTRIES}.
9
11
  class UnsupportedCountry < Error
10
12
  attr_reader :country
11
13
 
12
14
  def initialize(country)
13
15
  @country = country
14
- super("Unsupported country: #{country.inspect} (supported: #{SUPPORTED_COUNTRIES.join(", ")})")
16
+ super("Unsupported country: #{country.inspect} (supported: #{Supported::COUNTRIES.join(", ")})")
15
17
  end
16
18
  end
17
19
 
20
+ # Raised when a currency code is not in {Supported::CURRENCIES}.
18
21
  class UnsupportedCurrency < Error
19
22
  attr_reader :currency
20
23
 
21
24
  def initialize(currency)
22
25
  @currency = currency
23
- super("Unsupported currency: #{currency.inspect} (supported: #{SUPPORTED_CURRENCIES.join(", ")})")
26
+ super("Unsupported currency: #{currency.inspect} (supported: #{Supported::CURRENCIES.join(", ")})")
24
27
  end
25
28
  end
26
29
 
30
+ # Raised when a requested date falls outside the bundled data range.
27
31
  class DateOutOfRange < Error
28
32
  attr_reader :date, :range
29
33
 
@@ -34,12 +38,15 @@ module Timeprice
34
38
  end
35
39
  end
36
40
 
41
+ # Raised when a CPI or FX lookup has no usable data point.
37
42
  class DataNotFound < Error
38
43
  def initialize(message = "Data not found")
39
44
  super
40
45
  end
41
46
  end
42
47
 
48
+ # Raised when a bundled data file declares a schema_version this gem
49
+ # doesn't know how to parse (forward-compat guard).
43
50
  class UnsupportedSchemaVersion < Error
44
51
  attr_reader :version, :path
45
52
 
@@ -9,6 +9,10 @@ module Timeprice
9
9
  :amount, :original_amount, :from, :to, :date, :effective_date, :rate
10
10
  )
11
11
 
12
+ # Historical FX conversion using bundled per-year USD-base rate files.
13
+ # Handles identity (USD→USD), direct lookup, inverse, and triangulation
14
+ # through USD. Weekend/holiday dates fall back up to {MAX_FALLBACK_DAYS}
15
+ # days to the nearest prior trading day.
12
16
  module Exchange
13
17
  BASE = "USD"
14
18
  MAX_FALLBACK_DAYS = 7
@@ -16,7 +20,14 @@ module Timeprice
16
20
  module_function
17
21
 
18
22
  # Convert `amount` from currency `from` to currency `to` on `date`.
19
- # date: "YYYY-MM-DD".
23
+ #
24
+ # @param amount [Numeric]
25
+ # @param from [String] ISO 4217 source currency
26
+ # @param to [String] ISO 4217 destination currency
27
+ # @param date [String, Date] date as "YYYY-MM-DD" or a Date instance
28
+ # @return [ExchangeResult]
29
+ # @raise [UnsupportedCurrency] if `from` or `to` is not supported
30
+ # @raise [DataNotFound] if no FX point exists within {MAX_FALLBACK_DAYS}
20
31
  def convert(amount:, from:, to:, date:)
21
32
  from = from.to_s.upcase
22
33
  to = to.to_s.upcase
@@ -16,12 +16,21 @@ module Timeprice
16
16
  :from_index, :to_index, :granularity
17
17
  )
18
18
 
19
+ # CPI-based inflation adjustment for the {Supported::COUNTRIES} list.
19
20
  module Inflation
20
21
  module_function
21
22
 
22
23
  # Adjust `amount` from date `from` to date `to` using country CPI.
23
24
  #
24
25
  # Dates accept "YYYY" or "YYYY-MM".
26
+ #
27
+ # @param amount [Numeric]
28
+ # @param from [String] source date ("YYYY" or "YYYY-MM")
29
+ # @param to [String] target date ("YYYY" or "YYYY-MM")
30
+ # @param country [String] country code (see {Supported::COUNTRIES})
31
+ # @return [InflationResult]
32
+ # @raise [UnsupportedCountry] if `country` is not supported
33
+ # @raise [DataNotFound] if no CPI data covers the requested period
25
34
  def adjust(amount:, from:, to:, country:)
26
35
  data = DataLoader.load_cpi(country)
27
36
  from_index, from_gran = lookup_index(data, from)
@@ -41,6 +50,11 @@ module Timeprice
41
50
  end
42
51
 
43
52
  # Inflation rate as decimal (e.g. 0.42 = 42%).
53
+ #
54
+ # @param from [String]
55
+ # @param to [String]
56
+ # @param country [String]
57
+ # @return [Float] decimal rate (positive means inflation, negative deflation)
44
58
  def rate(from:, to:, country:)
45
59
  result = adjust(amount: 1.0, from: from, to: to, country: country)
46
60
  result.amount - 1.0
@@ -0,0 +1,43 @@
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
+ return input if input.is_a?(Point)
25
+
26
+ unless input.is_a?(Array) && input.size == 2
27
+ raise ArgumentError, "Expected Timeprice::Point or [currency, date] tuple, got #{input.inspect}"
28
+ end
29
+
30
+ a, b = input.map(&:to_s)
31
+ currency = [a, b].find { |s| s.match?(/\A[A-Za-z]{3}\z/) }
32
+ date = [a, b].find { |s| s.match?(/\A\d{4}(-\d{2}(-\d{2})?)?\z/) }
33
+
34
+ if currency.nil? || date.nil?
35
+ raise ArgumentError,
36
+ "Could not detect currency + date in #{input.inspect} " \
37
+ "(expected a 3-letter currency and a YYYY[-MM[-DD]] date)"
38
+ end
39
+
40
+ new(currency: currency.upcase, date: date)
41
+ end
42
+ end
43
+ end
@@ -77,6 +77,7 @@ 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
82
  ATTRIBUTIONS.map { |s| s.merge(coverage: coverage_for(s)) }
82
83
  end
@@ -0,0 +1,55 @@
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
+ module_function
25
+
26
+ # @param country [String]
27
+ # @return [Boolean]
28
+ def country?(country)
29
+ COUNTRIES.include?(country.to_s.upcase)
30
+ end
31
+
32
+ # @param currency [String]
33
+ # @return [Boolean]
34
+ def currency?(currency)
35
+ CURRENCIES.include?(currency.to_s.upcase)
36
+ end
37
+
38
+ # @param currency [String] ISO 4217 code (e.g. "USD")
39
+ # @return [String, nil] country code, or nil if unsupported
40
+ def country_for_currency(currency)
41
+ CURRENCY_TO_COUNTRY[currency.to_s.upcase]
42
+ end
43
+
44
+ # @param country [String] country code (e.g. "US")
45
+ # @return [String, nil] currency code, or nil if unsupported
46
+ def currency_for_country(country)
47
+ COUNTRY_TO_CURRENCY[country.to_s.upcase]
48
+ end
49
+ end
50
+
51
+ # Back-compat aliases — keep the old top-level constants pointing at the
52
+ # canonical lists so existing requires of "errors" keep working.
53
+ SUPPORTED_COUNTRIES = Supported::COUNTRIES
54
+ SUPPORTED_CURRENCIES = Supported::CURRENCIES
55
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Timeprice
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
@@ -162,7 +162,9 @@ files:
162
162
  - lib/timeprice/errors.rb
163
163
  - lib/timeprice/exchange.rb
164
164
  - lib/timeprice/inflation.rb
165
+ - lib/timeprice/point.rb
165
166
  - lib/timeprice/sources.rb
167
+ - lib/timeprice/supported.rb
166
168
  - lib/timeprice/version.rb
167
169
  homepage: https://github.com/patrick204nqh/timeprice
168
170
  licenses: