diff --git a/secop/datatypes.py b/secop/datatypes.py index 4757325..3b3a540 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -181,6 +181,52 @@ class IntRange(DataType): return self.validate(value) +class ScaledInteger(DataType): + """Scaled integer int type + + note: limits are for the scaled value (i.e. the internal value) + the scale is only used for calculating to/from transport serialisation""" + + def __init__(self, scale, minval=-16777216, maxval=16777216): + self.min = int(minval) + self.max = int(maxval) + self.scale = float(scale) + if self.min > self.max: + raise ValueError(u'Max must be larger then min!') + if not self.scale > 0: + raise ValueError(u'Scale MUST be positive!') + self.as_json = [u'scaled', dict(min=int(round(minval/scale)), max=int(round(maxval/scale)), scale=scale)] + + def validate(self, value): + try: + value = int(value) + if value < self.min: + raise ValueError(u'%r should be an int between %d and %d' % + (value, self.min, self.max)) + if value > self.max: + raise ValueError(u'%r should be an int between %d and %d' % + (value, self.min, self.max)) + return value + except Exception: + raise ValueError(u'Can not validate %r to int' % value) + + def __repr__(self): + return u'ScaledInteger(%f, %d, %d)' % (self.scale, self.min, self.max) + + def export_value(self, value): + """returns a python object fit for serialisation""" + # XXX: rounds toward even !!! (i.e. 12.5 -> 12, 13.5 -> 14) + return round(value / self.scale) + + def import_value(self, value): + """returns a python object from serialisation""" + return self.scale * int(value) + + def from_string(self, text): + value = int(text) + return self.validate(value) + + class EnumType(DataType): def __init__(self, enum_or_name='', **kwds): if 'members' in kwds: @@ -591,6 +637,7 @@ class Status(TupleOf): DATATYPES = dict( bool =BoolType, int =lambda min, max: IntRange(minval=min,maxval=max), + scaled =lambda scale, min, max: ScaledInteger(scale=scale,minval=min*scale,maxval=max*scale), double =lambda min=None, max=None: FloatRange(minval=min, maxval=max), blob =lambda min=0, max=None: BLOBType(minsize=min, maxsize=max), string =lambda min=0, max=None: StringType(minsize=min, maxsize=max), diff --git a/test/test_datatypes.py b/test/test_datatypes.py index f30a50d..e942425 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -28,7 +28,7 @@ import pytest from secop.datatypes import ArrayOf, BLOBType, BoolType, \ DataType, EnumType, FloatRange, IntRange, ProgrammingError, \ - StringType, StructOf, TupleOf, get_datatype + StringType, StructOf, TupleOf, get_datatype, ScaledInteger def test_DataType(): @@ -87,6 +87,33 @@ def test_IntRange(): assert dt.as_json[1]['min'] < 0 < dt.as_json[1]['max'] +def test_ScaledInteger(): + dt = ScaledInteger(0.01, -3, 3) + # serialisation of datatype contains limits on the 'integer' value + assert dt.as_json == ['scaled', {'scale':0.01, 'min':-300, 'max':300}] + + with pytest.raises(ValueError): + dt.validate(9) + with pytest.raises(ValueError): + dt.validate(-9) + with pytest.raises(ValueError): + dt.validate('XX') + with pytest.raises(ValueError): + dt.validate([19, 'X']) + dt.validate(1) + dt.validate(0) + with pytest.raises(ValueError): + ScaledInteger('xc', 'Yx') + with pytest.raises(ValueError): + ScaledInteger(scale=0, minval=1, maxval=2) + with pytest.raises(ValueError): + ScaledInteger(scale=-10, minval=1, maxval=2) + + assert dt.export_value(0.0001) == int(0) + assert dt.export_value(2.71819) == int(272) + assert dt.import_value(272) == 2.72 + + def test_EnumType(): # test constructor catching illegal arguments with pytest.raises(TypeError): @@ -311,6 +338,25 @@ def test_get_datatype(): with pytest.raises(ValueError): get_datatype(['double', 1, 2]) + with pytest.raises(ValueError): + get_datatype(['scaled', {'scale':0.01,'min':-2.718}]) + with pytest.raises(ValueError): + get_datatype(['scaled', {'scale':0.02,'max':3.14}]) + assert isinstance(get_datatype(['scaled', {'scale':0.03,'min':-99, 'max':111}]), ScaledInteger) + + dt = ScaledInteger(scale=0.03, minval=0, maxval=9.9) + assert dt.as_json == ['scaled', {'max':330, 'min':0, 'scale':0.03}] + assert get_datatype(dt.as_json).as_json == dt.as_json + + with pytest.raises(ValueError): + get_datatype(['scaled']) # dict missing + with pytest.raises(ValueError): + get_datatype(['scaled', {'min':-10, 'max':10}]) # no scale + with pytest.raises(ValueError): + get_datatype(['scaled', {'min':10, 'max':-10}]) # limits reversed + with pytest.raises(ValueError): + get_datatype(['scaled', {}, 1, 2]) # trailing data + with pytest.raises(ValueError): get_datatype(['enum']) with pytest.raises(ValueError):