Browse Source

Merge pull request #21837 from netbox-community/21795-update-humanize_speed-to-support-decimal-gbpstbps-output

Closes #21795: Improve humanize_speed formatting for decimal Gbps/Tbps values
bctiemann 5 days ago
parent
commit
7d71503ea2
2 changed files with 124 additions and 14 deletions
  1. 39 13
      netbox/utilities/templatetags/helpers.py
  2. 85 1
      netbox/utilities/tests/test_templatetags.py

+ 39 - 13
netbox/utilities/templatetags/helpers.py

@@ -186,26 +186,52 @@ def action_url(parser, token):
     return ActionURLNode(model, action, kwargs, asvar)
     return ActionURLNode(model, action, kwargs, asvar)
 
 
 
 
+def _format_speed(speed, divisor, unit):
+    """
+    Format a speed value with a given divisor and unit.
+
+    Handles decimal values and strips trailing zeros for clean output.
+    """
+    whole, remainder = divmod(speed, divisor)
+    if remainder == 0:
+        return f'{whole} {unit}'
+
+    # Divisors are powers of 10, so len(str(divisor)) - 1 matches the decimal precision.
+    precision = len(str(divisor)) - 1
+    fraction = f'{remainder:0{precision}d}'.rstrip('0')
+    return f'{whole}.{fraction} {unit}'
+
+
 @register.filter()
 @register.filter()
 def humanize_speed(speed):
 def humanize_speed(speed):
     """
     """
-    Humanize speeds given in Kbps. Examples:
+    Humanize speeds given in Kbps, always using the largest appropriate unit.
+
+    Decimal values are displayed when the result is not a whole number;
+    trailing zeros after the decimal point are stripped for clean output.
+
+    Examples:
 
 
-        1544 => "1.544 Mbps"
-        100000 => "100 Mbps"
-        10000000 => "10 Gbps"
+        1_544 => "1.544 Mbps"
+        100_000 => "100 Mbps"
+        1_000_000 => "1 Gbps"
+        2_500_000 => "2.5 Gbps"
+        10_000_000 => "10 Gbps"
+        800_000_000 => "800 Gbps"
+        1_600_000_000 => "1.6 Tbps"
     """
     """
     if not speed:
     if not speed:
         return ''
         return ''
-    if speed >= 1000000000 and speed % 1000000000 == 0:
-        return '{} Tbps'.format(int(speed / 1000000000))
-    if speed >= 1000000 and speed % 1000000 == 0:
-        return '{} Gbps'.format(int(speed / 1000000))
-    if speed >= 1000 and speed % 1000 == 0:
-        return '{} Mbps'.format(int(speed / 1000))
-    if speed >= 1000:
-        return '{} Mbps'.format(float(speed) / 1000)
-    return '{} Kbps'.format(speed)
+
+    speed = int(speed)
+
+    if speed >= 1_000_000_000:
+        return _format_speed(speed, 1_000_000_000, 'Tbps')
+    if speed >= 1_000_000:
+        return _format_speed(speed, 1_000_000, 'Gbps')
+    if speed >= 1_000:
+        return _format_speed(speed, 1_000, 'Mbps')
+    return f'{speed} Kbps'
 
 
 
 
 def _humanize_capacity(value, divisor=1000):
 def _humanize_capacity(value, divisor=1000):

+ 85 - 1
netbox/utilities/tests/test_templatetags.py

@@ -3,7 +3,7 @@ from unittest.mock import patch
 from django.test import TestCase, override_settings
 from django.test import TestCase, override_settings
 
 
 from utilities.templatetags.builtins.tags import static_with_params
 from utilities.templatetags.builtins.tags import static_with_params
-from utilities.templatetags.helpers import _humanize_capacity
+from utilities.templatetags.helpers import _humanize_capacity, humanize_speed
 
 
 
 
 class StaticWithParamsTest(TestCase):
 class StaticWithParamsTest(TestCase):
@@ -90,3 +90,87 @@ class HumanizeCapacityTest(TestCase):
 
 
     def test_default_divisor_is_1000(self):
     def test_default_divisor_is_1000(self):
         self.assertEqual(_humanize_capacity(2000), '2.00 GB')
         self.assertEqual(_humanize_capacity(2000), '2.00 GB')
+
+
+class HumanizeSpeedTest(TestCase):
+    """
+    Test the humanize_speed filter for correct unit selection and decimal formatting.
+    """
+
+    # Falsy / empty inputs
+
+    def test_none(self):
+        self.assertEqual(humanize_speed(None), '')
+
+    def test_zero(self):
+        self.assertEqual(humanize_speed(0), '')
+
+    def test_empty_string(self):
+        self.assertEqual(humanize_speed(''), '')
+
+    # Kbps (below 1000)
+
+    def test_kbps(self):
+        self.assertEqual(humanize_speed(100), '100 Kbps')
+
+    def test_kbps_low(self):
+        self.assertEqual(humanize_speed(1), '1 Kbps')
+
+    # Mbps (1,000 – 999,999)
+
+    def test_mbps_whole(self):
+        self.assertEqual(humanize_speed(100_000), '100 Mbps')
+
+    def test_mbps_decimal(self):
+        self.assertEqual(humanize_speed(1_544), '1.544 Mbps')
+
+    def test_mbps_10(self):
+        self.assertEqual(humanize_speed(10_000), '10 Mbps')
+
+    # Gbps (1,000,000 – 999,999,999)
+
+    def test_gbps_whole(self):
+        self.assertEqual(humanize_speed(1_000_000), '1 Gbps')
+
+    def test_gbps_decimal(self):
+        self.assertEqual(humanize_speed(2_500_000), '2.5 Gbps')
+
+    def test_gbps_10(self):
+        self.assertEqual(humanize_speed(10_000_000), '10 Gbps')
+
+    def test_gbps_25(self):
+        self.assertEqual(humanize_speed(25_000_000), '25 Gbps')
+
+    def test_gbps_40(self):
+        self.assertEqual(humanize_speed(40_000_000), '40 Gbps')
+
+    def test_gbps_100(self):
+        self.assertEqual(humanize_speed(100_000_000), '100 Gbps')
+
+    def test_gbps_400(self):
+        self.assertEqual(humanize_speed(400_000_000), '400 Gbps')
+
+    def test_gbps_800(self):
+        self.assertEqual(humanize_speed(800_000_000), '800 Gbps')
+
+    # Tbps (1,000,000,000+)
+
+    def test_tbps_whole(self):
+        self.assertEqual(humanize_speed(1_000_000_000), '1 Tbps')
+
+    def test_tbps_decimal(self):
+        self.assertEqual(humanize_speed(1_600_000_000), '1.6 Tbps')
+
+    # Edge cases
+
+    def test_string_input(self):
+        """Ensure string values are cast to int correctly."""
+        self.assertEqual(humanize_speed('2500000'), '2.5 Gbps')
+
+    def test_non_round_remainder_preserved(self):
+        """Ensure fractional parts with interior zeros are preserved."""
+        self.assertEqual(humanize_speed(1_001_000), '1.001 Gbps')
+
+    def test_trailing_zeros_stripped(self):
+        """Ensure trailing fractional zeros are stripped (5.500 → 5.5)."""
+        self.assertEqual(humanize_speed(5_500_000), '5.5 Gbps')