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 9bc90895ac..ad96ca0589 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(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