Skip to content

Commit 51b910f

Browse files
committed
Merge branch 'decimal128field' of github.com:jschlyter/mongoengine into clone_jschlyter_decimal128
2 parents 80b7c16 + 98b2a91 commit 51b910f

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

mongoengine/fields.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import gridfs
1313
import pymongo
1414
from bson import SON, Binary, DBRef, ObjectId
15+
from bson.decimal128 import Decimal128, create_decimal128_context
1516
from bson.int64 import Int64
1617
from pymongo import ReturnDocument
1718

@@ -95,6 +96,7 @@
9596
"MultiLineStringField",
9697
"MultiPolygonField",
9798
"GeoJsonBaseField",
99+
"Decimal128Field",
98100
)
99101

100102
RECURSIVE_REFERENCE_CONSTANT = "self"
@@ -2650,3 +2652,51 @@ def to_mongo(self, document):
26502652
)
26512653
else:
26522654
return super().to_mongo(document)
2655+
2656+
2657+
class Decimal128Field(BaseField):
2658+
2659+
DECIMAL_CONTEXT = create_decimal128_context()
2660+
2661+
def __init__(self, min_value=None, max_value=None, **kwargs):
2662+
self.min_value = min_value
2663+
self.max_value = max_value
2664+
super().__init__(**kwargs)
2665+
2666+
"""
2667+
128-bit decimal-based floating-point field capable of emulating decimal
2668+
rounding with exact precision. Stores the value as a `Decimal128`
2669+
intended for monetary data, such as financial, tax, and scientific
2670+
computations.
2671+
"""
2672+
2673+
def to_mongo(self, value):
2674+
if value is None:
2675+
return None
2676+
if isinstance(value, Decimal128):
2677+
return value
2678+
if not isinstance(value, decimal.Decimal):
2679+
with decimal.localcontext(self.DECIMAL_CONTEXT) as ctx:
2680+
value = ctx.create_decimal(value)
2681+
return Decimal128(value)
2682+
2683+
def to_python(self, value):
2684+
if value is None:
2685+
return None
2686+
return self.to_mongo(value).to_decimal()
2687+
2688+
def validate(self, value):
2689+
if not isinstance(value, Decimal128):
2690+
try:
2691+
value = Decimal128(value)
2692+
except (TypeError, ValueError, decimal.InvalidOperation) as exc:
2693+
self.error("Could not convert value to Decimal128: %s" % exc)
2694+
2695+
if self.min_value is not None and value.to_decimal() < self.min_value:
2696+
self.error("Decimal value is too small")
2697+
2698+
if self.max_value is not None and value.to_decimal() > self.max_value:
2699+
self.error("Decimal value is too large")
2700+
2701+
def prepare_query_value(self, op, value):
2702+
return super().prepare_query_value(op, self.to_mongo(value))
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import json
2+
import random
3+
from decimal import Decimal
4+
5+
import pytest
6+
from bson.decimal128 import Decimal128
7+
8+
from mongoengine import *
9+
from tests.utils import MongoDBTestCase, get_as_pymongo
10+
11+
12+
class Decimal128Document(Document):
13+
dec128_fld = Decimal128Field()
14+
dec128_min_0 = Decimal128Field(min_value=0)
15+
dec128_max_100 = Decimal128Field(max_value=100)
16+
17+
18+
def generate_test_cls() -> Document:
19+
Decimal128Document.drop_collection()
20+
Decimal128Document(dec128_fld=None).save()
21+
Decimal128Document(dec128_fld=Decimal(1)).save()
22+
return Decimal128Document
23+
24+
25+
class TestDecimal128Field(MongoDBTestCase):
26+
def test_decimal128_validation_good(self):
27+
"""Ensure that invalid values cannot be assigned."""
28+
29+
doc = Decimal128Document()
30+
31+
doc.dec128_fld = Decimal(0)
32+
doc.validate()
33+
34+
doc.dec128_fld = Decimal(50)
35+
doc.validate()
36+
37+
doc.dec128_fld = Decimal(110)
38+
doc.validate()
39+
40+
doc.dec128_fld = Decimal(110)
41+
doc.validate()
42+
43+
def test_decimal128_validation_invalid(self):
44+
"""Ensure that invalid values cannot be assigned."""
45+
46+
doc = Decimal128Document()
47+
48+
doc.dec128_fld = "ten"
49+
50+
with pytest.raises(ValidationError):
51+
doc.validate()
52+
53+
def test_decimal128_validation_min(self):
54+
"""Ensure that out of bounds values cannot be assigned."""
55+
56+
doc = Decimal128Document()
57+
58+
doc.dec128_min_0 = Decimal(50)
59+
doc.validate()
60+
61+
doc.dec128_min_0 = Decimal(-1)
62+
with pytest.raises(ValidationError):
63+
doc.validate()
64+
65+
def test_decimal128_validation_max(self):
66+
"""Ensure that out of bounds values cannot be assigned."""
67+
68+
doc = Decimal128Document()
69+
70+
doc.dec128_max_100 = Decimal(50)
71+
doc.validate()
72+
73+
doc.dec128_max_100 = Decimal(101)
74+
with pytest.raises(ValidationError):
75+
doc.validate()
76+
77+
def test_eq_operator(self):
78+
cls = generate_test_cls()
79+
assert 1 == cls.objects(dec128_fld=1.0).count()
80+
assert 0 == cls.objects(dec128_fld=2.0).count()
81+
82+
def test_ne_operator(self):
83+
cls = generate_test_cls()
84+
assert 1 == cls.objects(dec128_fld__ne=None).count()
85+
assert 1 == cls.objects(dec128_fld__ne=1).count()
86+
assert 1 == cls.objects(dec128_fld__ne=1.0).count()
87+
88+
def test_gt_operator(self):
89+
cls = generate_test_cls()
90+
assert 1 == cls.objects(dec128_fld__gt=0.5).count()
91+
92+
def test_lt_operator(self):
93+
cls = generate_test_cls()
94+
assert 1 == cls.objects(dec128_fld__lt=1.5).count()
95+
96+
def test_storage(self):
97+
# from int
98+
model = Decimal128Document(dec128_fld=100).save()
99+
assert get_as_pymongo(model) == {
100+
"_id": model.id,
101+
"dec128_fld": Decimal128("100"),
102+
}
103+
104+
# from float
105+
model = Decimal128Document(dec128_fld=100.0).save()
106+
assert get_as_pymongo(model) == {
107+
"_id": model.id,
108+
"dec128_fld": Decimal128("100"),
109+
}
110+
111+
# from Decimal
112+
model = Decimal128Document(dec128_fld=Decimal(100)).save()
113+
assert get_as_pymongo(model) == {
114+
"_id": model.id,
115+
"dec128_fld": Decimal128("100"),
116+
}
117+
118+
# from Decimal128
119+
model = Decimal128Document(dec128_fld=Decimal128("100")).save()
120+
assert get_as_pymongo(model) == {
121+
"_id": model.id,
122+
"dec128_fld": Decimal128("100"),
123+
}
124+
125+
def test_json(self):
126+
Decimal128Document.drop_collection()
127+
f = str(random.random())
128+
Decimal128Document(dec128_fld=f).save()
129+
json_str = Decimal128Document.objects.to_json()
130+
array = json.loads(json_str)
131+
assert array[0]["dec128_fld"] == {"$numberDecimal": str(f)}

0 commit comments

Comments
 (0)