All notable changes to this project will be documented in this file. Format: Keep a Changelog.
- Offline forecasting past the bundled data. New
Timeprice::Forecastmodule:CpiForecaster.projectandFxForecaster.projectuse a trailing-window CAGR for the point estimate and ±1σ of year-over-year changes for the band. Pure-Ruby, deterministic, no network calls or stats dependencies. Defaults: 10-year window for CPI, 5-year for FX; horizon caps 5y CPI / 2y FX (caps raisehorizon_exceeds_capwarnings, they don't block).Forecast::Resultis a frozen value object withvalue,low,high,projection_method,sigma_pct,last_known_date,target_date,horizon_months,basis_kind,warnings.Timeprice.forecast(...)is the public dispatcher. Timeprice.compareacceptsforecast: true; when the target date is past the bundled CPI window the result carries a populatedforecast: { basis_kind:, projection_method:, window_years:, sigma_pct:, last_known_date:, horizon_months:, low:, high:, warnings: }block andgranularity: :forecast.- CLI:
timeprice compare --forecastprints the range, basis, and caveats;--jsonexposes the same fields machine-readably. Timeprice::Compare.series_for(from:, to:, forecast:, amount:)returns annual sample points for the chart strip; forecast points carry:low/:highfor the ±1σ band.rake forecast:backtestreports per-country MAPE for the bundled data with assertions, so accuracy regressions are caught in CI.
- Inline 📈 Forecast this date affordance replaces the old form-level
checkbox. It only appears when the entered target year is past the
destination country's bundled CPI window. Clicking toggles
state.form.forecastand re-computes; the toggle round-trips through the existing&forecast=1URL param. - New SVG strip beneath the result paints the measured polyline, the dashed forecast line, a striped ±1σ fan, and tick labels — answers "how did we get this number?" visually.
- Amount input now carries locale thousands separators with a
caret-preserving live formatter;
parseAmountstrips them before parsing. - From/To rows reflow to
label + currency (60%) + 'in' + date (40%)on a single line, all three form rows ending at the same right edge. - Footer meta line ("refreshed …, gem v…") now hydrates from
Timeprice.metadataat boot; the HTML carries placeholders only, so releases no longer hand-edit the index.
thoris now a development dependency, not a runtime dependency. The CLI (exe/timeprice) still works exactly as before, but users who onlyrequire "timeprice"as a library no longer pull in thor and its transitive stdlib (set,digest,erb, etc.). The ruby.wasm site bundle drops ~200 KB raw / ~55 KB gzipped. CLI users need to rungem install thoronce;bin/timepriceprints a clear hint if it's missing.
- Public API methods (
inflation,exchange,compare) now acceptTimeprice::Datein addition to strings. The accepted string formats (YYYY,YYYY-MM,YYYY-Qn,YYYY-MM-DD) are unchanged. Unparseable input now raisesTimeprice::InvalidDateinstead of a genericArgumentErrorat the boundary. Timeprice.metadatareturns aMetadataSnapshotvalue object. Hash access (meta[:countries]),to_h, andJSON.generate(meta)keep working — the wire shape is byte-identical. New code should treat the result as a frozenDatainstance.Timeprice::PointgainsPoint.parse(currency, date)as the canonical constructor; it routes the date throughTimeprice::Date.- Implementation modules (
Timeprice::Inflation,::Exchange,::Compare,::Metadata) are marked@api private. They will be relocated toTimeprice::Internal::in a future major release; depend on theTimeprice.<method>facade.
Exchange.resolve_rateno longer raisesDataNotFoundwhen one triangulation leg falls back to the annual rate and the other resolves on a daily date. Annual rates are year-wide, so they now adopt the daily leg's effective date. Daily-vs-daily mismatches still raise.Compare.runshort-circuits CPI withcpi_ratio: 1.0when both points share a date — there's no time-elapsed inflation to apply, and the dailyYYYY-MM-DDgrain that this path produces would otherwise blow up CPI's monthly-max lookup.- Web UI:
humaniseErrornow recognisesDataNotFound, FX triangulation, and "No FX rate" patterns so a Ruby backtrace can no longer surface in the result block.
Timeprice::Schemaowns the on-disk format definition; both the reader (DataLoader) and the writer (data pipeline) reference it.- Data pipeline moved from
scripts/totools/data_pipeline/. Run viabundle exec rake data:refreshandbundle exec rake data:check_schema. - Provider self-registration replaces the hand-written orchestration list. Seven World Bank CPI subclasses collapsed into a factory call.
Tools::DataPipeline::Seriesvalue object replaces positional[monthly, annual]/[monthly, quarterly, annual]returns.- Typed
Tools::DataPipeline::Errorhierarchy (HttpError, ShapeError, ValidationError, SchemaError) replaces bare string raises. - Adopted the Weirich convention:
failfor new exceptions,raisefor re-throws inside rescue blocks.
- Five new countries: AU, CA, KR, CN, RU. CPI + FX coverage:
- AU (Australia): quarterly CPI from ABS 6401.0, annual baseline from World
Bank
FP.CPI.TOTL; AUD daily FX from Frankfurter. - CA (Canada): monthly CPI from Statistics Canada WDS (table 18-10-0004-01), annual baseline from World Bank; CAD daily FX from Frankfurter.
- KR (Korea, Rep.): monthly CPI from IMF Data Portal CPI dataflow, annual
baseline from World Bank; KRW daily FX from Frankfurter. (KOSIS Open API
is a future upgrade path — see
scripts/sources/kosis.rb.) - CN (China): annual CPI from World Bank, monthly layered from IMF Data Portal; CNY daily FX from Frankfurter.
- RU (Russia): annual CPI from World Bank, monthly layered from IMF; RUB FX
from IMF IFS
ENDA_XDC_USD_RATEwritten as annual averages to_annual.json(Frankfurter dropped RUB after the ECB suspended reference rates in March 2022, so daily RUB is intentionally not bundled).
- AU (Australia): quarterly CPI from ABS 6401.0, annual baseline from World
Bank
KRWadded toSupported::ZERO_DECIMAL_CURRENCIES.
- Schema v3 → v4. Backward-compatible bump:
- CPI files gain an optional
series.quarterlyblock (keyedYYYY-Qn) alongsideseries.monthlyandseries.annual. Files without quarterly data are byte-identical to v3 layout other than the version field. DataLoaderaccepts both v3 and v4 files; new writes are v4.Granularitygains:quarterly,:annual_from_quarterly_avg,:quarterly_from_annual_fallback, and:monthly_from_quarterly_fallback.CpiLookupresolves "YYYY-Qn" keys and falls back monthly → quarterly → annual.
- CPI files gain an optional
- Schema v2 → v3. Breaking change to the bundled data contract. v0.4.x
data files will not load on v0.5.0.
- New top-level
data/manifest.json— single source of truth for which countries and currencies the bundle supports.Supported.countries/Supported.currenciesderive from it at runtime; the hardcoded Ruby constantsSupported::COUNTRIES/CURRENCIESare gone. Custom callers that referenced those constants must switch to the method form. - CPI files use nested
series: { monthly, annual }and a structuredindex: { base_period, rebased_at }block in place of the freeformbase_yearstring. Top-levelsourceandupdated_atremoved —providers[]is the source of truth for provenance. - FX year files now carry
provenance+providersblocks, symmetric with CPI. - All annual FX (today only VND from World Bank) lives in
data/fx/usd/_annual.json— the single canonical source, every year. Per-year files (data/fx/usd/<year>.json) hold daily rates only. Pre-1999 stub year files are gone.Exchange.lookup_usd_basecollapses to a two-tier chain (daily → annual fallback), one source per tier.
- New top-level
- Schema v1 → v2. Two structural data-layer changes, bundled under a
single bump:
- FX year files gain an optional top-level
annualblock. World Bank's annual VND/USD average is stored there exactly once per year instead of being broadcast across every daily date key.Exchange.lookup_usd_basefalls back from daily to annual when no daily entry exists within ±7 days and tags the result withGranularity::ANNUAL.Comparemerges FX granularity with CPI granularity (worst-precision wins). - CPI provenance moves from a per-period map (
{"2025-01": "bls", ...}) to a compact range list ([{series, from, to, provider}, ...]). Single -provider files (US/UK/EU/JP) collapse to 1–2 ranges; VN keeps the WB→IMF transition explicit. ~42 kb saved across the 5 CPI files; one real US data gap at 2025-10 is now visible instead of buried.
- FX year files gain an optional top-level
country_file.rbrebase label now preserves the original base reference alongside the rebase date (e.g."2010=100 (rebased 2026-05-11)").
- Multi-source CPI chain for Vietnam: IMF Data Portal CPI dataflow
(
api.imf.org, SDMX 2.1) is the monthly primary; World BankFP.CPI.TOTLremains as the annual fallback. The on-diskdata/cpi/vn.jsongains two additive fields —provenance(per-period source id) andproviders(per-source status array) — so consumers can see which upstream supplied each datapoint. Schema version is unchanged; old readers ignore the new fields. Other country files (US, UK, EU, JP) gain the same fields on the next refresh. - Internal:
Sources::Provider/Sources::CountryFile/Sources::MergePolicyseam for layering multiple providers into a single country file with per-period provenance. Designed so future fresher sources (e.g. JP via e-Stat) can be added as one more entry without further plumbing.
Timeprice::CpiLookupandTimeprice::CpiPoint(Data.define of value + granularity). Owns all knowledge of the parsed CPI JSON shape soInflation.adjustis a 6-line orchestration.Timeprice::Sources::Coverage— isolates runtime filesystem walking (FX year scan, JSON.parse of rate files) from the attribution registry.Timeprice::Point#fx_anchor_date— resolves a year / month / dayPointto the day-resolved string FX lookup needs (mid-year forYYYY, mid-month forYYYY-MM).Timeprice::Supported.decimals_for(currency)— single source of truth for ISO 4217 minor-unit counts; non-CLI callers ofTimeprice.exchangecan now format results consistently.Timeprice::CLI::Presenters::{Inflation, Exchange, Compare, Sources}— each presenter exposes#text_linesand#json_hash; the CLI dispatches via a single#render(presenter)helper.
- CLI output redesigned for readability: every
inflation,fx, andcomparecommand now leads with the answer on line 1 (e.g.3,530,921 VND in 2024), followed by the calculation chain indented below.head -1extracts just the headline. Numbers are comma-grouped; JSON output is rounded to currency precision (no more1861291.9999999998). timeprice sourcesnow renders as an alignedID / SOURCE / LICENSE / COVERAGEtable by default. Usetimeprice sources --verbose(-v) for the previous detailed view with license URLs and full attribution.- Top-level
timeprice helprewritten — no more truncated descriptions; lists command names + descriptions, matching thegit/gh/cargoconvention. Point.coercerewritten with pattern matching; the CLI'sparse_compare_tokennow delegates to it instead of re-implementing the shape rules.Compare.resolve_pointsuses explicitraise … unlessguards instead of… || (raise …)nil-pun.
- Undocumented back-compat constants:
Timeprice::SUPPORTED_COUNTRIES,Timeprice::SUPPORTED_CURRENCIES, andTimeprice::Compare::CURRENCY_TO_COUNTRY. UseSupported::COUNTRIES,Supported::CURRENCIES, andSupported::CURRENCY_TO_COUNTRYdirectly. Lint/DuplicateBranchRuboCop exclusion forcli.rb— the duplicate was collapsed into a singlerescue Timeprice::Error, ArgumentError.
- Friendlier error messages:
Error: AMOUNT must be a number, got "abc"instead of Ruby's rawinvalid value for Float(): "abc". Missing-options errors now saymissing required options: --from, --towith aSee: timeprice help inflationhint.
Timeprice::Pointvalue object for compare inputs;Point.coerceacceptsPointinstances or 2-tuples in either[currency, date]or[date, currency]order.Timeprice::Supportedmodule — canonical home forCOUNTRIES,CURRENCIES, and the bidirectional currency↔country map. Replaces the duplicated maps inCompareand the CLI'sInflationResultmonkey-patch.Sources::Baseclass extracted from the CPI fetchers;BLS,ONS, andEurostatnow subclass it and implement onlyfetchreturning[monthly, annual]. The drift-check, rebase, merge, write, and summary-log flow is shared.- Per-fetcher GitHub Actions
::warning file=…,title=…::annotations inscripts/update_data.rb, so individual fetcher failures show up on the workflow run with a link to the responsible source file. - README "Using from Rails / Rake" section covering service objects, Sidekiq, Rake tasks, and
TIMEPRICE_DATA_ROOT. - YARD documentation on the public API (
Timeprice.{inflation,exchange,compare},Inflation,Exchange,Compare,DataLoader,Sources, error classes,Supported,Point).
SUPPORTED_COUNTRIES/SUPPORTED_CURRENCIESare now thin aliases forSupported::COUNTRIES/Supported::CURRENCIES; existing consumers keep working unchanged.Compare::CURRENCY_TO_COUNTRYis now an alias forSupported::CURRENCY_TO_COUNTRY.
- RuboCop with
rubocop-rake+rubocop-rspec, wired into Rake (rake defaultruns spec + rubocop) and CI (separateRuboCopjob alongsideRSpec).
DataLoader.load_cpinow distinguishes between "country isn't supported" (UnsupportedCountry) and "data file is missing on disk" (DataNotFoundwith the path the loader looked at). Previously both surfaced asUnsupportedCountry, masking install /TIMEPRICE_DATA_ROOTmisconfigurations.Timeprice.exchangenow rejects invalid calendar dates (e.g.2021-02-29) withArgumentErrorinstead of leaking aDate::Error. Honors the public error contract.- Trimmed
ZERO_DECIMAL_CURRENCIESto currencies actually supported by the gem (JPY, VND). Removed aspirational entries (KRW, IDR, HUF, CLP). - Inline source comments now reference README sections (
README.md "Compare semantics") instead ofPLAN.md(which is intentionally not shipped in the gem). CONTRIBUTING.mdupdated to match the single-Ruby CI; bothrspecandrubocopmust be green.
- CLI output formatting: currency-aware decimals (no
.0000on JPY/VND), magnitude-aware FX rate precision (no91.180000for a 91.18 rate). granularityis omitted from human output when it'smonthly(the happy path); surfaced only when the result used annual data.- 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. - Validate FX currencies against the supported list up front instead of failing with a generic "no FX rate" message.
- Tightened CLI command descriptions so
timeprice helpfits in a standard terminal.
- Hide Thor's built-in
treecommand — it was leaking intotimeprice helpas an internal-looking debug command.
- Initial gem release.
- Library API:
Timeprice.inflation,Timeprice.exchange,Timeprice.compare, returningData.definevalue objects. - CLI (
timeprice):inflation,fx,compare,sources,version, all with--json. - Bundled offline data for US, UK, Eurozone, Japan, Vietnam CPI and USD-base FX (EUR/GBP/JPY/VND).
- ECB reference rates via Frankfurter; VND via World Bank annual broadcast.
- Monthly GitHub Actions workflow that refreshes bundled data and opens a PR.
- Golden snapshot tests against real bundled data; property-style round-trip tests for inflation and FX.
- Attribution surfaces:
DATA_LICENSES.md,NOTICE,timeprice sourcesCLI command.