vtlengine 1.0__py3-none-any.whl → 1.0.2__py3-none-any.whl
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.
Potentially problematic release.
This version of vtlengine might be problematic. Click here for more details.
- vtlengine/API/_InternalApi.py +159 -102
- vtlengine/API/__init__.py +110 -68
- vtlengine/AST/ASTConstructor.py +188 -98
- vtlengine/AST/ASTConstructorModules/Expr.py +402 -205
- vtlengine/AST/ASTConstructorModules/ExprComponents.py +248 -104
- vtlengine/AST/ASTConstructorModules/Terminals.py +158 -95
- vtlengine/AST/ASTEncoders.py +1 -1
- vtlengine/AST/ASTTemplate.py +24 -9
- vtlengine/AST/ASTVisitor.py +8 -12
- vtlengine/AST/DAG/__init__.py +43 -35
- vtlengine/AST/DAG/_words.py +4 -4
- vtlengine/AST/Grammar/Vtl.g4 +49 -20
- vtlengine/AST/Grammar/VtlTokens.g4 +13 -1
- vtlengine/AST/Grammar/lexer.py +2012 -1312
- vtlengine/AST/Grammar/parser.py +7524 -4343
- vtlengine/AST/Grammar/tokens.py +140 -128
- vtlengine/AST/VtlVisitor.py +16 -5
- vtlengine/AST/__init__.py +41 -11
- vtlengine/DataTypes/NumericTypesHandling.py +5 -4
- vtlengine/DataTypes/TimeHandling.py +196 -301
- vtlengine/DataTypes/__init__.py +304 -218
- vtlengine/Exceptions/__init__.py +96 -27
- vtlengine/Exceptions/messages.py +149 -69
- vtlengine/Interpreter/__init__.py +817 -497
- vtlengine/Model/__init__.py +172 -121
- vtlengine/Operators/Aggregation.py +156 -95
- vtlengine/Operators/Analytic.py +167 -79
- vtlengine/Operators/Assignment.py +7 -4
- vtlengine/Operators/Boolean.py +27 -32
- vtlengine/Operators/CastOperator.py +177 -131
- vtlengine/Operators/Clause.py +137 -99
- vtlengine/Operators/Comparison.py +148 -117
- vtlengine/Operators/Conditional.py +290 -98
- vtlengine/Operators/General.py +68 -47
- vtlengine/Operators/HROperators.py +91 -72
- vtlengine/Operators/Join.py +217 -118
- vtlengine/Operators/Numeric.py +129 -46
- vtlengine/Operators/RoleSetter.py +16 -15
- vtlengine/Operators/Set.py +61 -36
- vtlengine/Operators/String.py +213 -139
- vtlengine/Operators/Time.py +467 -215
- vtlengine/Operators/Validation.py +117 -76
- vtlengine/Operators/__init__.py +340 -213
- vtlengine/Utils/__init__.py +232 -41
- vtlengine/__init__.py +1 -1
- vtlengine/files/output/__init__.py +15 -6
- vtlengine/files/output/_time_period_representation.py +10 -9
- vtlengine/files/parser/__init__.py +79 -52
- vtlengine/files/parser/_rfc_dialect.py +6 -5
- vtlengine/files/parser/_time_checking.py +48 -37
- vtlengine-1.0.2.dist-info/METADATA +245 -0
- vtlengine-1.0.2.dist-info/RECORD +58 -0
- {vtlengine-1.0.dist-info → vtlengine-1.0.2.dist-info}/WHEEL +1 -1
- vtlengine-1.0.dist-info/METADATA +0 -104
- vtlengine-1.0.dist-info/RECORD +0 -58
- {vtlengine-1.0.dist-info → vtlengine-1.0.2.dist-info}/LICENSE.md +0 -0
|
@@ -2,32 +2,20 @@ import calendar
|
|
|
2
2
|
import copy
|
|
3
3
|
import operator
|
|
4
4
|
from datetime import date, datetime as dt
|
|
5
|
-
from typing import Union, Optional
|
|
5
|
+
from typing import Union, Optional, Any, Dict
|
|
6
6
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"W": 2,
|
|
15
|
-
"D": 1
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
DURATION_MAPPING_REVERSED = {
|
|
19
|
-
6: "A",
|
|
20
|
-
5: "S",
|
|
21
|
-
4: "Q",
|
|
22
|
-
3: "M",
|
|
23
|
-
2: "W",
|
|
24
|
-
1: "D"
|
|
25
|
-
}
|
|
9
|
+
from vtlengine.Exceptions import SemanticError
|
|
10
|
+
|
|
11
|
+
DURATION_MAPPING = {"A": 6, "S": 5, "Q": 4, "M": 3, "W": 2, "D": 1}
|
|
12
|
+
|
|
13
|
+
DURATION_MAPPING_REVERSED = {6: "A", 5: "S", 4: "Q", 3: "M", 2: "W", 1: "D"}
|
|
26
14
|
|
|
27
15
|
PERIOD_INDICATORS = ["A", "S", "Q", "M", "W", "D"]
|
|
28
16
|
|
|
29
17
|
|
|
30
|
-
def date_to_period(date_value: date, period_indicator):
|
|
18
|
+
def date_to_period(date_value: date, period_indicator: str) -> Any:
|
|
31
19
|
if period_indicator == "A":
|
|
32
20
|
return TimePeriodHandler(f"{date_value.year}A")
|
|
33
21
|
elif period_indicator == "S":
|
|
@@ -43,76 +31,47 @@ def date_to_period(date_value: date, period_indicator):
|
|
|
43
31
|
return TimePeriodHandler(f"{date_value.year}D{date_value.timetuple().tm_yday}")
|
|
44
32
|
|
|
45
33
|
|
|
46
|
-
def period_to_date(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if period_indicator == 'Q':
|
|
64
|
-
if period_number == 1:
|
|
65
|
-
if start:
|
|
66
|
-
return date(year, 1, 1)
|
|
67
|
-
else:
|
|
68
|
-
return date(year, 3, 31)
|
|
69
|
-
elif period_number == 2:
|
|
70
|
-
if start:
|
|
71
|
-
return date(year, 4, 1)
|
|
72
|
-
else:
|
|
73
|
-
return date(year, 6, 30)
|
|
74
|
-
elif period_number == 3:
|
|
75
|
-
if start:
|
|
76
|
-
return date(year, 7, 1)
|
|
77
|
-
else:
|
|
78
|
-
return date(year, 9, 30)
|
|
79
|
-
else:
|
|
80
|
-
if start:
|
|
81
|
-
return date(year, 10, 1)
|
|
82
|
-
else:
|
|
83
|
-
return date(year, 12, 31)
|
|
34
|
+
def period_to_date(
|
|
35
|
+
year: int, period_indicator: str, period_number: int, start: bool = False
|
|
36
|
+
) -> date:
|
|
37
|
+
if period_indicator == "A":
|
|
38
|
+
return date(year, 1, 1) if start else date(year, 12, 31)
|
|
39
|
+
periods = {
|
|
40
|
+
"S": [(date(year, 1, 1), date(year, 6, 30)), (date(year, 7, 1), date(year, 12, 31))],
|
|
41
|
+
"Q": [
|
|
42
|
+
(date(year, 1, 1), date(year, 3, 31)),
|
|
43
|
+
(date(year, 4, 1), date(year, 6, 30)),
|
|
44
|
+
(date(year, 7, 1), date(year, 9, 30)),
|
|
45
|
+
(date(year, 10, 1), date(year, 12, 31)),
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
if period_indicator in periods:
|
|
49
|
+
start_date, end_date = periods[period_indicator][period_number - 1]
|
|
50
|
+
return start_date if start else end_date
|
|
84
51
|
if period_indicator == "M":
|
|
85
|
-
if start
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if period_indicator == "W": # 0 for Sunday, 1 for Monday in %w
|
|
91
|
-
if start:
|
|
92
|
-
return dt.strptime(f"{year}-W{period_number}-1", "%G-W%V-%w").date()
|
|
93
|
-
else:
|
|
94
|
-
return dt.strptime(f"{year}-W{period_number}-0", "%G-W%V-%w").date()
|
|
52
|
+
day = 1 if start else calendar.monthrange(year, period_number)[1]
|
|
53
|
+
return date(year, period_number, day)
|
|
54
|
+
if period_indicator == "W":
|
|
55
|
+
week_day = 1 if start else 0
|
|
56
|
+
return dt.strptime(f"{year}-W{period_number}-{week_day}", "%G-W%V-%w").date()
|
|
95
57
|
if period_indicator == "D":
|
|
96
58
|
return dt.strptime(f"{year}-D{period_number}", "%Y-D%j").date()
|
|
97
|
-
|
|
98
|
-
raise ValueError(f'Invalid Period Indicator {period_indicator}')
|
|
59
|
+
raise SemanticError("2-1-19-2", period=period_indicator)
|
|
99
60
|
|
|
100
61
|
|
|
101
|
-
def day_of_year(date: str):
|
|
62
|
+
def day_of_year(date: str) -> int:
|
|
102
63
|
"""
|
|
103
64
|
Returns the day of the year for a given date string
|
|
104
65
|
2020-01-01 -> 1
|
|
105
66
|
"""
|
|
106
67
|
# Convert the date string to a datetime object
|
|
107
|
-
date_object = dt.strptime(date,
|
|
108
|
-
|
|
68
|
+
date_object = dt.strptime(date, "%Y-%m-%d")
|
|
109
69
|
# Get the day number in the year
|
|
110
70
|
day_number = date_object.timetuple().tm_yday
|
|
111
|
-
|
|
112
71
|
return day_number
|
|
113
72
|
|
|
114
73
|
|
|
115
|
-
def from_input_customer_support_to_internal(period: str):
|
|
74
|
+
def from_input_customer_support_to_internal(period: str) -> tuple[int, str, int]:
|
|
116
75
|
"""
|
|
117
76
|
Converts a period string from the input customer support format to the internal format
|
|
118
77
|
2020-01-01 -> (2020, 'D', 1)
|
|
@@ -122,35 +81,25 @@ def from_input_customer_support_to_internal(period: str):
|
|
|
122
81
|
2020-M01 -> (2020, 'M', 1)
|
|
123
82
|
2020-W01 -> (2020, 'W', 1)
|
|
124
83
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
period_number = int(second_term[1:])
|
|
145
|
-
else:
|
|
146
|
-
period_indicator = 'M'
|
|
147
|
-
period_number = int(second_term)
|
|
148
|
-
else:
|
|
149
|
-
raise ValueError
|
|
150
|
-
|
|
151
|
-
return year, period_indicator, period_number
|
|
152
|
-
|
|
153
|
-
raise ValueError
|
|
84
|
+
parts = period.split("-")
|
|
85
|
+
year = int(parts[0])
|
|
86
|
+
if len(parts) == 3: # 'YYYY-MM-DD' case
|
|
87
|
+
return year, "D", int(day_of_year(period))
|
|
88
|
+
second_term = parts[1]
|
|
89
|
+
length = len(second_term)
|
|
90
|
+
if length == 4: # 'YYYY-Dxxx' case
|
|
91
|
+
return year, "D", int(second_term[1:])
|
|
92
|
+
if length == 3: # 'YYYY-Wxx' or 'YYYY-Mxx' case
|
|
93
|
+
return year, second_term[0], int(second_term[1:])
|
|
94
|
+
if length == 2: # 'YYYY-Qx', 'YYYY-Sx', 'YYYY-Ax', or 'YYYY-MM' case
|
|
95
|
+
indicator = second_term[0]
|
|
96
|
+
return (
|
|
97
|
+
(year, indicator, int(second_term[1:]))
|
|
98
|
+
if indicator in PERIOD_INDICATORS
|
|
99
|
+
else (year, "M", int(second_term))
|
|
100
|
+
)
|
|
101
|
+
raise SemanticError("2-1-19-6", period_format=period)
|
|
102
|
+
# raise ValueError
|
|
154
103
|
|
|
155
104
|
|
|
156
105
|
class SingletonMeta(type):
|
|
@@ -160,9 +109,9 @@ class SingletonMeta(type):
|
|
|
160
109
|
metaclass because it is best suited for this purpose.
|
|
161
110
|
"""
|
|
162
111
|
|
|
163
|
-
_instances = {}
|
|
112
|
+
_instances: Dict[Any, Any] = {}
|
|
164
113
|
|
|
165
|
-
def __call__(cls, *args, **kwargs):
|
|
114
|
+
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
|
|
166
115
|
"""
|
|
167
116
|
Possible changes to the value of the `__init__` argument do not affect
|
|
168
117
|
the returned instance.
|
|
@@ -174,25 +123,18 @@ class SingletonMeta(type):
|
|
|
174
123
|
|
|
175
124
|
|
|
176
125
|
class PeriodDuration(metaclass=SingletonMeta):
|
|
177
|
-
periods = {
|
|
178
|
-
'D': 366,
|
|
179
|
-
'W': 53,
|
|
180
|
-
'M': 12,
|
|
181
|
-
'Q': 4,
|
|
182
|
-
'S': 2,
|
|
183
|
-
'A': 1
|
|
184
|
-
}
|
|
126
|
+
periods = {"D": 366, "W": 53, "M": 12, "Q": 4, "S": 2, "A": 1}
|
|
185
127
|
|
|
186
|
-
def __contains__(self, item):
|
|
128
|
+
def __contains__(self, item: Any) -> bool:
|
|
187
129
|
return item in self.periods
|
|
188
130
|
|
|
189
131
|
@property
|
|
190
|
-
def member_names(self):
|
|
132
|
+
def member_names(self) -> list[str]:
|
|
191
133
|
return list(self.periods.keys())
|
|
192
134
|
|
|
193
135
|
@classmethod
|
|
194
|
-
def check_period_range(cls, letter, value):
|
|
195
|
-
if letter ==
|
|
136
|
+
def check_period_range(cls, letter: str, value: Any) -> bool:
|
|
137
|
+
if letter == "A":
|
|
196
138
|
return True
|
|
197
139
|
return value in range(1, cls.periods[letter] + 1)
|
|
198
140
|
|
|
@@ -202,23 +144,26 @@ class TimePeriodHandler:
|
|
|
202
144
|
_period_indicator: str
|
|
203
145
|
_period_number: int
|
|
204
146
|
|
|
205
|
-
def __init__(self, period: str):
|
|
147
|
+
def __init__(self, period: str) -> None:
|
|
148
|
+
if isinstance(period, int):
|
|
149
|
+
period = str(period)
|
|
206
150
|
if "-" in period:
|
|
207
|
-
self.year, self.period_indicator, self.period_number =
|
|
208
|
-
period)
|
|
151
|
+
self.year, self.period_indicator, self.period_number = (
|
|
152
|
+
from_input_customer_support_to_internal(period)
|
|
153
|
+
)
|
|
209
154
|
else:
|
|
210
155
|
self.year = int(period[:4])
|
|
211
156
|
if len(period) > 4:
|
|
212
157
|
self.period_indicator = period[4]
|
|
213
158
|
else:
|
|
214
|
-
self.period_indicator =
|
|
159
|
+
self.period_indicator = "A"
|
|
215
160
|
if len(period) > 5:
|
|
216
161
|
self.period_number = int(period[5:])
|
|
217
162
|
else:
|
|
218
163
|
self.period_number = 1
|
|
219
164
|
|
|
220
|
-
def __str__(self):
|
|
221
|
-
if self.period_indicator ==
|
|
165
|
+
def __str__(self) -> str:
|
|
166
|
+
if self.period_indicator == "A":
|
|
222
167
|
# return f"{self.year}{self.period_indicator}"
|
|
223
168
|
return f"{self.year}" # Drop A from exit time period year
|
|
224
169
|
if self.period_indicator in ["W", "M"]:
|
|
@@ -230,16 +175,17 @@ class TimePeriodHandler:
|
|
|
230
175
|
return f"{self.year}-{self.period_indicator}{period_number_str}"
|
|
231
176
|
|
|
232
177
|
@staticmethod
|
|
233
|
-
def _check_year(year: int):
|
|
178
|
+
def _check_year(year: int) -> None:
|
|
234
179
|
if year < 1900 or year > 9999:
|
|
235
|
-
raise
|
|
180
|
+
raise SemanticError("2-1-19-10", year=year)
|
|
181
|
+
# raise ValueError(f'Invalid year {year}, must be between 1900 and 9999.')
|
|
236
182
|
|
|
237
183
|
@property
|
|
238
184
|
def year(self) -> int:
|
|
239
185
|
return self._year
|
|
240
186
|
|
|
241
187
|
@year.setter
|
|
242
|
-
def year(self, value: int):
|
|
188
|
+
def year(self, value: int) -> None:
|
|
243
189
|
self._check_year(value)
|
|
244
190
|
self._year = value
|
|
245
191
|
|
|
@@ -248,10 +194,9 @@ class TimePeriodHandler:
|
|
|
248
194
|
return self._period_indicator
|
|
249
195
|
|
|
250
196
|
@period_indicator.setter
|
|
251
|
-
def period_indicator(self, value: str):
|
|
197
|
+
def period_indicator(self, value: str) -> None:
|
|
252
198
|
if value not in PeriodDuration():
|
|
253
|
-
raise
|
|
254
|
-
f'Cannot set period indicator as {value}. Possible values: {PeriodDuration().member_names}')
|
|
199
|
+
raise SemanticError("2-1-19-2", period=value)
|
|
255
200
|
self._period_indicator = value
|
|
256
201
|
|
|
257
202
|
@property
|
|
@@ -259,85 +204,92 @@ class TimePeriodHandler:
|
|
|
259
204
|
return self._period_number
|
|
260
205
|
|
|
261
206
|
@period_number.setter
|
|
262
|
-
def period_number(self, value: int):
|
|
207
|
+
def period_number(self, value: int) -> None:
|
|
263
208
|
if not PeriodDuration.check_period_range(self.period_indicator, value):
|
|
264
|
-
raise
|
|
265
|
-
|
|
266
|
-
|
|
209
|
+
raise SemanticError(
|
|
210
|
+
"2-1-19-7",
|
|
211
|
+
periods=PeriodDuration.periods[self.period_indicator],
|
|
212
|
+
period_inidcator=self.period_indicator,
|
|
213
|
+
)
|
|
214
|
+
# raise ValueError(f'Period Number must be between 1 and '
|
|
215
|
+
# f'{PeriodDuration.periods[self.period_indicator]} '
|
|
216
|
+
# f'for period indicator {self.period_indicator}.')
|
|
267
217
|
# check day is correct for year
|
|
268
|
-
if self.period_indicator ==
|
|
218
|
+
if self.period_indicator == "D":
|
|
269
219
|
if calendar.isleap(self.year):
|
|
270
220
|
if value > 366:
|
|
271
|
-
raise
|
|
221
|
+
raise SemanticError("2-1-19-9", day=value, year=self.year)
|
|
222
|
+
# raise ValueError(f'Invalid day {value} for year {self.year}.')
|
|
272
223
|
else:
|
|
273
224
|
if value > 365:
|
|
274
|
-
raise
|
|
225
|
+
raise SemanticError("2-1-19-9", day=value, year=self.year)
|
|
226
|
+
# raise ValueError(f'Invalid day {value} for year {self.year}.')
|
|
275
227
|
self._period_number = value
|
|
276
228
|
|
|
277
|
-
def _meta_comparison(self, other, py_op) -> Optional[bool]:
|
|
229
|
+
def _meta_comparison(self, other: Any, py_op: Any) -> Optional[bool]:
|
|
278
230
|
if pd.isnull(other):
|
|
279
231
|
return None
|
|
280
232
|
if isinstance(other, str):
|
|
281
233
|
if len(other) == 0:
|
|
282
234
|
return False
|
|
283
235
|
other = TimePeriodHandler(other)
|
|
236
|
+
return py_op(
|
|
237
|
+
DURATION_MAPPING[self.period_indicator], DURATION_MAPPING[other.period_indicator]
|
|
238
|
+
)
|
|
284
239
|
|
|
285
|
-
|
|
286
|
-
DURATION_MAPPING[other.period_indicator])
|
|
287
|
-
|
|
288
|
-
def start_date(self, as_date=False) -> Union[date, str]:
|
|
240
|
+
def start_date(self, as_date: bool = False) -> Union[date, str]:
|
|
289
241
|
"""
|
|
290
242
|
Gets the starting date of the Period
|
|
291
243
|
"""
|
|
292
|
-
date_value = period_to_date(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return date_value.isoformat()
|
|
299
|
-
|
|
300
|
-
def end_date(self, as_date=False) -> Union[date, str]:
|
|
244
|
+
date_value = period_to_date(
|
|
245
|
+
year=self.year,
|
|
246
|
+
period_indicator=self.period_indicator,
|
|
247
|
+
period_number=self.period_number,
|
|
248
|
+
start=True,
|
|
249
|
+
)
|
|
250
|
+
return date_value if as_date else date_value.isoformat()
|
|
251
|
+
|
|
252
|
+
def end_date(self, as_date: bool = False) -> Union[date, str]:
|
|
301
253
|
"""
|
|
302
254
|
Gets the ending date of the Period
|
|
303
255
|
"""
|
|
304
|
-
date_value = period_to_date(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return date_value.isoformat()
|
|
311
|
-
|
|
312
|
-
def __eq__(self, other) -> bool:
|
|
256
|
+
date_value = period_to_date(
|
|
257
|
+
year=self.year,
|
|
258
|
+
period_indicator=self.period_indicator,
|
|
259
|
+
period_number=self.period_number,
|
|
260
|
+
start=False,
|
|
261
|
+
)
|
|
262
|
+
return date_value if as_date else date_value.isoformat()
|
|
263
|
+
|
|
264
|
+
def __eq__(self, other: Any) -> Optional[bool]: # type: ignore[override]
|
|
313
265
|
return self._meta_comparison(other, operator.eq)
|
|
314
266
|
|
|
315
|
-
def __ne__(self, other) -> bool:
|
|
267
|
+
def __ne__(self, other: Any) -> Optional[bool]: # type: ignore[override]
|
|
316
268
|
return not self._meta_comparison(other, operator.eq)
|
|
317
269
|
|
|
318
|
-
def __lt__(self, other) -> bool:
|
|
270
|
+
def __lt__(self, other: Any) -> Optional[bool]:
|
|
319
271
|
return self._meta_comparison(other, operator.lt)
|
|
320
272
|
|
|
321
|
-
def __le__(self, other) -> bool:
|
|
273
|
+
def __le__(self, other: Any) -> Optional[bool]:
|
|
322
274
|
return self._meta_comparison(other, operator.le)
|
|
323
275
|
|
|
324
|
-
def __gt__(self, other) -> bool:
|
|
276
|
+
def __gt__(self, other: Any) -> Optional[bool]:
|
|
325
277
|
return self._meta_comparison(other, operator.gt)
|
|
326
278
|
|
|
327
|
-
def __ge__(self, other) -> bool:
|
|
279
|
+
def __ge__(self, other: Any) -> Optional[bool]:
|
|
328
280
|
return self._meta_comparison(other, operator.ge)
|
|
329
281
|
|
|
330
|
-
def change_indicator(self, new_indicator):
|
|
282
|
+
def change_indicator(self, new_indicator: str) -> None:
|
|
331
283
|
if self.period_indicator == new_indicator:
|
|
332
284
|
return
|
|
333
285
|
date_value = period_to_date(self.year, self.period_indicator, self.period_number)
|
|
334
286
|
self.period_indicator = new_indicator
|
|
335
|
-
self.period_number = date_to_period(
|
|
336
|
-
|
|
287
|
+
self.period_number = date_to_period(
|
|
288
|
+
date_value, period_indicator=new_indicator
|
|
289
|
+
).period_number
|
|
337
290
|
|
|
338
|
-
def vtl_representation(self):
|
|
339
|
-
if self.period_indicator ==
|
|
340
|
-
# return f"{self.year}{self.period_indicator}"
|
|
291
|
+
def vtl_representation(self) -> str:
|
|
292
|
+
if self.period_indicator == "A":
|
|
341
293
|
return f"{self.year}" # Drop A from exit time period year
|
|
342
294
|
if self.period_indicator in ["W", "M"]:
|
|
343
295
|
period_number_str = f"{self.period_number:02}"
|
|
@@ -347,106 +299,106 @@ class TimePeriodHandler:
|
|
|
347
299
|
period_number_str = str(self.period_number)
|
|
348
300
|
return f"{self.year}{self.period_indicator}{period_number_str}"
|
|
349
301
|
|
|
350
|
-
def sdmx_gregorian_representation(self):
|
|
302
|
+
def sdmx_gregorian_representation(self) -> None:
|
|
351
303
|
raise NotImplementedError
|
|
352
304
|
|
|
353
305
|
|
|
354
306
|
class TimeIntervalHandler:
|
|
355
|
-
_date1: str =
|
|
356
|
-
_date2: str =
|
|
307
|
+
_date1: str = "0"
|
|
308
|
+
_date2: str = "Z"
|
|
357
309
|
|
|
358
|
-
def __init__(self, date1: str, date2: str):
|
|
359
|
-
self.date1
|
|
360
|
-
self.date2
|
|
361
|
-
if date1 > date2:
|
|
362
|
-
|
|
310
|
+
def __init__(self, date1: str, date2: str) -> None:
|
|
311
|
+
self.set_date1(date1)
|
|
312
|
+
self.set_date2(date2)
|
|
313
|
+
# if date1 > date2:
|
|
314
|
+
# raise ValueError(f'Invalid Time with duration less than 0 ({self.length} days)')
|
|
363
315
|
|
|
364
316
|
@classmethod
|
|
365
|
-
def from_dates(cls, date1: date, date2: date):
|
|
317
|
+
def from_dates(cls, date1: date, date2: date) -> "TimeIntervalHandler":
|
|
366
318
|
return cls(date1.isoformat(), date2.isoformat())
|
|
367
319
|
|
|
368
320
|
@classmethod
|
|
369
|
-
def from_iso_format(cls, dates: str):
|
|
370
|
-
return cls(*dates.split(
|
|
321
|
+
def from_iso_format(cls, dates: str) -> "TimeIntervalHandler":
|
|
322
|
+
return cls(*dates.split("/", maxsplit=1))
|
|
371
323
|
|
|
372
324
|
@property
|
|
373
|
-
def date1(self, as_date=False) -> Union[date, str]:
|
|
374
|
-
if as_date
|
|
375
|
-
return date.fromisoformat(self._date1)
|
|
376
|
-
return self._date1
|
|
325
|
+
def date1(self, as_date: bool = False) -> Union[date, str]:
|
|
326
|
+
return date.fromisoformat(self._date1) if as_date else self._date1
|
|
377
327
|
|
|
378
328
|
@property
|
|
379
|
-
def date2(self, as_date=False) -> Union[date, str]:
|
|
380
|
-
if as_date
|
|
381
|
-
return date.fromisoformat(self._date2)
|
|
382
|
-
return self._date2
|
|
329
|
+
def date2(self, as_date: bool = False) -> Union[date, str]:
|
|
330
|
+
return date.fromisoformat(self._date2) if as_date else self._date2
|
|
383
331
|
|
|
384
|
-
@date1.setter
|
|
385
|
-
def
|
|
332
|
+
# @date1.setter
|
|
333
|
+
def set_date1(self, value: str) -> None:
|
|
386
334
|
date.fromisoformat(value)
|
|
387
|
-
if value > self.date2:
|
|
388
|
-
raise
|
|
389
|
-
|
|
335
|
+
if value > self.date2.__str__():
|
|
336
|
+
raise SemanticError("2-1-19-4", date=self.date2, value=value)
|
|
337
|
+
# raise ValueError(f"({value} > {self.date2}).
|
|
338
|
+
# Cannot set date1 with a value greater than date2.")
|
|
390
339
|
self._date1 = value
|
|
391
340
|
|
|
392
|
-
|
|
393
|
-
def date2(self, value: str):
|
|
341
|
+
def set_date2(self, value: str) -> None:
|
|
394
342
|
date.fromisoformat(value)
|
|
395
|
-
if value < self.date1:
|
|
396
|
-
raise
|
|
397
|
-
|
|
343
|
+
if value < self.date1.__str__():
|
|
344
|
+
raise SemanticError("2-1-19-5", date=self.date1, value=value)
|
|
345
|
+
# raise ValueError(f"({value} < {self.date1}).
|
|
346
|
+
# Cannot set date2 with a value lower than date1.")
|
|
398
347
|
self._date2 = value
|
|
399
348
|
|
|
400
349
|
@property
|
|
401
350
|
def length(self) -> int:
|
|
402
|
-
date_left = date.fromisoformat(self.date1)
|
|
403
|
-
date_right = date.fromisoformat(self.date2)
|
|
351
|
+
date_left = date.fromisoformat(self.date1.__str__())
|
|
352
|
+
date_right = date.fromisoformat(self.date2.__str__())
|
|
404
353
|
return (date_right - date_left).days
|
|
405
354
|
|
|
406
355
|
__len__ = length
|
|
407
356
|
|
|
408
|
-
def __str__(self):
|
|
357
|
+
def __str__(self) -> str:
|
|
409
358
|
return f"{self.date1}/{self.date2}"
|
|
410
359
|
|
|
411
360
|
__repr__ = __str__
|
|
412
361
|
|
|
413
|
-
def _meta_comparison(self, other, py_op) -> Optional[bool]:
|
|
362
|
+
def _meta_comparison(self, other: Any, py_op: Any) -> Optional[bool]:
|
|
414
363
|
if pd.isnull(other):
|
|
415
364
|
return None
|
|
416
365
|
if isinstance(other, str):
|
|
417
366
|
if len(other) == 0:
|
|
418
367
|
return False
|
|
419
|
-
other = TimeIntervalHandler(*other.split(
|
|
368
|
+
other = TimeIntervalHandler(*other.split("/", maxsplit=1))
|
|
420
369
|
return py_op(self.length, other.length)
|
|
421
370
|
|
|
422
|
-
def __eq__(self, other) -> bool:
|
|
371
|
+
def __eq__(self, other: Any) -> Optional[bool]: # type: ignore[override]
|
|
423
372
|
return self._meta_comparison(other, operator.eq)
|
|
424
373
|
|
|
425
|
-
def __ne__(self, other) -> bool:
|
|
374
|
+
def __ne__(self, other: Any) -> Optional[bool]: # type: ignore[override]
|
|
426
375
|
return self._meta_comparison(other, operator.ne)
|
|
427
376
|
|
|
428
|
-
def __lt__(self, other) -> bool:
|
|
377
|
+
def __lt__(self, other: Any) -> Optional[bool]:
|
|
429
378
|
return self._meta_comparison(other, operator.lt)
|
|
430
379
|
|
|
431
|
-
def __le__(self, other) -> bool:
|
|
380
|
+
def __le__(self, other: Any) -> Optional[bool]:
|
|
432
381
|
return self._meta_comparison(other, operator.le)
|
|
433
382
|
|
|
434
|
-
def __gt__(self, other) -> bool:
|
|
383
|
+
def __gt__(self, other: Any) -> Optional[bool]:
|
|
435
384
|
return self._meta_comparison(other, operator.gt)
|
|
436
385
|
|
|
437
|
-
def __ge__(self, other) -> bool:
|
|
386
|
+
def __ge__(self, other: Any) -> Optional[bool]:
|
|
438
387
|
return self._meta_comparison(other, operator.ge)
|
|
439
388
|
|
|
440
389
|
@classmethod
|
|
441
|
-
def from_time_period(cls, value: TimePeriodHandler):
|
|
390
|
+
def from_time_period(cls, value: TimePeriodHandler) -> "TimeIntervalHandler":
|
|
442
391
|
date1 = period_to_date(value.year, value.period_indicator, value.period_number, start=True)
|
|
443
392
|
date2 = period_to_date(value.year, value.period_indicator, value.period_number, start=False)
|
|
444
393
|
return cls.from_dates(date1, date2)
|
|
445
394
|
|
|
446
395
|
|
|
447
|
-
def sort_dataframe_by_period_column(
|
|
396
|
+
def sort_dataframe_by_period_column(
|
|
397
|
+
data: pd.DataFrame, name: str, identifiers_names: list[str]
|
|
398
|
+
) -> pd.DataFrame:
|
|
448
399
|
"""
|
|
449
|
-
Sorts dataframe by TimePeriod period_indicator and period_number.
|
|
400
|
+
Sorts dataframe by TimePeriod period_indicator and period_number.
|
|
401
|
+
Assuming all values are present (only for identifiers)
|
|
450
402
|
"""
|
|
451
403
|
new_component_name = "@period_number"
|
|
452
404
|
|
|
@@ -463,11 +415,10 @@ def sort_dataframe_by_period_column(data, name, identifiers_names):
|
|
|
463
415
|
|
|
464
416
|
identifiers_names.remove("duration_var")
|
|
465
417
|
identifiers_names.remove(new_component_name)
|
|
466
|
-
|
|
467
418
|
return data
|
|
468
419
|
|
|
469
420
|
|
|
470
|
-
def next_period(x: TimePeriodHandler):
|
|
421
|
+
def next_period(x: TimePeriodHandler) -> TimePeriodHandler:
|
|
471
422
|
y = copy.copy(x)
|
|
472
423
|
if y.period_number == PeriodDuration.periods[x.period_indicator]:
|
|
473
424
|
y.year += 1
|
|
@@ -477,7 +428,7 @@ def next_period(x: TimePeriodHandler):
|
|
|
477
428
|
return y
|
|
478
429
|
|
|
479
430
|
|
|
480
|
-
def previous_period(x: TimePeriodHandler):
|
|
431
|
+
def previous_period(x: TimePeriodHandler) -> TimePeriodHandler:
|
|
481
432
|
y = copy.copy(x)
|
|
482
433
|
if x.period_number == 1:
|
|
483
434
|
y.year -= 1
|
|
@@ -487,31 +438,32 @@ def previous_period(x: TimePeriodHandler):
|
|
|
487
438
|
return y
|
|
488
439
|
|
|
489
440
|
|
|
490
|
-
def shift_period(x: TimePeriodHandler, shift_param: int):
|
|
441
|
+
def shift_period(x: TimePeriodHandler, shift_param: int) -> TimePeriodHandler:
|
|
491
442
|
if x.period_indicator == "A":
|
|
492
443
|
x.year += shift_param
|
|
493
444
|
return x
|
|
494
|
-
|
|
495
445
|
for _ in range(abs(shift_param)):
|
|
496
|
-
if shift_param >= 0
|
|
497
|
-
x = next_period(x)
|
|
498
|
-
else:
|
|
499
|
-
x = previous_period(x)
|
|
446
|
+
x = next_period(x) if shift_param >= 0 else previous_period(x)
|
|
500
447
|
return x
|
|
501
448
|
|
|
502
449
|
|
|
503
|
-
def sort_time_period(series:
|
|
504
|
-
values_sorted = sorted(
|
|
505
|
-
|
|
506
|
-
|
|
450
|
+
def sort_time_period(series: Any) -> Any:
|
|
451
|
+
values_sorted = sorted(
|
|
452
|
+
series.to_list(),
|
|
453
|
+
key=lambda s: (s.year, DURATION_MAPPING[s.period_indicator], s.period_number),
|
|
454
|
+
)
|
|
507
455
|
return pd.Series(values_sorted, name=series.name)
|
|
508
456
|
|
|
509
457
|
|
|
510
|
-
def generate_period_range(
|
|
458
|
+
def generate_period_range(
|
|
459
|
+
start: TimePeriodHandler, end: TimePeriodHandler
|
|
460
|
+
) -> list[TimePeriodHandler]:
|
|
511
461
|
period_range = [start]
|
|
512
462
|
if start.period_indicator != end.period_indicator:
|
|
513
|
-
raise
|
|
514
|
-
|
|
463
|
+
raise SemanticError(
|
|
464
|
+
"2-1-19-3", period1=start.period_indicator, period2=end.period_indicator
|
|
465
|
+
)
|
|
466
|
+
# raise Exception("Only same period indicator allowed")
|
|
515
467
|
if start.period_indicator == "A":
|
|
516
468
|
for _ in range(end.year - start.year):
|
|
517
469
|
period_range.append(next_period(period_range[-1]))
|
|
@@ -522,90 +474,33 @@ def generate_period_range(start: TimePeriodHandler, end: TimePeriodHandler):
|
|
|
522
474
|
return period_range
|
|
523
475
|
|
|
524
476
|
|
|
525
|
-
def
|
|
526
|
-
if
|
|
527
|
-
if start:
|
|
528
|
-
return date(year, 1, 1)
|
|
529
|
-
else:
|
|
530
|
-
return date(year, 12, 31)
|
|
531
|
-
if period_indicator == 'S':
|
|
532
|
-
if period_number == 1:
|
|
533
|
-
if start:
|
|
534
|
-
return date(year, 1, 1)
|
|
535
|
-
else:
|
|
536
|
-
return date(year, 6, 30)
|
|
537
|
-
else:
|
|
538
|
-
if start:
|
|
539
|
-
return date(year, 7, 1)
|
|
540
|
-
else:
|
|
541
|
-
return date(year, 12, 31)
|
|
542
|
-
if period_indicator == 'Q':
|
|
543
|
-
if period_number == 1:
|
|
544
|
-
if start:
|
|
545
|
-
return date(year, 1, 1)
|
|
546
|
-
else:
|
|
547
|
-
return date(year, 3, 31)
|
|
548
|
-
elif period_number == 2:
|
|
549
|
-
if start:
|
|
550
|
-
return date(year, 4, 1)
|
|
551
|
-
else:
|
|
552
|
-
return date(year, 6, 30)
|
|
553
|
-
elif period_number == 3:
|
|
554
|
-
if start:
|
|
555
|
-
return date(year, 7, 1)
|
|
556
|
-
else:
|
|
557
|
-
return date(year, 9, 30)
|
|
558
|
-
else:
|
|
559
|
-
if start:
|
|
560
|
-
return date(year, 10, 1)
|
|
561
|
-
else:
|
|
562
|
-
return date(year, 12, 31)
|
|
563
|
-
if period_indicator == "M":
|
|
564
|
-
if start:
|
|
565
|
-
return date(year, period_number, 1)
|
|
566
|
-
else:
|
|
567
|
-
day = int(calendar.monthrange(year, period_number)[1])
|
|
568
|
-
return date(year, period_number, day)
|
|
569
|
-
if period_indicator == "W": # 0 for Sunday, 1 for Monday in %w
|
|
570
|
-
if start:
|
|
571
|
-
return dt.strptime(f"{year}-W{period_number}-1", "%G-W%V-%w").date()
|
|
572
|
-
else:
|
|
573
|
-
return dt.strptime(f"{year}-W{period_number}-0", "%G-W%V-%w").date()
|
|
574
|
-
if period_indicator == "D":
|
|
575
|
-
return dt.strptime(f"{year}-D{period_number}", "%Y-D%j").date()
|
|
576
|
-
|
|
577
|
-
raise ValueError(f'Invalid Period Indicator {period_indicator}')
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
def check_max_date(str_: str):
|
|
581
|
-
if pd.isnull(str_) or str_ == 'nan' or str_ == 'NaT':
|
|
477
|
+
def check_max_date(str_: Optional[str]) -> Optional[str]:
|
|
478
|
+
if pd.isnull(str_) or str_ == "nan" or str_ == "NaT":
|
|
582
479
|
return None
|
|
583
480
|
|
|
584
|
-
if len(str_) == 9 and str_[7] ==
|
|
585
|
-
str_ = str_[:-1] +
|
|
481
|
+
if len(str_) == 9 and str_[7] == "-":
|
|
482
|
+
str_ = str_[:-1] + "0" + str_[-1]
|
|
586
483
|
|
|
587
484
|
# Format 2010-01-01. Prevent passthrough of other ISO 8601 formats.
|
|
588
|
-
if len(str_) != 10 or str_[7] !=
|
|
589
|
-
raise
|
|
485
|
+
if len(str_) != 10 or str_[7] != "-":
|
|
486
|
+
raise SemanticError("2-1-19-8", date=str)
|
|
487
|
+
# raise ValueError(f"Invalid date format, must be YYYY-MM-DD: {str_}")
|
|
590
488
|
|
|
591
489
|
result = date.fromisoformat(str_)
|
|
592
490
|
return result.isoformat()
|
|
593
491
|
|
|
594
492
|
|
|
595
|
-
def str_period_to_date(value: str, start=False) ->
|
|
493
|
+
def str_period_to_date(value: str, start: bool = False) -> Any:
|
|
596
494
|
if len(value) < 6:
|
|
597
|
-
if start:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
return TimePeriodHandler(value).start_date(as_date=False)
|
|
604
|
-
else:
|
|
605
|
-
return TimePeriodHandler(value).end_date(as_date=False)
|
|
495
|
+
return date(int(value[:4]), 1, 1) if start else date(int(value[:4]), 12, 31)
|
|
496
|
+
return (
|
|
497
|
+
TimePeriodHandler(value).start_date(as_date=False)
|
|
498
|
+
if start
|
|
499
|
+
else (TimePeriodHandler(value).end_date(as_date=False))
|
|
500
|
+
)
|
|
606
501
|
|
|
607
502
|
|
|
608
|
-
def date_to_period_str(date_value: date, period_indicator):
|
|
503
|
+
def date_to_period_str(date_value: date, period_indicator: str) -> Any:
|
|
609
504
|
if isinstance(date_value, str):
|
|
610
505
|
date_value = check_max_date(date_value)
|
|
611
506
|
date_value = date.fromisoformat(date_value)
|