Repository: ecmwf/earthkit-data
Pull request #606 (2025-02-06 12:16:49)

feat: add strftimedelta pattern
Suggestion for a new pattern type to support passing a date with a timedelta. The syntax would look as the following.

```py
Pattern("/path/to/data/{date:strftimedelta(-6h;%Y%m%d)}{date:strftimedelta(-6h;%H)}_006")
```

Would allow an easier selection of data at validity time in forecast files.

FussyDuck: [![CLA assistant check](https://bol-claassistant.ecmwf.int:443/pull/badge/not_signed)](https://bol-claassistant.ecmwf.int:443/ecmwf/earthkit-data?pullRequest=606) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://bol-claassistant.ecmwf.int:443/ecmwf/earthkit-data?pullRequest=606) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://bol-claassistant.ecmwf.int:443/check/ecmwf/earthkit-data?pullRequest=606) it.</sub>
sandorkertesz: @chebertpinard, I added a test for Pattern:

tests/utils/test_patterns.py

Can I ask you to add at least one test to it for you new feature?
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/ecmwf/earthkit-data/pull/606?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.29%. Comparing base [(`ea24172`)](https://app.codecov.io/gh/ecmwf/earthkit-data/commit/ea241723f85b0e27bb84b702ea183b2e4faf21fc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf) to head [(`2ceac47`)](https://app.codecov.io/gh/ecmwf/earthkit-data/commit/2ceac4721f55c9c9069634230df300fbb7066ccc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           develop     #606   +/-   ##
========================================
  Coverage    90.29%   90.29%           
========================================
  Files          154      154           
  Lines        11572    11572           
  Branches       546      546           
========================================
  Hits         10449    10449           
  Misses         953      953           
  Partials       170      170           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ecmwf/earthkit-data/pull/606?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf).


Patch:
diff --git a/src/earthkit/data/utils/patterns.py b/src/earthkit/data/utils/patterns.py
index 0fc5f24..592d62d 100644
--- a/src/earthkit/data/utils/patterns.py
+++ b/src/earthkit/data/utils/patterns.py
@@ -11,6 +11,7 @@ import itertools
 import re
 
 from .dates import to_datetime
+from .dates import to_timedelta
 
 RE1 = re.compile(r"{([^}]*)}")
 RE2 = re.compile(r"\(([^}]*)\)")
@@ -62,6 +63,31 @@ class Datetime:
         return to_datetime(value).strftime(self.format)
 
 
+class DatetimeDelta:
+    def __init__(self, params):
+        params_list = params.split(";")
+        if len(params_list) != 2:
+            raise ValueError(
+                "Invalid parameters '{}' for class DatetimeDelta, expected (delta;format)".format(params)
+            )
+        self.delta = params_list[0].strip()
+        self.format = params_list[1].strip()
+
+    def substitute(self, value, name):
+        sign = -1 if self.delta[0] == "-" else 1
+        if re.fullmatch(r"[-\+]?\d+[hms]?", self.delta):
+            delta = re.search(r"\d+[hms]?", self.delta).group(0)
+        else:
+            raise ValueError(
+                "Invalid value '{}' for delta, expected time in hour (h), minute (m) or second (s)".format(
+                    self.delta
+                )
+            )
+
+        valid_date = to_datetime(value) + sign * to_timedelta(delta)
+        return valid_date.strftime(self.format)
+
+
 class Str:
     def __init__(self, format="%s"):
         self.format = format
@@ -78,6 +104,7 @@ TYPES = {
     "float": Float,
     "date": Datetime,
     "strftime": Datetime,
+    "strftimedelta": DatetimeDelta,
     "enum": Enum,
 }
 


Test patch:
diff --git a/tests/utils/test_patterns.py b/tests/utils/test_patterns.py
index 09221eb..dc9fddc 100644
--- a/tests/utils/test_patterns.py
+++ b/tests/utils/test_patterns.py
@@ -26,6 +26,16 @@ from earthkit.data.utils.patterns import Pattern
             {"my_date": datetime.datetime(2020, 5, 13), "name": ["t2", "msl"]},
             ["test_2020-05-13_t2.grib", "test_2020-05-13_msl.grib"],
         ),
+        (
+            "test_{date:strftimedelta(-6;%Y-%m-%d_%H)}.grib",
+            {"date": [datetime.datetime(2020, 5, 11), datetime.datetime(2020, 5, 11, 6)]},
+            ["test_2020-05-10_18.grib", "test_2020-05-11_00.grib"],
+        ),
+        (
+            "test_{date:strftimedelta(60m;%Y-%m-%d_%H)}.grib",
+            {"date": [datetime.datetime(2020, 5, 11), datetime.datetime(2020, 5, 11, 6)]},
+            ["test_2020-05-11_01.grib", "test_2020-05-11_07.grib"],
+        ),
     ],
 )
 def test_pattern_core(pattern, values, expected_value):