Skip to content

Commit

Permalink
Literal datetime sub (#1870)
Browse files Browse the repository at this point in the history
rebased PR #1089 from 2 yrs ago, adding DateTime arithmetic operations for Literals - an extension of #629, additional tests of numerics added, as requested in #1089 (comment) and limited modernisation of `test/test_literal/test_literal.py`

Co-authored-by: Iwan Aucamp <aucampia@gmail.com>
  • Loading branch information
Graham Higgins and aucampia committed May 16, 2022
1 parent 8a096ab commit 06a4395
Show file tree
Hide file tree
Showing 5 changed files with 828 additions and 40 deletions.
171 changes: 171 additions & 0 deletions rdflib/term.py
Expand Up @@ -736,10 +736,22 @@ def __setstate__(self, arg: Tuple[Any, Dict[str, str]]) -> None:

def __add__(self, val: Any) -> "Literal":
"""
>>> from rdflib.namespace import XSD
>>> Literal(1) + 1
rdflib.term.Literal(u'2', datatype=rdflib.term.URIRef(u'http://www.w3.org/2001/XMLSchema#integer'))
>>> Literal("1") + "1"
rdflib.term.Literal(u'11')
# Handling dateTime/date/time based operations in Literals
>>> a = Literal('2006-01-01T20:50:00', datatype=XSD.dateTime)
>>> b = Literal('P31D', datatype=XSD.duration)
>>> (a + b)
rdflib.term.Literal('2006-02-01T20:50:00', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#dateTime'))
>>> from rdflib.namespace import XSD
>>> a = Literal('2006-07-01T20:52:00', datatype=XSD.dateTime)
>>> b = Literal('P122DT15H58M', datatype=XSD.duration)
>>> (a + b)
rdflib.term.Literal('2006-11-01T12:50:00', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#dateTime'))
"""

# if no val is supplied, return this Literal
Expand All @@ -750,6 +762,45 @@ def __add__(self, val: Any) -> "Literal":
if not isinstance(val, Literal):
val = Literal(val)

# if self is datetime based and value is duration
if (
self.datatype in (_XSD_DATETIME, _XSD_DATE)
and val.datatype in _TIME_DELTA_TYPES
):
date1: Union[datetime, date] = self.toPython()
duration: Union[Duration, timedelta] = val.toPython()
difference = date1 + duration
return Literal(difference, datatype=self.datatype)

# if self is time based and value is duration
elif self.datatype == _XSD_TIME and val.datatype in _TIME_DELTA_TYPES:
selfv: time = self.toPython()
valv: Union[Duration, timedelta] = val.toPython()
sdt = datetime.combine(date(2000, 1, 1), selfv) + valv
return Literal(sdt.time(), datatype=self.datatype)

# if self is datetime based and value is not or vice versa
elif (
(
self.datatype in _ALL_DATE_AND_TIME_TYPES
and val.datatype not in _ALL_DATE_AND_TIME_TYPES
)
or (
self.datatype not in _ALL_DATE_AND_TIME_TYPES
and val.datatype in _ALL_DATE_AND_TIME_TYPES
)
or (
self.datatype in _TIME_DELTA_TYPES
and (
(val.datatype not in _TIME_DELTA_TYPES)
or (self.datatype != val.datatype)
)
)
):
raise TypeError(
f"Cannot add a Literal of datatype {str(val.datatype)} to a Literal of datatype {str(self.datatype)}"
)

# if the datatypes are the same, just add the Python values and convert back
if self.datatype == val.datatype:
return Literal(
Expand Down Expand Up @@ -788,6 +839,109 @@ def __add__(self, val: Any) -> "Literal":

return Literal(s, self.language, datatype=new_datatype)

def __sub__(self, val: Any) -> "Literal":
"""
>>> from rdflib.namespace import XSD
>>> Literal(2) - 1
rdflib.term.Literal('1', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#integer'))
>>> Literal(1.1) - 1.0
rdflib.term.Literal('0.10000000000000009', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#double'))
>>> Literal(1.1) - 1
rdflib.term.Literal('0.1', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#decimal'))
>>> Literal(1.1, datatype=XSD.float) - Literal(1.0, datatype=XSD.float)
rdflib.term.Literal('0.10000000000000009', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#float'))
>>> Literal("1.1") - 1.0 # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: Not a number; rdflib.term.Literal('1.1')
>>> Literal(1.1, datatype=XSD.integer) - Literal(1.0, datatype=XSD.integer)
rdflib.term.Literal('0.10000000000000009', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#integer'))
# Handling dateTime/date/time based operations in Literals
>>> a = Literal('2006-01-01T20:50:00', datatype=XSD.dateTime)
>>> b = Literal('2006-02-01T20:50:00', datatype=XSD.dateTime)
>>> (b - a)
rdflib.term.Literal('P31D', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#duration'))
>>> from rdflib.namespace import XSD
>>> a = Literal('2006-07-01T20:52:00', datatype=XSD.dateTime)
>>> b = Literal('2006-11-01T12:50:00', datatype=XSD.dateTime)
>>> (a - b)
rdflib.term.Literal('-P122DT15H58M', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#duration'))
>>> (b - a)
rdflib.term.Literal('P122DT15H58M', datatype=rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#duration'))
"""
# if no val is supplied, return this Literal
if val is None:
return self

# convert the val to a Literal, if it isn't already one
if not isinstance(val, Literal):
val = Literal(val)

if not getattr(self, "datatype"):
raise TypeError(
"Minuend Literal must have Numeric, Date, Datetime or Time datatype."
)
elif not getattr(val, "datatype"):
raise TypeError(
"Subtrahend Literal must have Numeric, Date, Datetime or Time datatype."
)

if (
self.datatype in (_XSD_DATETIME, _XSD_DATE)
and val.datatype in _TIME_DELTA_TYPES
):
date1: Union[datetime, date] = self.toPython()
duration: Union[Duration, timedelta] = val.toPython()
difference = date1 - duration
return Literal(difference, datatype=self.datatype)

# if self is time based and value is duration
elif self.datatype == _XSD_TIME and val.datatype in _TIME_DELTA_TYPES:
selfv: time = self.toPython()
valv: Union[Duration, timedelta] = val.toPython()
sdt = datetime.combine(date(2000, 1, 1), selfv) - valv
return Literal(sdt.time(), datatype=self.datatype)

# if the datatypes are the same, just subtract the Python values and convert back
if self.datatype == val.datatype:
if self.datatype == _XSD_TIME:
sdt = datetime.combine(date.today(), self.toPython())
vdt = datetime.combine(date.today(), val.toPython())
return Literal(sdt - vdt, datatype=_XSD_DURATION)
else:
return Literal(
self.toPython() - val.toPython(),
self.language,
datatype=_XSD_DURATION
if self.datatype in (_XSD_DATETIME, _XSD_DATE, _XSD_TIME)
else self.datatype,
)

# if the datatypes are not the same but are both numeric, subtract the Python values and strip off decimal junk
# (i.e. tiny numbers (more than 17 decimal places) and trailing zeros) and return as a decimal
elif (
self.datatype in _NUMERIC_LITERAL_TYPES
and val.datatype in _NUMERIC_LITERAL_TYPES
):
return Literal(
Decimal(
(
"%f"
% round(Decimal(self.toPython()) - Decimal(val.toPython()), 15)
)
.rstrip("0")
.rstrip(".")
),
datatype=_XSD_DECIMAL,
)
# in all other cases, perform string concatenation
else:
raise TypeError(
f"Cannot subtract a Literal of datatype {str(val.datatype)} from a Literal of datatype {str(self.datatype)}"
)

def __bool__(self) -> bool:
"""
Is the Literal "True"
Expand Down Expand Up @@ -1695,6 +1849,23 @@ def _well_formed_negative_integer(lexical: Union[str, bytes], value: Any) -> boo
_XSD_DECIMAL,
)

# these need dedicated operators
_DATE_AND_TIME_TYPES: Tuple[URIRef, ...] = (
_XSD_DATETIME,
_XSD_DATE,
_XSD_TIME,
)

# These are recognized datatype IRIs
# (https://www.w3.org/TR/rdf11-concepts/#dfn-recognized-datatype-iris) that
# represents durations.
_TIME_DELTA_TYPES: Tuple[URIRef, ...] = (
_XSD_DURATION,
_XSD_DAYTIMEDURATION,
)

_ALL_DATE_AND_TIME_TYPES: Tuple[URIRef, ...] = _DATE_AND_TIME_TYPES + _TIME_DELTA_TYPES

# the following types need special treatment for reasonable sorting because
# certain instances can't be compared to each other. We treat this by
# partitioning and then sorting within those partitions.
Expand Down
10 changes: 10 additions & 0 deletions test/test_literal/test_duration.py
Expand Up @@ -36,3 +36,13 @@ def test_duration_sum(self):
assert Literal("P1Y2M4DT5H6M7S", datatype=XSD.duration) + Literal(
"P1Y2M4DT5H6M7S", datatype=XSD.duration
).toPython() == Literal("P2Y4M8DT10H12M14S", datatype=XSD.duration)

def test_duration_sub_pos(self):
assert Literal("P1Y2M4DT5H6M7S", datatype=XSD.duration) - Literal(
"P1Y2M3DT4H7M8S", datatype=XSD.duration
).toPython() == Literal("P1DT58M59S", datatype=XSD.duration)

def test_duration_sub_neg(self):
assert Literal("P1Y2M3DT4H7M8S", datatype=XSD.duration) - Literal(
"P1Y2M4DT5H6M7S", datatype=XSD.duration
).toPython() == Literal("-P1DT58M59S", datatype=XSD.duration)

0 comments on commit 06a4395

Please sign in to comment.