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.
Files changed (66) hide show
  1. vtlengine/API/_InternalApi.py +791 -0
  2. vtlengine/API/__init__.py +612 -0
  3. vtlengine/API/data/schema/external_routines_schema.json +34 -0
  4. vtlengine/API/data/schema/json_schema_2.1.json +116 -0
  5. vtlengine/API/data/schema/value_domain_schema.json +97 -0
  6. vtlengine/AST/ASTComment.py +57 -0
  7. vtlengine/AST/ASTConstructor.py +598 -0
  8. vtlengine/AST/ASTConstructorModules/Expr.py +1928 -0
  9. vtlengine/AST/ASTConstructorModules/ExprComponents.py +995 -0
  10. vtlengine/AST/ASTConstructorModules/Terminals.py +790 -0
  11. vtlengine/AST/ASTConstructorModules/__init__.py +50 -0
  12. vtlengine/AST/ASTDataExchange.py +10 -0
  13. vtlengine/AST/ASTEncoders.py +32 -0
  14. vtlengine/AST/ASTString.py +675 -0
  15. vtlengine/AST/ASTTemplate.py +558 -0
  16. vtlengine/AST/ASTVisitor.py +25 -0
  17. vtlengine/AST/DAG/__init__.py +479 -0
  18. vtlengine/AST/DAG/_words.py +10 -0
  19. vtlengine/AST/Grammar/Vtl.g4 +705 -0
  20. vtlengine/AST/Grammar/VtlTokens.g4 +409 -0
  21. vtlengine/AST/Grammar/__init__.py +0 -0
  22. vtlengine/AST/Grammar/lexer.py +2139 -0
  23. vtlengine/AST/Grammar/parser.py +16597 -0
  24. vtlengine/AST/Grammar/tokens.py +169 -0
  25. vtlengine/AST/VtlVisitor.py +824 -0
  26. vtlengine/AST/__init__.py +674 -0
  27. vtlengine/DataTypes/TimeHandling.py +562 -0
  28. vtlengine/DataTypes/__init__.py +863 -0
  29. vtlengine/DataTypes/_time_checking.py +135 -0
  30. vtlengine/Exceptions/__exception_file_generator.py +96 -0
  31. vtlengine/Exceptions/__init__.py +159 -0
  32. vtlengine/Exceptions/messages.py +1004 -0
  33. vtlengine/Interpreter/__init__.py +2048 -0
  34. vtlengine/Model/__init__.py +501 -0
  35. vtlengine/Operators/Aggregation.py +357 -0
  36. vtlengine/Operators/Analytic.py +455 -0
  37. vtlengine/Operators/Assignment.py +23 -0
  38. vtlengine/Operators/Boolean.py +106 -0
  39. vtlengine/Operators/CastOperator.py +451 -0
  40. vtlengine/Operators/Clause.py +366 -0
  41. vtlengine/Operators/Comparison.py +488 -0
  42. vtlengine/Operators/Conditional.py +495 -0
  43. vtlengine/Operators/General.py +191 -0
  44. vtlengine/Operators/HROperators.py +254 -0
  45. vtlengine/Operators/Join.py +447 -0
  46. vtlengine/Operators/Numeric.py +422 -0
  47. vtlengine/Operators/RoleSetter.py +77 -0
  48. vtlengine/Operators/Set.py +176 -0
  49. vtlengine/Operators/String.py +578 -0
  50. vtlengine/Operators/Time.py +1144 -0
  51. vtlengine/Operators/Validation.py +275 -0
  52. vtlengine/Operators/__init__.py +900 -0
  53. vtlengine/Utils/__Virtual_Assets.py +34 -0
  54. vtlengine/Utils/__init__.py +479 -0
  55. vtlengine/__extras_check.py +17 -0
  56. vtlengine/__init__.py +27 -0
  57. vtlengine/files/__init__.py +0 -0
  58. vtlengine/files/output/__init__.py +35 -0
  59. vtlengine/files/output/_time_period_representation.py +55 -0
  60. vtlengine/files/parser/__init__.py +240 -0
  61. vtlengine/files/parser/_rfc_dialect.py +22 -0
  62. vtlengine/py.typed +0 -0
  63. vtlengine-1.4.0rc2.dist-info/METADATA +89 -0
  64. vtlengine-1.4.0rc2.dist-info/RECORD +66 -0
  65. vtlengine-1.4.0rc2.dist-info/WHEEL +4 -0
  66. 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}"