Skip to content

Commit 0892140

Browse files
dmosorastJacob Bacajacobrobertbaca
authored
Add support for custom decimal string formatter (#125)
* Handle possible input types to format as decimals * Stringify resulting data post-decimal conversion, fix return * Pylint fixes * added tests for singer.decimal logic * modified transform to parse NaN values as NaNs * changed logic to transform occurrence of snan into a nan Co-authored-by: Jacob Baca <jbaca@stitchdata.com> Co-authored-by: Jacob Baca <52418765+jacobrobertbaca@users.noreply.github.com>
1 parent 49c9f08 commit 0892140

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

singer/transform.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import decimal
23
import logging
34
import re
45
from jsonschema import RefResolver
@@ -271,7 +272,25 @@ def _transform(self, data, typ, schema, path):
271272
return False, None
272273

273274
return True, data
275+
elif schema.get("format") == "singer.decimal":
276+
if data is None:
277+
return False, None
278+
279+
if isinstance(data, (str, float, int)):
280+
try:
281+
return True, str(decimal.Decimal(str(data)).normalize())
282+
except:
283+
return False, None
284+
elif isinstance(data, decimal.Decimal):
285+
try:
286+
if data.is_snan():
287+
return True, 'NaN'
288+
else:
289+
return True, str(data.normalize())
290+
except:
291+
return False, None
274292

293+
return False, None
275294
elif typ == "object":
276295
# Objects do not necessarily specify properties
277296
return self._transform_object(data,

tests/test_transform.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
import decimal
23
from singer import transform
34
from singer.transform import *
45

@@ -252,6 +253,68 @@ def test_null_object_transform(self):
252253
empty_data = {'addrs': {}}
253254
self.assertDictEqual(empty_data, transform(empty_data, schema))
254255

256+
def test_decimal_types_transform(self):
257+
schema = {"type": "object",
258+
"properties": {"percentage": {"type": ["string"],
259+
"format": "singer.decimal"}}}
260+
261+
inf = {'percentage': 'Infinity'}
262+
negative_inf = {'percentage': '-Infinity'}
263+
root2 = {'percentage': 1.4142135623730951}
264+
nan = {'percentage': decimal.Decimal('NaN')}
265+
snan = {'percentage': decimal.Decimal('sNaN')}
266+
267+
self.assertEquals(inf, transform(inf, schema))
268+
self.assertEquals(negative_inf, transform(negative_inf, schema))
269+
self.assertEquals({'percentage': '1.4142135623730951'}, transform(root2, schema))
270+
self.assertEquals({'percentage': 'NaN'}, transform(nan, schema))
271+
self.assertEquals({'percentage': 'NaN'}, transform(snan, schema))
272+
273+
274+
str1 = {'percentage':'0.1'}
275+
str2 = {'percentage': '0.0000000000001'}
276+
str3 = {'percentage': '1E+13'}
277+
str4 = {'percentage': '100'}
278+
str5 = {'percentage': '-100'}
279+
self.assertEquals(str1, transform(str1, schema))
280+
self.assertEquals({'percentage': '1E-13'}, transform(str2, schema))
281+
self.assertEquals({'percentage': '1E+13'}, transform(str3, schema))
282+
self.assertEquals({'percentage': '1E+2'}, transform(str4, schema))
283+
self.assertEquals({'percentage': '-1E+2'}, transform(str5, schema))
284+
285+
float1 = {'percentage': 12.0000000000000000000000000001234556}
286+
float2 = {'percentage': 0.0123}
287+
float3 = {'percentage': 100.0123}
288+
float4 = {'percentage': -100.0123}
289+
self.assertEquals({'percentage':'12'}, transform(float1, schema))
290+
self.assertEquals({'percentage':'0.0123'}, transform(float2, schema))
291+
self.assertEquals({'percentage':'100.0123'}, transform(float3, schema))
292+
self.assertEquals({'percentage':'-100.0123'}, transform(float4, schema))
293+
294+
int1 = {'percentage': 123}
295+
int2 = {'percentage': 0}
296+
int3 = {'percentage': -1000}
297+
self.assertEquals({'percentage':'123'}, transform(int1, schema))
298+
self.assertEquals({'percentage':'0'}, transform(int2, schema))
299+
self.assertEquals({'percentage':'-1E+3'}, transform(int3, schema))
300+
301+
dec1 = {'percentage': decimal.Decimal('1.1010101')}
302+
dec2 = {'percentage': decimal.Decimal('.111111111111111111111111')}
303+
dec3 = {'percentage': decimal.Decimal('-.111111111111111111111111')}
304+
dec4 = {'percentage': decimal.Decimal('100')}
305+
self.assertEquals({'percentage':'1.1010101'}, transform(dec1, schema))
306+
self.assertEquals({'percentage':'0.111111111111111111111111'}, transform(dec2, schema))
307+
self.assertEquals({'percentage':'-0.111111111111111111111111'}, transform(dec3, schema))
308+
self.assertEquals({'percentage':'1E+2'}, transform(dec4, schema))
309+
310+
bad1 = {'percentage': 'fsdkjl'}
311+
with self.assertRaises(SchemaMismatch):
312+
transform(bad1, schema)
313+
314+
badnull = {'percentage': None}
315+
with self.assertRaises(SchemaMismatch):
316+
self.assertEquals({'percentage':None}, transform(badnull, schema))
317+
255318
class TestTransformsWithMetadata(unittest.TestCase):
256319

257320
def test_drops_no_data_when_not_dict(self):

0 commit comments

Comments
 (0)