timeprice 0.5.0 → 0.6.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 +30 -0
- data/DATA_LICENSES.md +16 -1
- data/README.md +14 -6
- data/data/cpi/eu.json +1 -1
- data/data/cpi/jp.json +1 -1
- data/data/cpi/uk.json +1 -1
- data/data/cpi/us.json +1 -1
- data/data/cpi/vn.json +1 -1
- data/data/fx/usd/1999.json +1 -1
- data/data/fx/usd/2000.json +1 -1
- data/data/fx/usd/2001.json +1 -1
- data/data/fx/usd/2002.json +1 -1
- data/data/fx/usd/2003.json +1 -1
- data/data/fx/usd/2004.json +1 -1
- data/data/fx/usd/2005.json +1 -1
- data/data/fx/usd/2006.json +1 -1
- data/data/fx/usd/2007.json +1 -1
- data/data/fx/usd/2008.json +1 -1
- data/data/fx/usd/2009.json +1 -1
- data/data/fx/usd/2010.json +1 -1
- data/data/fx/usd/2011.json +1 -1
- data/data/fx/usd/2012.json +1 -1
- data/data/fx/usd/2013.json +1 -1
- data/data/fx/usd/2014.json +1 -1
- data/data/fx/usd/2015.json +1 -1
- data/data/fx/usd/2016.json +1 -1
- data/data/fx/usd/2017.json +1 -1
- data/data/fx/usd/2018.json +1 -1
- data/data/fx/usd/2019.json +1 -1
- data/data/fx/usd/2020.json +1 -1
- data/data/fx/usd/2021.json +1 -1
- data/data/fx/usd/2022.json +1 -1
- data/data/fx/usd/2023.json +1 -1
- data/data/fx/usd/2024.json +1 -1
- data/data/fx/usd/2025.json +1 -1
- data/data/fx/usd/2026.json +1 -1
- data/data/fx/usd/_annual.json +1 -1
- data/data/manifest.json +1 -1
- data/lib/timeprice/cli.rb +3 -3
- data/lib/timeprice/cpi_lookup.rb +64 -18
- data/lib/timeprice/data_loader.rb +6 -2
- data/lib/timeprice/granularity.rb +41 -10
- data/lib/timeprice/inflation.rb +3 -3
- data/lib/timeprice/supported.rb +1 -1
- data/lib/timeprice/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f350843e8e12653e2f798d8f9afec193001e911e0f36910d0473724f33d79f8
|
|
4
|
+
data.tar.gz: 3d1107eb101dd95ec6c2203fe6f1843f9249cf5046cf838577094c67a0fe63c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7efbb6d151b4fd4c9c6d4697fc9c755c9a0d2e6b8e1e32d828a1416737dbfbd00d9143c64edde191eaea3f7f758b96ac1d4b93c9f765fbc5d0993bf68b0be7a
|
|
7
|
+
data.tar.gz: 3a5b598b62cf93b992ed4ba5d069f84475c91a0be636600451fb3247caaa5c494c44b39268ed5e6107c34f923606521d42374d3300de42ad5a7449f53ae37e39
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-05-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Five new countries: AU, CA, KR, CN, RU.** CPI + FX coverage:
|
|
12
|
+
- AU (Australia): quarterly CPI from ABS 6401.0, annual baseline from World
|
|
13
|
+
Bank `FP.CPI.TOTL`; AUD daily FX from Frankfurter.
|
|
14
|
+
- CA (Canada): monthly CPI from Statistics Canada WDS (table 18-10-0004-01),
|
|
15
|
+
annual baseline from World Bank; CAD daily FX from Frankfurter.
|
|
16
|
+
- KR (Korea, Rep.): monthly CPI from IMF Data Portal CPI dataflow, annual
|
|
17
|
+
baseline from World Bank; KRW daily FX from Frankfurter. (KOSIS Open API
|
|
18
|
+
is a future upgrade path — see `scripts/sources/kosis.rb`.)
|
|
19
|
+
- CN (China): annual CPI from World Bank, monthly layered from IMF Data
|
|
20
|
+
Portal; CNY daily FX from Frankfurter.
|
|
21
|
+
- RU (Russia): annual CPI from World Bank, monthly layered from IMF; RUB FX
|
|
22
|
+
from IMF IFS `ENDA_XDC_USD_RATE` written as annual averages to
|
|
23
|
+
`_annual.json` (Frankfurter dropped RUB after the ECB suspended reference
|
|
24
|
+
rates in March 2022, so daily RUB is intentionally not bundled).
|
|
25
|
+
- `KRW` added to `Supported::ZERO_DECIMAL_CURRENCIES`.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- **Schema v3 → v4.** Backward-compatible bump:
|
|
29
|
+
- CPI files gain an optional `series.quarterly` block (keyed `YYYY-Qn`)
|
|
30
|
+
alongside `series.monthly` and `series.annual`. Files without quarterly
|
|
31
|
+
data are byte-identical to v3 layout other than the version field.
|
|
32
|
+
- `DataLoader` accepts both v3 and v4 files; new writes are v4.
|
|
33
|
+
- `Granularity` gains `:quarterly`, `:annual_from_quarterly_avg`,
|
|
34
|
+
`:quarterly_from_annual_fallback`, and `:monthly_from_quarterly_fallback`.
|
|
35
|
+
`CpiLookup` resolves "YYYY-Qn" keys and falls back monthly → quarterly →
|
|
36
|
+
annual.
|
|
37
|
+
|
|
8
38
|
## [0.5.0] - 2026-05-11
|
|
9
39
|
|
|
10
40
|
### Changed
|
data/DATA_LICENSES.md
CHANGED
|
@@ -14,8 +14,15 @@ string.
|
|
|
14
14
|
| World Bank | `FP.CPI.TOTL` (JP CPI fallback) | Creative Commons Attribution 4.0 International (CC BY 4.0) | https://datacatalog.worldbank.org/public-licenses#cc-by | Source: World Bank, FP.CPI.TOTL |
|
|
15
15
|
| World Bank | `FP.CPI.TOTL` (VN CPI, annual fallback) | Creative Commons Attribution 4.0 International (CC BY 4.0) | https://datacatalog.worldbank.org/public-licenses#cc-by | Source: World Bank, FP.CPI.TOTL |
|
|
16
16
|
| International Monetary Fund | CPI dataflow `VNM.CPI._T.IX.M` via IMF Data Portal (VN CPI, monthly primary) | Free reuse with attribution per IMF terms | https://www.imf.org/external/terms.htm | Source: IMF Data Portal CPI dataflow |
|
|
17
|
-
| European Central Bank (via Frankfurter) | Daily reference rates, USD base, EUR/GBP/JPY | ECB reference rates — free reuse; Frankfurter is a non-commercial republisher with no separate license | https://www.ecb.europa.eu/services/disclaimer/html/index.en.html | FX data: European Central Bank reference rates via Frankfurter |
|
|
17
|
+
| European Central Bank (via Frankfurter) | Daily reference rates, USD base, EUR/GBP/JPY/AUD/CAD/KRW/CNY | ECB reference rates — free reuse; Frankfurter is a non-commercial republisher with no separate license | https://www.ecb.europa.eu/services/disclaimer/html/index.en.html | FX data: European Central Bank reference rates via Frankfurter |
|
|
18
18
|
| World Bank | `PA.NUS.FCRF` (VND annual average FX, broadcast daily) | Creative Commons Attribution 4.0 International (CC BY 4.0) | https://datacatalog.worldbank.org/public-licenses#cc-by | VND FX: World Bank, PA.NUS.FCRF |
|
|
19
|
+
| Australian Bureau of Statistics | `CPI` dataflow, key `3.10001.10.50.Q` (AU CPI, quarterly, all groups, weighted average of eight capital cities) | Creative Commons Attribution 4.0 International (CC BY 4.0) | https://www.abs.gov.au/website-privacy-copyright-and-disclaimer/copyright-and-creative-commons | Source: Australian Bureau of Statistics, 6401.0 Consumer Price Index |
|
|
20
|
+
| Statistics Canada | Table 18-10-0004-01, vector `v41690973` (CA CPI, monthly, all-items, not seasonally adjusted) | Statistics Canada Open License | https://www.statcan.gc.ca/en/reference/licence | Source: Statistics Canada, table 18-10-0004-01 |
|
|
21
|
+
| International Monetary Fund | CPI dataflow `KOR.CPI._T.IX.M` (KR CPI, monthly) | Free reuse with attribution per IMF terms | https://www.imf.org/external/terms.htm | Source: IMF Data Portal CPI dataflow |
|
|
22
|
+
| International Monetary Fund | CPI dataflow `CHN.CPI._T.IX.M` (CN CPI, monthly) | Free reuse with attribution per IMF terms | https://www.imf.org/external/terms.htm | Source: IMF Data Portal CPI dataflow |
|
|
23
|
+
| International Monetary Fund | CPI dataflow `RUS.CPI._T.IX.M` (RU CPI, monthly) | Free reuse with attribution per IMF terms | https://www.imf.org/external/terms.htm | Source: IMF Data Portal CPI dataflow |
|
|
24
|
+
| International Monetary Fund | IFS dataflow `M.RUS.ENDA_XDC_USD_RATE` (RUB/USD, period-average, annual mean written to `_annual.json`) | Free reuse with attribution per IMF terms | https://www.imf.org/external/terms.htm | Source: IMF International Financial Statistics |
|
|
25
|
+
| World Bank | `FP.CPI.TOTL` (AU/CA/KR/CN/RU CPI annual baselines) | Creative Commons Attribution 4.0 International (CC BY 4.0) | https://datacatalog.worldbank.org/public-licenses#cc-by | Source: World Bank, FP.CPI.TOTL |
|
|
19
26
|
|
|
20
27
|
## Notes
|
|
21
28
|
|
|
@@ -25,6 +32,14 @@ string.
|
|
|
25
32
|
- The Vietnam VND FX series is the **annual average** broadcast to every day in
|
|
26
33
|
the year — it is intentionally not a daily market rate. Do not use it for
|
|
27
34
|
intraday or trade-execution purposes.
|
|
35
|
+
- **RUB FX is annual-only** (from IMF IFS period averages). Frankfurter (ECB)
|
|
36
|
+
stopped publishing RUB daily reference rates in March 2022 after the ECB
|
|
37
|
+
suspended the rate, and no other free, no-API-key daily source covers the
|
|
38
|
+
full series. Daily RUB lookups fall back to the annual average and the
|
|
39
|
+
result is tagged so consumers can detect the degradation.
|
|
40
|
+
- **AU CPI is published quarterly only.** Lookups against "YYYY-MM" keys for
|
|
41
|
+
Australia fall back to the quarter that contains the month, and the result
|
|
42
|
+
is tagged `:monthly_from_quarterly_fallback`.
|
|
28
43
|
- Eurostat HICP is harmonized across the Eurozone and is **not** the same as
|
|
29
44
|
any national CPI. We use it for the `EU` country code; national CPIs are out
|
|
30
45
|
of scope for v0.1.
|
data/README.md
CHANGED
|
@@ -99,11 +99,19 @@ Coverage is derived from the bundled `data/` files. Re-check with `timeprice sou
|
|
|
99
99
|
| Eurozone (EA) | EUR | Eurostat HICP (`prc_hicp_midx`) | Monthly + annual | 1996-01 → present |
|
|
100
100
|
| Japan | JPY | World Bank `FP.CPI.TOTL` (fallback) | Annual | 1960 → 2024 |
|
|
101
101
|
| Vietnam | VND | IMF Data Portal CPI dataflow (monthly primary) + World Bank `FP.CPI.TOTL` (annual fallback) | Monthly + annual | 1995 → present |
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
| Australia | AUD | ABS 6401.0 (quarterly) + World Bank `FP.CPI.TOTL` (annual baseline) | Quarterly + annual | 1948-Q3 → present |
|
|
103
|
+
| Canada | CAD | Statistics Canada WDS (table 18-10-0004-01, vector `v41690973`) + World Bank annual baseline | Monthly + annual | 1914-01 → present |
|
|
104
|
+
| Korea (Rep.) | KRW | IMF Data Portal CPI dataflow + World Bank annual baseline | Monthly + annual | 1990-01 → present |
|
|
105
|
+
| China | CNY | World Bank `FP.CPI.TOTL` (annual primary) + IMF Data Portal CPI dataflow (monthly layer) | Monthly + annual | 1990-01 → present |
|
|
106
|
+
| Russia | RUB | World Bank `FP.CPI.TOTL` (annual primary) + IMF Data Portal CPI dataflow (monthly layer) | Monthly + annual | 1992-01 → present |
|
|
107
|
+
|
|
108
|
+
**FX (USD base):** ECB reference rates via Frankfurter for **EUR / GBP / JPY / AUD /
|
|
109
|
+
CAD / KRW / CNY**, daily 1999 → present. **VND** uses the World Bank annual average
|
|
110
|
+
(`PA.NUS.FCRF`), one value per year, from 1983 → present. **RUB** uses IMF IFS
|
|
111
|
+
period-average rates as annual means (`ENDA_XDC_USD_RATE`) — Frankfurter dropped RUB
|
|
112
|
+
after the ECB suspended its reference rate in March 2022, so daily RUB is intentionally
|
|
113
|
+
not bundled. Annual-fallback results (VND, RUB) are tagged `granularity: :annual` so
|
|
114
|
+
callers know they did not get a daily rate.
|
|
107
115
|
|
|
108
116
|
Triangulated cross-rates (e.g. GBP → JPY) go through USD on the same effective date.
|
|
109
117
|
Weekend/holiday dates fall back up to 7 days to the nearest prior trading day.
|
|
@@ -223,7 +231,7 @@ namespace :inflation do
|
|
|
223
231
|
desc "Print 1990→today inflation for the supported countries"
|
|
224
232
|
task :report do
|
|
225
233
|
today = Date.today.strftime("%Y-%m")
|
|
226
|
-
%w[US UK EU JP VN].each do |c|
|
|
234
|
+
%w[US UK EU JP VN AU CA KR CN RU].each do |c|
|
|
227
235
|
r = Timeprice.inflation(amount: 100, from: "1990", to: today, country: c)
|
|
228
236
|
puts "#{c}: 100 in 1990 → #{r.amount.round(2)} in #{today} (#{r.granularity})"
|
|
229
237
|
end
|
data/data/cpi/eu.json
CHANGED
data/data/cpi/jp.json
CHANGED
data/data/cpi/uk.json
CHANGED
data/data/cpi/us.json
CHANGED
data/data/cpi/vn.json
CHANGED
data/data/fx/usd/1999.json
CHANGED
data/data/fx/usd/2000.json
CHANGED
data/data/fx/usd/2001.json
CHANGED
data/data/fx/usd/2002.json
CHANGED
data/data/fx/usd/2003.json
CHANGED
data/data/fx/usd/2004.json
CHANGED
data/data/fx/usd/2005.json
CHANGED
data/data/fx/usd/2006.json
CHANGED
data/data/fx/usd/2007.json
CHANGED
data/data/fx/usd/2008.json
CHANGED
data/data/fx/usd/2009.json
CHANGED
data/data/fx/usd/2010.json
CHANGED
data/data/fx/usd/2011.json
CHANGED
data/data/fx/usd/2012.json
CHANGED
data/data/fx/usd/2013.json
CHANGED
data/data/fx/usd/2014.json
CHANGED
data/data/fx/usd/2015.json
CHANGED
data/data/fx/usd/2016.json
CHANGED
data/data/fx/usd/2017.json
CHANGED
data/data/fx/usd/2018.json
CHANGED
data/data/fx/usd/2019.json
CHANGED
data/data/fx/usd/2020.json
CHANGED
data/data/fx/usd/2021.json
CHANGED
data/data/fx/usd/2022.json
CHANGED
data/data/fx/usd/2023.json
CHANGED
data/data/fx/usd/2024.json
CHANGED
data/data/fx/usd/2025.json
CHANGED
data/data/fx/usd/2026.json
CHANGED
data/data/fx/usd/_annual.json
CHANGED
data/data/manifest.json
CHANGED
data/lib/timeprice/cli.rb
CHANGED
|
@@ -76,9 +76,9 @@ module Timeprice
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
desc "inflation AMOUNT", "Inflation-adjust an amount between two dates"
|
|
79
|
-
method_option :from, type: :string, required: true, desc: "Source date (YYYY or YYYY-
|
|
80
|
-
method_option :to, type: :string, required: true, desc: "Target date (YYYY or YYYY-
|
|
81
|
-
method_option :country, type: :string, required: true, desc: "Country code (US, UK, EU, JP, VN)"
|
|
79
|
+
method_option :from, type: :string, required: true, desc: "Source date (YYYY, YYYY-MM, or YYYY-Qn)"
|
|
80
|
+
method_option :to, type: :string, required: true, desc: "Target date (YYYY, YYYY-MM, or YYYY-Qn)"
|
|
81
|
+
method_option :country, type: :string, required: true, desc: "Country code (US, UK, EU, JP, VN, AU, CA, KR, CN, RU)"
|
|
82
82
|
def inflation(amount)
|
|
83
83
|
with_error_handling do
|
|
84
84
|
result = Timeprice.inflation(
|
data/lib/timeprice/cpi_lookup.rb
CHANGED
|
@@ -8,54 +8,100 @@ module Timeprice
|
|
|
8
8
|
# resolved. See {Granularity} for the full set of possible tags.
|
|
9
9
|
CpiPoint = Data.define(:value, :granularity)
|
|
10
10
|
|
|
11
|
-
# Resolves CPI keys ("YYYY" or "YYYY-
|
|
12
|
-
# country's parsed CPI data hash. Knowing the JSON shape ("monthly"
|
|
13
|
-
# "annual" string keys) is isolated here — Inflation just
|
|
11
|
+
# Resolves CPI keys ("YYYY", "YYYY-MM", or "YYYY-Qn") to a CpiPoint against
|
|
12
|
+
# a single country's parsed CPI data hash. Knowing the JSON shape ("monthly"
|
|
13
|
+
# / "quarterly" / "annual" string keys) is isolated here — Inflation just
|
|
14
|
+
# asks for points.
|
|
14
15
|
class CpiLookup
|
|
16
|
+
QUARTER_RE = /\A(\d{4})-Q([1-4])\z/
|
|
17
|
+
|
|
15
18
|
def initialize(data)
|
|
16
19
|
@data = data
|
|
17
|
-
@monthly
|
|
18
|
-
@
|
|
20
|
+
@monthly = data.dig("series", "monthly") || {}
|
|
21
|
+
@quarterly = data.dig("series", "quarterly") || {}
|
|
22
|
+
@annual = data.dig("series", "annual") || {}
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
# @param key [String] "YYYY" or "YYYY-
|
|
25
|
+
# @param key [String] "YYYY", "YYYY-MM", or "YYYY-Qn"
|
|
22
26
|
# @return [CpiPoint]
|
|
23
27
|
# @raise [DataNotFound] if no CPI value covers `key`
|
|
24
28
|
# @raise [ArgumentError] on malformed `key`
|
|
25
29
|
def at(key)
|
|
26
30
|
key = key.to_s
|
|
27
31
|
case key
|
|
28
|
-
when
|
|
29
|
-
when /\A\d{4}\z/
|
|
30
|
-
|
|
32
|
+
when QUARTER_RE then quarterly_or_fallbacks(key)
|
|
33
|
+
when /\A\d{4}-\d{2}\z/ then monthly_or_fallbacks(key)
|
|
34
|
+
when /\A\d{4}\z/ then annual_or_derived(key)
|
|
35
|
+
else raise ArgumentError, "Invalid date format: #{key.inspect} (use YYYY, YYYY-MM, or YYYY-Qn)"
|
|
31
36
|
end
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
private
|
|
35
40
|
|
|
36
|
-
def
|
|
41
|
+
def monthly_or_fallbacks(month_key)
|
|
37
42
|
return CpiPoint.new(value: @monthly[month_key], granularity: Granularity::MONTHLY) if @monthly.key?(month_key)
|
|
38
43
|
|
|
39
|
-
year = month_key
|
|
40
|
-
|
|
44
|
+
year, month = month_key.split("-").map(&:to_i)
|
|
45
|
+
qkey = format("%04d-Q%d", year, ((month - 1) / 3) + 1)
|
|
46
|
+
if @quarterly.key?(qkey)
|
|
47
|
+
return CpiPoint.new(value: @quarterly[qkey], granularity: Granularity::MONTHLY_FROM_QUARTERLY_FALLBACK)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
year_key = month_key[0, 4]
|
|
51
|
+
raise DataNotFound, missing_message(month_key) unless @annual.key?(year_key)
|
|
41
52
|
|
|
42
|
-
CpiPoint.new(value: @annual[
|
|
53
|
+
CpiPoint.new(value: @annual[year_key], granularity: Granularity::MONTHLY_FROM_ANNUAL_FALLBACK)
|
|
43
54
|
end
|
|
44
55
|
|
|
45
|
-
def
|
|
56
|
+
def quarterly_or_fallbacks(quarter_key)
|
|
57
|
+
if @quarterly.key?(quarter_key)
|
|
58
|
+
return CpiPoint.new(value: @quarterly[quarter_key],
|
|
59
|
+
granularity: Granularity::QUARTERLY)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
year_int, q = quarter_key.match(QUARTER_RE).captures.map(&:to_i)
|
|
63
|
+
first_month = ((q - 1) * 3) + 1
|
|
64
|
+
last_month = q * 3
|
|
65
|
+
months = (first_month..last_month).map { |m| format("%04d-%02d", year_int, m) }
|
|
66
|
+
.map { |k| @monthly[k] }
|
|
67
|
+
.compact
|
|
68
|
+
if months.size == 3
|
|
69
|
+
return CpiPoint.new(value: months.sum.to_f / 3,
|
|
70
|
+
granularity: Granularity::QUARTERLY_FROM_MONTHLY_AVG)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
year = quarter_key[0, 4]
|
|
74
|
+
raise DataNotFound, missing_message(quarter_key) unless @annual.key?(year)
|
|
75
|
+
|
|
76
|
+
CpiPoint.new(value: @annual[year], granularity: Granularity::QUARTERLY_FROM_ANNUAL_FALLBACK)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def annual_or_derived(year)
|
|
46
80
|
return CpiPoint.new(value: @annual[year], granularity: Granularity::ANNUAL) if @annual.key?(year)
|
|
47
81
|
|
|
48
|
-
months
|
|
49
|
-
|
|
82
|
+
months = @monthly.select { |k, _| k.start_with?("#{year}-") }
|
|
83
|
+
quarters = @quarterly.select { |k, _| k.start_with?("#{year}-Q") }
|
|
84
|
+
|
|
85
|
+
# Prefer complete-period averages over partials, and within each, prefer
|
|
86
|
+
# monthly resolution. Partial tags distinguish biased estimates (e.g.
|
|
87
|
+
# only Jan-Feb populated) from a true full-year mean.
|
|
88
|
+
return average(months, 12, Granularity::ANNUAL_FROM_MONTHLY_AVG) if months.size == 12
|
|
89
|
+
return average(quarters, 4, Granularity::ANNUAL_FROM_QUARTERLY_AVG) if quarters.size == 4
|
|
90
|
+
return average(months, months.size, Granularity::ANNUAL_FROM_PARTIAL_MONTHS) if months.any?
|
|
91
|
+
return average(quarters, quarters.size, Granularity::ANNUAL_FROM_PARTIAL_QUARTERS) if quarters.any?
|
|
92
|
+
|
|
93
|
+
raise DataNotFound, missing_message(year)
|
|
94
|
+
end
|
|
50
95
|
|
|
51
|
-
|
|
52
|
-
CpiPoint.new(value:
|
|
96
|
+
def average(series, divisor, granularity)
|
|
97
|
+
CpiPoint.new(value: series.values.sum.to_f / divisor, granularity: granularity)
|
|
53
98
|
end
|
|
54
99
|
|
|
55
100
|
def missing_message(key)
|
|
56
101
|
country = @data["country"]
|
|
57
102
|
ranges = []
|
|
58
103
|
ranges << "monthly #{@monthly.keys.min}..#{@monthly.keys.max}" if @monthly.any?
|
|
104
|
+
ranges << "quarterly #{@quarterly.keys.min}..#{@quarterly.keys.max}" if @quarterly.any?
|
|
59
105
|
ranges << "annual #{@annual.keys.min}..#{@annual.keys.max}" if @annual.any?
|
|
60
106
|
hint = ranges.empty? ? "" : " (supported: #{ranges.join(", ")})"
|
|
61
107
|
"No CPI data for #{key.inspect} in #{country}#{hint}"
|
|
@@ -8,7 +8,11 @@ module Timeprice
|
|
|
8
8
|
# by setting `TIMEPRICE_DATA_ROOT` in the environment or assigning
|
|
9
9
|
# {DataLoader.data_root=}.
|
|
10
10
|
module DataLoader
|
|
11
|
-
SUPPORTED_SCHEMA_VERSION =
|
|
11
|
+
SUPPORTED_SCHEMA_VERSION = 4
|
|
12
|
+
|
|
13
|
+
# Files written by older toolchains remain readable: v3 is monthly+annual
|
|
14
|
+
# only; v4 adds an optional `series.quarterly` block.
|
|
15
|
+
SUPPORTED_SCHEMA_VERSIONS = [3, 4].freeze
|
|
12
16
|
|
|
13
17
|
DEFAULT_DATA_ROOT = File.expand_path("../../data", __dir__)
|
|
14
18
|
|
|
@@ -114,7 +118,7 @@ module Timeprice
|
|
|
114
118
|
def parse_with_schema(path)
|
|
115
119
|
data = JSON.parse(File.read(path))
|
|
116
120
|
version = data["schema_version"]
|
|
117
|
-
raise UnsupportedSchemaVersion.new(version, path) unless version
|
|
121
|
+
raise UnsupportedSchemaVersion.new(version, path) unless SUPPORTED_SCHEMA_VERSIONS.include?(version)
|
|
118
122
|
|
|
119
123
|
data
|
|
120
124
|
end
|
|
@@ -4,20 +4,44 @@ module Timeprice
|
|
|
4
4
|
# Closed set of CPI-resolution granularities and the rules for combining /
|
|
5
5
|
# rendering them. Owns the lattice so callers don't hand-maintain it.
|
|
6
6
|
module Granularity
|
|
7
|
-
DAILY
|
|
8
|
-
MONTHLY
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
DAILY = :daily
|
|
8
|
+
MONTHLY = :monthly
|
|
9
|
+
QUARTERLY = :quarterly
|
|
10
|
+
ANNUAL = :annual
|
|
11
|
+
ANNUAL_FROM_MONTHLY_AVG = :annual_from_monthly_avg
|
|
12
|
+
ANNUAL_FROM_QUARTERLY_AVG = :annual_from_quarterly_avg
|
|
13
|
+
ANNUAL_FROM_PARTIAL_MONTHS = :annual_from_partial_months
|
|
14
|
+
ANNUAL_FROM_PARTIAL_QUARTERS = :annual_from_partial_quarters
|
|
15
|
+
QUARTERLY_FROM_ANNUAL_FALLBACK = :quarterly_from_annual_fallback
|
|
16
|
+
QUARTERLY_FROM_MONTHLY_AVG = :quarterly_from_monthly_avg
|
|
17
|
+
MONTHLY_FROM_QUARTERLY_FALLBACK = :monthly_from_quarterly_fallback
|
|
18
|
+
MONTHLY_FROM_ANNUAL_FALLBACK = :monthly_from_annual_fallback
|
|
12
19
|
|
|
13
|
-
# Most-degraded first — `merge` returns the first match.
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
20
|
+
# Most-degraded first — `merge` returns the first match. DAILY is the
|
|
21
|
+
# highest-precision FX tag; MONTHLY is the highest-precision CPI tag.
|
|
22
|
+
# Compare uses merge() across both legs, so the most-degraded tag in
|
|
23
|
+
# either leg wins.
|
|
24
|
+
#
|
|
25
|
+
# Ordering rationale (worst → best):
|
|
26
|
+
# 1. Cross-grain fallbacks where the asked resolution is finer than
|
|
27
|
+
# what's available (annual stretched to month/quarter).
|
|
28
|
+
# 2. Partial-period averages — asked annual but only some months/
|
|
29
|
+
# quarters in the year are populated. Highly biased by seasonality.
|
|
30
|
+
# 3. Same-or-coarser fallback (quarter stretched to month).
|
|
31
|
+
# 4. Full-period derived averages (complete 4-quarter or 12-month mean
|
|
32
|
+
# standing in for the asked coarser resolution).
|
|
33
|
+
# 5. Native series at the asked resolution.
|
|
17
34
|
PRECEDENCE = [
|
|
18
35
|
MONTHLY_FROM_ANNUAL_FALLBACK,
|
|
36
|
+
ANNUAL_FROM_PARTIAL_QUARTERS,
|
|
37
|
+
ANNUAL_FROM_PARTIAL_MONTHS,
|
|
38
|
+
QUARTERLY_FROM_ANNUAL_FALLBACK,
|
|
39
|
+
MONTHLY_FROM_QUARTERLY_FALLBACK,
|
|
40
|
+
ANNUAL_FROM_QUARTERLY_AVG,
|
|
41
|
+
QUARTERLY_FROM_MONTHLY_AVG,
|
|
19
42
|
ANNUAL_FROM_MONTHLY_AVG,
|
|
20
43
|
ANNUAL,
|
|
44
|
+
QUARTERLY,
|
|
21
45
|
MONTHLY,
|
|
22
46
|
DAILY,
|
|
23
47
|
].freeze
|
|
@@ -25,9 +49,16 @@ module Timeprice
|
|
|
25
49
|
HUMAN_LABELS = {
|
|
26
50
|
DAILY => "daily",
|
|
27
51
|
MONTHLY => "monthly",
|
|
52
|
+
QUARTERLY => "quarterly",
|
|
28
53
|
ANNUAL => "annual",
|
|
29
54
|
ANNUAL_FROM_MONTHLY_AVG => "annual (avg of months)",
|
|
30
|
-
|
|
55
|
+
ANNUAL_FROM_QUARTERLY_AVG => "annual (avg of quarters)",
|
|
56
|
+
ANNUAL_FROM_PARTIAL_MONTHS => "annual (partial-year, avg of available months)",
|
|
57
|
+
ANNUAL_FROM_PARTIAL_QUARTERS => "annual (partial-year, avg of available quarters)",
|
|
58
|
+
QUARTERLY_FROM_ANNUAL_FALLBACK => "quarter (annual fallback)",
|
|
59
|
+
QUARTERLY_FROM_MONTHLY_AVG => "quarter (avg of months)",
|
|
60
|
+
MONTHLY_FROM_QUARTERLY_FALLBACK => "month (quarter unavailable)",
|
|
61
|
+
MONTHLY_FROM_ANNUAL_FALLBACK => "month (annual fallback)",
|
|
31
62
|
}.freeze
|
|
32
63
|
|
|
33
64
|
module_function
|
data/lib/timeprice/inflation.rb
CHANGED
|
@@ -27,11 +27,11 @@ module Timeprice
|
|
|
27
27
|
|
|
28
28
|
# Adjust `amount` from date `from` to date `to` using country CPI.
|
|
29
29
|
#
|
|
30
|
-
# Dates accept "YYYY" or "YYYY-
|
|
30
|
+
# Dates accept "YYYY", "YYYY-MM", or "YYYY-Qn" (Q1..Q4).
|
|
31
31
|
#
|
|
32
32
|
# @param amount [Numeric]
|
|
33
|
-
# @param from [String] source date ("YYYY" or "YYYY-
|
|
34
|
-
# @param to [String] target date ("YYYY" or "YYYY-
|
|
33
|
+
# @param from [String] source date ("YYYY", "YYYY-MM", or "YYYY-Qn")
|
|
34
|
+
# @param to [String] target date ("YYYY", "YYYY-MM", or "YYYY-Qn")
|
|
35
35
|
# @param country [String] country code (see {Supported.countries})
|
|
36
36
|
# @return [InflationResult]
|
|
37
37
|
# @raise [UnsupportedCountry] if `country` is not supported
|
data/lib/timeprice/supported.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Timeprice
|
|
|
10
10
|
module Supported
|
|
11
11
|
# Currencies with no minor unit — formatted as whole numbers. This is
|
|
12
12
|
# ISO 4217 metadata, not bundled data, so it stays hardcoded.
|
|
13
|
-
ZERO_DECIMAL_CURRENCIES = %w[JPY VND].freeze
|
|
13
|
+
ZERO_DECIMAL_CURRENCIES = %w[JPY KRW VND].freeze
|
|
14
14
|
|
|
15
15
|
module_function
|
|
16
16
|
|
data/lib/timeprice/version.rb
CHANGED