|
| 1 | +from datetime import datetime, timedelta, date |
| 2 | +from typing import Optional |
| 3 | +import logging |
| 4 | +from classes.date.date_description import DateDescription |
| 5 | + |
| 6 | + |
| 7 | +class DateDescriptionUtils: |
| 8 | + """ |
| 9 | + Utility class for interpreting and converting date descriptions to Python date objects or formatted strings. |
| 10 | + """ |
| 11 | + |
| 12 | + DATE_FORMAT_YYYY_MM_DD = "%Y-%m-%d" |
| 13 | + DATE_FORMAT_DD_MM_YYYY = "%d/%m/%Y" |
| 14 | + |
| 15 | + @staticmethod |
| 16 | + def interpret_date(date_field_name: str, date_value: str) -> str: |
| 17 | + """ |
| 18 | + Interprets a date description and returns a formatted date string (dd/MM/yyyy). |
| 19 | + If the date cannot be interpreted, returns the original value. |
| 20 | + """ |
| 21 | + logging.debug(f"interpret_date: {date_field_name}, {date_value}") |
| 22 | + try: |
| 23 | + return DateDescriptionUtils.convert_description_to_string_date( |
| 24 | + date_field_name, date_value, DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY |
| 25 | + ) |
| 26 | + except Exception as e: |
| 27 | + logging.error(f"Could not interpret date: {e}") |
| 28 | + return date_value |
| 29 | + |
| 30 | + @staticmethod |
| 31 | + def convert_description_to_sql_date( |
| 32 | + which_date: str, date_description: str |
| 33 | + ) -> Optional[str]: |
| 34 | + """ |
| 35 | + Converts a date description to an Oracle TO_DATE SQL string. |
| 36 | + Returns None if the date cannot be interpreted. |
| 37 | + """ |
| 38 | + logging.debug( |
| 39 | + f"convert_description_to_sql_date: {which_date}, {date_description}" |
| 40 | + ) |
| 41 | + return_date_string = None |
| 42 | + return_date = None |
| 43 | + |
| 44 | + date_description_words = date_description.split(" ") |
| 45 | + |
| 46 | + # First handle actual dates |
| 47 | + if DateDescriptionUtils.is_valid_date( |
| 48 | + date_description, DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY |
| 49 | + ): |
| 50 | + return_date_string = DateDescriptionUtils.oracle_to_date_function( |
| 51 | + date_description, "dd/mm/yyyy" |
| 52 | + ) |
| 53 | + elif DateDescriptionUtils.is_valid_date( |
| 54 | + date_description, DateDescriptionUtils.DATE_FORMAT_YYYY_MM_DD |
| 55 | + ): |
| 56 | + return_date_string = DateDescriptionUtils.oracle_to_date_function( |
| 57 | + date_description, "yyyy-mm-dd" |
| 58 | + ) |
| 59 | + elif date_description.endswith(" ago") and len(date_description_words) == 3: |
| 60 | + return_date = DateDescriptionUtils.convert_description_to_local_date( |
| 61 | + which_date, date_description |
| 62 | + ) |
| 63 | + else: |
| 64 | + # If the date description is in the enum, use the suggested suitable date, plus allow for NULL and NOT NULL |
| 65 | + enum_val = DateDescription.by_description_case_insensitive(date_description) |
| 66 | + if enum_val is not None: |
| 67 | + if enum_val.name == "NULL": |
| 68 | + return_date_string = "NULL" |
| 69 | + elif enum_val.name == "NOT_NULL": |
| 70 | + return_date_string = "NOT NULL" |
| 71 | + else: |
| 72 | + return_date = enum_val.suitable_date |
| 73 | + |
| 74 | + if return_date is not None and return_date_string is None: |
| 75 | + return_date_string = DateDescriptionUtils.oracle_to_date_function( |
| 76 | + return_date.strftime(DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY), |
| 77 | + "dd/mm/yyyy", |
| 78 | + ) |
| 79 | + |
| 80 | + return return_date_string |
| 81 | + |
| 82 | + @staticmethod |
| 83 | + def convert_description_to_local_date( |
| 84 | + which_date: str, date_description: str |
| 85 | + ) -> date: |
| 86 | + """ |
| 87 | + Converts a date description to a Python date object. |
| 88 | + Raises ValueError if the description cannot be interpreted. |
| 89 | + """ |
| 90 | + logging.debug( |
| 91 | + f"convert_description_to_local_date: {which_date}, {date_description}" |
| 92 | + ) |
| 93 | + date_description_words = date_description.split(" ") |
| 94 | + |
| 95 | + # Handle actual dates |
| 96 | + if DateDescriptionUtils.is_valid_date( |
| 97 | + date_description, DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY |
| 98 | + ): |
| 99 | + return datetime.strptime( |
| 100 | + date_description, DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY |
| 101 | + ).date() |
| 102 | + if DateDescriptionUtils.is_valid_date( |
| 103 | + date_description, DateDescriptionUtils.DATE_FORMAT_YYYY_MM_DD |
| 104 | + ): |
| 105 | + return datetime.strptime( |
| 106 | + date_description, DateDescriptionUtils.DATE_FORMAT_YYYY_MM_DD |
| 107 | + ).date() |
| 108 | + |
| 109 | + # Handle relative dates ("ago" or "ahead") |
| 110 | + if DateDescriptionUtils._is_relative_date( |
| 111 | + date_description, date_description_words, "ago" |
| 112 | + ): |
| 113 | + return DateDescriptionUtils._calculate_relative_date( |
| 114 | + date_description_words, is_ago=True |
| 115 | + ) |
| 116 | + if DateDescriptionUtils._is_relative_date( |
| 117 | + date_description, date_description_words, "ahead" |
| 118 | + ): |
| 119 | + return DateDescriptionUtils._calculate_relative_date( |
| 120 | + date_description_words, is_ago=False |
| 121 | + ) |
| 122 | + |
| 123 | + # Handle enum-based descriptions |
| 124 | + enum_val = DateDescription.by_description_case_insensitive(date_description) |
| 125 | + if enum_val is not None: |
| 126 | + if enum_val.name in ["NULL", "NOT_NULL"]: |
| 127 | + raise ValueError(f"Cannot convert '{date_description}' to a date.") |
| 128 | + if enum_val.suitable_date is not None: |
| 129 | + return enum_val.suitable_date |
| 130 | + |
| 131 | + raise ValueError(f"Cannot interpret date description '{date_description}'.") |
| 132 | + |
| 133 | + @staticmethod |
| 134 | + def _is_relative_date(date_description, words, suffix): |
| 135 | + return date_description.endswith(f" {suffix}") and len(words) == 3 |
| 136 | + |
| 137 | + @staticmethod |
| 138 | + def _calculate_relative_date(words, is_ago): |
| 139 | + if not words[0].isdigit(): |
| 140 | + raise ValueError(f"Invalid period number in '{' '.join(words)}'") |
| 141 | + number_of_periods = int(words[0]) |
| 142 | + period_type = words[1] |
| 143 | + today = date.today() |
| 144 | + delta_days = DateDescriptionUtils._get_delta_days( |
| 145 | + number_of_periods, period_type |
| 146 | + ) |
| 147 | + if is_ago: |
| 148 | + return today - timedelta(days=delta_days) |
| 149 | + else: |
| 150 | + return today + timedelta(days=delta_days) |
| 151 | + |
| 152 | + @staticmethod |
| 153 | + def _get_delta_days(number_of_periods, period_type): |
| 154 | + if period_type in ["year", "years"]: |
| 155 | + return number_of_periods * 365 |
| 156 | + if period_type in ["month", "months"]: |
| 157 | + return number_of_periods * 30 |
| 158 | + if period_type in ["week", "weeks"]: |
| 159 | + return number_of_periods * 7 |
| 160 | + if period_type in ["day", "days"]: |
| 161 | + return number_of_periods |
| 162 | + raise ValueError(f"Unknown period type '{period_type}'") |
| 163 | + |
| 164 | + @staticmethod |
| 165 | + def convert_description_to_string_date( |
| 166 | + which_date: str, date_description: str, date_format: str |
| 167 | + ) -> str: |
| 168 | + """ |
| 169 | + Converts a date description to a formatted date string. |
| 170 | + Raises ValueError if the description cannot be interpreted. |
| 171 | + """ |
| 172 | + logging.debug( |
| 173 | + f"convert_description_to_string_date: {which_date}, {date_description}, {date_format}" |
| 174 | + ) |
| 175 | + local_date = DateDescriptionUtils.convert_description_to_local_date( |
| 176 | + which_date, date_description |
| 177 | + ) |
| 178 | + return local_date.strftime(date_format) |
| 179 | + |
| 180 | + @staticmethod |
| 181 | + def is_valid_date(date_str: str, date_format: str) -> bool: |
| 182 | + """ |
| 183 | + Checks if the given string is a valid date in the specified format. |
| 184 | + """ |
| 185 | + try: |
| 186 | + datetime.strptime(date_str, date_format) |
| 187 | + return True |
| 188 | + except ValueError: |
| 189 | + return False |
| 190 | + |
| 191 | + @staticmethod |
| 192 | + def oracle_to_date_function(date_str: str, oracle_format: str) -> str: |
| 193 | + """ |
| 194 | + Constructs an Oracle TO_DATE function to convert a string to a date. |
| 195 | + This method formats the date string according to the specified format. |
| 196 | + Args: |
| 197 | + date_str (str): The date string to be converted. |
| 198 | + oracle_format (str): The format in which the date string is provided. |
| 199 | + Returns: |
| 200 | + str: The SQL expression for the Oracle TO_DATE function. |
| 201 | + """ |
| 202 | + return f" TO_DATE( '{date_str}', '{oracle_format}') " |
0 commit comments