Skip to content

Commit

Permalink
date, time, and datetime literal updates to match DM-WG
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelBender committed Apr 22, 2024
1 parent 9d31f6e commit 1420915
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 27 deletions.
2 changes: 1 addition & 1 deletion bacpypes3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# Project Metadata
#

__version__ = "0.0.93"
__version__ = "0.0.94"
__author__ = "Joel Bender"
__email__ = "[email protected]"

Expand Down
79 changes: 79 additions & 0 deletions bacpypes3/basetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import struct
import time
import datetime

from typing import (
Union,
Expand Down Expand Up @@ -1845,6 +1846,35 @@ class DateTime(Sequence):
date = Date
time = Time

def __init__(
self,
arg: Optional[datetime.datetime] = None,
**kwargs,
) -> None:
if arg is None:
pass
elif isinstance(arg, DateTime):
kwargs["date"] = arg.date
kwargs["time"] = arg.time
elif isinstance(arg, datetime.datetime):
date_value = (
arg.year - 1900,
arg.month,
arg.day,
arg.weekday() + 1,
)
time_value = (
arg.hour,
arg.minute,
arg.second,
arg.microsecond // 10000,
)

kwargs["date"] = date_value
kwargs["time"] = time_value

super().__init__(**kwargs)

@classmethod
def now(cls: type, when: Optional[float] = None) -> DateTime:
"""
Expand All @@ -1856,6 +1886,55 @@ def now(cls: type, when: Optional[float] = None) -> DateTime:
# return an instance
return cast(DateTime, cls(date=Date.now(when), time=Time.now(when)))

@property
def is_special(self) -> bool:
return self.date.is_special or self.time.is_special

@property
def datetime(self) -> datetime.datetime:
"""Return the value as a datetime.datetime object."""
if self.is_special:
raise ValueError("datetime contains special values")

year, month, day, day_of_week = self.date
hour, minute, second, hundredth = self.time
return datetime.datetime(
year + 1900, month, day, hour, minute, second, hundredth * 10000
)

def isoformat(self) -> str:
return self.datetime.isoformat()

@classmethod
def fromisoformat(cls: type, datetime_string: str) -> Date:
datetime_value = datetime.datetime.fromisoformat(datetime_string)

date_value = (
datetime_value.year - 1900,
datetime_value.month,
datetime_value.day,
datetime_value.weekday() + 1,
)
time_value = (
datetime_value.hour,
datetime_value.minute,
datetime_value.second,
datetime_value.microsecond // 10000,
)

# return an instance
return cast(Date, cls(date=date_value, time=time_value))

@classmethod
def cast(cls, arg):
if _debug:
HostAddress._debug("DeviceAddress.cast %r", arg)

if isinstance(arg, (DateTime, datetime.datetime)):
return arg
else:
raise TypeError(type(arg))

def __str__(self) -> str:
return f"{self.date} {self.time}"

Expand Down
59 changes: 58 additions & 1 deletion bacpypes3/primitivedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2143,7 +2143,9 @@ def cast(cls: type, arg: _Any) -> _Any:
day_of_week = 255
elif not day_of_week:
try:
day_of_week = datetime.datetime(year + 1900, month, day).weekday() + 1
day_of_week = (
datetime.datetime(year + 1900, month, day).weekday() + 1
)
except OverflowError:
pass

Expand Down Expand Up @@ -2183,6 +2185,34 @@ def is_special(self) -> bool:
or (day_of_week == 255)
)

@property
def date(self) -> datetime.date:
"""Return the value as a datetime.date object."""
if self.is_special:
raise ValueError("date contains special values")

# rip it apart
year, month, day, day_of_week = self

return datetime.date(year + 1900, month, day)

def isoformat(self) -> str:
return self.date.isoformat()

@classmethod
def fromisoformat(cls: type, date_string: str) -> Date:
date_value = datetime.date.fromisoformat(date_string)

value = (
date_value.year - 1900,
date_value.month,
date_value.day,
date_value.weekday() + 1,
)

# return an instance
return cast(Date, cls(value))

def __str__(self) -> str:
"""String representation of the date."""
# rip it apart
Expand Down Expand Up @@ -2338,6 +2368,33 @@ def is_special(self) -> bool:

return (hour == 255) or (minute == 255) or (second == 255) or (hundredth == 255)

@property
def time(self) -> datetime.time:
"""Return the value as a datetime.time object."""
if self.is_special:
raise ValueError("time contains special values")
# rip it apart
hour, minute, second, hundredth = self

return datetime.time(hour, minute, second, hundredth * 10000)

def isoformat(self) -> str:
return self.time.isoformat()

@classmethod
def fromisoformat(cls: type, time_string: str) -> Time:
time_value = datetime.time.fromisoformat(time_string)

value = (
time_value.hour,
time_value.minute,
time_value.second,
time_value.microsecond // 10000,
)

# return an instance
return cast(Time, cls(value))

def __str__(self) -> str:
# rip it apart
hour, minute, second, hundredth = self
Expand Down
58 changes: 33 additions & 25 deletions bacpypes3/rdf/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,15 +286,14 @@ def enumerated_decode(graph: Graph, value, class_):

def date_encode(graph: Graph, value):
if value.is_special:
return Literal(str(value), datatype=BACnetNS.Date)
return Literal(str(value), datatype=BACnetNS.datePattern)
else:
date_string = "{:04d}-{:02d}-{:02d}".format(value[0] + 1900, value[1], value[2])
return Literal(date_string, datatype=XSD.date, normalize=False)
return Literal(value.date)


def date_decode(graph: Graph, value):
assert isinstance(value, Literal)
if value.datatype == BACnetNS.Date:
if value.datatype == BACnetNS.datePattern:
return Date(str(value))
elif value.datatype == XSD.date:
return Date(value.value)
Expand All @@ -303,16 +302,15 @@ def date_decode(graph: Graph, value):


def time_encode(graph: Graph, value):
time_string = str(value)
if value.is_special:
return Literal(time_string, datatype=BACnetNS.Time)
return Literal(str(value), datatype=BACnetNS.timePattern)
else:
return Literal(time_string, datatype=XSD.time, normalize=False)
return Literal(value.time) # normalize=False?


def time_decode(graph: Graph, value):
assert isinstance(value, Literal)
if value.datatype == BACnetNS.Time:
if value.datatype == BACnetNS.timePattern:
return Time(str(value))
elif value.datatype == XSD.time:
return Time(value.value)
Expand All @@ -333,6 +331,23 @@ def objectidentifier_decode(graph: Graph, value):
return ObjectIdentifier(str(value))


def datetime_encode(graph: Graph, value):
if value.is_special:
return Literal(str(value), datatype=BACnetNS.dateTimePattern)
else:
return Literal(value.datetime) # normalize=False?


def datetime_decode(graph: Graph, value):
assert isinstance(value, Literal)
if value.datatype == BACnetNS.dateTimePattern:
return DateTime(str(value))
elif value.datatype == XSD.dateTime:
return DateTime(value.value)
else:
raise TypeError(value.datatype)


#
#
#
Expand Down Expand Up @@ -365,6 +380,8 @@ def atomic_encode(graph: Graph, value) -> Literal:
literal = time_encode(graph, value)
elif isinstance(value, ObjectIdentifier):
literal = objectidentifier_encode(graph, value)
elif isinstance(value, DateTime):
literal = datetime_encode(graph, value)
else:
raise TypeError("atomic element expected: " + str(type(value)))

Expand Down Expand Up @@ -398,6 +415,8 @@ def atomic_decode(graph: Graph, literal, class_) -> Atomic:
value = time_decode(graph, literal)
elif issubclass(class_, ObjectIdentifier):
value = objectidentifier_decode(graph, literal)
elif issubclass(class_, DateTime):
value = datetime_decode(graph, literal)
else:
raise TypeError("not an atomic element")

Expand Down Expand Up @@ -433,6 +452,8 @@ def sequence_to_graph(

if isinstance(value, Atomic):
literal = atomic_encode(graph, value)
elif isinstance(value, DateTime):
literal = datetime_encode(graph, value)
elif isinstance(value, Sequence):
literal = BNode()
sequence_to_graph(value, literal, graph)
Expand All @@ -443,9 +464,6 @@ def sequence_to_graph(
value = value.get_value()
if isinstance(value, Atomic):
literal = atomic_encode(graph, value)
elif isinstance(value, DateTime):
literal = BNode()
sequence_to_graph(value, literal, graph)
else:
raise TypeError(value)

Expand Down Expand Up @@ -476,25 +494,15 @@ def graph_to_sequence(graph: Graph, node: URIRef, seq_class: type) -> Sequence:

if issubclass(element, Atomic):
value = atomic_decode(graph, literal, element)
elif issubclass(element, DateTime):
value = datetime_decode(graph, literal)
elif issubclass(element, Sequence):
value = graph_to_sequence(graph, literal, element)
elif issubclass(element, ExtendedList):
value = graph_to_extendedlist(graph, node, attr_to_predicate(attr), element)
elif issubclass(element, (AnyAtomic, AnyAtomicExtended)):
if literal == BACnetNS.Null:
value = Null(())

elif isinstance(literal, (BNode, URIRef)):
date_literal = graph.value(subject=literal, predicate=BACnetNS["date"])
time_literal = graph.value(subject=literal, predicate=BACnetNS["time"])
if (date_literal is not None) and (time_literal is not None):
value = DateTime(
date=date_decode(graph, date_literal),
time=time_decode(graph, time_literal),
)
else:
raise ValueError("DateTime expected")

elif isinstance(literal, Literal):
if literal.datatype == XSD.boolean:
value = boolean_decode(graph, literal)
Expand All @@ -514,9 +522,9 @@ def graph_to_sequence(graph: Graph, node: URIRef, seq_class: type) -> Sequence:
value = characterstring_decode(graph, literal)
elif literal.datatype == BACnetNS.BitString:
value = bitstring_decode(graph, literal)
elif literal.datatype in (BACnetNS.Date, XSD.date):
elif literal.datatype in (BACnetNS.datePattern, XSD.date):
value = date_decode(graph, literal)
elif literal.datatype in (BACnetNS.Time, XSD.time):
elif literal.datatype in (BACnetNS.timePattern, XSD.time):
value = time_decode(graph, literal)
elif literal.datatype == BACnetNS.ObjectIdentifier:
value = objectidentifier_decode(graph, literal)
Expand Down
38 changes: 38 additions & 0 deletions tests/test_primitive_data/test_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

from bacpypes3.debugging import bacpypes_debugging, ModuleLogger, xtob
from bacpypes3.primitivedata import Tag, TagClass, TagNumber, TagList, Date
from bacpypes3.rdf.util import (
date_encode as rdf_date_encode,
date_decode as rdf_date_decode,
)

# some debugging
_debug = 0
Expand Down Expand Up @@ -141,3 +145,37 @@ def test_date_endec(self):
TestDate._debug("test_date_endec")

date_endec((1, 2, 3, 4), "01020304")

def test_date_isoformat(self):
if _debug:
TestDate._debug("test_date_isoformat")

obj1 = Date("1901-2-3")
obj1_string = obj1.isoformat()
if _debug:
TestDate._debug(" - obj1_string: %r", obj1_string)

obj2 = Date.fromisoformat(obj1_string)
assert obj1 == obj2

def test_time_literal(self):
if _debug:
TestDate._debug("test_time_literal")

# specific date
obj1 = Date("1901-2-3")
obj1_literal = rdf_date_encode(None, obj1)
if _debug:
TestDate._debug(" - obj1_literal: %r", obj1_literal)

obj2 = rdf_date_decode(None, obj1_literal)
assert obj1 == obj2

# date pattern
obj3 = Date("1901-*-*")
obj3_literal = rdf_date_encode(None, obj3)
if _debug:
TestDate._debug(" - obj3_literal: %r", obj3_literal)

obj4 = rdf_date_decode(None, obj3_literal)
assert obj3 == obj4
Loading

0 comments on commit 1420915

Please sign in to comment.