diff --git a/metomi/isodatetime/data.py b/metomi/isodatetime/data.py index a4b9c2b..0db5db2 100644 --- a/metomi/isodatetime/data.py +++ b/metomi/isodatetime/data.py @@ -1112,28 +1112,49 @@ def num_expanded_year_digits(self): return self._num_expanded_year_digits def year(self): return self._year @property - def month_of_year(self): return self._month_of_year + def month_of_year(self): + if self._month_of_year is None: + return self.get_calendar_date()[1] + return self._month_of_year @property - def week_of_year(self): return self._week_of_year + def week_of_year(self): + if self._week_of_year is None: + return self.get_week_date()[1] + return self._week_of_year @property - def day_of_year(self): return self._day_of_year + def day_of_year(self): + if self._day_of_year is None: + return self.get_ordinal_date()[1] + return self._day_of_year @property - def day_of_month(self): return self._day_of_month + def day_of_month(self): + if self._day_of_month is None: + return self.get_calendar_date()[2] + return self._day_of_month @property - def day_of_week(self): return self._day_of_week + def day_of_week(self): + if self._day_of_week is None: + return self.get_week_date()[2] + return self._day_of_week @property def hour_of_day(self): return self._hour_of_day @property - def minute_of_hour(self): return self._minute_of_hour + def minute_of_hour(self): + if self._minute_of_hour is None: + return self.get_hour_minute_second()[1] + return self._minute_of_hour @property - def second_of_minute(self): return self._second_of_minute + def second_of_minute(self): + if self._second_of_minute is None: + return self.get_hour_minute_second()[2] + return self._second_of_minute @property def time_zone(self): return self._time_zone @@ -1226,6 +1247,18 @@ def year_of_decade(self): return abs(self._year) % 10 def decade_of_century(self): return (abs(self._year) % 100 - abs(self._year) % 10) // 10 + @property + def hour_of_day_decimal_string(self): + return self._decimal_string("hour_of_day") + + @property + def minute_of_hour_decimal_string(self): + return self._decimal_string("minute_of_hour") + + @property + def second_of_minute_decimal_string(self): + return self._decimal_string("second_of_minute") + @property def time_zone_minute_abs(self): return abs(self._time_zone._minutes) @@ -1247,58 +1280,22 @@ def seconds_since_unix_epoch(self): return str(int(CALENDAR.SECONDS_IN_DAY * days + seconds)) def get(self, property_name): - """Return a calculated value for property name.""" - if property_name == "month_of_year": - if self._month_of_year is not None: - return self._month_of_year - return self.get_calendar_date()[1] - if property_name == "day_of_year": - if self._day_of_year is not None: - return self._day_of_year - return self.get_ordinal_date()[1] - if property_name == "day_of_month": - if self._day_of_month is not None: - return self._day_of_month - return self.get_calendar_date()[2] - if property_name == "week_of_year": - if self._week_of_year is not None: - return self._week_of_year - return self.get_week_date()[1] - if property_name == "day_of_week": - if self._day_of_week is not None: - return self._day_of_week - return self.get_week_date()[2] - if property_name == "minute_of_hour": - if self._minute_of_hour is None: - return self.get_hour_minute_second()[1] - return int(self._minute_of_hour) - if property_name == "hour_of_day": - return int(self._hour_of_day) - if property_name == "hour_of_day_decimal_string": - string = "%f" % (float(self._hour_of_day) - int(self._hour_of_day)) - string = string.replace("0.", "", 1).rstrip("0") - if not string: - return "0" - return string - if property_name == "minute_of_hour_decimal_string": - string = "%f" % (float(self._minute_of_hour) - - int(self._minute_of_hour)) - string = string.replace("0.", "", 1).rstrip("0") - if not string: - return "0" - return string - if property_name == "second_of_minute": - if self._second_of_minute is None: - return self.get_hour_minute_second()[2] - return int(self._second_of_minute) - if property_name == "second_of_minute_decimal_string": # FIXME: #184 - string = "%f" % (float(self._second_of_minute) - - int(self._second_of_minute)) - string = string.replace("0.", "", 1).rstrip("0") - if not string: - return "0" - return string - raise NotImplementedError(property_name) + """Obsolete method for returning calculated value for property name.""" + raise NotImplementedError( + "The method TimePoint.get('{0}') is obsolete; use TimePoint.{0} " + "or getattr() instead".format(property_name)) + # return getattr(self, property_name) + + def _decimal_string(self, attr): + """Return the decimal digits (after the '.') of the specified attribute + as a string. Rounds to 6 d.p.""" + string = "%f" % ( + float(getattr(self, attr)) - int(getattr(self, attr))) + # Note: 0.9999999 rounds up to 1.0; this is handled in TimePointDumper + string = string.split(".", 1)[1].rstrip("0") + if not string: + return "0" + return string def get_second_of_day(self): """Return the seconds elapsed since the start of the day.""" @@ -1447,7 +1444,7 @@ def get_truncated_properties(self): for attr in ["month_of_year", "week_of_year", "day_of_year", "day_of_month", "day_of_week", "hour_of_day", "minute_of_hour", "second_of_minute"]: - value = getattr(self, attr) + value = getattr(self, "_{0}".format(attr)) if value is not None: props.update({attr: value}) return props diff --git a/metomi/isodatetime/dumpers.py b/metomi/isodatetime/dumpers.py index 80e1850..58ff9fc 100644 --- a/metomi/isodatetime/dumpers.py +++ b/metomi/isodatetime/dumpers.py @@ -61,6 +61,8 @@ class TimePointDumper(object): """ + FLOAT_DECIMAL_PLACES = 6 + def __init__(self, num_expanded_year_digits=2): self._timepoint_parser = None self._rec_formats = {"date": [], "time": [], "time_zone": []} @@ -144,7 +146,7 @@ def _dump_expression_with_properties(self, timepoint, expression, timepoint = timepoint.to_time_zone(new_time_zone) property_map = {} for property_ in properties: - property_map[property_] = timepoint.get(property_) + property_map[property_] = getattr(timepoint, property_) if (property_ == "century" and ("expanded_year_digits" not in properties or not self.num_expanded_year_digits)): @@ -153,6 +155,15 @@ def _dump_expression_with_properties(self, timepoint, expression, elif property_ == "expanded_year_digits": max_value = (10 ** (self.num_expanded_year_digits + 4)) - 1 min_value = -max_value + elif property_ in ("hour_of_day", "minute_of_hour", + "second_of_minute"): + # Handle cases where decimal is rounded up + prop_decimal_str = "{0}_decimal_string".format(property_) + if prop_decimal_str in properties: + fp_value = property_map[property_] + rounded = int(round(fp_value, self.FLOAT_DECIMAL_PLACES)) + property_map[property_] = rounded + continue else: continue value = timepoint.year diff --git a/metomi/isodatetime/parsers.py b/metomi/isodatetime/parsers.py index acae7de..7005e31 100644 --- a/metomi/isodatetime/parsers.py +++ b/metomi/isodatetime/parsers.py @@ -590,17 +590,17 @@ def parse(self, expression): if timepoint.get_is_week_date(): raise ISO8601SyntaxError("duration", expression) result_map = {} - result_map["years"] = timepoint.year + result_map["years"] = timepoint._year if timepoint.get_is_calendar_date(): - result_map["months"] = timepoint.month_of_year - result_map["days"] = timepoint.day_of_month + result_map["months"] = timepoint._month_of_year + result_map["days"] = timepoint._day_of_month if timepoint.get_is_ordinal_date(): - result_map["days"] = timepoint.day_of_year - result_map["hours"] = timepoint.hour_of_day - if timepoint.minute_of_hour is not None: - result_map["minutes"] = timepoint.minute_of_hour - if timepoint.second_of_minute is not None: - result_map["seconds"] = timepoint.second_of_minute + result_map["days"] = timepoint._day_of_year + result_map["hours"] = timepoint._hour_of_day + if timepoint._minute_of_hour is not None: + result_map["minutes"] = timepoint._minute_of_hour + if timepoint._second_of_minute is not None: + result_map["seconds"] = timepoint._second_of_minute return data.Duration(**result_map) raise ISO8601SyntaxError("duration", expression)