From 8cc226d05932f5dc263566dce6f88f818e839171 Mon Sep 17 00:00:00 2001 From: "Md. Amdadul Bari Imad" Date: Thu, 13 Aug 2026 08:49:51 +0000 Subject: [PATCH] Fix convert_to_dapr_duration double-counting sub-second time convert_to_dapr_duration() derived the seconds field from divmod(td.total_seconds(), 60), so it still carried the sub-second fraction that is also emitted via the ms/us fields, and formatting it with {:.0f} rounded a fraction >= 0.5 up into an extra whole second. As a result timedelta(milliseconds=1500) serialized to '0h0m2s500ms0us' (2.5s) and did not round-trip. Truncate the whole seconds with int() so the sub-second fraction is only counted once (via ms/us). Add a regression test. Signed-off-by: Md. Amdadul Bari Imad --- dapr/serializers/util.py | 5 ++++- tests/serializers/test_util.py | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dapr/serializers/util.py b/dapr/serializers/util.py index 522ad03d3..deb4f34d2 100644 --- a/dapr/serializers/util.py +++ b/dapr/serializers/util.py @@ -74,4 +74,7 @@ def convert_to_dapr_duration(td: timedelta) -> str: milliseconds, microseconds = divmod(td.microseconds, 1000.0) hours, mins = divmod(total_minutes, 60.0) - return f'{hours:.0f}h{mins:.0f}m{seconds:.0f}s{milliseconds:.0f}ms{microseconds:.0f}μs' + # `seconds` still carries the sub-second fraction, which is also emitted via + # the ms/μs fields below; truncate it so the fraction is not double-counted + # (and so a fraction >= 0.5 is not rounded up into an extra whole second). + return f'{hours:.0f}h{mins:.0f}m{int(seconds)}s{milliseconds:.0f}ms{microseconds:.0f}μs' diff --git a/tests/serializers/test_util.py b/tests/serializers/test_util.py index 25124fdf6..07ce8a2ae 100644 --- a/tests/serializers/test_util.py +++ b/tests/serializers/test_util.py @@ -63,6 +63,13 @@ def test_convert_timedelta_to_dapr_duration(self): ) self.assertEqual(duration, '4h15m40s123ms35μs') + def test_convert_timedelta_to_dapr_duration_subsecond(self): + # A sub-second fraction >= 0.5 must not be rounded up into an extra whole + # second nor double-counted with the ms/μs fields; the round-trip must be exact. + duration = convert_to_dapr_duration(timedelta(milliseconds=1500)) + self.assertEqual(duration, '0h0m1s500ms0μs') + self.assertEqual(convert_from_dapr_duration(duration), timedelta(milliseconds=1500)) + def test_convert_invalid_duration_string(self): TESTSTRING = '4h15m40s123ms35μshello' with self.assertRaises(ValueError) as exeception_context: