Repository: sdv-dev/SDV

Issue:
`DatetimeFormatter`: When `ValueError` occurs, the `pd.to_datetime` can fail due to format miss-match
### Environment Details

Please indicate the following details about the environment in which you found the bug:

* SDV version: 1.21
* Python version: 3.8 / 3.12
* Operating System:

### Error Description

As seen in [this](https://github.com/sdv-dev/SDV/actions/runs/15073231297/job/42374545352) workflow, this error originates through the `DatetimeFromatter` class that we have in `data_processing`. 

What happened there is that if a `ValueError` is raised, we try to use `pd.to_datetime` without considering the already provided datetime format and this is not always accurate as shown in the example below.

Therefore, we should aim to make this more robust by:
1. Try to cast the data with the provided format
2. Convert the already parsed datetime to string.
3. If the 'default conversion' of pandas fails, we should try to apply the format back in a safer way with `errors='coerce'` and avoid `ValueErrors`.

### Steps to reproduce

```python
series = pd.Series(["31 May 2021", "02 Apr 2021"])
pd.to_datetime(series)

...
ValueError: time data "02 Apr 2021" doesn't match format "%d %B %Y", at position 1. You might want to try:
    - passing `format` if your strings have a consistent format;
    - passing `format='ISO8601'` if your strings are all ISO8601 but not necessarily in exactly the same format;
    - passing `format='mixed'`, and the format will be inferred for each element individually. You might want to use `dayfirst` alongside this.
```



Discussion:

Patch:
diff --git a/sdv/data_processing/datetime_formatter.py b/sdv/data_processing/datetime_formatter.py
index c68979b27..6103eeb2c 100644
--- a/sdv/data_processing/datetime_formatter.py
+++ b/sdv/data_processing/datetime_formatter.py
@@ -58,9 +58,10 @@ def format_data(self, column):
             try:
                 datetime_column = pd.to_datetime(column, format=self.datetime_format)
                 column = datetime_column.dt.strftime(self.datetime_format)
-            except ValueError:
-                column = pd.to_datetime(column).dt.strftime(self.datetime_format)
             except (AttributeError, ValueError):
-                return column
+                try:
+                    column = pd.to_datetime(column).dt.strftime(self.datetime_format)
+                except (AttributeError, ValueError):
+                    return column
 
         return column.astype(self._dtype)


Test patch:
diff --git a/tests/unit/data_processing/test_datetime_formatter.py b/tests/unit/data_processing/test_datetime_formatter.py
index 1ede42171..5283d3adf 100644
--- a/tests/unit/data_processing/test_datetime_formatter.py
+++ b/tests/unit/data_processing/test_datetime_formatter.py
@@ -126,3 +126,25 @@ def test_format_data_parsing_fallback_on_value_error(self):
         # Assert
         expected = pd.Series(['2024/06/01', '2025/06/01'])
         pd.testing.assert_series_equal(result, expected)
+
+    def test_format_data_fallback_with_indepth_value_errors(self):
+        """Test fallback formatting when parsing with format raises ValueError."""
+        # Setup
+        formatter = DatetimeFormatter('%d %B %Y')
+        formatter._dtype = 'O'
+        column = pd.Series(['31 May 2021', '23-04-2021'])
+
+        # Run
+        result = formatter.format_data(column)
+
+        # Assert
+        try:
+            # Pandas 1.4 has different behavior to newer versions
+            # This works with lower pandas versions and casts the second string as the first format
+            expected = pd.to_datetime(column).dt.strftime('%d %B %Y')
+        except ValueError:
+            # For pandas 2.0 or bigger, the second string won't be casted and error is raised
+            # Therefore the original column is being returned
+            expected = pd.Series(['31 May 2021', '23-04-2021'])
+
+        assert all(result == expected)