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