From 4908b926c01dffd55f44000e6f7664f6ed86517b Mon Sep 17 00:00:00 2001 From: "daniel.radl" Date: Wed, 22 Oct 2025 11:51:36 +0000 Subject: [PATCH 1/2] fix(utils): format_duration for negative values --- frappe/utils/data.py | 51 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index a4cfb9a999..24f3ea7976 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -797,38 +797,37 @@ def format_datetime(datetime_string: DateTimeLikeObject, format_string: str | No return formatted_datetime -def format_duration(seconds, hide_days=False): - """Convert the given duration value in float(seconds) to duration format. +def format_duration(seconds: float | int, hide_days: bool = False) -> str: + """Convert the given duration value in seconds to duration format. - example: convert 12885 to '3h 34m 45s' where 12885 = seconds in float + example: + convert 12885 to '3h 34m 45s' where 12885 = seconds in float + -12885 to '-3h 34m 45s' """ - seconds = cint(seconds) + negative = seconds < 0 + seconds = abs(cint(seconds)) - total_duration = { - "days": math.floor(seconds / (3600 * 24)), - "hours": math.floor(seconds % (3600 * 24) / 3600), - "minutes": math.floor(seconds % 3600 / 60), - "seconds": math.floor(seconds % 60), - } + days = (seconds // (3600 * 24)) if not hide_days else 0 + hours = ((seconds % (3600 * 24)) // 3600) if not hide_days else (seconds // 3600) + minutes = (seconds % 3600) // 60 + seconds = seconds % 60 - if hide_days: - total_duration["hours"] = math.floor(seconds / 3600) - total_duration["days"] = 0 + total_duration = [] - duration = "" - if total_duration: - if total_duration.get("days"): - duration += str(total_duration.get("days")) + "d" - if total_duration.get("hours"): - duration += " " if len(duration) else "" - duration += str(total_duration.get("hours")) + "h" - if total_duration.get("minutes"): - duration += " " if len(duration) else "" - duration += str(total_duration.get("minutes")) + "m" - if total_duration.get("seconds"): - duration += " " if len(duration) else "" - duration += str(total_duration.get("seconds")) + "s" + if days: + total_duration.append(f"{days}d") + if hours: + total_duration.append(f"{hours}h") + if minutes: + total_duration.append(f"{minutes}m") + if seconds: + total_duration.append(f"{seconds}s") + + duration = " ".join(total_duration) + + if negative and duration: + duration = "-" + duration return duration From 45a0471f5c1de709baad090fb72a79096d99512d Mon Sep 17 00:00:00 2001 From: "daniel.radl" Date: Sat, 25 Oct 2025 16:52:36 +0000 Subject: [PATCH 2/2] fix(utils): added format_duration test --- frappe/tests/test_utils.py | 19 +++++++++++++++++++ frappe/utils/data.py | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 9c5a59bc82..ef4407685d 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -64,6 +64,7 @@ from frappe.utils.data import ( duration_to_seconds, evaluate_filters, expand_relative_urls, + format_duration, get_datetime, get_first_day_of_week, get_time, @@ -784,6 +785,24 @@ class TestDateUtils(IntegrationTestCase): self.assertEqual(duration_to_seconds("110m"), 110 * 60) self.assertEqual(duration_to_seconds("110m"), 110 * 60) + def test_format_duration(self): + # Basic positive durations + self.assertEqual(format_duration(0), "") + self.assertEqual(format_duration(45.7), "45s") + self.assertEqual(format_duration(90.9), "1m 30s") + self.assertEqual(format_duration(3600), "1h") + self.assertEqual(format_duration("12885"), "3h 34m 45s") + self.assertEqual(format_duration(86400), "1d") + self.assertEqual(format_duration(86401), "1d 1s") + + # Negative durations + self.assertEqual(format_duration(-45.3), "-45s") + self.assertEqual(format_duration(-12885), "-3h 34m 45s") + + # hide_days parameter + self.assertEqual(format_duration(86400, hide_days=True), "24h") + self.assertEqual(format_duration(90061, hide_days=True), "25h 1m 1s") + def test_get_timespan_date_range(self): supported_timespans = [ "last week", diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 24f3ea7976..4974296233 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -804,9 +804,9 @@ def format_duration(seconds: float | int, hide_days: bool = False) -> str: convert 12885 to '3h 34m 45s' where 12885 = seconds in float -12885 to '-3h 34m 45s' """ - + seconds = cint(seconds) negative = seconds < 0 - seconds = abs(cint(seconds)) + seconds = abs(seconds) days = (seconds // (3600 * 24)) if not hide_days else 0 hours = ((seconds % (3600 * 24)) // 3600) if not hide_days else (seconds // 3600)