Repository: encode/django-rest-framework
Pull request: Possibility to remove trailing zeros on DecimalFields representation
## Description

It would be good to have a way of remove trailing zeros in output. For example when working with a lot of decimal precision you now get very long strings when representing them as string in outputs.

``` python
# Current
Decimal('0.001000000') >> "0.001000000"
# Changed
Decimal('0.001000000') >> "0.001"

```
This is good as a default. But mostly the string representation ends up in a json-response where we don't know how the receiver of the data handles the precision. 

An option to trim trailing zeros would be good. It would reduce the amount of data you need to send and you wouldn't loose any data.

By using the decimal builtin `.normalize()` a decimal can be stripped of it rightmost trailing zeros. [reference](https://docs.python.org/3/library/decimal.html#decimal.Decimal.normalize)

By introducing the `normalize_output` option on DecimalFields it would be possible to opt in on this behaviour and it wouldn't break other peoples code.

## Example use:

 ``` python

class MySerializer(Serializer)

   value = fields.DecimalField(max_digits=4, decimal_places=3, normalize_output=True)


```

``` python

# Wanted behaviour
field = fields.DecimalField(max_digits=4, decimal_places=3, normalize_output=True)
output = field.to_representation(Decimal('1.100'))
>> output == '1.1'

```

Fixes #6151 




Problem statement:
DecimalField: str value without trailing zeros
I guess, `coerce_to_string=True` in DecimalField should safely remove trailing zeros and `coerce_to_string=False` should leave value unchanged. If it's true - why my values are with trailing zeros?
In old version of DRF values were without trailing zeros as I remember
```
| vaue | 24.471635150166826 |

value = serializers.DecimalField(decimal_places=30, max_digits=35, default=0, coerce_to_string=True)
>> 24.471635150166826000000000000000
```

Hints from issue discussion:
Just ran into this. I am using it to store SI multipliers for values series and having a series in kilo{something} it is not needed to output "1000.000000000000" in my JSON and its annoying to have to remember the amount of decimal places for simple tests.

This on the DecimalField seems to do it:

``` python

def quantize(self, value):
        """
        Quantize the decimal value to the configured precision.
        """
        if self.decimal_places is None:
            return value

        context = decimal.getcontext().copy()
        if self.max_digits is not None:
            context.prec = self.max_digits
        return value.quantize(
            decimal.Decimal('.1') ** self.decimal_places,
            rounding=self.rounding,
            context=context
        )

```

Maybe another option is to use .normalize() on the value?

``` python
decimal.Decimal('0.001000000).normalize()`
>> decimal.Decimal('0.001')

# but the following might be a problem for serializing but according to json standard for number it should be ok. (https://json.org/)

decimal.Decimal('1000.00000000).normalize()
>> decimal.Decimal('1E+3')
```

``` python

multiplier = fields.DecimalField(max_digits=25, decimal_places=12, normalize=True)

```

Another option could be to create a subclass of the field that makes the quantize function do nothing. but that would get problems if you got a result that has more decimal places than wanted and there would be no rounding.

Not sure what the best way to solve it would be.

@carltongibson added the rounding parameter on DecimalField. Maybe he could give some insight to what could be a good direction to solve the problem.

Seems to me like adding a normalize option would be good.
> Another option could be to create a subclass...

This is always the first option. Capture the exact behaviour you need for your application. Then propose a change if you think it appropriate, with a set of test cases that show exactly the what behaviour is altered. 

I suspect all the options here are a matter of choice. If we change the default behaviour someone else will complain. I'm not sure there's much we can do. 
I assume this is related to changes in #4075 or #4339. Also, this sounds related to #6311, which has additional, extensive discussion. Not 100% sure, since I don't have the bandwidth to read all of these threads extensively.


in #6311 @tomchristie say that he would accept `strip_trailing_zeros` flag. Would a `normalize` argument achieving pretty much the same thing also be acceptable?

I think the normalize flag could be added without breaking any existing stuff. I'm working on a deadline right now but afterwards I could try and put together a PR for it.

Hey @Krolken. A PR is always worth looking at. 🙂

Commit message: Fixed linting error in test_fields

Patch to review:
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index ede4f15ad5..6ee3c7308d 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -281,6 +281,7 @@ Corresponds to `django.db.models.fields.DecimalField`.
 - `min_value` Validate that the number provided is no less than this value.
 - `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file.
 - `rounding` Sets the rounding mode used when quantising to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`.
+- `normalize` Will normalize the decimal value. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without loosing data. Defaults to `False`.
 
 #### Example usage
 
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index c8f65db0e5..612568e573 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1014,10 +1014,11 @@ class DecimalField(Field):
     MAX_STRING_LENGTH = 1000  # Guard against malicious string inputs.
 
     def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None,
-                 localize=False, rounding=None, **kwargs):
+                 localize=False, rounding=None, normalize=False, **kwargs):
         self.max_digits = max_digits
         self.decimal_places = decimal_places
         self.localize = localize
+        self.normalize = normalize
         if coerce_to_string is not None:
             self.coerce_to_string = coerce_to_string
         if self.localize:
@@ -1125,6 +1126,11 @@ def to_representation(self, value):
 
         quantized = self.quantize(value)
 
+        # TODO: Should maybe name the value to something not bound to
+        #   quantized. Ex: out_value
+        if self.normalize:
+            quantized = quantized.normalize()
+
         if not coerce_to_string:
             return quantized
         if self.localize:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 12c936b229..1d44c72605 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1170,6 +1170,27 @@ def test_part_precision_string_quantized_value_for_decimal(self):
         assert value == expected_digit_tuple
 
 
+class TestNormalizedValueDecimalField(TestCase):
+    """
+    Test that we get the expected behavior of on DecimalField when normalize=True
+    """
+
+    def test_normalize_output(self):
+        field = serializers.DecimalField(max_digits=4, decimal_places=3, normalize=True)
+        output = field.to_representation(Decimal('1.000'))
+        assert output == '1'
+
+    def test_non_normalize_output(self):
+        field = serializers.DecimalField(max_digits=4, decimal_places=3, normalize=False)
+        output = field.to_representation(Decimal('1.000'))
+        assert output == '1.000'
+
+    def test_normalize_coeherce_to_string(self):
+        field = serializers.DecimalField(max_digits=4, decimal_places=3, normalize=True, coerce_to_string=False)
+        output = field.to_representation(Decimal('1.000'))
+        assert output == Decimal('1')
+
+
 class TestNoDecimalPlaces(FieldValues):
     valid_inputs = {
         '0.12345': Decimal('0.12345'),


Code review comments:
diff_hunk: @@ -1125,6 +1126,11 @@ def to_representation(self, value):
 
         quantized = self.quantize(value)
 
+        # TODO: Should maybe name the value to something not bound to
+        #   quantized. Ex: out_value
path: rest_framework/fields.py
text: @user1:
Let's drop the comment here.

@author:
Sure. But what of the variable name? I found it a bit confusing to keep the `quantized` variable name since once you normalize you have effectively  removed the precision that was enforced by `quantize`.
I put it in a note for discussion as I thought a more neutral variable name would be better.

diff_hunk: @@ -281,6 +281,7 @@ Corresponds to `django.db.models.fields.DecimalField`.
 - `min_value` Validate that the number provided is no less than this value.
 - `localize` Set to `True` to enable localization of input and output based on the current locale. This will also force `coerce_to_string` to `True`. Defaults to `False`. Note that data formatting is enabled if you have set `USE_L10N=True` in your settings file.
 - `rounding` Sets the rounding mode used when quantising to the configured precision. Valid values are [`decimal` module rounding modes][python-decimal-rounding-modes]. Defaults to `None`.
+- `normalize` Will normalize the decimal value. This will strip all trailing zeroes and change the value's precision to the minimum required precision to be able to represent the value without loosing data. Defaults to `False`.
path: docs/api-guide/fields.md
text: @user1:
Seems neat yup. Perhaps we want to make it clear that `normalize` is about the serialized *output*?
(It doesn't change anything on data validation right?)


@author:
Yeah. Everything happens in `to_representation`.
I agree that it could be more clear that it is just acting on the output.
`normalize_output` would be a good name too.