timeprice 0.1.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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +18 -0
  3. data/DATA_LICENSES.md +31 -0
  4. data/LICENSE.txt +21 -0
  5. data/NOTICE +12 -0
  6. data/README.md +187 -0
  7. data/data/cpi/eu.json +401 -0
  8. data/data/cpi/jp.json +75 -0
  9. data/data/cpi/uk.json +508 -0
  10. data/data/cpi/us.json +480 -0
  11. data/data/cpi/vn.json +40 -0
  12. data/data/fx/usd/1983.json +12 -0
  13. data/data/fx/usd/1986.json +12 -0
  14. data/data/fx/usd/1987.json +12 -0
  15. data/data/fx/usd/1988.json +12 -0
  16. data/data/fx/usd/1989.json +12 -0
  17. data/data/fx/usd/1990.json +12 -0
  18. data/data/fx/usd/1991.json +12 -0
  19. data/data/fx/usd/1992.json +12 -0
  20. data/data/fx/usd/1993.json +12 -0
  21. data/data/fx/usd/1994.json +12 -0
  22. data/data/fx/usd/1995.json +12 -0
  23. data/data/fx/usd/1996.json +12 -0
  24. data/data/fx/usd/1997.json +12 -0
  25. data/data/fx/usd/1998.json +12 -0
  26. data/data/fx/usd/1999.json +1566 -0
  27. data/data/fx/usd/2000.json +1542 -0
  28. data/data/fx/usd/2001.json +1533 -0
  29. data/data/fx/usd/2002.json +1539 -0
  30. data/data/fx/usd/2003.json +1539 -0
  31. data/data/fx/usd/2004.json +1563 -0
  32. data/data/fx/usd/2005.json +1554 -0
  33. data/data/fx/usd/2006.json +1539 -0
  34. data/data/fx/usd/2007.json +1539 -0
  35. data/data/fx/usd/2008.json +1545 -0
  36. data/data/fx/usd/2009.json +1545 -0
  37. data/data/fx/usd/2010.json +1560 -0
  38. data/data/fx/usd/2011.json +1554 -0
  39. data/data/fx/usd/2012.json +1545 -0
  40. data/data/fx/usd/2013.json +1539 -0
  41. data/data/fx/usd/2014.json +1539 -0
  42. data/data/fx/usd/2015.json +1545 -0
  43. data/data/fx/usd/2016.json +1554 -0
  44. data/data/fx/usd/2017.json +1539 -0
  45. data/data/fx/usd/2018.json +1539 -0
  46. data/data/fx/usd/2019.json +1539 -0
  47. data/data/fx/usd/2020.json +1551 -0
  48. data/data/fx/usd/2021.json +1560 -0
  49. data/data/fx/usd/2022.json +1554 -0
  50. data/data/fx/usd/2023.json +1539 -0
  51. data/data/fx/usd/2024.json +1545 -0
  52. data/data/fx/usd/2025.json +1284 -0
  53. data/data/fx/usd/2026.json +449 -0
  54. data/exe/timeprice +7 -0
  55. data/lib/timeprice/cli.rb +179 -0
  56. data/lib/timeprice/compare.rb +99 -0
  57. data/lib/timeprice/data_loader.rb +59 -0
  58. data/lib/timeprice/errors.rb +49 -0
  59. data/lib/timeprice/exchange.rb +98 -0
  60. data/lib/timeprice/inflation.rb +89 -0
  61. data/lib/timeprice/sources.rb +128 -0
  62. data/lib/timeprice/version.rb +5 -0
  63. data/lib/timeprice.rb +25 -0
  64. metadata +150 -0
@@ -0,0 +1,449 @@
1
+ {
2
+ "base": "USD",
3
+ "rates": {
4
+ "2026-01-02": {
5
+ "EUR": 0.85317,
6
+ "GBP": 0.74388,
7
+ "JPY": 156.93
8
+ },
9
+ "2026-01-05": {
10
+ "EUR": 0.85734,
11
+ "GBP": 0.74383,
12
+ "JPY": 156.83
13
+ },
14
+ "2026-01-06": {
15
+ "EUR": 0.85419,
16
+ "GBP": 0.73998,
17
+ "JPY": 156.44
18
+ },
19
+ "2026-01-07": {
20
+ "EUR": 0.85587,
21
+ "GBP": 0.74153,
22
+ "JPY": 156.55
23
+ },
24
+ "2026-01-08": {
25
+ "EUR": 0.85653,
26
+ "GBP": 0.74407,
27
+ "JPY": 156.72
28
+ },
29
+ "2026-01-09": {
30
+ "EUR": 0.85896,
31
+ "GBP": 0.74532,
32
+ "JPY": 157.64
33
+ },
34
+ "2026-01-12": {
35
+ "EUR": 0.85529,
36
+ "GBP": 0.74187,
37
+ "JPY": 157.73
38
+ },
39
+ "2026-01-13": {
40
+ "EUR": 0.85807,
41
+ "GBP": 0.74309,
42
+ "JPY": 158.85
43
+ },
44
+ "2026-01-14": {
45
+ "EUR": 0.8583,
46
+ "GBP": 0.74397,
47
+ "JPY": 158.63
48
+ },
49
+ "2026-01-15": {
50
+ "EUR": 0.86029,
51
+ "GBP": 0.74621,
52
+ "JPY": 158.56
53
+ },
54
+ "2026-01-16": {
55
+ "EUR": 0.86081,
56
+ "GBP": 0.74632,
57
+ "JPY": 158.1
58
+ },
59
+ "2026-01-19": {
60
+ "EUR": 0.85977,
61
+ "GBP": 0.74551,
62
+ "JPY": 157.93
63
+ },
64
+ "2026-01-20": {
65
+ "EUR": 0.85266,
66
+ "GBP": 0.74369,
67
+ "JPY": 157.9
68
+ },
69
+ "2026-01-21": {
70
+ "EUR": 0.85186,
71
+ "GBP": 0.74487,
72
+ "JPY": 157.79
73
+ },
74
+ "2026-01-22": {
75
+ "EUR": 0.85426,
76
+ "GBP": 0.74509,
77
+ "JPY": 158.79
78
+ },
79
+ "2026-01-23": {
80
+ "EUR": 0.85164,
81
+ "GBP": 0.73931,
82
+ "JPY": 158.16
83
+ },
84
+ "2026-01-26": {
85
+ "EUR": 0.84488,
86
+ "GBP": 0.73293,
87
+ "JPY": 154.21
88
+ },
89
+ "2026-01-27": {
90
+ "EUR": 0.83829,
91
+ "GBP": 0.72789,
92
+ "JPY": 153.34
93
+ },
94
+ "2026-01-28": {
95
+ "EUR": 0.83514,
96
+ "GBP": 0.72532,
97
+ "JPY": 152.63
98
+ },
99
+ "2026-01-29": {
100
+ "EUR": 0.83556,
101
+ "GBP": 0.72376,
102
+ "JPY": 153.31
103
+ },
104
+ "2026-01-30": {
105
+ "EUR": 0.839,
106
+ "GBP": 0.72674,
107
+ "JPY": 154.03
108
+ },
109
+ "2026-02-02": {
110
+ "EUR": 0.84459,
111
+ "GBP": 0.73125,
112
+ "JPY": 155.06
113
+ },
114
+ "2026-02-03": {
115
+ "EUR": 0.84739,
116
+ "GBP": 0.7307,
117
+ "JPY": 155.85
118
+ },
119
+ "2026-02-04": {
120
+ "EUR": 0.84602,
121
+ "GBP": 0.72893,
122
+ "JPY": 156.64
123
+ },
124
+ "2026-02-05": {
125
+ "EUR": 0.8476,
126
+ "GBP": 0.73665,
127
+ "JPY": 156.9
128
+ },
129
+ "2026-02-06": {
130
+ "EUR": 0.84789,
131
+ "GBP": 0.73588,
132
+ "JPY": 157.09
133
+ },
134
+ "2026-02-09": {
135
+ "EUR": 0.84133,
136
+ "GBP": 0.73204,
137
+ "JPY": 156.19
138
+ },
139
+ "2026-02-10": {
140
+ "EUR": 0.84076,
141
+ "GBP": 0.73104,
142
+ "JPY": 155.13
143
+ },
144
+ "2026-02-11": {
145
+ "EUR": 0.84034,
146
+ "GBP": 0.73101,
147
+ "JPY": 153.61
148
+ },
149
+ "2026-02-12": {
150
+ "EUR": 0.84218,
151
+ "GBP": 0.73362,
152
+ "JPY": 153.71
153
+ },
154
+ "2026-02-13": {
155
+ "EUR": 0.84303,
156
+ "GBP": 0.73478,
157
+ "JPY": 153.29
158
+ },
159
+ "2026-02-16": {
160
+ "EUR": 0.84353,
161
+ "GBP": 0.73302,
162
+ "JPY": 153.34
163
+ },
164
+ "2026-02-17": {
165
+ "EUR": 0.84559,
166
+ "GBP": 0.73846,
167
+ "JPY": 153.1
168
+ },
169
+ "2026-02-18": {
170
+ "EUR": 0.84424,
171
+ "GBP": 0.73651,
172
+ "JPY": 153.64
173
+ },
174
+ "2026-02-19": {
175
+ "EUR": 0.85085,
176
+ "GBP": 0.74347,
177
+ "JPY": 154.9
178
+ },
179
+ "2026-02-20": {
180
+ "EUR": 0.84983,
181
+ "GBP": 0.74174,
182
+ "JPY": 155.21
183
+ },
184
+ "2026-02-23": {
185
+ "EUR": 0.84861,
186
+ "GBP": 0.74117,
187
+ "JPY": 154.82
188
+ },
189
+ "2026-02-24": {
190
+ "EUR": 0.84911,
191
+ "GBP": 0.74136,
192
+ "JPY": 155.84
193
+ },
194
+ "2026-02-25": {
195
+ "EUR": 0.84861,
196
+ "GBP": 0.73948,
197
+ "JPY": 156.74
198
+ },
199
+ "2026-02-26": {
200
+ "EUR": 0.84645,
201
+ "GBP": 0.73802,
202
+ "JPY": 156.06
203
+ },
204
+ "2026-02-27": {
205
+ "EUR": 0.8471,
206
+ "GBP": 0.74231,
207
+ "JPY": 155.98
208
+ },
209
+ "2026-03-02": {
210
+ "EUR": 0.85485,
211
+ "GBP": 0.74705,
212
+ "JPY": 157.45
213
+ },
214
+ "2026-03-03": {
215
+ "EUR": 0.86162,
216
+ "GBP": 0.75108,
217
+ "JPY": 157.66
218
+ },
219
+ "2026-03-04": {
220
+ "EUR": 0.85844,
221
+ "GBP": 0.74727,
222
+ "JPY": 157.11
223
+ },
224
+ "2026-03-05": {
225
+ "EUR": 0.86073,
226
+ "GBP": 0.74841,
227
+ "JPY": 157.54
228
+ },
229
+ "2026-03-06": {
230
+ "EUR": 0.86498,
231
+ "GBP": 0.74987,
232
+ "JPY": 157.92
233
+ },
234
+ "2026-03-09": {
235
+ "EUR": 0.86543,
236
+ "GBP": 0.74885,
237
+ "JPY": 158.5
238
+ },
239
+ "2026-03-10": {
240
+ "EUR": 0.85903,
241
+ "GBP": 0.74345,
242
+ "JPY": 157.78
243
+ },
244
+ "2026-03-11": {
245
+ "EUR": 0.86348,
246
+ "GBP": 0.74573,
247
+ "JPY": 158.56
248
+ },
249
+ "2026-03-12": {
250
+ "EUR": 0.86603,
251
+ "GBP": 0.74689,
252
+ "JPY": 158.86
253
+ },
254
+ "2026-03-13": {
255
+ "EUR": 0.87138,
256
+ "GBP": 0.75377,
257
+ "JPY": 159.33
258
+ },
259
+ "2026-03-16": {
260
+ "EUR": 0.87123,
261
+ "GBP": 0.75281,
262
+ "JPY": 159.14
263
+ },
264
+ "2026-03-17": {
265
+ "EUR": 0.86723,
266
+ "GBP": 0.74954,
267
+ "JPY": 158.95
268
+ },
269
+ "2026-03-18": {
270
+ "EUR": 0.86957,
271
+ "GBP": 0.75124,
272
+ "JPY": 159.56
273
+ },
274
+ "2026-03-19": {
275
+ "EUR": 0.8704,
276
+ "GBP": 0.75192,
277
+ "JPY": 158.81
278
+ },
279
+ "2026-03-20": {
280
+ "EUR": 0.86543,
281
+ "GBP": 0.74806,
282
+ "JPY": 158.77
283
+ },
284
+ "2026-03-23": {
285
+ "EUR": 0.86237,
286
+ "GBP": 0.74526,
287
+ "JPY": 158.55
288
+ },
289
+ "2026-03-24": {
290
+ "EUR": 0.86415,
291
+ "GBP": 0.74785,
292
+ "JPY": 158.93
293
+ },
294
+ "2026-03-25": {
295
+ "EUR": 0.86266,
296
+ "GBP": 0.7467,
297
+ "JPY": 158.96
298
+ },
299
+ "2026-03-26": {
300
+ "EUR": 0.86663,
301
+ "GBP": 0.74976,
302
+ "JPY": 159.62
303
+ },
304
+ "2026-03-27": {
305
+ "EUR": 0.86828,
306
+ "GBP": 0.75297,
307
+ "JPY": 159.9
308
+ },
309
+ "2026-03-30": {
310
+ "EUR": 0.87078,
311
+ "GBP": 0.75586,
312
+ "JPY": 159.46
313
+ },
314
+ "2026-03-31": {
315
+ "EUR": 0.86972,
316
+ "GBP": 0.7552,
317
+ "JPY": 159.5
318
+ },
319
+ "2026-04-01": {
320
+ "EUR": 0.8617,
321
+ "GBP": 0.75065,
322
+ "JPY": 158.32
323
+ },
324
+ "2026-04-02": {
325
+ "EUR": 0.86768,
326
+ "GBP": 0.75708,
327
+ "JPY": 159.6
328
+ },
329
+ "2026-04-07": {
330
+ "EUR": 0.86528,
331
+ "GBP": 0.75502,
332
+ "JPY": 159.84
333
+ },
334
+ "2026-04-08": {
335
+ "EUR": 0.85426,
336
+ "GBP": 0.74229,
337
+ "JPY": 158.15
338
+ },
339
+ "2026-04-09": {
340
+ "EUR": 0.8558,
341
+ "GBP": 0.745,
342
+ "JPY": 158.92
343
+ },
344
+ "2026-04-10": {
345
+ "EUR": 0.8539,
346
+ "GBP": 0.74379,
347
+ "JPY": 159.19
348
+ },
349
+ "2026-04-13": {
350
+ "EUR": 0.85587,
351
+ "GBP": 0.7451,
352
+ "JPY": 159.83
353
+ },
354
+ "2026-04-14": {
355
+ "EUR": 0.84796,
356
+ "GBP": 0.73699,
357
+ "JPY": 158.85
358
+ },
359
+ "2026-04-15": {
360
+ "EUR": 0.8489,
361
+ "GBP": 0.73799,
362
+ "JPY": 159.09
363
+ },
364
+ "2026-04-16": {
365
+ "EUR": 0.84875,
366
+ "GBP": 0.73836,
367
+ "JPY": 158.98
368
+ },
369
+ "2026-04-17": {
370
+ "EUR": 0.84767,
371
+ "GBP": 0.7389,
372
+ "JPY": 159.13
373
+ },
374
+ "2026-04-20": {
375
+ "EUR": 0.85034,
376
+ "GBP": 0.74018,
377
+ "JPY": 158.91
378
+ },
379
+ "2026-04-21": {
380
+ "EUR": 0.84983,
381
+ "GBP": 0.73965,
382
+ "JPY": 159.04
383
+ },
384
+ "2026-04-22": {
385
+ "EUR": 0.8523,
386
+ "GBP": 0.74067,
387
+ "JPY": 159.22
388
+ },
389
+ "2026-04-23": {
390
+ "EUR": 0.85514,
391
+ "GBP": 0.74034,
392
+ "JPY": 159.48
393
+ },
394
+ "2026-04-24": {
395
+ "EUR": 0.85383,
396
+ "GBP": 0.74115,
397
+ "JPY": 159.42
398
+ },
399
+ "2026-04-27": {
400
+ "EUR": 0.85114,
401
+ "GBP": 0.73691,
402
+ "JPY": 159.21
403
+ },
404
+ "2026-04-28": {
405
+ "EUR": 0.85616,
406
+ "GBP": 0.74242,
407
+ "JPY": 159.74
408
+ },
409
+ "2026-04-29": {
410
+ "EUR": 0.85426,
411
+ "GBP": 0.74016,
412
+ "JPY": 159.79
413
+ },
414
+ "2026-04-30": {
415
+ "EUR": 0.85455,
416
+ "GBP": 0.74026,
417
+ "JPY": 156.56
418
+ },
419
+ "2026-05-04": {
420
+ "EUR": 0.8547,
421
+ "GBP": 0.7381,
422
+ "JPY": 157.12
423
+ },
424
+ "2026-05-05": {
425
+ "EUR": 0.85572,
426
+ "GBP": 0.73886,
427
+ "JPY": 157.81
428
+ },
429
+ "2026-05-06": {
430
+ "EUR": 0.8502,
431
+ "GBP": 0.73431,
432
+ "JPY": 156.21
433
+ },
434
+ "2026-05-07": {
435
+ "EUR": 0.84962,
436
+ "GBP": 0.73415,
437
+ "JPY": 156.39
438
+ },
439
+ "2026-05-08": {
440
+ "EUR": 0.85027,
441
+ "GBP": 0.73472,
442
+ "JPY": 156.76
443
+ }
444
+ },
445
+ "schema_version": 1,
446
+ "source": "Frankfurter (ECB) — daily reference rates",
447
+ "updated_at": "2026-05-11",
448
+ "year": 2026
449
+ }
data/exe/timeprice ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "timeprice"
5
+ require "timeprice/cli"
6
+
7
+ Timeprice::CLI.start(ARGV)
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "json"
5
+ require_relative "../timeprice"
6
+
7
+ module Timeprice
8
+ class CLI < Thor
9
+ class_option :json, type: :boolean, default: false, desc: "Output result as JSON"
10
+
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ desc "inflation AMOUNT", "Adjust AMOUNT for inflation between two dates"
16
+ method_option :from, type: :string, required: true, desc: "Source date (YYYY or YYYY-MM)"
17
+ method_option :to, type: :string, required: true, desc: "Target date (YYYY or YYYY-MM)"
18
+ method_option :country, type: :string, required: true, desc: "Country code (US, UK, EU, JP, VN)"
19
+ def inflation(amount)
20
+ with_error_handling do
21
+ result = Timeprice.inflation(
22
+ amount: Float(amount),
23
+ from: options[:from],
24
+ to: options[:to],
25
+ country: options[:country]
26
+ )
27
+ emit_inflation(result)
28
+ end
29
+ end
30
+
31
+ desc "fx AMOUNT FROM_CURRENCY TO_CURRENCY", "Convert AMOUNT between currencies on a given date"
32
+ method_option :date, type: :string, required: true, desc: "Date (YYYY-MM-DD)"
33
+ def fx(amount, from_currency, to_currency)
34
+ with_error_handling do
35
+ result = Timeprice.exchange(
36
+ amount: Float(amount),
37
+ from: from_currency,
38
+ to: to_currency,
39
+ date: options[:date]
40
+ )
41
+ emit_exchange(result)
42
+ end
43
+ end
44
+
45
+ desc "compare AMOUNT", "Combine FX + inflation across two (year, currency) points"
46
+ method_option :from, type: :string, required: true, desc: "Source as \"YEAR CURRENCY\" or \"CURRENCY YEAR\""
47
+ method_option :to, type: :string, required: true, desc: "Target as \"YEAR CURRENCY\" or \"CURRENCY YEAR\""
48
+ def compare(amount)
49
+ with_error_handling do
50
+ from_tuple = parse_compare_token(options[:from], label: "--from")
51
+ to_tuple = parse_compare_token(options[:to], label: "--to")
52
+ result = Timeprice.compare(
53
+ amount: Float(amount),
54
+ from: from_tuple,
55
+ to: to_tuple
56
+ )
57
+ emit_compare(result)
58
+ end
59
+ end
60
+
61
+ desc "sources", "List bundled data sources, licenses, attribution, and coverage"
62
+ def sources
63
+ list = Timeprice::Sources.list
64
+ if options[:json]
65
+ say JSON.generate(list)
66
+ else
67
+ list.each do |s|
68
+ say "#{s[:name]}"
69
+ say " id: #{s[:id]}"
70
+ say " license: #{s[:license]}"
71
+ say " license_url: #{s[:license_url]}"
72
+ say " attribution: #{s[:attribution]}"
73
+ say " coverage: #{s[:coverage]}"
74
+ say ""
75
+ end
76
+ end
77
+ end
78
+
79
+ desc "version", "Print the installed timeprice version"
80
+ def version
81
+ if options[:json]
82
+ say JSON.generate({ version: Timeprice::VERSION, repo: "patrick204nqh/timeprice" })
83
+ else
84
+ say "timeprice #{Timeprice::VERSION} — patrick204nqh/timeprice"
85
+ end
86
+ end
87
+
88
+ no_commands do
89
+ def with_error_handling
90
+ yield
91
+ rescue Timeprice::Error => e
92
+ warn "Error: #{e.message}"
93
+ exit 1
94
+ rescue ArgumentError => e
95
+ # Bad numeric/date format from Float() or library parsers — treat as user error.
96
+ warn "Error: #{e.message}"
97
+ exit 1
98
+ end
99
+
100
+ # Accepts "1995 USD" or "USD 1995" — order-agnostic.
101
+ # Returns [currency, year_string] tuple matching Timeprice.compare's API.
102
+ def parse_compare_token(token, label:)
103
+ raise ArgumentError, "#{label} is required" if token.nil? || token.strip.empty?
104
+ parts = token.strip.split(/\s+/)
105
+ unless parts.size == 2
106
+ raise ArgumentError,
107
+ "#{label} must be \"YEAR CURRENCY\" or \"CURRENCY YEAR\", got #{token.inspect}"
108
+ end
109
+ year = parts.find { |p| p.match?(/\A\d{4}\z/) }
110
+ currency = parts.find { |p| p.match?(/\A[A-Za-z]{3}\z/) }
111
+ if year.nil? || currency.nil?
112
+ raise ArgumentError,
113
+ "#{label} must contain a 4-digit year and a 3-letter currency code, got #{token.inspect}"
114
+ end
115
+ [currency.upcase, year]
116
+ end
117
+
118
+ def emit_inflation(result)
119
+ if options[:json]
120
+ say JSON.generate(result.to_h)
121
+ else
122
+ say format(
123
+ "%.2f %s in %s is %.2f %s in %s (%s, granularity: %s)",
124
+ result.original_amount, result.country_currency_label,
125
+ result.from, result.amount, result.country_currency_label,
126
+ result.to, result.country, result.granularity
127
+ )
128
+ end
129
+ end
130
+
131
+ def emit_exchange(result)
132
+ if options[:json]
133
+ say JSON.generate(result.to_h)
134
+ else
135
+ line = format(
136
+ "%.2f %s on %s = %.2f %s (rate: %.4f)",
137
+ result.original_amount, result.from, result.date,
138
+ result.amount, result.to, result.rate
139
+ )
140
+ if result.effective_date && result.effective_date != result.date
141
+ line += " (effective date: #{result.effective_date} — fallback)"
142
+ end
143
+ say line
144
+ end
145
+ end
146
+
147
+ def emit_compare(result)
148
+ if options[:json]
149
+ say JSON.generate(result.to_h)
150
+ else
151
+ say format(
152
+ "%.2f %s in %s -> %.2f %s in %s",
153
+ result.original_amount, result.from_currency, result.from_date,
154
+ result.amount, result.to_currency, result.to_date
155
+ )
156
+ say format(
157
+ " steps: convert at %s (fx rate %.6f) -> %.4f %s, then inflate in %s (cpi ratio %.6f, granularity: %s)",
158
+ result.from_date, result.fx_rate, result.converted_amount,
159
+ result.to_currency, result.country, result.cpi_ratio, result.granularity
160
+ )
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # Tiny shim so we can include currency context in the inflation line without
168
+ # bloating the value object — the result doesn't carry currency, only country.
169
+ module Timeprice
170
+ class InflationResult
171
+ COUNTRY_TO_CURRENCY = {
172
+ "US" => "USD", "UK" => "GBP", "EU" => "EUR", "JP" => "JPY", "VN" => "VND"
173
+ }.freeze
174
+
175
+ def country_currency_label
176
+ COUNTRY_TO_CURRENCY[country.to_s.upcase] || country.to_s.upcase
177
+ end
178
+ end
179
+ end