|
1 | 1 | from datetime import datetime, timedelta, date |
| 2 | +from dateutil.relativedelta import relativedelta |
2 | 3 | from typing import Optional |
3 | 4 | import logging |
4 | 5 | from classes.date.date_description import DateDescription |
@@ -235,12 +236,129 @@ def is_valid_date(date_str: str, date_format: str) -> bool: |
235 | 236 | @staticmethod |
236 | 237 | def oracle_to_date_function(date_str: str, oracle_format: str) -> str: |
237 | 238 | """ |
238 | | - Constructs an Oracle TO_DATE function to convert a string to a date. |
239 | | - This method formats the date string according to the specified format. |
| 239 | + Constructs a safe Oracle TO_DATE() expression from a Python date string. |
240 | 240 | Args: |
241 | | - date_str (str): The date string to be converted. |
242 | | - oracle_format (str): The format in which the date string is provided. |
| 241 | + date_str (str): The date string to be converted (e.g., '06/10/1950'). |
| 242 | + oracle_format (str): The Oracle format mask (e.g., 'dd/mm/yyyy'). |
243 | 243 | Returns: |
244 | | - str: The SQL expression for the Oracle TO_DATE function. |
| 244 | + str: The SQL-safe TO_DATE(...) expression, or 'NULL' if date_str is None/empty. |
245 | 245 | """ |
246 | | - return f" TO_DATE( '{date_str}', '{oracle_format}') " |
| 246 | + if not date_str or str(date_str).strip().lower() == "none": |
| 247 | + return "NULL" |
| 248 | + |
| 249 | + clean = str(date_str).strip().replace("\u00a0", "") |
| 250 | + py_fmt = ( |
| 251 | + oracle_format.lower() |
| 252 | + .replace("dd", "%d") |
| 253 | + .replace("mm", "%m") |
| 254 | + .replace("yyyy", "%Y") |
| 255 | + ) |
| 256 | + |
| 257 | + # Validate in Python before sending to Oracle |
| 258 | + datetime.strptime(clean, py_fmt) |
| 259 | + |
| 260 | + return f"TO_DATE('{clean}', '{oracle_format}')" |
| 261 | + |
| 262 | + @staticmethod |
| 263 | + def convert_description_to_python_date( |
| 264 | + which_date: str, date_description: str |
| 265 | + ) -> Optional[date]: |
| 266 | + """ |
| 267 | + Converts a date description (e.g. '2 years ago', '15/08/2020', 'NULL') |
| 268 | + into a Python datetime.date object or None. |
| 269 | + Args: |
| 270 | + which_date (str): Label for logging. |
| 271 | + date_description (str): Human-readable or exact date description. |
| 272 | + Returns: |
| 273 | + Optional[date]: The corresponding date object, or None if not applicable. |
| 274 | + """ |
| 275 | + logging.debug( |
| 276 | + f"convert_description_to_python_date: {which_date}, {date_description}" |
| 277 | + ) |
| 278 | + |
| 279 | + if not date_description or date_description.strip().upper() in ( |
| 280 | + DateDescriptionUtils.NULL_STRING, |
| 281 | + "NONE", |
| 282 | + ): |
| 283 | + return None |
| 284 | + |
| 285 | + date_description = date_description.strip() |
| 286 | + today = datetime.today().date() |
| 287 | + |
| 288 | + # Try direct date formats |
| 289 | + abs_date = _parse_absolute_date(date_description) |
| 290 | + if abs_date: |
| 291 | + return abs_date |
| 292 | + |
| 293 | + # Try relative phrases like "3 years ago" |
| 294 | + rel_date = _parse_relative_date(date_description, today) |
| 295 | + if rel_date: |
| 296 | + return rel_date |
| 297 | + |
| 298 | + # Try enum-based descriptions |
| 299 | + enum_date = _parse_enum_date(date_description, today) |
| 300 | + if enum_date is not None: |
| 301 | + return enum_date |
| 302 | + |
| 303 | + return None |
| 304 | + |
| 305 | + |
| 306 | +def _parse_absolute_date(date_description: str) -> Optional[date]: |
| 307 | + """ |
| 308 | + Tries to parse an absolute date from the description. |
| 309 | + Args: |
| 310 | + date_description (str): The date description to parse. |
| 311 | + Returns: |
| 312 | + Optional[date]: The parsed absolute date or None if parsing failed. |
| 313 | + """ |
| 314 | + for fmt in ( |
| 315 | + DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY, |
| 316 | + DateDescriptionUtils.DATE_FORMAT_YYYY_MM_DD, |
| 317 | + ): |
| 318 | + if DateDescriptionUtils.is_valid_date(date_description, fmt): |
| 319 | + return datetime.strptime(date_description, fmt).date() |
| 320 | + return None |
| 321 | + |
| 322 | + |
| 323 | +def _parse_relative_date(date_description: str, today: date) -> Optional[date]: |
| 324 | + """ |
| 325 | + Tries to parse a relative date from the description (e.g., "3 years ago"). |
| 326 | + Args: |
| 327 | + date_description (str): The date description to parse. |
| 328 | + today (date): The reference date for relative calculations. |
| 329 | + Returns: |
| 330 | + Optional[date]: The parsed relative date or None if parsing failed. |
| 331 | + """ |
| 332 | + words = date_description.split(" ") |
| 333 | + if date_description.endswith(" ago") and len(words) == 3: |
| 334 | + try: |
| 335 | + number = int(words[0]) |
| 336 | + unit = words[1] |
| 337 | + if "year" in unit: |
| 338 | + return today - relativedelta(years=number) |
| 339 | + if "month" in unit: |
| 340 | + return today - relativedelta(months=number) |
| 341 | + if "day" in unit: |
| 342 | + return today - timedelta(days=number) |
| 343 | + except Exception as e: |
| 344 | + logging.warning(f"Could not parse relative date '{date_description}': {e}") |
| 345 | + return None |
| 346 | + |
| 347 | + |
| 348 | +def _parse_enum_date(date_description: str, today: date) -> Optional[date]: |
| 349 | + """ |
| 350 | + Tries to parse a date from the DateDescription enum. |
| 351 | + Args: |
| 352 | + date_description (str): The date description to parse. |
| 353 | + today (date): The reference date for special cases. |
| 354 | + Returns: |
| 355 | + Optional[date]: The parsed enum date or None if parsing failed. |
| 356 | + """ |
| 357 | + enum_val = DateDescription.by_description_case_insensitive(date_description) |
| 358 | + if enum_val is not None: |
| 359 | + if enum_val.name == DateDescriptionUtils.NULL_STRING: |
| 360 | + return None |
| 361 | + if enum_val.name == DateDescriptionUtils.NOT_NULL_STRING_UNDERSCORE: |
| 362 | + return today |
| 363 | + return enum_val.suitable_date |
| 364 | + return None |
0 commit comments