timeprice 0.6.0 → 0.7.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 +46 -0
- data/README.md +33 -2
- data/data/cpi/au.json +419 -0
- data/data/cpi/ca.json +1501 -0
- data/data/cpi/cn.json +487 -0
- data/data/cpi/eu.json +1 -1
- data/data/cpi/jp.json +1 -1
- data/data/cpi/kr.json +549 -0
- data/data/cpi/ru.json +487 -0
- data/data/cpi/uk.json +1 -1
- data/data/cpi/us.json +1 -1
- data/data/cpi/vn.json +26 -26
- data/data/fx/usd/1999.json +1042 -262
- data/data/fx/usd/2000.json +1274 -258
- data/data/fx/usd/2001.json +1277 -257
- data/data/fx/usd/2002.json +1282 -258
- data/data/fx/usd/2003.json +1282 -258
- data/data/fx/usd/2004.json +1302 -262
- data/data/fx/usd/2005.json +1292 -260
- data/data/fx/usd/2006.json +1282 -258
- data/data/fx/usd/2007.json +1282 -258
- data/data/fx/usd/2008.json +1287 -259
- data/data/fx/usd/2009.json +1287 -259
- data/data/fx/usd/2010.json +1297 -261
- data/data/fx/usd/2011.json +1292 -260
- data/data/fx/usd/2012.json +1287 -259
- data/data/fx/usd/2013.json +1282 -258
- data/data/fx/usd/2014.json +1282 -258
- data/data/fx/usd/2015.json +1287 -259
- data/data/fx/usd/2016.json +1292 -260
- data/data/fx/usd/2017.json +1282 -258
- data/data/fx/usd/2018.json +1282 -258
- data/data/fx/usd/2019.json +1282 -258
- data/data/fx/usd/2020.json +1292 -260
- data/data/fx/usd/2021.json +1297 -261
- data/data/fx/usd/2022.json +1292 -260
- data/data/fx/usd/2023.json +1282 -258
- data/data/fx/usd/2024.json +1287 -259
- data/data/fx/usd/2025.json +1282 -258
- data/data/fx/usd/2026.json +457 -92
- data/data/fx/usd/_annual.json +46 -1
- data/data/manifest.json +155 -7
- data/lib/timeprice/cli.rb +3 -3
- data/lib/timeprice/compare.rb +36 -3
- data/lib/timeprice/cpi_lookup.rb +4 -4
- data/lib/timeprice/data_loader.rb +8 -17
- data/lib/timeprice/date.rb +62 -0
- data/lib/timeprice/exchange.rb +49 -23
- data/lib/timeprice/inflation.rb +12 -4
- data/lib/timeprice/metadata.rb +121 -0
- data/lib/timeprice/metadata_snapshot.rb +23 -0
- data/lib/timeprice/point.rb +11 -3
- data/lib/timeprice/schema.rb +78 -0
- data/lib/timeprice/version.rb +1 -1
- data/lib/timeprice.rb +14 -1
- metadata +24 -1
data/data/fx/usd/_annual.json
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"VND": 11202.19
|
|
26
26
|
},
|
|
27
27
|
"1993": {
|
|
28
|
+
"RUB": 0.9917,
|
|
28
29
|
"VND": 10640.96
|
|
29
30
|
},
|
|
30
31
|
"1994": {
|
|
@@ -34,90 +35,119 @@
|
|
|
34
35
|
"VND": 11038.25
|
|
35
36
|
},
|
|
36
37
|
"1996": {
|
|
38
|
+
"RUB": 5.1208,
|
|
37
39
|
"VND": 11032.58
|
|
38
40
|
},
|
|
39
41
|
"1997": {
|
|
42
|
+
"RUB": 5.7848,
|
|
40
43
|
"VND": 11683.33
|
|
41
44
|
},
|
|
42
45
|
"1998": {
|
|
46
|
+
"RUB": 9.7051,
|
|
43
47
|
"VND": 13268.0
|
|
44
48
|
},
|
|
45
49
|
"1999": {
|
|
50
|
+
"RUB": 24.6199,
|
|
46
51
|
"VND": 13943.17
|
|
47
52
|
},
|
|
48
53
|
"2000": {
|
|
54
|
+
"RUB": 28.1292,
|
|
49
55
|
"VND": 14167.75
|
|
50
56
|
},
|
|
51
57
|
"2001": {
|
|
58
|
+
"RUB": 29.1685,
|
|
52
59
|
"VND": 14725.17
|
|
53
60
|
},
|
|
54
61
|
"2002": {
|
|
62
|
+
"RUB": 31.3485,
|
|
55
63
|
"VND": 15279.5
|
|
56
64
|
},
|
|
57
65
|
"2003": {
|
|
66
|
+
"RUB": 30.692,
|
|
58
67
|
"VND": 15509.58
|
|
59
68
|
},
|
|
60
69
|
"2004": {
|
|
70
|
+
"RUB": 28.8137,
|
|
61
71
|
"VND": 15746.0
|
|
62
72
|
},
|
|
63
73
|
"2005": {
|
|
74
|
+
"RUB": 28.2844,
|
|
64
75
|
"VND": 15858.92
|
|
65
76
|
},
|
|
66
77
|
"2006": {
|
|
78
|
+
"RUB": 27.191,
|
|
67
79
|
"VND": 15994.25
|
|
68
80
|
},
|
|
69
81
|
"2007": {
|
|
82
|
+
"RUB": 25.5808,
|
|
70
83
|
"VND": 16105.13
|
|
71
84
|
},
|
|
72
85
|
"2008": {
|
|
86
|
+
"RUB": 24.8529,
|
|
73
87
|
"VND": 16302.25
|
|
74
88
|
},
|
|
75
89
|
"2009": {
|
|
90
|
+
"RUB": 31.7404,
|
|
76
91
|
"VND": 17065.08
|
|
77
92
|
},
|
|
78
93
|
"2010": {
|
|
94
|
+
"RUB": 30.3679,
|
|
79
95
|
"VND": 18612.92
|
|
80
96
|
},
|
|
81
97
|
"2011": {
|
|
98
|
+
"RUB": 29.3823,
|
|
82
99
|
"VND": 20509.75
|
|
83
100
|
},
|
|
84
101
|
"2012": {
|
|
102
|
+
"RUB": 30.8398,
|
|
85
103
|
"VND": 20828.0
|
|
86
104
|
},
|
|
87
105
|
"2013": {
|
|
106
|
+
"RUB": 31.8371,
|
|
88
107
|
"VND": 20933.42
|
|
89
108
|
},
|
|
90
109
|
"2014": {
|
|
110
|
+
"RUB": 38.3782,
|
|
91
111
|
"VND": 21148.0
|
|
92
112
|
},
|
|
93
113
|
"2015": {
|
|
114
|
+
"RUB": 60.9377,
|
|
94
115
|
"VND": 21697.57
|
|
95
116
|
},
|
|
96
117
|
"2016": {
|
|
118
|
+
"RUB": 67.0559,
|
|
97
119
|
"VND": 21935.0
|
|
98
120
|
},
|
|
99
121
|
"2017": {
|
|
122
|
+
"RUB": 58.3428,
|
|
100
123
|
"VND": 22370.09
|
|
101
124
|
},
|
|
102
125
|
"2018": {
|
|
126
|
+
"RUB": 62.6681,
|
|
103
127
|
"VND": 22602.05
|
|
104
128
|
},
|
|
105
129
|
"2019": {
|
|
130
|
+
"RUB": 64.7377,
|
|
106
131
|
"VND": 23050.24
|
|
107
132
|
},
|
|
108
133
|
"2020": {
|
|
134
|
+
"RUB": 72.1049,
|
|
109
135
|
"VND": 23208.37
|
|
110
136
|
},
|
|
111
137
|
"2021": {
|
|
138
|
+
"RUB": 73.6544,
|
|
112
139
|
"VND": 23159.78
|
|
113
140
|
},
|
|
114
141
|
"2022": {
|
|
142
|
+
"RUB": 68.4849,
|
|
115
143
|
"VND": 23271.21
|
|
116
144
|
},
|
|
117
145
|
"2023": {
|
|
146
|
+
"RUB": 85.162,
|
|
118
147
|
"VND": 23787.32
|
|
119
148
|
},
|
|
120
149
|
"2024": {
|
|
150
|
+
"RUB": 92.5524,
|
|
121
151
|
"VND": 24164.89
|
|
122
152
|
}
|
|
123
153
|
},
|
|
@@ -131,14 +161,29 @@
|
|
|
131
161
|
"provider": "world_bank",
|
|
132
162
|
"series": "annual",
|
|
133
163
|
"to": "2024"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"currencies": [
|
|
167
|
+
"RUB"
|
|
168
|
+
],
|
|
169
|
+
"from": "1993",
|
|
170
|
+
"provider": "imf",
|
|
171
|
+
"series": "annual",
|
|
172
|
+
"to": "2024"
|
|
134
173
|
}
|
|
135
174
|
],
|
|
136
175
|
"providers": [
|
|
137
176
|
{
|
|
138
|
-
"fetched_at": "2026-05-
|
|
177
|
+
"fetched_at": "2026-05-12",
|
|
139
178
|
"id": "world_bank",
|
|
140
179
|
"label": "World Bank PA.NUS.FCRF",
|
|
141
180
|
"status": "ok"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"fetched_at": "2026-05-12",
|
|
184
|
+
"id": "imf",
|
|
185
|
+
"label": "IMF ER dataflow XDC_USD/PA_RT (period-average, annual mean)",
|
|
186
|
+
"status": "ok"
|
|
142
187
|
}
|
|
143
188
|
],
|
|
144
189
|
"schema_version": 4
|
data/data/manifest.json
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"countries": [
|
|
3
|
+
{
|
|
4
|
+
"code": "AU",
|
|
5
|
+
"cpi_file": "cpi/au.json",
|
|
6
|
+
"currency": "AUD",
|
|
7
|
+
"granularities": [
|
|
8
|
+
"quarterly",
|
|
9
|
+
"annual"
|
|
10
|
+
],
|
|
11
|
+
"cpi_ranges": {
|
|
12
|
+
"annual": {
|
|
13
|
+
"min": "1960",
|
|
14
|
+
"max": "2024"
|
|
15
|
+
},
|
|
16
|
+
"quarterly": {
|
|
17
|
+
"min": "1948-Q3",
|
|
18
|
+
"max": "2026-Q1"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"code": "CA",
|
|
24
|
+
"cpi_file": "cpi/ca.json",
|
|
25
|
+
"currency": "CAD",
|
|
26
|
+
"granularities": [
|
|
27
|
+
"monthly",
|
|
28
|
+
"annual"
|
|
29
|
+
],
|
|
30
|
+
"cpi_ranges": {
|
|
31
|
+
"annual": {
|
|
32
|
+
"min": "1914",
|
|
33
|
+
"max": "2025"
|
|
34
|
+
},
|
|
35
|
+
"monthly": {
|
|
36
|
+
"min": "1914-01",
|
|
37
|
+
"max": "2026-03"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"code": "CN",
|
|
43
|
+
"cpi_file": "cpi/cn.json",
|
|
44
|
+
"currency": "CNY",
|
|
45
|
+
"granularities": [
|
|
46
|
+
"monthly",
|
|
47
|
+
"annual"
|
|
48
|
+
],
|
|
49
|
+
"cpi_ranges": {
|
|
50
|
+
"annual": {
|
|
51
|
+
"min": "1986",
|
|
52
|
+
"max": "2025"
|
|
53
|
+
},
|
|
54
|
+
"monthly": {
|
|
55
|
+
"min": "1993-01",
|
|
56
|
+
"max": "2026-03"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
3
60
|
{
|
|
4
61
|
"code": "EU",
|
|
5
62
|
"cpi_file": "cpi/eu.json",
|
|
@@ -7,7 +64,17 @@
|
|
|
7
64
|
"granularities": [
|
|
8
65
|
"monthly",
|
|
9
66
|
"annual"
|
|
10
|
-
]
|
|
67
|
+
],
|
|
68
|
+
"cpi_ranges": {
|
|
69
|
+
"annual": {
|
|
70
|
+
"min": "1996",
|
|
71
|
+
"max": "2025"
|
|
72
|
+
},
|
|
73
|
+
"monthly": {
|
|
74
|
+
"min": "1996-01",
|
|
75
|
+
"max": "2025-12"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
11
78
|
},
|
|
12
79
|
{
|
|
13
80
|
"code": "JP",
|
|
@@ -15,7 +82,51 @@
|
|
|
15
82
|
"currency": "JPY",
|
|
16
83
|
"granularities": [
|
|
17
84
|
"annual"
|
|
18
|
-
]
|
|
85
|
+
],
|
|
86
|
+
"cpi_ranges": {
|
|
87
|
+
"annual": {
|
|
88
|
+
"min": "1960",
|
|
89
|
+
"max": "2024"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"code": "KR",
|
|
95
|
+
"cpi_file": "cpi/kr.json",
|
|
96
|
+
"currency": "KRW",
|
|
97
|
+
"granularities": [
|
|
98
|
+
"monthly",
|
|
99
|
+
"annual"
|
|
100
|
+
],
|
|
101
|
+
"cpi_ranges": {
|
|
102
|
+
"annual": {
|
|
103
|
+
"min": "1960",
|
|
104
|
+
"max": "2025"
|
|
105
|
+
},
|
|
106
|
+
"monthly": {
|
|
107
|
+
"min": "1990-01",
|
|
108
|
+
"max": "2026-03"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"code": "RU",
|
|
114
|
+
"cpi_file": "cpi/ru.json",
|
|
115
|
+
"currency": "RUB",
|
|
116
|
+
"granularities": [
|
|
117
|
+
"monthly",
|
|
118
|
+
"annual"
|
|
119
|
+
],
|
|
120
|
+
"cpi_ranges": {
|
|
121
|
+
"annual": {
|
|
122
|
+
"min": "1992",
|
|
123
|
+
"max": "2025"
|
|
124
|
+
},
|
|
125
|
+
"monthly": {
|
|
126
|
+
"min": "1992-01",
|
|
127
|
+
"max": "2026-03"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
19
130
|
},
|
|
20
131
|
{
|
|
21
132
|
"code": "UK",
|
|
@@ -24,7 +135,17 @@
|
|
|
24
135
|
"granularities": [
|
|
25
136
|
"monthly",
|
|
26
137
|
"annual"
|
|
27
|
-
]
|
|
138
|
+
],
|
|
139
|
+
"cpi_ranges": {
|
|
140
|
+
"annual": {
|
|
141
|
+
"min": "1988",
|
|
142
|
+
"max": "2025"
|
|
143
|
+
},
|
|
144
|
+
"monthly": {
|
|
145
|
+
"min": "1988-01",
|
|
146
|
+
"max": "2026-03"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
28
149
|
},
|
|
29
150
|
{
|
|
30
151
|
"code": "US",
|
|
@@ -33,7 +154,17 @@
|
|
|
33
154
|
"granularities": [
|
|
34
155
|
"monthly",
|
|
35
156
|
"annual"
|
|
36
|
-
]
|
|
157
|
+
],
|
|
158
|
+
"cpi_ranges": {
|
|
159
|
+
"annual": {
|
|
160
|
+
"min": "1990",
|
|
161
|
+
"max": "2024"
|
|
162
|
+
},
|
|
163
|
+
"monthly": {
|
|
164
|
+
"min": "1990-01",
|
|
165
|
+
"max": "2026-03"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
37
168
|
},
|
|
38
169
|
{
|
|
39
170
|
"code": "VN",
|
|
@@ -42,16 +173,31 @@
|
|
|
42
173
|
"granularities": [
|
|
43
174
|
"monthly",
|
|
44
175
|
"annual"
|
|
45
|
-
]
|
|
176
|
+
],
|
|
177
|
+
"cpi_ranges": {
|
|
178
|
+
"annual": {
|
|
179
|
+
"min": "1995",
|
|
180
|
+
"max": "2025"
|
|
181
|
+
},
|
|
182
|
+
"monthly": {
|
|
183
|
+
"min": "2001-12",
|
|
184
|
+
"max": "2026-03"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
46
187
|
}
|
|
47
188
|
],
|
|
48
189
|
"fx": {
|
|
49
190
|
"annual_file": "fx/usd/_annual.json",
|
|
50
191
|
"base": "USD",
|
|
51
192
|
"currencies": [
|
|
193
|
+
"AUD",
|
|
194
|
+
"CAD",
|
|
195
|
+
"CNY",
|
|
52
196
|
"EUR",
|
|
53
197
|
"GBP",
|
|
54
198
|
"JPY",
|
|
199
|
+
"KRW",
|
|
200
|
+
"RUB",
|
|
55
201
|
"VND"
|
|
56
202
|
],
|
|
57
203
|
"daily_years": [
|
|
@@ -83,8 +229,10 @@
|
|
|
83
229
|
2024,
|
|
84
230
|
2025,
|
|
85
231
|
2026
|
|
86
|
-
]
|
|
232
|
+
],
|
|
233
|
+
"daily_min": "1999-01-04",
|
|
234
|
+
"daily_max": "2026-05-11"
|
|
87
235
|
},
|
|
88
|
-
"generated_at": "2026-05-
|
|
236
|
+
"generated_at": "2026-05-12",
|
|
89
237
|
"schema_version": 4
|
|
90
238
|
}
|
data/lib/timeprice/cli.rb
CHANGED
|
@@ -160,12 +160,12 @@ module Timeprice
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
def parse_compare_token(token, label:)
|
|
163
|
-
|
|
163
|
+
fail ArgumentError, "#{label} is required" if token.nil? || token.strip.empty?
|
|
164
164
|
|
|
165
165
|
parts = token.strip.split(/\s+/)
|
|
166
166
|
unless parts.size == 2
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
fail ArgumentError,
|
|
168
|
+
"#{label} must be \"YEAR CURRENCY\" or \"CURRENCY YEAR\", got #{token.inspect}"
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
Point.coerce(parts)
|
data/lib/timeprice/compare.rb
CHANGED
|
@@ -28,6 +28,11 @@ module Timeprice
|
|
|
28
28
|
#
|
|
29
29
|
# If a future refactor flips the order, the regression test in
|
|
30
30
|
# spec/timeprice/compare_spec.rb will fail.
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
33
|
+
# The supported public entry point is {Timeprice.compare}. Direct
|
|
34
|
+
# references will move to `Timeprice::Internal::Compare` in a future
|
|
35
|
+
# release.
|
|
31
36
|
module Compare
|
|
32
37
|
module_function
|
|
33
38
|
|
|
@@ -52,7 +57,17 @@ module Timeprice
|
|
|
52
57
|
converted = fx_result.amount
|
|
53
58
|
|
|
54
59
|
# Step 2: inflate that destination-currency amount from source date to
|
|
55
|
-
# destination date using destination-country CPI.
|
|
60
|
+
# destination date using destination-country CPI. When both points
|
|
61
|
+
# share a date there's no time-elapsed inflation to apply — short-
|
|
62
|
+
# circuit with a ratio of 1.0 so daily-grain FX dates (which CPI's
|
|
63
|
+
# monthly-max resolution can't accept) still resolve cleanly.
|
|
64
|
+
if from_point.date == to_point.date
|
|
65
|
+
return fx_only_result(
|
|
66
|
+
amount: amount, from_point: from_point, to_point: to_point,
|
|
67
|
+
to_country: to_country, fx_result: fx_result
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
56
71
|
infl = Inflation.adjust(
|
|
57
72
|
amount: converted,
|
|
58
73
|
from: from_point.date.to_s,
|
|
@@ -75,14 +90,32 @@ module Timeprice
|
|
|
75
90
|
)
|
|
76
91
|
end
|
|
77
92
|
|
|
93
|
+
# Same-date branch: no time-elapsed inflation, so the FX leg alone is
|
|
94
|
+
# the answer. Builds a CompareResult with cpi_ratio=1.0.
|
|
95
|
+
def fx_only_result(amount:, from_point:, to_point:, to_country:, fx_result:)
|
|
96
|
+
CompareResult.new(
|
|
97
|
+
amount: fx_result.amount,
|
|
98
|
+
original_amount: amount.to_f,
|
|
99
|
+
from_currency: from_point.currency,
|
|
100
|
+
from_date: from_point.date.to_s,
|
|
101
|
+
to_currency: to_point.currency,
|
|
102
|
+
to_date: to_point.date.to_s,
|
|
103
|
+
country: to_country,
|
|
104
|
+
fx_rate: fx_result.rate,
|
|
105
|
+
cpi_ratio: 1.0,
|
|
106
|
+
converted_amount: fx_result.amount,
|
|
107
|
+
granularity: fx_result.granularity
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
78
111
|
# Coerce both points and resolve to_country.
|
|
79
112
|
def resolve_points(from, to)
|
|
80
113
|
from_point = Point.coerce(from)
|
|
81
114
|
to_point = Point.coerce(to)
|
|
82
|
-
|
|
115
|
+
fail UnsupportedCurrency, from_point.currency unless Supported.country_for_currency(from_point.currency)
|
|
83
116
|
|
|
84
117
|
to_country = Supported.country_for_currency(to_point.currency)
|
|
85
|
-
|
|
118
|
+
fail UnsupportedCurrency, to_point.currency unless to_country
|
|
86
119
|
|
|
87
120
|
[from_point, to_point, to_country]
|
|
88
121
|
end
|
data/lib/timeprice/cpi_lookup.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Timeprice
|
|
|
32
32
|
when QUARTER_RE then quarterly_or_fallbacks(key)
|
|
33
33
|
when /\A\d{4}-\d{2}\z/ then monthly_or_fallbacks(key)
|
|
34
34
|
when /\A\d{4}\z/ then annual_or_derived(key)
|
|
35
|
-
else
|
|
35
|
+
else fail ArgumentError, "Invalid date format: #{key.inspect} (use YYYY, YYYY-MM, or YYYY-Qn)"
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -48,7 +48,7 @@ module Timeprice
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
year_key = month_key[0, 4]
|
|
51
|
-
|
|
51
|
+
fail DataNotFound, missing_message(month_key) unless @annual.key?(year_key)
|
|
52
52
|
|
|
53
53
|
CpiPoint.new(value: @annual[year_key], granularity: Granularity::MONTHLY_FROM_ANNUAL_FALLBACK)
|
|
54
54
|
end
|
|
@@ -71,7 +71,7 @@ module Timeprice
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
year = quarter_key[0, 4]
|
|
74
|
-
|
|
74
|
+
fail DataNotFound, missing_message(quarter_key) unless @annual.key?(year)
|
|
75
75
|
|
|
76
76
|
CpiPoint.new(value: @annual[year], granularity: Granularity::QUARTERLY_FROM_ANNUAL_FALLBACK)
|
|
77
77
|
end
|
|
@@ -90,7 +90,7 @@ module Timeprice
|
|
|
90
90
|
return average(months, months.size, Granularity::ANNUAL_FROM_PARTIAL_MONTHS) if months.any?
|
|
91
91
|
return average(quarters, quarters.size, Granularity::ANNUAL_FROM_PARTIAL_QUARTERS) if quarters.any?
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
fail DataNotFound, missing_message(year)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def average(series, divisor, granularity)
|
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "errors"
|
|
5
|
+
require_relative "schema"
|
|
5
6
|
|
|
6
7
|
module Timeprice
|
|
7
8
|
# Loads and caches the bundled JSON data files. Override the search root
|
|
8
9
|
# by setting `TIMEPRICE_DATA_ROOT` in the environment or assigning
|
|
9
10
|
# {DataLoader.data_root=}.
|
|
10
11
|
module DataLoader
|
|
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
|
|
16
|
-
|
|
17
12
|
DEFAULT_DATA_ROOT = File.expand_path("../../data", __dir__)
|
|
18
13
|
|
|
19
14
|
class << self
|
|
@@ -46,8 +41,8 @@ module Timeprice
|
|
|
46
41
|
manifest_cache[data_root] ||= begin
|
|
47
42
|
path = File.join(data_root, "manifest.json")
|
|
48
43
|
unless File.exist?(path)
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
fail DataNotFound, "manifest.json missing (looked in #{path}). " \
|
|
45
|
+
"Check TIMEPRICE_DATA_ROOT or reinstall the gem."
|
|
51
46
|
end
|
|
52
47
|
|
|
53
48
|
parse_with_schema(path)
|
|
@@ -64,12 +59,12 @@ module Timeprice
|
|
|
64
59
|
key = country.to_s.downcase
|
|
65
60
|
code = country.to_s.upcase
|
|
66
61
|
cpi_cache[[data_root, key]] ||= begin
|
|
67
|
-
|
|
62
|
+
fail UnsupportedCountry, code unless Supported.country?(code)
|
|
68
63
|
|
|
69
64
|
path = File.join(data_root, "cpi", "#{key}.json")
|
|
70
65
|
unless File.exist?(path)
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
fail DataNotFound, "CPI data file missing for #{code} (looked in #{path}). " \
|
|
67
|
+
"Check TIMEPRICE_DATA_ROOT or reinstall the gem."
|
|
73
68
|
end
|
|
74
69
|
|
|
75
70
|
parse_with_schema(path)
|
|
@@ -84,7 +79,7 @@ module Timeprice
|
|
|
84
79
|
key = year.to_i
|
|
85
80
|
fx_cache[[data_root, key]] ||= begin
|
|
86
81
|
path = File.join(data_root, "fx", "usd", "#{key}.json")
|
|
87
|
-
|
|
82
|
+
fail DataNotFound, "No FX data for year #{key}" unless File.exist?(path)
|
|
88
83
|
|
|
89
84
|
parse_with_schema(path)
|
|
90
85
|
end
|
|
@@ -116,11 +111,7 @@ module Timeprice
|
|
|
116
111
|
end
|
|
117
112
|
|
|
118
113
|
def parse_with_schema(path)
|
|
119
|
-
|
|
120
|
-
version = data["schema_version"]
|
|
121
|
-
raise UnsupportedSchemaVersion.new(version, path) unless SUPPORTED_SCHEMA_VERSIONS.include?(version)
|
|
122
|
-
|
|
123
|
-
data
|
|
114
|
+
Schema.load_cpi(JSON.parse(File.read(path)), path: path)
|
|
124
115
|
end
|
|
125
116
|
end
|
|
126
117
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module Timeprice
|
|
6
|
+
# Raised when a user-supplied date string can't be parsed into a
|
|
7
|
+
# {Timeprice::Date} value.
|
|
8
|
+
class InvalidDate < Error; end
|
|
9
|
+
|
|
10
|
+
# Immutable value object representing "a date at some granularity": a
|
|
11
|
+
# year, a year+month, a year+quarter, or a full calendar day. Used as
|
|
12
|
+
# the canonical input shape for the public API (`Timeprice.inflation`,
|
|
13
|
+
# `Timeprice.exchange`, `Timeprice.compare`) — strings are accepted for
|
|
14
|
+
# convenience and coerced via {.coerce} at the boundary.
|
|
15
|
+
# rubocop:disable Lint/ConstantDefinitionInBlock
|
|
16
|
+
Date = Data.define(:year, :month, :quarter, :day) do
|
|
17
|
+
ANNUAL_RE = /\A(\d{4})\z/
|
|
18
|
+
MONTHLY_RE = /\A(\d{4})-(\d{2})\z/
|
|
19
|
+
QUARTERLY_RE = /\A(\d{4})-Q([1-4])\z/i
|
|
20
|
+
DAILY_RE = /\A(\d{4})-(\d{2})-(\d{2})\z/
|
|
21
|
+
|
|
22
|
+
def self.parse(str)
|
|
23
|
+
case str.to_s
|
|
24
|
+
when DAILY_RE
|
|
25
|
+
new(year: ::Regexp.last_match(1).to_i, month: ::Regexp.last_match(2).to_i,
|
|
26
|
+
quarter: nil, day: ::Regexp.last_match(3).to_i)
|
|
27
|
+
when QUARTERLY_RE
|
|
28
|
+
new(year: ::Regexp.last_match(1).to_i, month: nil,
|
|
29
|
+
quarter: ::Regexp.last_match(2).to_i, day: nil)
|
|
30
|
+
when MONTHLY_RE
|
|
31
|
+
new(year: ::Regexp.last_match(1).to_i, month: ::Regexp.last_match(2).to_i,
|
|
32
|
+
quarter: nil, day: nil)
|
|
33
|
+
when ANNUAL_RE
|
|
34
|
+
new(year: ::Regexp.last_match(1).to_i, month: nil, quarter: nil, day: nil)
|
|
35
|
+
else
|
|
36
|
+
fail InvalidDate, "Cannot parse #{str.inspect} as a Timeprice::Date"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.coerce(input)
|
|
41
|
+
input.is_a?(self) ? input : parse(input)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def granularity
|
|
45
|
+
return :daily if day
|
|
46
|
+
return :monthly if month
|
|
47
|
+
return :quarterly if quarter
|
|
48
|
+
|
|
49
|
+
:annual
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
case granularity
|
|
54
|
+
when :daily then format("%04d-%02d-%02d", year, month, day)
|
|
55
|
+
when :monthly then format("%04d-%02d", year, month)
|
|
56
|
+
when :quarterly then format("%04d-Q%d", year, quarter)
|
|
57
|
+
else format("%04d", year)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
# rubocop:enable Lint/ConstantDefinitionInBlock
|
|
62
|
+
end
|