Skip to content

Numbers

django_rubble.utils.numbers

Percent

Bases: BaseModel

A model for handling percentages.

This model is designed to handle percentages in a way that is more accurate than using floats.

Parameters:

Name Type Description Default
value Decimal | float | str
required
per_hundred Decimal | float | str | None
None
decimal_places int | None
None
has_decimal_places bool | None
None

Attributes:

Name Type Description
value (Decimal, float, str)

The value of the percentage.

per_hundred (Decimal, float, str)

The value of the percentage out of 100.

decimal_places int

The number of decimal places to use.

has_decimal_places bool

Whether the value has decimal places.

Source code in django_rubble\utils\numbers.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
class Percent(BaseModel):
    """A model for handling percentages.

    This model is designed to handle percentages in a way that is more accurate than
    using floats.

    Attributes:
        value (Decimal, float, str): The value of the percentage.
        per_hundred (Decimal, float, str): The value of the percentage out of 100.
        decimal_places (int): The number of decimal places to use.
        has_decimal_places (bool): Whether the value has decimal places.
    """

    value: Decimal | float | str
    per_hundred: Decimal | float | str | None = None
    decimal_places: int | None = None
    has_decimal_places: bool | None = None

    def model_post_init(self, __context: Any) -> None:
        new_value = trim_trailing_zeros(self.value)
        per_hundred_dec = trim_trailing_zeros(ratio_to_whole(self.value))

        if self.decimal_places is not None:
            new_value = round(new_value, self.decimal_places + 2)
            per_hundred_dec = round(per_hundred_dec, self.decimal_places)
            self.has_decimal_places = True
        else:
            self.has_decimal_places = False

        self.value = set_zero(new_value)
        self.per_hundred = set_zero(per_hundred_dec)

        super().model_post_init(__context)

    @classmethod
    def fromform(
        cls, val: Decimal, field_decimal_places: int | None = None
    ) -> "Percent":
        """Create Percent from human-entry (out of 100)

        Examples:
            >>> Percent.fromform(Decimal(3))
            Percent(value=Decimal('0.03'), per_hundred=3)
            >>> Percent.fromform(Decimal("100"))
            Percent(value=Decimal('1'), per_hundred=100)

        Args:
            val (Decimal): The value of the percentage.
            field_decimal_places (int): The number of decimal places to use.

        Returns:
            Percent: The percentage model.
        """
        ratio_decimal = whole_to_ratio(val)
        return cls(value=ratio_decimal, decimal_places=field_decimal_places)

    def __mul__(self, other):
        """Multiply using the ratio (out of 1) instead of human-readable out of 100

        Examples:
            >>> Percent(0.03) * 100
            Decimal('3')
            >>> Percent(1) * 100
            Decimal('100')"""
        return self.value.__mul__(other)

    def __float__(self):
        return float(self.value)

    def as_tuple(self) -> DecimalTuple:
        """Return the value as a decimal tuple.

        Returns:
            DecimalTuple: The value as a decimal tuple."""
        if not isinstance(self.value, Decimal):
            return Decimal(str(self.value)).as_tuple()
        return self.value.as_tuple()

    def is_finite(self):
        return self.value.is_finite()

    def __repr__(self) -> str:
        return f"Percentage('{self.value}', '{self.per_hundred}%')"

    def __str__(self):
        return f"{self.per_hundred}%"

__mul__(other)

Multiply using the ratio (out of 1) instead of human-readable out of 100

Examples:

>>> Percent(0.03) * 100
Decimal('3')
>>> Percent(1) * 100
Decimal('100')
Source code in django_rubble\utils\numbers.py
202
203
204
205
206
207
208
209
210
def __mul__(self, other):
    """Multiply using the ratio (out of 1) instead of human-readable out of 100

    Examples:
        >>> Percent(0.03) * 100
        Decimal('3')
        >>> Percent(1) * 100
        Decimal('100')"""
    return self.value.__mul__(other)

as_tuple()

Return the value as a decimal tuple.

Returns:

Name Type Description
DecimalTuple DecimalTuple

The value as a decimal tuple.

Source code in django_rubble\utils\numbers.py
215
216
217
218
219
220
221
222
def as_tuple(self) -> DecimalTuple:
    """Return the value as a decimal tuple.

    Returns:
        DecimalTuple: The value as a decimal tuple."""
    if not isinstance(self.value, Decimal):
        return Decimal(str(self.value)).as_tuple()
    return self.value.as_tuple()

fromform(val, field_decimal_places=None) classmethod

Create Percent from human-entry (out of 100)

Examples:

>>> Percent.fromform(Decimal(3))
Percent(value=Decimal('0.03'), per_hundred=3)
>>> Percent.fromform(Decimal("100"))
Percent(value=Decimal('1'), per_hundred=100)

Parameters:

Name Type Description Default
val Decimal

The value of the percentage.

required
field_decimal_places int

The number of decimal places to use.

None

Returns:

Name Type Description
Percent Percent

The percentage model.

Source code in django_rubble\utils\numbers.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
@classmethod
def fromform(
    cls, val: Decimal, field_decimal_places: int | None = None
) -> "Percent":
    """Create Percent from human-entry (out of 100)

    Examples:
        >>> Percent.fromform(Decimal(3))
        Percent(value=Decimal('0.03'), per_hundred=3)
        >>> Percent.fromform(Decimal("100"))
        Percent(value=Decimal('1'), per_hundred=100)

    Args:
        val (Decimal): The value of the percentage.
        field_decimal_places (int): The number of decimal places to use.

    Returns:
        Percent: The percentage model.
    """
    ratio_decimal = whole_to_ratio(val)
    return cls(value=ratio_decimal, decimal_places=field_decimal_places)

any_to_float(s, default=0)

Cast value as float, return default if invalid type.

Examples:

>>> any_to_float("5.5", 2.3)
5.5
>>> any_to_float("test", 0)
0

Parameters:

Name Type Description Default
s Any

The value to cast.

required

Returns:

Name Type Description
float float

The value as a float, or the default if the value is not a number.

Source code in django_rubble\utils\numbers.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def any_to_float(s: Any, default: float = 0) -> float:
    """Cast value as float, return default if invalid type.

    Examples:
        >>> any_to_float("5.5", 2.3)
        5.5
        >>> any_to_float("test", 0)
        0

    Args:
        s (Any): The value to cast.

    Returns:
        float: The value as a float, or the default if the value is not a number."""
    if not is_number(default):
        msg = f"Default must be of type `float` [{default}]"
        raise TypeError(msg)
    try:
        value_float = float(s)
    except ValueError:
        value_float = default

    return value_float

is_number(s)

Check if a value can be coerced into a number type.

Examples:

>>> is_number(10)
True
>>> is_number("hello")
False
>>> is_number(Decimal("3.14"))
True

Parameters:

Name Type Description Default
s Any

The value to check.

required

Returns:

Name Type Description
bool bool

True if the value can be coerced into a number type, False otherwise.

Source code in django_rubble\utils\numbers.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def is_number(s: Any) -> bool:
    """Check if a value can be coerced into a number type.

    Examples:
        >>> is_number(10)
        True
        >>> is_number("hello")
        False
        >>> is_number(Decimal("3.14"))
        True

    Args:
        s (Any): The value to check.

    Returns:
        bool: True if the value can be coerced into a number type, False otherwise."""
    if s is None:
        return False
    try:
        float(s)
    except ValueError:
        return False
    else:
        return True

ratio_to_whole(ratio)

Convert a ratio to a whole number.

This is useful for converting a ratio to a percentage.

Examples:

>>> ratio_to_whole(0.03)
3
>>> ratio_to_whole(Decimal("1"))
100

Parameters:

Name Type Description Default
ratio (Decimal, float, str)

The ratio to be converted.

required

Returns:

Type Description
T

The whole number.

Source code in django_rubble\utils\numbers.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def ratio_to_whole(ratio: T) -> T:
    """Convert a ratio to a whole number.

    This is useful for converting a ratio to a percentage.

    Examples:
        >>> ratio_to_whole(0.03)
        3
        >>> ratio_to_whole(Decimal("1"))
        100

    Args:
        ratio (Decimal, float, str): The ratio to be converted.

    Returns:
        The whole number.
    """
    multiplier = Decimal("100") if isinstance(ratio, Decimal) else 100

    return ratio * multiplier

set_zero(value)

Set a value to a true Decimal zero if it is zero.

Examples:

>>> set_zero(0)
Decimal('0')
>>> set_zero(0.0)
Decimal('0')

Parameters:

Name Type Description Default
value (int, float, str)

The value to be checked.

required

Returns:

Type Description
Decimal

The value as a Decimal if it is zero, otherwise the original value.

Source code in django_rubble\utils\numbers.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def set_zero(value: T) -> Decimal:
    """Set a value to a true Decimal zero if it is zero.

    Examples:
        >>> set_zero(0)
        Decimal('0')
        >>> set_zero(0.0)
        Decimal('0')

    Args:
        value (int, float, str): The value to be checked.

    Returns:
        The value as a Decimal if it is zero, otherwise the original value."""
    decimal_from_string = Decimal(str(value))

    if decimal_from_string == Decimal(0):
        return Decimal()

    return decimal_from_string

trim_trailing_zeros(value)

Remove trailing zeros from a decimal value.

This is useful for ensuring that a value can be safely compared with another value.

Examples:

>>> trim_trailing_zeros(3.1400)
Decimal('3.14')
>>> trim_trailing_zeros(Decimal("3.1400"))
Decimal('3.14')

Parameters:

Name Type Description Default
value (float, Decimal, str)

The value to be trimmed.

required

Returns:

Type Description
Decimal

The trimmed value. Decimal

Source code in django_rubble\utils\numbers.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def trim_trailing_zeros(value: T) -> Decimal:
    """Remove trailing zeros from a decimal value.

    This is useful for ensuring that a value can be safely compared with another value.

    Examples:
        >>> trim_trailing_zeros(3.1400)
        Decimal('3.14')
        >>> trim_trailing_zeros(Decimal("3.1400"))
        Decimal('3.14')

    Args:
        value (float, Decimal, str): The value to be trimmed.

    Returns:
        The trimmed value. Decimal
    """
    return Decimal(str(value)).normalize()

whole_to_ratio(whole)

Convert a whole number to a ratio.

This is useful for converting a percentage to a ratio.

Examples:

>>> whole_to_ratio(3)
0.03
>>> whole_to_ratio(100)
1

Parameters:

Name Type Description Default
whole (Decimal, float, str)

The whole number to be converted.

required

Returns:

Type Description
T

The ratio. Decimal

Source code in django_rubble\utils\numbers.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def whole_to_ratio(whole: T) -> T:
    """Convert a whole number to a ratio.

    This is useful for converting a percentage to a ratio.

    Examples:
        >>> whole_to_ratio(3)
        0.03
        >>> whole_to_ratio(100)
        1

    Args:
        whole (Decimal, float, str): The whole number to be converted.

    Returns:
        The ratio. Decimal
    """
    multiplier = Decimal("100") if isinstance(whole, Decimal) else 100
    return whole / multiplier