Skip to content

Commit 26a749b

Browse files
committed
Fixes parsing of some ISO 8601 strings.
Fixes #84
1 parent 20e7655 commit 26a749b

File tree

3 files changed

+181
-19
lines changed

3 files changed

+181
-19
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Change Log
22

3+
## [Unreleased]
4+
5+
### Fixed
6+
7+
- Fixed parsing of some ISO 8601 strings.
8+
9+
310
## [0.7.0] - 2016-12-07
411

512
### Added
@@ -272,6 +279,7 @@ This version causes major breaking API changes to simplify it and making it more
272279
Initial release
273280

274281

282+
[Unreleased]: https://github.com/sdispater/pendulum/compare/0.7.0...master
275283
[0.7.0]: https://github.com/sdispater/pendulum/releases/tag/0.7.0
276284
[0.6.6]: https://github.com/sdispater/pendulum/releases/tag/0.6.6
277285
[0.6.5]: https://github.com/sdispater/pendulum/releases/tag/0.6.5

pendulum/parsing/parser.py

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,30 @@
1111
class Parser(object):
1212

1313
COMMON = re.compile(
14-
'(\d{4})(?:-|/)(\d{1,2})(?:-|/)(\d{1,2})' # YMD or YDM
14+
# Date
15+
'^'
16+
'(\d{4})' # Year
1517
'('
16-
'(?:T| )' # Separator
17-
'(\d{2}):(\d{2}):(\d{2})' # HH:mm:ss
18-
'(\.(\d{1,9}))?' # Subsecond
19-
'((-|\+)\d{2}:\d{2}|Z)?' # Timezone offset
18+
' ((?:-|/)?(\d{1,2}))?' # Month (optional)
19+
' ((?:-|/)?(\d{1,2}))?' # Day (optional)
2020
')?'
21+
22+
# Time (Optional)
23+
'('
24+
' (?:T|\ )' # Separator (T or space)
25+
' (\d{1,2}):?(\d{1,2})?:?(\d{1,2})?' # HH:mm:ss (optional mm and ss)
26+
# Subsecond part (optional)
27+
' ('
28+
' (?:\.|,)' # Subsecond separator (optional)
29+
' (\d{1,9})' # Subsecond
30+
' )?'
31+
# Timezone offset
32+
' ('
33+
' (-|\+)\d{2}:?\d{2}|Z' # Offset (+HH:mm or +HHmm or Z)
34+
' )?'
35+
')?'
36+
'$',
37+
re.VERBOSE
2138
)
2239

2340
DEFAULT_OPTIONS = {
@@ -40,12 +57,24 @@ def parse_common(self, text):
4057
m = self.COMMON.match(text)
4158
if m:
4259
year = int(m.group(1))
43-
if self._options['day_first']:
44-
month = int(m.group(3))
45-
day = int(m.group(2))
60+
61+
if not m.group(2):
62+
# No month and day
63+
month = 1
64+
day = 1
4665
else:
47-
month = int(m.group(2))
48-
day = int(m.group(3))
66+
if m.group(4) and m.group(6):
67+
# Month and day
68+
if self._options['day_first']:
69+
month = int(m.group(6))
70+
day = int(m.group(4))
71+
else:
72+
month = int(m.group(4))
73+
day = int(m.group(6))
74+
else:
75+
# Only month
76+
month = int(m.group(4) or m.group(6))
77+
day = 1
4978

5079
parsed = {
5180
'year': year,
@@ -57,26 +86,35 @@ def parse_common(self, text):
5786
'subsecond': 0,
5887
'offset': None,
5988
}
60-
if not m.group(4):
89+
if not m.group(7):
6190
return parsed
6291

6392
# Grabbing hh:mm:ss
64-
parsed['hour'] = int(m.group(5))
65-
parsed['minute'] = int(m.group(6))
66-
parsed['second'] = int(m.group(7))
93+
parsed['hour'] = int(m.group(8))
94+
95+
if m.group(9):
96+
parsed['minute'] = int(m.group(9))
97+
98+
if m.group(10):
99+
parsed['second'] = int(m.group(10))
67100

68101
# Grabbing subseconds, if any
69-
if m.group(8):
70-
parsed['subsecond'] = int('{:0<9}'.format(m.group(9)))
102+
if m.group(11):
103+
parsed['subsecond'] = int('{:0<9}'.format(m.group(12)))
71104

72105
# Grabbing timezone, if any
73-
tz = m.group(10)
106+
tz = m.group(13)
74107
if tz:
75108
if tz == 'Z':
76109
offset = 0
77110
else:
78111
negative = True if tz.startswith('-') else False
79-
off_hour, off_minute = tz[1:].split(':')
112+
tz = tz[1:]
113+
if ':' not in tz:
114+
off_hour = tz[0:2]
115+
off_minute = tz[2:4]
116+
else:
117+
off_hour, off_minute = tz.split(':')
80118

81119
offset = ((int(off_hour) * 60) + int(off_minute)) * 60
82120

tests/parsing_test/test_parser.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
11
# -*- coding: utf-8 -*-
22

33
from .. import AbstractTestCase
4-
from pendulum.parsing.parser import Parser
4+
from pendulum.parsing.parser import Parser, ParserError
55

66

77
class ParserTest(AbstractTestCase):
88

9+
def test_y(self):
10+
text = '2016'
11+
12+
parsed = Parser().parse(text)
13+
self.assertEqual(2016, parsed['year'])
14+
self.assertEqual(1, parsed['month'])
15+
self.assertEqual(1, parsed['day'])
16+
self.assertEqual(0, parsed['hour'])
17+
self.assertEqual(0, parsed['minute'])
18+
self.assertEqual(0, parsed['second'])
19+
self.assertEqual(0, parsed['subsecond'])
20+
self.assertEqual(None, parsed['offset'])
21+
22+
def test_ym(self):
23+
text = '2016-10'
24+
25+
parsed = Parser().parse(text)
26+
self.assertEqual(2016, parsed['year'])
27+
self.assertEqual(10, parsed['month'])
28+
self.assertEqual(1, parsed['day'])
29+
self.assertEqual(0, parsed['hour'])
30+
self.assertEqual(0, parsed['minute'])
31+
self.assertEqual(0, parsed['second'])
32+
self.assertEqual(0, parsed['subsecond'])
33+
self.assertEqual(None, parsed['offset'])
34+
935
def test_ymd(self):
1036
text = '2016-10-06'
1137

@@ -120,3 +146,93 @@ def test_rfc_3339_extended_nanoseconds(self):
120146
self.assertEqual(56, parsed['second'])
121147
self.assertEqual(123456789, parsed['subsecond'])
122148
self.assertEqual(19800, parsed['offset'])
149+
150+
def test_iso_8601(self):
151+
text = '201610'
152+
153+
parsed = Parser().parse(text)
154+
self.assertEqual(2016, parsed['year'])
155+
self.assertEqual(10, parsed['month'])
156+
self.assertEqual(1, parsed['day'])
157+
self.assertEqual(0, parsed['hour'])
158+
self.assertEqual(0, parsed['minute'])
159+
self.assertEqual(0, parsed['second'])
160+
self.assertEqual(0, parsed['subsecond'])
161+
self.assertEqual(None, parsed['offset'])
162+
163+
text = '2016-10-01T14'
164+
165+
parsed = Parser().parse(text)
166+
self.assertEqual(2016, parsed['year'])
167+
self.assertEqual(10, parsed['month'])
168+
self.assertEqual(1, parsed['day'])
169+
self.assertEqual(14, parsed['hour'])
170+
self.assertEqual(0, parsed['minute'])
171+
self.assertEqual(0, parsed['second'])
172+
self.assertEqual(0, parsed['subsecond'])
173+
self.assertEqual(None, parsed['offset'])
174+
175+
text = '2016-10-01T14:30'
176+
177+
parsed = Parser().parse(text)
178+
self.assertEqual(2016, parsed['year'])
179+
self.assertEqual(10, parsed['month'])
180+
self.assertEqual(1, parsed['day'])
181+
self.assertEqual(14, parsed['hour'])
182+
self.assertEqual(30, parsed['minute'])
183+
self.assertEqual(0, parsed['second'])
184+
self.assertEqual(0, parsed['subsecond'])
185+
self.assertEqual(None, parsed['offset'])
186+
187+
text = '20161001T14'
188+
189+
parsed = Parser().parse(text)
190+
self.assertEqual(2016, parsed['year'])
191+
self.assertEqual(10, parsed['month'])
192+
self.assertEqual(1, parsed['day'])
193+
self.assertEqual(14, parsed['hour'])
194+
self.assertEqual(0, parsed['minute'])
195+
self.assertEqual(0, parsed['second'])
196+
self.assertEqual(0, parsed['subsecond'])
197+
self.assertEqual(None, parsed['offset'])
198+
199+
text = '20161001T1430'
200+
201+
parsed = Parser().parse(text)
202+
self.assertEqual(2016, parsed['year'])
203+
self.assertEqual(10, parsed['month'])
204+
self.assertEqual(1, parsed['day'])
205+
self.assertEqual(14, parsed['hour'])
206+
self.assertEqual(30, parsed['minute'])
207+
self.assertEqual(0, parsed['second'])
208+
self.assertEqual(0, parsed['subsecond'])
209+
self.assertEqual(None, parsed['offset'])
210+
211+
text = '20161001T1430+0530'
212+
213+
parsed = Parser().parse(text)
214+
self.assertEqual(2016, parsed['year'])
215+
self.assertEqual(10, parsed['month'])
216+
self.assertEqual(1, parsed['day'])
217+
self.assertEqual(14, parsed['hour'])
218+
self.assertEqual(30, parsed['minute'])
219+
self.assertEqual(0, parsed['second'])
220+
self.assertEqual(0, parsed['subsecond'])
221+
self.assertEqual(19800, parsed['offset'])
222+
223+
text = '20161001T1430,4+0530'
224+
225+
parsed = Parser().parse(text)
226+
self.assertEqual(2016, parsed['year'])
227+
self.assertEqual(10, parsed['month'])
228+
self.assertEqual(1, parsed['day'])
229+
self.assertEqual(14, parsed['hour'])
230+
self.assertEqual(30, parsed['minute'])
231+
self.assertEqual(0, parsed['second'])
232+
self.assertEqual(400000000, parsed['subsecond'])
233+
self.assertEqual(19800, parsed['offset'])
234+
235+
def test_invalid(self):
236+
text = '201610T'
237+
238+
self.assertRaises(ParserError, Parser().parse, text)

0 commit comments

Comments
 (0)