Skip to content

Commit e161fbc

Browse files
committed
now() today() timeOfDay() date and time functions
1 parent 754264f commit e161fbc

File tree

8 files changed

+150
-31
lines changed

8 files changed

+150
-31
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,4 @@ dmypy.json
130130

131131
# Other
132132
.DS_Store
133+
.idea/

fhirpathpy/__init__.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from fhirpathpy.engine.invocations.constants import Constants
12
from fhirpathpy.parser import parse
23
from fhirpathpy.engine import do_eval
34
from fhirpathpy.engine.util import arraify, get_data
@@ -14,7 +15,7 @@
1415

1516

1617
def apply_parsed_path(resource, parsedPath, context={}, model=None):
17-
# constants.reset();
18+
Constants.reset()
1819
dataRoot = arraify(resource)
1920

2021
"""
@@ -48,33 +49,33 @@ def visit(node):
4849

4950

5051
def evaluate(resource, path, context={}, model=None):
51-
"""
52+
"""
5253
Evaluates the "path" FHIRPath expression on the given resource, using data
5354
from "context" for variables mentioned in the "path" expression.
5455
55-
Parameters:
56+
Parameters:
5657
resource (dict|list): FHIR resource, bundle as js object or array of resources This resource will be modified by this function to add type information.
5758
path (string): fhirpath expression, sample 'Patient.name.given'
5859
context (dict): a hash of variable name/value pairs.
5960
model (dict): The "model" data object specific to a domain, e.g. R4.
6061
61-
Returns:
62-
int: Description of return value
62+
Returns:
63+
int: Description of return value
6364
6465
"""
6566
node = parse(path)
6667
return apply_parsed_path(resource, node, context, model)
6768

6869

6970
def compile(path, model=None):
70-
"""
71+
"""
7172
Returns a function that takes a resource and an optional context hash (see
7273
"evaluate"), and returns the result of evaluating the given FHIRPath
7374
expression on that resource. The advantage of this function over "evaluate"
7475
is that if you have multiple resources, the given FHIRPath expression will
7576
only be parsed once.
7677
77-
Parameters:
78+
Parameters:
7879
path (string) - the FHIRPath expression to be parsed.
7980
model (dict) - The "model" data object specific to a domain, e.g. R4.
8081

fhirpathpy/engine/invocations/__init__.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import fhirpathpy.engine.invocations.misc as misc
1010
import fhirpathpy.engine.invocations.equality as equality
1111
import fhirpathpy.engine.invocations.logic as logic
12+
import fhirpathpy.engine.invocations.datetime as datetime
1213

1314
invocations = {
1415
"empty": {"fn": existence.empty_fn},
@@ -68,9 +69,9 @@
6869
"round": {"fn": math.rround, "arity": {1: ["Number"]}},
6970
"sqrt": {"fn": math.sqrt},
7071
"truncate": {"fn": math.truncate},
71-
# now: {fn: datetime.now },
72-
# today: {fn: datetime.today },
73-
#
72+
"now": {"fn": datetime.now},
73+
"today": {"fn": datetime.today},
74+
"timeOfDay": {"fn": datetime.timeOfDay},
7475
"children": {"fn": navigation.children},
7576
"descendants": {"fn": navigation.descendants},
7677
"|": {"fn": combining.union_op, "arity": {2: ["Any", "Any"]}},
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import datetime
2+
3+
4+
class Constants:
5+
"""
6+
These are values that should not change during an evaluation of a FHIRPath
7+
expression (e.g. the return value of today(), per the spec.) They are
8+
constant during at least one evaluation.
9+
"""
10+
11+
nowDate = datetime.datetime.now()
12+
today = None
13+
now = None
14+
timeOfDay = None
15+
localTimezoneOffset = None
16+
17+
@classmethod
18+
def reset(cls):
19+
cls.nowDate = datetime.datetime.now()
20+
cls.today = None
21+
cls.now = None
22+
cls.timeOfDay = None
23+
cls.localTimezoneOffset = None
24+
25+
constants = Constants()
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from fhirpathpy.engine.invocations.constants import constants
2+
from fhirpathpy.engine.nodes import FP_DateTime, FP_Time, FP_Date
3+
4+
5+
def now(ctx, data):
6+
if not constants.now:
7+
now = constants.nowDate
8+
if not now.tzinfo:
9+
now = now.astimezone()
10+
isoStr = now.replace(microsecond=0).isoformat() # YYYY-MM-DDThh:mm:ss+zz:zz
11+
constants.now = FP_DateTime(isoStr).timeMatchData
12+
return constants.now
13+
14+
15+
def today(ctx, data):
16+
if not constants.today:
17+
now = constants.nowDate
18+
isoStr = now.date().isoformat() # YYYY-MM-DD
19+
constants.today = FP_Date(isoStr).timeMatchData
20+
return constants.today
21+
22+
23+
def timeOfDay(ctx, data):
24+
if not constants.timeOfDay:
25+
now = constants.nowDate
26+
isoStr = now.time().replace(microsecond=0).isoformat() # hh:mm:ss
27+
constants.timeOfDay = FP_Time(isoStr).timeMatchData
28+
return constants.timeOfDay

fhirpathpy/engine/nodes.py

+60-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import json
2+
import re
3+
4+
dateFormat = '([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])'
5+
dateRe = '%s)?)?' % dateFormat
6+
7+
timeRE = '([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?'
8+
9+
dateTimeRE = '%s(T%s(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?' % (dateFormat, timeRE)
210

311

412
class FP_Type:
@@ -89,37 +97,79 @@ def toString(self):
8997

9098

9199
# TODO
92-
class FP_TimeBase:
93-
pass
100+
class FP_TimeBase(FP_Type):
101+
102+
def __init__(self, timeStr):
103+
self.asStr = timeStr
104+
self.timeMatchData = None
105+
106+
def _getMatchData(self, regEx):
107+
if not self.timeMatchData:
108+
self.timeMatchData = re.match(regEx, self.asStr).group(0)
109+
return self.timeMatchData
94110

95111

96112
# TODO
97-
class FP_Time:
113+
class FP_DateTime(FP_TimeBase):
114+
115+
def __init__(self, timeStr):
116+
super(FP_DateTime, self).__init__(timeStr)
117+
self.timeMatchData = self._getMatchData()
118+
119+
def _getMatchData(self, regEx=dateTimeRE):
120+
return super(FP_DateTime, self)._getMatchData(regEx)
121+
98122
@staticmethod
99123
def check_string(value):
100124
"""
101125
Tests str to see if it is convertible to a DateTime.
102126
* @return If str is convertible to a DateTime, returns an FP_DateTime otherwise returns None
103127
"""
104-
d = FP_Time(value)
128+
d = FP_DateTime(value)
105129
if not d._getMatchData():
106130
return None
107131
return d
108132

109133

110134
# TODO
111-
class FP_DateTime:
112-
# TODO
113-
def _getMatchData(self, data):
114-
pass
135+
class FP_Date(FP_TimeBase):
136+
137+
def __init__(self, timeStr):
138+
super(FP_Date, self).__init__(timeStr)
139+
self.timeMatchData = self._getMatchData()
140+
141+
def _getMatchData(self, regEx=dateRe):
142+
return super(FP_Date, self)._getMatchData(regEx)
115143

116144
@staticmethod
117145
def check_string(value):
118146
"""
119147
Tests str to see if it is convertible to a DateTime.
120-
* @return If str is convertible to a DateTime, returns an FP_DateTime otherwise returns None
148+
* @return If str is convertible to a DateTime, returns an FP_Date otherwise returns None
121149
"""
122-
d = FP_DateTime(value)
150+
d = FP_Date(value)
151+
if not d._getMatchData():
152+
return None
153+
return d
154+
155+
156+
# TODO
157+
class FP_Time(FP_TimeBase):
158+
159+
def __init__(self, timeStr):
160+
super(FP_Time, self).__init__(timeStr)
161+
self.timeMatchData = self._getMatchData()
162+
163+
def _getMatchData(self, regEx=timeRE):
164+
return super(FP_Time, self)._getMatchData(regEx)
165+
166+
@staticmethod
167+
def check_string(value):
168+
"""
169+
Tests str to see if it is convertible to a DateTime.
170+
* @return If str is convertible to a DateTime, returns an FP_Time otherwise returns None
171+
"""
172+
d = FP_Time(value)
123173
if not d._getMatchData():
124174
return None
125175
return d

tests/cases/5.9_utility_functions.yaml

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
tests:
22
- desc: '5. Functions'
33
- desc: '5.8. Utility functions'
4+
45
- desc: '5.8.1. trace(name : string) : collection'
56
# Add a string representation of the input collection to the diagnostic log, using the parameter name as the name in the log. This log should be made available to the user in some appropriate fashion. Does not change the input, so returns the input collection as output.
67

@@ -9,21 +10,27 @@ tests:
910
disableConsoleLog: true
1011
result: [1, 2, 3, 4, 5, 6]
1112

12-
- desc: '5.8.2. today() : dateTime'
13+
- desc: '5.8.2. now() : dateTime'
14+
# Returns a dateTime containing the current date and time, including timezone.
15+
16+
- desc: '** TBD: now()'
17+
expression: now()
18+
result: ['2020-08-20T17:52:15+03:00']
19+
20+
- desc: '5.8.3. today() : dateTime'
1321
# Returns a dateTime containing the current date.
1422

1523
- desc: '** TBD: today()'
16-
# expression: Functions.today()
17-
# result: '1234'
18-
24+
expression: today()
25+
result: ['2020-08-20']
1926

20-
- desc: '5.8.3. now() : dateTime'
21-
# Returns a dateTime containing the current date and time, including timezone.
2227

23-
- desc: '** TBD: now()'
24-
# expression: now()
25-
# result: '10:10'
28+
- desc: '5.8.4 timeOfDay() : dateTime'
29+
# Returns a dateTime containing the current time.
2630

31+
- desc: '** TBD: timeOfDay()'
32+
expression: timeOfDay()
33+
result: ['17:52:15']
2734

2835
subject:
2936
coll:

tests/test_evaluators.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import datetime
12
import math
23
import pytest
34

45
from fhirpathpy import evaluate
6+
from fhirpathpy.engine.invocations.constants import constants
57

68

79
@pytest.mark.parametrize(
@@ -190,11 +192,15 @@ def existence_functions_test(resource, path, expected):
190192
({"a": False}, "a.toDecimal()", [0]),
191193
({"a": False}, "a.toString()", ["False"]),
192194
({"a": 101.99}, "a.toString()", ["101.99"]),
193-
# toDateTime
194-
# toTime
195+
({}, "now()", ["2020-08-20T17:52:15+03:00"]),
196+
({}, "today()", ["2020-08-20"]),
197+
({}, "timeOfDay()", ["17:52:15"]),
195198
],
196199
)
197200
def misc_functions_test(resource, path, expected):
201+
# Monkeypatching for Constants.nowDate
202+
tz = datetime.timezone(datetime.timedelta(seconds=10800))
203+
constants.nowDate = datetime.datetime(year=2020, month=8, day=20, hour=17, minute=52, second=15, tzinfo=tz)
198204
assert evaluate(resource, path) == expected
199205

200206

0 commit comments

Comments
 (0)