Rename timestamps to dates
- ID
689a15f- date
2025-11-29 07:37:33+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
5f67edc- message
Rename `timestamps` to `dates`- changed files
6 files, 140 additions, 136 deletions
Changed files
CHANGELOG.md (454) → CHANGELOG.md (521)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0be58e0..d9ec7f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# CHANGELOG
+## v4 - 2025-11-29
+
+Rename `chives.timestamps` to `chives.dates`.
+
## v3 - 2025-11-29
Add the `clean_youtube_url()` function and `urls` extra.
src/chives/__init__.py (390) → src/chives/__init__.py (390)
diff --git a/src/chives/__init__.py b/src/chives/__init__.py
index 5d290d7..6c780be 100644
--- a/src/chives/__init__.py
+++ b/src/chives/__init__.py
@@ -11,4 +11,4 @@ I share across multiple sites.
"""
-__version__ = "3"
+__version__ = "4"
src/chives/dates.py (0) → src/chives/dates.py (2122)
diff --git a/src/chives/dates.py b/src/chives/dates.py
new file mode 100644
index 0000000..272d651
--- /dev/null
+++ b/src/chives/dates.py
@@ -0,0 +1,73 @@
+"""
+Functions for interacting with timestamps and date strings.
+
+References:
+* https://alexwlchan.net/2025/messy-dates-in-json/
+
+"""
+
+from collections.abc import Iterable, Iterator
+from datetime import datetime, timezone
+from typing import Any
+
+
+def find_all_dates(json_value: Any) -> Iterator[tuple[dict[str, Any], str, str]]:
+ """
+ Find all the timestamps in a heavily nested JSON object.
+
+ This function looks for any JSON objects with a key-value pair
+ where the key starts with `date_` and the value is a string, and
+ emits a 3-tuple:
+
+ * the JSON object
+ * the key
+ * the value
+
+ """
+ if isinstance(json_value, dict):
+ for key, value in json_value.items():
+ if (
+ isinstance(key, str)
+ and key.startswith("date_")
+ and isinstance(value, str)
+ ):
+ yield json_value, key, value
+ else:
+ yield from find_all_dates(value)
+ elif isinstance(json_value, list):
+ for value in json_value:
+ yield from find_all_dates(value)
+
+
+def date_matches_format(date_string: str, format: str) -> bool:
+ """
+ Returns True if `date_string` can be parsed as a datetime
+ using `format`, False otherwise.
+ """
+ try:
+ datetime.strptime(date_string, format)
+ return True
+ except ValueError:
+ return False
+
+
+def date_matches_any_format(date_string: str, formats: Iterable[str]) -> bool:
+ """
+ Returns True if `date_string` can be parsed as a datetime
+ with any of the `formats`, False otherwise.
+ """
+ return any(date_matches_format(date_string, fmt) for fmt in formats)
+
+
+def reformat_date(s: str, /, orig_fmt: str) -> str:
+ """
+ Reformat a date to one of my desired formats.
+ """
+ if "%Z" in orig_fmt:
+ d = datetime.strptime(s, orig_fmt)
+ else:
+ d = datetime.strptime(s.replace("Z", "+0000"), orig_fmt.replace("Z", "%z"))
+ d = d.replace(microsecond=0)
+ if d.tzinfo is None:
+ d = d.replace(tzinfo=timezone.utc)
+ return d.strftime("%Y-%m-%dT%H:%M:%S%z").replace("+0000", "Z")
src/chives/timestamps.py (2122) → src/chives/timestamps.py (0)
diff --git a/src/chives/timestamps.py b/src/chives/timestamps.py
deleted file mode 100644
index 272d651..0000000
--- a/src/chives/timestamps.py
+++ /dev/null
@@ -1,73 +0,0 @@
-"""
-Functions for interacting with timestamps and date strings.
-
-References:
-* https://alexwlchan.net/2025/messy-dates-in-json/
-
-"""
-
-from collections.abc import Iterable, Iterator
-from datetime import datetime, timezone
-from typing import Any
-
-
-def find_all_dates(json_value: Any) -> Iterator[tuple[dict[str, Any], str, str]]:
- """
- Find all the timestamps in a heavily nested JSON object.
-
- This function looks for any JSON objects with a key-value pair
- where the key starts with `date_` and the value is a string, and
- emits a 3-tuple:
-
- * the JSON object
- * the key
- * the value
-
- """
- if isinstance(json_value, dict):
- for key, value in json_value.items():
- if (
- isinstance(key, str)
- and key.startswith("date_")
- and isinstance(value, str)
- ):
- yield json_value, key, value
- else:
- yield from find_all_dates(value)
- elif isinstance(json_value, list):
- for value in json_value:
- yield from find_all_dates(value)
-
-
-def date_matches_format(date_string: str, format: str) -> bool:
- """
- Returns True if `date_string` can be parsed as a datetime
- using `format`, False otherwise.
- """
- try:
- datetime.strptime(date_string, format)
- return True
- except ValueError:
- return False
-
-
-def date_matches_any_format(date_string: str, formats: Iterable[str]) -> bool:
- """
- Returns True if `date_string` can be parsed as a datetime
- with any of the `formats`, False otherwise.
- """
- return any(date_matches_format(date_string, fmt) for fmt in formats)
-
-
-def reformat_date(s: str, /, orig_fmt: str) -> str:
- """
- Reformat a date to one of my desired formats.
- """
- if "%Z" in orig_fmt:
- d = datetime.strptime(s, orig_fmt)
- else:
- d = datetime.strptime(s.replace("Z", "+0000"), orig_fmt.replace("Z", "%z"))
- d = d.replace(microsecond=0)
- if d.tzinfo is None:
- d = d.replace(tzinfo=timezone.utc)
- return d.strftime("%Y-%m-%dT%H:%M:%S%z").replace("+0000", "Z")
tests/test_dates.py (0) → tests/test_dates.py (2021)
diff --git a/tests/test_dates.py b/tests/test_dates.py
new file mode 100644
index 0000000..74c9128
--- /dev/null
+++ b/tests/test_dates.py
@@ -0,0 +1,62 @@
+"""Tests for `chives.dates`."""
+
+import json
+
+import pytest
+
+from chives.dates import date_matches_any_format, find_all_dates, reformat_date
+
+
+def test_find_all_dates() -> None:
+ """find_all_dates finds all the nested dates in a JSON object."""
+ json_value = json.loads("""{
+ "doc1": {"id": "1", "date_created": "2025-10-14T05:34:07+0000"},
+ "shapes": [
+ {"color": "blue", "date_saved": "2015-03-01 23:34:39 +00:00"},
+ {"color": "yellow", "date_saved": "2013-9-21 13:43:00Z", "is_square": true},
+ {"color": "green", "date_saved": null}
+ ],
+ "date_verified": "2024-08-30"
+ }""")
+
+ assert list(find_all_dates(json_value)) == [
+ (
+ {"id": "1", "date_created": "2025-10-14T05:34:07+0000"},
+ "date_created",
+ "2025-10-14T05:34:07+0000",
+ ),
+ (
+ {"color": "blue", "date_saved": "2015-03-01 23:34:39 +00:00"},
+ "date_saved",
+ "2015-03-01 23:34:39 +00:00",
+ ),
+ (
+ {"color": "yellow", "date_saved": "2013-9-21 13:43:00Z", "is_square": True},
+ "date_saved",
+ "2013-9-21 13:43:00Z",
+ ),
+ (json_value, "date_verified", "2024-08-30"),
+ ]
+
+
+def test_date_matches_any_format() -> None:
+ """
+ Tests for `date_matches_any_format`.
+ """
+ assert date_matches_any_format(
+ "2001-01-01", formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S%z"]
+ )
+ assert not date_matches_any_format("2001-01-01", formats=["%Y-%m-%dT%H:%M:%S%z"])
+
+
+@pytest.mark.parametrize(
+ "s, orig_fmt, formatted_date",
+ [
+ ("2025-11-12T15:34:39.570Z", "%Y-%m-%dT%H:%M:%S.%fZ", "2025-11-12T15:34:39Z"),
+ ("2025-03-12 09:57:03", "%Y-%m-%d %H:%M:%S", "2025-03-12T09:57:03Z"),
+ ("2016-02-25 05:28:35 GMT", "%Y-%m-%d %H:%M:%S %Z", "2016-02-25T05:28:35Z"),
+ ],
+)
+def test_reformat_date(s: str, orig_fmt: str, formatted_date: str) -> None:
+ """Tests for `reformat_date`."""
+ assert reformat_date(s, orig_fmt) == formatted_date
tests/test_timestamps.py (2031) → tests/test_timestamps.py (0)
diff --git a/tests/test_timestamps.py b/tests/test_timestamps.py
deleted file mode 100644
index 6b64184..0000000
--- a/tests/test_timestamps.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""Tests for `chives.timestamps`."""
-
-import json
-
-import pytest
-
-from chives.timestamps import date_matches_any_format, find_all_dates, reformat_date
-
-
-def test_find_all_dates() -> None:
- """find_all_dates finds all the nested dates in a JSON object."""
- json_value = json.loads("""{
- "doc1": {"id": "1", "date_created": "2025-10-14T05:34:07+0000"},
- "shapes": [
- {"color": "blue", "date_saved": "2015-03-01 23:34:39 +00:00"},
- {"color": "yellow", "date_saved": "2013-9-21 13:43:00Z", "is_square": true},
- {"color": "green", "date_saved": null}
- ],
- "date_verified": "2024-08-30"
- }""")
-
- assert list(find_all_dates(json_value)) == [
- (
- {"id": "1", "date_created": "2025-10-14T05:34:07+0000"},
- "date_created",
- "2025-10-14T05:34:07+0000",
- ),
- (
- {"color": "blue", "date_saved": "2015-03-01 23:34:39 +00:00"},
- "date_saved",
- "2015-03-01 23:34:39 +00:00",
- ),
- (
- {"color": "yellow", "date_saved": "2013-9-21 13:43:00Z", "is_square": True},
- "date_saved",
- "2013-9-21 13:43:00Z",
- ),
- (json_value, "date_verified", "2024-08-30"),
- ]
-
-
-def test_date_matches_any_format() -> None:
- """
- Tests for `date_matches_any_format`.
- """
- assert date_matches_any_format(
- "2001-01-01", formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S%z"]
- )
- assert not date_matches_any_format("2001-01-01", formats=["%Y-%m-%dT%H:%M:%S%z"])
-
-
-@pytest.mark.parametrize(
- "s, orig_fmt, formatted_date",
- [
- ("2025-11-12T15:34:39.570Z", "%Y-%m-%dT%H:%M:%S.%fZ", "2025-11-12T15:34:39Z"),
- ("2025-03-12 09:57:03", "%Y-%m-%d %H:%M:%S", "2025-03-12T09:57:03Z"),
- ("2016-02-25 05:28:35 GMT", "%Y-%m-%d %H:%M:%S %Z", "2016-02-25T05:28:35Z"),
- ],
-)
-def test_reformat_date(s: str, orig_fmt: str, formatted_date: str) -> None:
- """Tests for `reformat_date`."""
- assert reformat_date(s, orig_fmt) == formatted_date