1"""
2This module provides an ISO 8601:2004 duration parser.
3
4It also provides a wrapper to strftime. This wrapper makes it easier to
5format timedelta or Duration instances as ISO conforming strings.
6"""
7
8import re
9from datetime import timedelta
10from decimal import Decimal
11
12from isodate.duration import Duration
13from isodate.isodatetime import parse_datetime
14from isodate.isoerror import ISO8601Error
15from isodate.isostrf import D_DEFAULT, strftime
16
17ISO8601_PERIOD_REGEX = re.compile(
18 r"^(?P<sign>[+-])?"
19 r"P(?!\b)"
20 r"(?P<years>[0-9]+([,.][0-9]+)?Y)?"
21 r"(?P<months>[0-9]+([,.][0-9]+)?M)?"
22 r"(?P<weeks>[0-9]+([,.][0-9]+)?W)?"
23 r"(?P<days>[0-9]+([,.][0-9]+)?D)?"
24 r"((?P<separator>T)(?P<hours>[0-9]+([,.][0-9]+)?H)?"
25 r"(?P<minutes>[0-9]+([,.][0-9]+)?M)?"
26 r"(?P<seconds>[0-9]+([,.][0-9]+)?S)?)?$"
27)
28# regular expression to parse ISO duration strings.
29
30
31def parse_duration(datestring, as_timedelta_if_possible=True):
32 """
33 Parses an ISO 8601 durations into datetime.timedelta or Duration objects.
34
35 If the ISO date string does not contain years or months, a timedelta
36 instance is returned, else a Duration instance is returned.
37
38 The following duration formats are supported:
39 -PnnW duration in weeks
40 -PnnYnnMnnDTnnHnnMnnS complete duration specification
41 -PYYYYMMDDThhmmss basic alternative complete date format
42 -PYYYY-MM-DDThh:mm:ss extended alternative complete date format
43 -PYYYYDDDThhmmss basic alternative ordinal date format
44 -PYYYY-DDDThh:mm:ss extended alternative ordinal date format
45
46 The '-' is optional.
47
48 Limitations: ISO standard defines some restrictions about where to use
49 fractional numbers and which component and format combinations are
50 allowed. This parser implementation ignores all those restrictions and
51 returns something when it is able to find all necessary components.
52 In detail:
53 it does not check, whether only the last component has fractions.
54 it allows weeks specified with all other combinations
55
56 The alternative format does not support durations with years, months or
57 days set to 0.
58 """
59 if not isinstance(datestring, str):
60 raise TypeError("Expecting a string %r" % datestring)
61 match = ISO8601_PERIOD_REGEX.match(datestring)
62 if not match:
63 # try alternative format:
64 if datestring.startswith("P"):
65 durdt = parse_datetime(datestring[1:])
66 if as_timedelta_if_possible and durdt.year == 0 and durdt.month == 0:
67 # FIXME: currently not possible in alternative format
68 # create timedelta
69 ret = timedelta(
70 days=durdt.day,
71 seconds=durdt.second,
72 microseconds=durdt.microsecond,
73 minutes=durdt.minute,
74 hours=durdt.hour,
75 )
76 else:
77 # create Duration
78 ret = Duration(
79 days=durdt.day,
80 seconds=durdt.second,
81 microseconds=durdt.microsecond,
82 minutes=durdt.minute,
83 hours=durdt.hour,
84 months=durdt.month,
85 years=durdt.year,
86 )
87 return ret
88 raise ISO8601Error("Unable to parse duration string %r" % datestring)
89 groups = match.groupdict()
90 for key, val in groups.items():
91 if key not in ("separator", "sign"):
92 if val is None:
93 groups[key] = "0n"
94 # print groups[key]
95 if key in ("years", "months"):
96 groups[key] = Decimal(groups[key][:-1].replace(",", "."))
97 else:
98 # these values are passed into a timedelta object,
99 # which works with floats.
100 groups[key] = float(groups[key][:-1].replace(",", "."))
101 if as_timedelta_if_possible and groups["years"] == 0 and groups["months"] == 0:
102 ret = timedelta(
103 days=groups["days"],
104 hours=groups["hours"],
105 minutes=groups["minutes"],
106 seconds=groups["seconds"],
107 weeks=groups["weeks"],
108 )
109 if groups["sign"] == "-":
110 ret = timedelta(0) - ret
111 else:
112 ret = Duration(
113 years=groups["years"],
114 months=groups["months"],
115 days=groups["days"],
116 hours=groups["hours"],
117 minutes=groups["minutes"],
118 seconds=groups["seconds"],
119 weeks=groups["weeks"],
120 )
121 if groups["sign"] == "-":
122 ret = Duration(0) - ret
123 return ret
124
125
126def duration_isoformat(tduration, format=D_DEFAULT):
127 """
128 Format duration strings.
129
130 This method is just a wrapper around isodate.isostrf.strftime and uses
131 P%P (D_DEFAULT) as default format.
132 """
133 # TODO: implement better decision for negative Durations.
134 # should be done in Duration class in consistent way with timedelta.
135 if (
136 isinstance(tduration, Duration)
137 and (
138 tduration.years < 0
139 or tduration.months < 0
140 or tduration.tdelta < timedelta(0)
141 )
142 ) or (isinstance(tduration, timedelta) and (tduration < timedelta(0))):
143 ret = "-"
144 else:
145 ret = ""
146 ret += strftime(tduration, format)
147 return ret