Skip to content

Commit 2c94f22

Browse files
committed
[#29] Extend Range to support operators
1 parent a2babc3 commit 2c94f22

File tree

9 files changed

+224
-25
lines changed

9 files changed

+224
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
### How to run tests
2121

22-
```> tox -e py310```
22+
```> tox -e py312```
2323
or
2424
```> python -m pytest --cov=jdmn tests/```
2525

ci/ci_requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-r ../requirements.txt
22
-r ../requirements.testing.txt
33

4-
tox>=3.25.1 # Command line driven CI frontend and development task automation tool
4+
tox>=4.11.3 # Command line driven CI frontend and development task automation tool
55
setuptools >= 40.0.4 # build dist
66
setuptools_scm >= 2.0.0 # build dist
77
wheel >= 0.37.1 # build dist

ci_build.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
call ci/make_env.bat .venv
22

3-
tox -e py310
3+
tox -e py312
44
tox -e linters

src/jdmn/feel/lib/BaseStandardFEELLib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ def is_(self, value1: Any, value2: Any) -> BOOLEAN:
554554
try:
555555
if value1 is None or value2 is None:
556556
return value1 == value2
557-
elif type(value1) != type(value2):
557+
elif type(value1) is not type(value2):
558558
# Different kind
559559
return False
560560
elif self.isNumber(value1):

src/jdmn/feel/lib/type/ComparableComparator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
# specific language governing permissions and limitations under the License.
1212
#
1313
import datetime
14+
import decimal
1415
from typing import Union
1516

1617
from jdmn.feel.lib.Types import BOOLEAN
1718
from jdmn.feel.lib.type.RelationalComparator import RelationalComparator
1819

19-
comparable = Union[float, str, datetime.date, datetime.time, datetime.datetime]
20+
comparable = Union[decimal.Decimal, str, datetime.date, datetime.time, datetime.datetime, datetime.timedelta]
2021

2122

2223
class ComparableComparator(RelationalComparator):

src/jdmn/runtime/Range.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,86 @@
1010
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
1111
# specific language governing permissions and limitations under the License.
1212
#
13+
import datetime
14+
import decimal
15+
import typing
1316
from typing import Any
1417

18+
import isodate
19+
from jdmn.runtime.DMNRuntimeException import DMNRuntimeException
20+
21+
comparable = typing.Optional[typing.Union[decimal.Decimal, str, datetime.date, datetime.time, datetime.datetime, datetime.timedelta, isodate.Duration]]
22+
1523

1624
class Range:
17-
def __init__(self, startIncluded: bool, start: Any, endIncluded: bool, end: Any):
18-
self.startIncluded = startIncluded
19-
self.start = start
20-
self.endIncluded = endIncluded
21-
self.end = end
25+
def __init__(self, *args):
26+
if len(args) == 0:
27+
self.startIncluded = False
28+
self.start = None
29+
self.endIncluded = False
30+
self.end = None
31+
self.operator = None
32+
elif len(args) == 4:
33+
self.startIncluded = args[0]
34+
self.start = args[1]
35+
self.endIncluded = args[2]
36+
self.end = args[3]
37+
self.operator = None
38+
# Check if both ends are comparable types
39+
if not isinstance(self.start, comparable) or not isinstance(self.end, comparable):
40+
raise DMNRuntimeException("Invalid range: start type {} and type {} must be comparable.".format(type(self.start), type(self.end)))
41+
if self.start is not None and self.end is not None:
42+
# Check if endpoints have same type
43+
if type(self.start) is not type(self.end):
44+
raise DMNRuntimeException("Invalid range: start type {} and type {} must be the same.".format(type(self.start), type(self.end)))
45+
# Check if start <= end; Duration does not support relational operators
46+
if not isinstance(self.start, isodate.Duration) and self.start > self.end:
47+
raise DMNRuntimeException("Invalid range: start {} cannot be greater than end {}.".format(self.start, self.end))
48+
49+
elif len(args) == 2:
50+
self.operator = args[0]
51+
if self.operator is None:
52+
self.operator = "="
53+
if len(self.operator.strip()) == 0:
54+
self.operator = "="
55+
endpoint = args[1]
56+
match self.operator:
57+
case "=":
58+
self.startIncluded = True
59+
self.start = endpoint
60+
self.end = endpoint
61+
self.endIncluded = True
62+
case "!=":
63+
self.startIncluded = False
64+
self.start = None
65+
self.end = None
66+
self.endIncluded = False
67+
case "<":
68+
self.startIncluded = False
69+
self.start = None
70+
self.end = endpoint
71+
self.endIncluded = False
72+
case "<=":
73+
self.startIncluded = False
74+
self.start = None
75+
self.end = endpoint
76+
self.endIncluded = True
77+
case ">":
78+
self.startIncluded = False
79+
self.start = endpoint
80+
self.end = None
81+
self.endIncluded = False
82+
case ">=":
83+
self.startIncluded = True
84+
self.start = endpoint
85+
self.end = None
86+
self.endIncluded = False
87+
case _:
88+
raise DMNRuntimeException("Illegal operator '{}'".format(self.operator))
89+
if not isinstance(self.start, comparable):
90+
raise DMNRuntimeException("Invalid range: endpoint most be comparable {}.".format(type(endpoint)))
91+
else:
92+
raise DMNRuntimeException("Illegal Range constructor '{}'".format(*args))
2293

2394
def isStartIncluded(self) -> bool:
2495
return self.startIncluded
@@ -32,6 +103,9 @@ def isEndIncluded(self) -> bool:
32103
def getEnd(self) -> Any:
33104
return self.end
34105

106+
def getOperator(self) -> str:
107+
return self.end
108+
35109
def __eq__(self, other: Any) -> bool:
36110
if self is other:
37111
return True
@@ -46,6 +120,8 @@ def __eq__(self, other: Any) -> bool:
46120
return False
47121
if self.end != other.end:
48122
return False
123+
if self.operator != other.operator:
124+
return False
49125
return True
50126

51127
def __hash__(self):
@@ -54,7 +130,8 @@ def __hash__(self):
54130
result = 31 * result + (0 if self.start is None else hash(self.start))
55131
result = 31 * result + (0 if self.endIncluded is None else hash(self.endIncluded))
56132
result = 31 * result + (0 if self.end is None else hash(self.end))
133+
result = 31 * result + (0 if self.operator is None else hash(self.operator))
57134
return result
58135

59136
def __str__(self):
60-
return f"Range({self.startIncluded},{self.start},{self.end},{self.endIncluded})"
137+
return "Range({},{},{},{},{})".format(self.startIncluded, self.start, self.end, self.endIncluded, self.operator)

tests/jdmn/feel/lib/FEELOperatorsTest.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def testIsNumeric(self):
4444
self.assertFalse(self.getLib().isNumber(self.getLib().duration("P1Y1M")))
4545
self.assertFalse(self.getLib().isNumber(self.getLib().asList("a")))
4646
self.assertFalse(self.getLib().isNumber(Context()))
47-
self.assertFalse(self.getLib().isNumber(Range(True, 0, True, 1)))
47+
self.assertFalse(self.getLib().isNumber(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
4848

4949
def testNumericValue(self):
5050
self.assertIsNone(self.getLib().numericValue(None))
@@ -175,7 +175,7 @@ def testIsBoolean(self):
175175
self.assertFalse(self.getLib().isBoolean(self.getLib().duration("P1Y1M")))
176176
self.assertFalse(self.getLib().isBoolean(self.getLib().asList("a")))
177177
self.assertFalse(self.getLib().isBoolean(Context()))
178-
self.assertFalse(self.getLib().isBoolean(Range(True, 0, True, 1)))
178+
self.assertFalse(self.getLib().isBoolean(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
179179

180180
def testBooleanValue(self):
181181
self.assertIsNone(self.getLib().booleanValue(None))
@@ -310,7 +310,7 @@ def testIsString(self):
310310
self.assertFalse(self.getLib().isString(self.getLib().duration("P1Y1M")))
311311
self.assertFalse(self.getLib().isString(self.getLib().asList("a")))
312312
self.assertFalse(self.getLib().isString(Context()))
313-
self.assertFalse(self.getLib().isString(Range(True, 0, True, 1)))
313+
self.assertFalse(self.getLib().isString(Range(True, "0", True, "1")))
314314

315315
def testStringValue(self):
316316
self.assertIsNone(self.getLib().stringValue(None))
@@ -362,7 +362,7 @@ def testIsDate(self):
362362
self.assertFalse(self.getLib().isDate(self.getLib().duration("P1Y1M")))
363363
self.assertFalse(self.getLib().isDate(self.getLib().asList("a")))
364364
self.assertFalse(self.getLib().isDate(Context()))
365-
self.assertFalse(self.getLib().isDate(Range(True, 0, True, 1)))
365+
self.assertFalse(self.getLib().isDate(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
366366

367367
def testDateValue(self):
368368
self.assertIsNone(self.getLib().dateValue(None))
@@ -471,7 +471,7 @@ def testIsTime(self):
471471
self.assertFalse(self.getLib().isTime(self.getLib().duration("P1Y1M")))
472472
self.assertFalse(self.getLib().isTime(self.getLib().asList("a")))
473473
self.assertFalse(self.getLib().isTime(Context()))
474-
self.assertFalse(self.getLib().isTime(Range(True, 0, True, 1)))
474+
self.assertFalse(self.getLib().isTime(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
475475

476476
def testTimeValue(self):
477477
self.assertIsNone(self.getLib().timeValue(None))
@@ -627,7 +627,7 @@ def testIsDateTime(self):
627627
self.assertFalse(self.getLib().isDateTime(self.getLib().duration("P1Y1M")))
628628
self.assertFalse(self.getLib().isDateTime(self.getLib().asList("a")))
629629
self.assertFalse(self.getLib().isDateTime(Context()))
630-
self.assertFalse(self.getLib().isDateTime(Range(True, 0, True, 1)))
630+
self.assertFalse(self.getLib().isDateTime(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
631631

632632
def testDateTimeValue(self):
633633
self.assertIsNone(self.getLib().dateTimeValue(None))
@@ -988,7 +988,7 @@ def testIsList(self):
988988
self.assertFalse(self.getLib().isList(self.getLib().duration("P1Y1M")))
989989
self.assertTrue(self.getLib().isList(self.getLib().asList("a")))
990990
self.assertFalse(self.getLib().isList(Context()))
991-
self.assertFalse(self.getLib().isList(Range(True, 0, True, 1)))
991+
self.assertFalse(self.getLib().isList(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
992992

993993
def testListIs(self):
994994
self.assertTrue(self.getLib().listIs(None, None))
@@ -1028,7 +1028,7 @@ def testIsContext(self):
10281028
self.assertFalse(self.getLib().isContext(self.getLib().duration("P1Y1M")))
10291029
self.assertFalse(self.getLib().isContext(self.getLib().asList("a")))
10301030
self.assertTrue(self.getLib().isContext(Context()))
1031-
self.assertFalse(self.getLib().isContext(Range(True, 0, True, 1)))
1031+
self.assertFalse(self.getLib().isContext(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
10321032

10331033
def testContextValue(self):
10341034
c1 = Context()
@@ -1088,7 +1088,7 @@ def testIsRange(self):
10881088
self.assertFalse(self.getLib().isRange(self.getLib().duration("P1Y1M")))
10891089
self.assertFalse(self.getLib().isRange(self.getLib().asList("a")))
10901090
self.assertFalse(self.getLib().isRange(Context()))
1091-
self.assertTrue(self.getLib().isRange(Range(True, 0, True, 1)))
1091+
self.assertTrue(self.getLib().isRange(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
10921092

10931093
def testRangeValue(self):
10941094
r1 = Range(True, self.getLib().number("1"), True, self.getLib().number("2"))
@@ -1148,7 +1148,7 @@ def testIsFunction(self):
11481148
self.assertFalse(self.getLib().isFunction(self.getLib().duration("P1Y1M")))
11491149
self.assertFalse(self.getLib().isFunction(self.getLib().asList("a")))
11501150
self.assertFalse(self.getLib().isFunction(Context()))
1151-
self.assertFalse(self.getLib().isFunction(Range(True, 0, True, 1)))
1151+
self.assertFalse(self.getLib().isFunction(Range(True, self.makeNumber("0"), True, self.makeNumber("1"))))
11521152

11531153
def testFunctionValue(self):
11541154
self.assertIsNone(self.getLib().functionValue(None))
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#
2+
# Copyright 2016 Goldman Sachs.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License") you may not use this file except in compliance with the License.
5+
#
6+
# You may obtain a copy of the License at
7+
# http:#www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
10+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
# specific language governing permissions and limitations under the License.
12+
#
13+
from unittest import TestCase
14+
15+
from jdmn.runtime.Range import Range
16+
17+
from jdmn.runtime.DMNRuntimeException import DMNRuntimeException
18+
19+
from src.jdmn.feel.lib.type.numeric.DefaultNumericLib import DefaultNumericLib
20+
21+
22+
class RangeTest(TestCase):
23+
"""
24+
Base test class for Range
25+
"""
26+
27+
def testDefaultConstructor(self):
28+
r = Range()
29+
self.assertFalse(r.startIncluded)
30+
self.assertIsNone(r.start)
31+
self.assertFalse(r.endIncluded)
32+
self.assertIsNone(r.end)
33+
self.assertIsNone(r.operator)
34+
35+
def testConstructorWithEndpoints(self):
36+
r = Range(True, self.makeNumber(3), False, self.makeNumber(4))
37+
self.assertTrue(r.startIncluded)
38+
self.assertEqual(3, r.start)
39+
self.assertFalse(r.endIncluded)
40+
self.assertEqual(4, r.end)
41+
self.assertIsNone(r.operator)
42+
43+
def testConstructorWithEqualOperator(self):
44+
r = Range("=", self.makeNumber(4))
45+
self.assertTrue(r.startIncluded)
46+
self.assertEqual(4, r.start)
47+
self.assertTrue(r.endIncluded)
48+
self.assertEqual(4, r.end)
49+
self.assertEqual("=", r.operator)
50+
51+
def testConstructorWithNullOperator(self):
52+
r = Range(None, self.makeNumber(4))
53+
self.assertTrue(r.startIncluded)
54+
self.assertEqual(4, r.start)
55+
self.assertTrue(r.endIncluded)
56+
self.assertEqual(4, r.end)
57+
self.assertEqual("=", r.operator)
58+
59+
def testConstructorWithEmptyOperator(self):
60+
r = Range(" ", self.makeNumber(4))
61+
self.assertTrue(r.startIncluded)
62+
self.assertEqual(4, r.start)
63+
self.assertTrue(r.endIncluded)
64+
self.assertEqual(4, r.end)
65+
self.assertEqual("=", r.operator)
66+
67+
def testConstructorWithNotEqualOperator(self):
68+
r = Range("!=", self.makeNumber(4))
69+
self.assertFalse(r.startIncluded)
70+
self.assertIsNone(r.start)
71+
self.assertFalse(r.endIncluded)
72+
self.assertIsNone(r.end)
73+
self.assertEqual("!=", r.operator)
74+
75+
def testConstructorWithLessOperator(self):
76+
r = Range("<", self.makeNumber(4))
77+
self.assertFalse(r.startIncluded)
78+
self.assertIsNone(r.start)
79+
self.assertFalse(r.endIncluded)
80+
self.assertEqual(4, r.end)
81+
self.assertEqual("<", r.operator)
82+
83+
def testConstructorWithLessEqualOperator(self):
84+
r = Range("<=", self.makeNumber(4))
85+
self.assertFalse(r.startIncluded)
86+
self.assertIsNone(r.start)
87+
self.assertTrue(r.endIncluded)
88+
self.assertEqual(4, r.end)
89+
self.assertEqual("<=", r.operator)
90+
91+
def testConstructorWithGreaterOperator(self):
92+
r = Range(">", self.makeNumber(4))
93+
self.assertFalse(r.startIncluded)
94+
self.assertEqual(4, r.start)
95+
self.assertFalse(r.endIncluded)
96+
self.assertIsNone(r.end)
97+
self.assertEqual(">", r.operator)
98+
99+
def testConstructorWithGreaterEqualOperator(self):
100+
r = Range(">=", self.makeNumber(4))
101+
self.assertTrue(r.startIncluded)
102+
self.assertEqual(4, r.start)
103+
self.assertFalse(r.endIncluded)
104+
self.assertIsNone(r.end)
105+
self.assertEqual(">=", r.operator)
106+
107+
def testConstructorWithIncorrectEndpoints(self):
108+
with self.assertRaises(DMNRuntimeException):
109+
Range(True, self.makeNumber(4), False, self.makeNumber(3))
110+
111+
def testConstructorWithIncorrectOperator(self):
112+
with self.assertRaises(DMNRuntimeException):
113+
Range("abc", 4)
114+
115+
def testConstructorWithIncorrectEndpointOperator(self):
116+
with self.assertRaises(DMNRuntimeException):
117+
Range("=", 4)
118+
119+
@staticmethod
120+
def makeNumber(number: int):
121+
return DefaultNumericLib.number(str(number))

tox.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[tox]
2-
minversion = 3.25.0
3-
envlist = py{310}, pylint, flake8, linters
2+
minversion = 4.11.3
3+
envlist = py{312}, pylint, flake8, linters
44
isolated_build = True
55

66
[testenv]
77
deps =
8-
-Ur{toxinidir}/requirements.txt
9-
-Ur{toxinidir}/requirements.testing.txt
8+
-r{toxinidir}/requirements.txt
9+
-r{toxinidir}/requirements.testing.txt
1010
commands =
1111
coverage run -m pytest {posargs}
1212
coverage report --include src/* --show-missing --fail-under 70

0 commit comments

Comments
 (0)