From 30e0047183ad732f74a5387c889e336636e90ca4 Mon Sep 17 00:00:00 2001 From: czh <2565523901@qq.com> Date: Mon, 9 Mar 2026 19:10:02 +0800 Subject: [PATCH] feat: improve DateTime handling with better error messages Enhance datetime parsing in Card and ReviewLog modules: - Support DateTime structs, ISO8601 strings, and unix timestamps - Add proper error handling with descriptive messages - Automatically convert non-UTC datetimes to UTC - Add ensure_utc/1 helper function This improves robustness when deserializing data from various sources and provides better developer experience with clear error messages. --- lib/lulucat/fsrs_card.ex | 32 +++++++++++++++++++++++++++++--- lib/lulucat/fsrs_review_log.ex | 32 +++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/lib/lulucat/fsrs_card.ex b/lib/lulucat/fsrs_card.ex index b3bfa01..6164fcf 100644 --- a/lib/lulucat/fsrs_card.ex +++ b/lib/lulucat/fsrs_card.ex @@ -175,9 +175,35 @@ defmodule Fsrs.Card do end end - defp parse_datetime(iso_string) do - {:ok, datetime, _offset} = DateTime.from_iso8601(iso_string) - datetime + defp parse_datetime(%DateTime{} = datetime), do: ensure_utc(datetime) + + defp parse_datetime(iso_string) when is_binary(iso_string) do + case DateTime.from_iso8601(iso_string) do + {:ok, datetime, _offset} -> + ensure_utc(datetime) + + {:error, reason} -> + raise ArgumentError, + "Invalid datetime format: #{iso_string}, reason: #{inspect(reason)}" + end + end + + defp parse_datetime(unix_ts) when is_integer(unix_ts) do + DateTime.from_unix!(unix_ts, :millisecond) + end + + defp parse_datetime(other) do + raise ArgumentError, + "Invalid datetime value: #{inspect(other)}. Expected DateTime struct, ISO8601 string, or unix timestamp." + end + + defp ensure_utc(%DateTime{time_zone: "Etc/UTC"} = datetime), do: datetime + + defp ensure_utc(datetime) do + case DateTime.shift_zone(datetime, "Etc/UTC") do + {:ok, utc_datetime} -> utc_datetime + {:error, reason} -> raise ArgumentError, "Failed to convert to UTC: #{inspect(reason)}" + end end defp datetime_to_python_iso8601(datetime) do diff --git a/lib/lulucat/fsrs_review_log.ex b/lib/lulucat/fsrs_review_log.ex index aec5613..9b33e40 100644 --- a/lib/lulucat/fsrs_review_log.ex +++ b/lib/lulucat/fsrs_review_log.ex @@ -132,9 +132,35 @@ defmodule Fsrs.ReviewLog do # Private functions - defp parse_datetime(iso_string) do - {:ok, datetime, _offset} = DateTime.from_iso8601(iso_string) - datetime + defp parse_datetime(%DateTime{} = datetime), do: ensure_utc(datetime) + + defp parse_datetime(iso_string) when is_binary(iso_string) do + case DateTime.from_iso8601(iso_string) do + {:ok, datetime, _offset} -> + ensure_utc(datetime) + + {:error, reason} -> + raise ArgumentError, + "Invalid datetime format: #{iso_string}, reason: #{inspect(reason)}" + end + end + + defp parse_datetime(unix_ts) when is_integer(unix_ts) do + DateTime.from_unix!(unix_ts, :millisecond) + end + + defp parse_datetime(other) do + raise ArgumentError, + "Invalid datetime value: #{inspect(other)}. Expected DateTime struct, ISO8601 string, or unix timestamp." + end + + defp ensure_utc(%DateTime{time_zone: "Etc/UTC"} = datetime), do: datetime + + defp ensure_utc(datetime) do + case DateTime.shift_zone(datetime, "Etc/UTC") do + {:ok, utc_datetime} -> utc_datetime + {:error, reason} -> raise ArgumentError, "Failed to convert to UTC: #{inspect(reason)}" + end end defp datetime_to_python_iso8601(datetime) do