From f63479c823a590d73319bcea259a4d2d033ed2a9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 4 May 2021 12:52:14 +1000 Subject: [PATCH] Code review and use isodate library to handle durations for robustly --- src/blenderbim/Makefile | 7 ++ .../bim/module/sequence/operator.py | 7 +- .../ifcopenshell/util/date.py | 67 +++++++------------ 3 files changed, 35 insertions(+), 46 deletions(-) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 973b705fc2..4806b2e195 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -184,6 +184,13 @@ endif cp -r dist/working/python-dateutil-2.8.1/dateutil dist/blenderbim/libs/site/packages/ rm -rf dist/working + # Provides duration parsing for construction sequencing + mkdir dist/working + cd dist/working && wget https://files.pythonhosted.org/packages/b1/80/fb8c13a4cd38eb5021dc3741a9e588e4d1de88d895c1910c6fc8a08b7a70/isodate-0.6.0.tar.gz + cd dist/working && tar -xzvf isodate* + cp -r dist/working/isodate-0.6.0/src/isodate dist/blenderbim/libs/site/packages/ + rm -rf dist/working + # Provides jsgantt-improved supports for web-based construction sequencing gantt charts mkdir dist/working cd dist/working && wget https://raw.githubusercontent.com/jsGanttImproved/jsgantt-improved/master/dist/jsgantt.js diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index 4270d89a18..6b631bdb51 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -2,6 +2,7 @@ import os import bpy import json import time +import isodate import pystache import webbrowser import ifcopenshell.api @@ -302,7 +303,7 @@ class LoadTaskProperties(bpy.types.Operator): task_time = Data.task_times[task["TaskTime"]] item.start = self.canonicalise_time(task_time["ScheduleStart"]) item.finish = self.canonicalise_time(task_time["ScheduleFinish"]) - item.duration = str(task_time["ScheduleDuration"].days) if task_time["ScheduleDuration"] else "-" + item.duration = isodate.duration_isoformat(task_time["ScheduleDuration"]) if task_time["ScheduleDuration"] else "-" else: item.start = "-" item.finish = "-" @@ -431,7 +432,7 @@ class EnableEditingTaskTime(bpy.types.Operator): if data_type == "string": if isinstance(data[attribute.name()], datetime): new.string_value = "" if new.is_null else data[attribute.name()].isoformat() - elif isinstance(data[attribute.name()], timedelta): + elif isinstance(data[attribute.name()], isodate.Duration): new.string_value = "" if new.is_null else ifcopenshell.util.date.datetime2ifc(data[attribute.name()], "IfcDuration") else: new.string_value = "" if new.is_null else data[attribute.name()] @@ -500,7 +501,7 @@ class EditTaskTime(bpy.types.Operator): except: attributes[key] = None elif key == "ScheduleDuration": - attributes[key] = ifcopenshell.util.date.ifc2datetime(value) + attributes[key] = isodate.parse_duration(value) return attributes diff --git a/src/ifcopenshell-python/ifcopenshell/util/date.py b/src/ifcopenshell-python/ifcopenshell/util/date.py index f75370d348..ea5a06bb91 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/date.py +++ b/src/ifcopenshell-python/ifcopenshell/util/date.py @@ -1,50 +1,28 @@ import datetime from re import findall -# https://gist.github.com/spatialtime/c1924a3b178b4fe721fe406e0bf1a1dc -import re -from datetime import timedelta - - -def format_duration(td): - s = td.seconds - ms = td.microseconds - if ms != 0: # Round microseconds to milliseconds. - ms /= 1000000 - ms = round(ms,3) - s += ms - return "P{}DT{}S".format(td.days,s) - -def parse_duration(iso_duration): - m = re.match(r'^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:.\d+)?)S)?$', - iso_duration) - if m is None: - raise ValueError("invalid ISO 8601 duration string") - days = 0 - hours = 0 - minutes = 0 - seconds = 0.0 - if m[3]: - days = int(m[3]) - if m[4]: - hours = int(m[4]) - if m[5]: - minutes = int(m[5]) - if m[6]: - seconds = float(m[6]) - return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) - - -def duration2dict(duration): - results = {} - for number, unit in findall("(?P\d+)(?PS|M|H|D|W|Y)", duration): - results[unit] = number - return results +try: + import isodate +except: + pass # Duration parsing not supported def ifc2datetime(element): if isinstance(element, str) and element[0] == "P": # IfcDuration - return parse_duration(element) + duration = isodate.parse_duration(element) + if isinstance(duration, datetime.timedelta): + components = { + "days": getattr(duration, "days", 0), + "hours": 0, + "minutes": 0, + "seconds": getattr(duration, "seconds", 0), + } + if components["seconds"]: + components["hours"], components["minutes"], components["seconds"] = [ + int(i) for i in str(datetime.timedelta(seconds=components["seconds"])).split(":") + ] + duration = isodate.Duration(**components) + return duration elif isinstance(element, str) and element[2] == ":": # IfcTime return datetime.time.fromisoformat(element) elif isinstance(element, str) and ":" in element: # IfcDateTime @@ -72,11 +50,14 @@ def ifc2datetime(element): def datetime2ifc(dt, ifc_type): - if isinstance(dt, str) and ifc_type != "IfcDuration": + if isinstance(dt, str): + if ifc_type == "IfcDuration": + return dt dt = datetime.datetime.fromisoformat(dt) + if ifc_type == "IfcDuration": - return format_duration(dt) - if ifc_type == "IfcTimeStamp": + return isodate.duration_isoformat(dt) + elif ifc_type == "IfcTimeStamp": return int(dt.timestamp()) elif ifc_type == "IfcDateTime": if isinstance(dt, datetime.datetime):