Coverage for utilities/conversion.py: 36%
24 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from decimal import Decimal, InvalidOperation
3from django.utils.translation import gettext as _
5from dcim.choices import CableLengthUnitChoices
6from netbox.choices import (
7 DiameterUnitChoices,
8 FlowRateUnitChoices,
9 WeightUnitChoices,
10)
12__all__ = (
13 'to_grams',
14 'to_liters_per_minute',
15 'to_meters',
16 'to_millimeters',
17)
20def _normalized_measurement(value, unit, converters, quantity, precision=4) -> Decimal:
21 """
22 Shared implementation for the unit-normalization helpers below. Coerce `value` to a non-negative
23 Decimal, apply the matching per-unit converter, and round the result to `precision` decimal places.
24 `converters` maps each valid unit to a callable receiving the Decimal value; `quantity` labels the
25 value in error messages. Pass `precision=None` to return the unrounded result.
26 """
27 try:
28 value = Decimal(value)
29 except (InvalidOperation, TypeError, ValueError):
30 raise TypeError(
31 _("Invalid value '{value}' for {quantity} (must be a number)").format(value=value, quantity=quantity)
32 )
33 if value < 0:
34 raise ValueError(_("Invalid value for {quantity}: must be a positive number").format(quantity=quantity))
35 if unit not in converters:
36 raise ValueError(
37 _("Unknown unit {unit}. Must be one of the following: {valid_units}").format(
38 unit=unit,
39 valid_units=', '.join(converters)
40 )
41 )
42 result = converters[unit](value)
43 return result if precision is None else round(result, precision)
46def to_grams(weight, unit) -> int:
47 """
48 Convert the given weight to integer grams.
49 """
50 # Rounding is suppressed so that the result is truncated exactly as the caller's int() expects; rounding
51 # first could nudge a value across an integer boundary.
52 return int(_normalized_measurement(weight, unit, {
53 WeightUnitChoices.UNIT_KILOGRAM: lambda v: v * 1000,
54 WeightUnitChoices.UNIT_GRAM: lambda v: v,
55 WeightUnitChoices.UNIT_POUND: lambda v: v * Decimal(453.592),
56 WeightUnitChoices.UNIT_OUNCE: lambda v: v * Decimal(28.3495),
57 }, _('weight'), precision=None))
60def to_meters(length, unit) -> Decimal:
61 """
62 Convert the given length to meters, returning a Decimal value.
63 """
64 return _normalized_measurement(length, unit, {
65 CableLengthUnitChoices.UNIT_KILOMETER: lambda v: v * 1000,
66 CableLengthUnitChoices.UNIT_METER: lambda v: v,
67 CableLengthUnitChoices.UNIT_CENTIMETER: lambda v: v / 100,
68 CableLengthUnitChoices.UNIT_MILE: lambda v: v * Decimal(1609.344),
69 CableLengthUnitChoices.UNIT_FOOT: lambda v: v * Decimal(0.3048),
70 CableLengthUnitChoices.UNIT_INCH: lambda v: v * Decimal(0.0254),
71 }, _('length'))
74def to_millimeters(diameter, unit) -> Decimal:
75 """
76 Convert the given diameter to millimeters, returning a Decimal value.
77 """
78 return _normalized_measurement(diameter, unit, {
79 DiameterUnitChoices.UNIT_MILLIMETER: lambda v: v,
80 DiameterUnitChoices.UNIT_CENTIMETER: lambda v: v * 10,
81 DiameterUnitChoices.UNIT_INCH: lambda v: v * Decimal('25.4'),
82 }, _('diameter'))
85def to_liters_per_minute(flow_rate, unit) -> Decimal:
86 """
87 Convert the given flow rate to liters per minute, returning a Decimal value.
88 """
89 return _normalized_measurement(flow_rate, unit, {
90 FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE: lambda v: v,
91 FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR: lambda v: v * Decimal(1000) / Decimal(60),
92 FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE: lambda v: v * Decimal('3.785411784'),
93 }, _('flow rate'))