Module: Timeprice::Compare

Defined in:
lib/timeprice/compare.rb

Overview

Compare combines FX and inflation across two (currency, date) points.

CONVENTION (critical): convert at SOURCE date first, then inflate in destination currency. See README.md “Compare semantics” section.

This preserves purchasing-power equivalence in the destination economy. The naive alternative (inflate in source currency first, then convert at destination date) double-counts source-country inflation because nominal FX rates already absorb relative inflation between the two currencies.

If a future refactor flips the order, the regression test in spec/timeprice/compare_spec.rb will fail.

Constant Summary collapse

CURRENCY_TO_COUNTRY =

Map ISO currency → CPI country code. Kept as a back-compat alias; the canonical map lives in Supported::CURRENCY_TO_COUNTRY.

Supported::CURRENCY_TO_COUNTRY

Class Method Summary collapse

Class Method Details

.normalize_fx_date(date) ⇒ Object

If the user gave a year like “2010”, anchor FX to mid-year (2010-06-30). If they gave “YYYY-MM”, anchor to the 15th. Full dates pass through.



97
98
99
100
101
102
103
104
105
# File 'lib/timeprice/compare.rb', line 97

def normalize_fx_date(date)
  s = date.to_s
  case s
  when /\A\d{4}\z/ then "#{s}-06-30"
  when /\A\d{4}-\d{2}\z/ then "#{s}-15"
  when /\A\d{4}-\d{2}-\d{2}\z/ then s
  else raise ArgumentError, "Invalid date for compare: #{date.inspect}"
  end
end

.resolve_points(from, to) ⇒ Object

Coerce both points and resolve to_country. Returns a 5-element tuple.



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/timeprice/compare.rb', line 83

def resolve_points(from, to)
  from_point = Point.coerce(from)
  to_point   = Point.coerce(to)
  from_currency = from_point.currency
  to_currency   = to_point.currency
  to_country = Supported.country_for_currency(to_currency) ||
               (raise UnsupportedCurrency, to_currency)
  Supported.country_for_currency(from_currency) || (raise UnsupportedCurrency, from_currency)

  [from_currency, from_point.date, to_currency, to_point.date, to_country]
end

.run(amount:, from:, to:) ⇒ CompareResult

Compare an amount across two (currency, date) points.

Parameters:

  • amount (Numeric)
  • from (Timeprice::Point, Array(String, String))

    source point; accepts a Point or a 2-tuple like ‘[“USD”, “2010”]` or `[“USD”, “2010-06”]`

  • to (Timeprice::Point, Array(String, String))

    destination point

Returns:

Raises:



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/timeprice/compare.rb', line 45

def run(amount:, from:, to:)
  from_currency, from_date, to_currency, to_date, to_country = resolve_points(from, to)

  # Step 1: convert at source date into destination currency.
  fx_date = normalize_fx_date(from_date)
  fx_result = Exchange.convert(
    amount: amount,
    from: from_currency,
    to: to_currency,
    date: fx_date
  )
  converted = fx_result.amount

  # Step 2: inflate that destination-currency amount from source date to
  # destination date using destination-country CPI.
  infl = Inflation.adjust(
    amount: converted,
    from: from_date.to_s,
    to: to_date.to_s,
    country: to_country
  )

  CompareResult.new(
    amount: infl.amount,
    original_amount: amount.to_f,
    from_currency: from_currency,
    from_date: from_date.to_s,
    to_currency: to_currency,
    to_date: to_date.to_s,
    country: to_country,
    fx_rate: fx_result.rate,
    cpi_ratio: infl.to_index.to_f / infl.from_index,
    converted_amount: converted,
    granularity: infl.granularity
  )
end