test_isoparser.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. from datetime import datetime, timedelta, date, time
  4. import itertools as it
  5. from dateutil import tz
  6. from dateutil.tz import UTC
  7. from dateutil.parser import isoparser, isoparse
  8. import pytest
  9. import six
  10. def _generate_tzoffsets(limited):
  11. def _mkoffset(hmtuple, fmt):
  12. h, m = hmtuple
  13. m_td = (-1 if h < 0 else 1) * m
  14. tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td))
  15. return tzo, fmt.format(h, m)
  16. out = []
  17. if not limited:
  18. # The subset that's just hours
  19. hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)]
  20. out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h])
  21. # Ones that have hours and minutes
  22. hm_out = [] + hm_out_h
  23. hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)]
  24. else:
  25. hm_out = [(-5, -0)]
  26. fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}']
  27. out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts]
  28. # Also add in UTC and naive
  29. out.append((UTC, 'Z'))
  30. out.append((None, ''))
  31. return out
  32. FULL_TZOFFSETS = _generate_tzoffsets(False)
  33. FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]]
  34. TZOFFSETS = _generate_tzoffsets(True)
  35. DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)]
  36. @pytest.mark.parametrize('dt', tuple(DATES))
  37. def test_year_only(dt):
  38. dtstr = dt.strftime('%Y')
  39. assert isoparse(dtstr) == dt
  40. DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)]
  41. @pytest.mark.parametrize('dt', tuple(DATES))
  42. def test_year_month(dt):
  43. fmt = '%Y-%m'
  44. dtstr = dt.strftime(fmt)
  45. assert isoparse(dtstr) == dt
  46. DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)]
  47. YMD_FMTS = ('%Y%m%d', '%Y-%m-%d')
  48. @pytest.mark.parametrize('dt', tuple(DATES))
  49. @pytest.mark.parametrize('fmt', YMD_FMTS)
  50. def test_year_month_day(dt, fmt):
  51. dtstr = dt.strftime(fmt)
  52. assert isoparse(dtstr) == dt
  53. def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset,
  54. microsecond_precision=None):
  55. tzi, offset_str = tzoffset
  56. fmt = date_fmt + 'T' + time_fmt
  57. dt = dt.replace(tzinfo=tzi)
  58. dtstr = dt.strftime(fmt)
  59. if microsecond_precision is not None:
  60. if not fmt.endswith('%f'): # pragma: nocover
  61. raise ValueError('Time format has no microseconds!')
  62. if microsecond_precision != 6:
  63. dtstr = dtstr[: -(6 - microsecond_precision)]
  64. elif microsecond_precision > 6: # pragma: nocover
  65. raise ValueError("Precision must be 1-6")
  66. dtstr += offset_str
  67. assert isoparse(dtstr) == dt
  68. DATETIMES = [datetime(1998, 4, 16, 12),
  69. datetime(2019, 11, 18, 23),
  70. datetime(2014, 12, 16, 4)]
  71. @pytest.mark.parametrize('dt', tuple(DATETIMES))
  72. @pytest.mark.parametrize('date_fmt', YMD_FMTS)
  73. @pytest.mark.parametrize('tzoffset', TZOFFSETS)
  74. def test_ymd_h(dt, date_fmt, tzoffset):
  75. _isoparse_date_and_time(dt, date_fmt, '%H', tzoffset)
  76. DATETIMES = [datetime(2012, 1, 6, 9, 37)]
  77. @pytest.mark.parametrize('dt', tuple(DATETIMES))
  78. @pytest.mark.parametrize('date_fmt', YMD_FMTS)
  79. @pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M'))
  80. @pytest.mark.parametrize('tzoffset', TZOFFSETS)
  81. def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset):
  82. _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
  83. DATETIMES = [datetime(2003, 9, 2, 22, 14, 2),
  84. datetime(2003, 8, 8, 14, 9, 14),
  85. datetime(2003, 4, 7, 6, 14, 59)]
  86. HMS_FMTS = ('%H%M%S', '%H:%M:%S')
  87. @pytest.mark.parametrize('dt', tuple(DATETIMES))
  88. @pytest.mark.parametrize('date_fmt', YMD_FMTS)
  89. @pytest.mark.parametrize('time_fmt', HMS_FMTS)
  90. @pytest.mark.parametrize('tzoffset', TZOFFSETS)
  91. def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset):
  92. _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
  93. DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)]
  94. @pytest.mark.parametrize('dt', tuple(DATETIMES))
  95. @pytest.mark.parametrize('date_fmt', YMD_FMTS)
  96. @pytest.mark.parametrize('time_fmt', (x + sep + '%f' for x in HMS_FMTS
  97. for sep in '.,'))
  98. @pytest.mark.parametrize('tzoffset', TZOFFSETS)
  99. @pytest.mark.parametrize('precision', list(range(3, 7)))
  100. def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision):
  101. # Truncate the microseconds to the desired precision for the representation
  102. dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6)))
  103. _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision)
  104. ###
  105. # Truncation of extra digits beyond microsecond precision
  106. @pytest.mark.parametrize('dt_str', [
  107. '2018-07-03T14:07:00.123456000001',
  108. '2018-07-03T14:07:00.123456999999',
  109. ])
  110. def test_extra_subsecond_digits(dt_str):
  111. assert isoparse(dt_str) == datetime(2018, 7, 3, 14, 7, 0, 123456)
  112. @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
  113. def test_full_tzoffsets(tzoffset):
  114. dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
  115. date_fmt = '%Y-%m-%d'
  116. time_fmt = '%H:%M:%S.%f'
  117. _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
  118. @pytest.mark.parametrize('dt_str', [
  119. '2014-04-11T00',
  120. '2014-04-10T24',
  121. '2014-04-11T00:00',
  122. '2014-04-10T24:00',
  123. '2014-04-11T00:00:00',
  124. '2014-04-10T24:00:00',
  125. '2014-04-11T00:00:00.000',
  126. '2014-04-10T24:00:00.000',
  127. '2014-04-11T00:00:00.000000',
  128. '2014-04-10T24:00:00.000000']
  129. )
  130. def test_datetime_midnight(dt_str):
  131. assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0)
  132. @pytest.mark.parametrize('datestr', [
  133. '2014-01-01',
  134. '20140101',
  135. ])
  136. @pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-'])
  137. def test_isoparse_sep_none(datestr, sep):
  138. isostr = datestr + sep + '14:33:09'
  139. assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9)
  140. ##
  141. # Uncommon date formats
  142. TIME_ARGS = ('time_args',
  143. ((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz)
  144. for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)],
  145. TZOFFSETS)))
  146. @pytest.mark.parametrize('isocal,dt_expected',[
  147. ((2017, 10), datetime(2017, 3, 6)),
  148. ((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year
  149. ((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014
  150. ])
  151. def test_isoweek(isocal, dt_expected):
  152. # TODO: Figure out how to parametrize this on formats, too
  153. for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'):
  154. dtstr = fmt.format(*isocal)
  155. assert isoparse(dtstr) == dt_expected
  156. @pytest.mark.parametrize('isocal,dt_expected',[
  157. ((2016, 13, 7), datetime(2016, 4, 3)),
  158. ((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year
  159. ((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year
  160. ((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year
  161. ])
  162. def test_isoweek_day(isocal, dt_expected):
  163. # TODO: Figure out how to parametrize this on formats, too
  164. for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'):
  165. dtstr = fmt.format(*isocal)
  166. assert isoparse(dtstr) == dt_expected
  167. @pytest.mark.parametrize('isoord,dt_expected', [
  168. ((2004, 1), datetime(2004, 1, 1)),
  169. ((2016, 60), datetime(2016, 2, 29)),
  170. ((2017, 60), datetime(2017, 3, 1)),
  171. ((2016, 366), datetime(2016, 12, 31)),
  172. ((2017, 365), datetime(2017, 12, 31))
  173. ])
  174. def test_iso_ordinal(isoord, dt_expected):
  175. for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'):
  176. dtstr = fmt.format(*isoord)
  177. assert isoparse(dtstr) == dt_expected
  178. ###
  179. # Acceptance of bytes
  180. @pytest.mark.parametrize('isostr,dt', [
  181. (b'2014', datetime(2014, 1, 1)),
  182. (b'20140204', datetime(2014, 2, 4)),
  183. (b'2014-02-04', datetime(2014, 2, 4)),
  184. (b'2014-02-04T12', datetime(2014, 2, 4, 12)),
  185. (b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)),
  186. (b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)),
  187. (b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
  188. (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
  189. (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000,
  190. UTC)),
  191. (b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000,
  192. UTC)),
  193. (b'2014-02-04T12:30:15.224+05:00',
  194. datetime(2014, 2, 4, 12, 30, 15, 224000,
  195. tzinfo=tz.tzoffset(None, timedelta(hours=5))))])
  196. def test_bytes(isostr, dt):
  197. assert isoparse(isostr) == dt
  198. ###
  199. # Invalid ISO strings
  200. @pytest.mark.parametrize('isostr,exception', [
  201. ('201', ValueError), # ISO string too short
  202. ('2012-0425', ValueError), # Inconsistent date separators
  203. ('201204-25', ValueError), # Inconsistent date separators
  204. ('20120425T0120:00', ValueError), # Inconsistent time separators
  205. ('20120425T01:2000', ValueError), # Inconsistent time separators
  206. ('14:3015', ValueError), # Inconsistent time separator
  207. ('20120425T012500-334', ValueError), # Wrong microsecond separator
  208. ('2001-1', ValueError), # YYYY-M not valid
  209. ('2012-04-9', ValueError), # YYYY-MM-D not valid
  210. ('201204', ValueError), # YYYYMM not valid
  211. ('20120411T03:30+', ValueError), # Time zone too short
  212. ('20120411T03:30+1234567', ValueError), # Time zone too long
  213. ('20120411T03:30-25:40', ValueError), # Time zone invalid
  214. ('2012-1a', ValueError), # Invalid month
  215. ('20120411T03:30+00:60', ValueError), # Time zone invalid minutes
  216. ('20120411T03:30+00:61', ValueError), # Time zone invalid minutes
  217. ('20120411T033030.123456012:00', # No sign in time zone
  218. ValueError),
  219. ('2012-W00', ValueError), # Invalid ISO week
  220. ('2012-W55', ValueError), # Invalid ISO week
  221. ('2012-W01-0', ValueError), # Invalid ISO week day
  222. ('2012-W01-8', ValueError), # Invalid ISO week day
  223. ('2013-000', ValueError), # Invalid ordinal day
  224. ('2013-366', ValueError), # Invalid ordinal day
  225. ('2013366', ValueError), # Invalid ordinal day
  226. ('2014-03-12Т12:30:14', ValueError), # Cyrillic T
  227. ('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight
  228. ('2014_W01-1', ValueError), # Invalid separator
  229. ('2014W01-1', ValueError), # Inconsistent use of dashes
  230. ('2014-W011', ValueError), # Inconsistent use of dashes
  231. ])
  232. def test_iso_raises(isostr, exception):
  233. with pytest.raises(exception):
  234. isoparse(isostr)
  235. @pytest.mark.parametrize('sep_act, valid_sep, exception', [
  236. ('T', 'C', ValueError),
  237. ('C', 'T', ValueError),
  238. ])
  239. def test_iso_with_sep_raises(sep_act, valid_sep, exception):
  240. parser = isoparser(sep=valid_sep)
  241. isostr = '2012-04-25' + sep_act + '01:25:00'
  242. with pytest.raises(exception):
  243. parser.isoparse(isostr)
  244. ###
  245. # Test ISOParser constructor
  246. @pytest.mark.parametrize('sep', [' ', '9', '🍛'])
  247. def test_isoparser_invalid_sep(sep):
  248. with pytest.raises(ValueError):
  249. isoparser(sep=sep)
  250. # This only fails on Python 3
  251. @pytest.mark.xfail(not six.PY2, reason="Fails on Python 3 only")
  252. def test_isoparser_byte_sep():
  253. dt = datetime(2017, 12, 6, 12, 30, 45)
  254. dt_str = dt.isoformat(sep=str('T'))
  255. dt_rt = isoparser(sep=b'T').isoparse(dt_str)
  256. assert dt == dt_rt
  257. ###
  258. # Test parse_tzstr
  259. @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
  260. def test_parse_tzstr(tzoffset):
  261. dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
  262. date_fmt = '%Y-%m-%d'
  263. time_fmt = '%H:%M:%S.%f'
  264. _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
  265. @pytest.mark.parametrize('tzstr', [
  266. '-00:00', '+00:00', '+00', '-00', '+0000', '-0000'
  267. ])
  268. @pytest.mark.parametrize('zero_as_utc', [True, False])
  269. def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc):
  270. tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
  271. assert tzi == UTC
  272. assert (type(tzi) == tz.tzutc) == zero_as_utc
  273. @pytest.mark.parametrize('tzstr,exception', [
  274. ('00:00', ValueError), # No sign
  275. ('05:00', ValueError), # No sign
  276. ('_00:00', ValueError), # Invalid sign
  277. ('+25:00', ValueError), # Offset too large
  278. ('00:0000', ValueError), # String too long
  279. ])
  280. def test_parse_tzstr_fails(tzstr, exception):
  281. with pytest.raises(exception):
  282. isoparser().parse_tzstr(tzstr)
  283. ###
  284. # Test parse_isodate
  285. def __make_date_examples():
  286. dates_no_day = [
  287. date(1999, 12, 1),
  288. date(2016, 2, 1)
  289. ]
  290. if not six.PY2:
  291. # strftime does not support dates before 1900 in Python 2
  292. dates_no_day.append(date(1000, 11, 1))
  293. # Only one supported format for dates with no day
  294. o = zip(dates_no_day, it.repeat('%Y-%m'))
  295. dates_w_day = [
  296. date(1969, 12, 31),
  297. date(1900, 1, 1),
  298. date(2016, 2, 29),
  299. date(2017, 11, 14)
  300. ]
  301. dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d')
  302. o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts))
  303. return list(o)
  304. @pytest.mark.parametrize('d,dt_fmt', __make_date_examples())
  305. @pytest.mark.parametrize('as_bytes', [True, False])
  306. def test_parse_isodate(d, dt_fmt, as_bytes):
  307. d_str = d.strftime(dt_fmt)
  308. if isinstance(d_str, six.text_type) and as_bytes:
  309. d_str = d_str.encode('ascii')
  310. elif isinstance(d_str, bytes) and not as_bytes:
  311. d_str = d_str.decode('ascii')
  312. iparser = isoparser()
  313. assert iparser.parse_isodate(d_str) == d
  314. @pytest.mark.parametrize('isostr,exception', [
  315. ('243', ValueError), # ISO string too short
  316. ('2014-0423', ValueError), # Inconsistent date separators
  317. ('201404-23', ValueError), # Inconsistent date separators
  318. ('2014日03月14', ValueError), # Not ASCII
  319. ('2013-02-29', ValueError), # Not a leap year
  320. ('2014/12/03', ValueError), # Wrong separators
  321. ('2014-04-19T', ValueError), # Unknown components
  322. ('201202', ValueError), # Invalid format
  323. ])
  324. def test_isodate_raises(isostr, exception):
  325. with pytest.raises(exception):
  326. isoparser().parse_isodate(isostr)
  327. def test_parse_isodate_error_text():
  328. with pytest.raises(ValueError) as excinfo:
  329. isoparser().parse_isodate('2014-0423')
  330. # ensure the error message does not contain b' prefixes
  331. if six.PY2:
  332. expected_error = "String contains unknown ISO components: u'2014-0423'"
  333. else:
  334. expected_error = "String contains unknown ISO components: '2014-0423'"
  335. assert expected_error == str(excinfo.value)
  336. ###
  337. # Test parse_isotime
  338. def __make_time_examples():
  339. outputs = []
  340. # HH
  341. time_h = [time(0), time(8), time(22)]
  342. time_h_fmts = ['%H']
  343. outputs.append(it.product(time_h, time_h_fmts))
  344. # HHMM / HH:MM
  345. time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)]
  346. time_hm_fmts = ['%H%M', '%H:%M']
  347. outputs.append(it.product(time_hm, time_hm_fmts))
  348. # HHMMSS / HH:MM:SS
  349. time_hms = [time(0, 0, 0), time(0, 15, 30),
  350. time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)]
  351. time_hms_fmts = ['%H%M%S', '%H:%M:%S']
  352. outputs.append(it.product(time_hms, time_hms_fmts))
  353. # HHMMSS.ffffff / HH:MM:SS.ffffff
  354. time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993),
  355. time(14, 21, 59, 948730),
  356. time(23, 59, 59, 999999)]
  357. time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f']
  358. outputs.append(it.product(time_hmsu, time_hmsu_fmts))
  359. outputs = list(map(list, outputs))
  360. # Time zones
  361. ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs))
  362. o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr))
  363. o = ((t.replace(tzinfo=tzi), fmt + off_str)
  364. for (t, fmt), (tzi, off_str) in o)
  365. outputs.append(o)
  366. return list(it.chain.from_iterable(outputs))
  367. @pytest.mark.parametrize('time_val,time_fmt', __make_time_examples())
  368. @pytest.mark.parametrize('as_bytes', [True, False])
  369. def test_isotime(time_val, time_fmt, as_bytes):
  370. tstr = time_val.strftime(time_fmt)
  371. if isinstance(tstr, six.text_type) and as_bytes:
  372. tstr = tstr.encode('ascii')
  373. elif isinstance(tstr, bytes) and not as_bytes:
  374. tstr = tstr.decode('ascii')
  375. iparser = isoparser()
  376. assert iparser.parse_isotime(tstr) == time_val
  377. @pytest.mark.parametrize('isostr', [
  378. '24:00',
  379. '2400',
  380. '24:00:00',
  381. '240000',
  382. '24:00:00.000',
  383. '24:00:00,000',
  384. '24:00:00.000000',
  385. '24:00:00,000000',
  386. ])
  387. def test_isotime_midnight(isostr):
  388. iparser = isoparser()
  389. assert iparser.parse_isotime(isostr) == time(0, 0, 0, 0)
  390. @pytest.mark.parametrize('isostr,exception', [
  391. ('3', ValueError), # ISO string too short
  392. ('14時30分15秒', ValueError), # Not ASCII
  393. ('14_30_15', ValueError), # Invalid separators
  394. ('1430:15', ValueError), # Inconsistent separator use
  395. ('25', ValueError), # Invalid hours
  396. ('25:15', ValueError), # Invalid hours
  397. ('14:60', ValueError), # Invalid minutes
  398. ('14:59:61', ValueError), # Invalid seconds
  399. ('14:30:15.34468305:00', ValueError), # No sign in time zone
  400. ('14:30:15+', ValueError), # Time zone too short
  401. ('14:30:15+1234567', ValueError), # Time zone invalid
  402. ('14:59:59+25:00', ValueError), # Invalid tz hours
  403. ('14:59:59+12:62', ValueError), # Invalid tz minutes
  404. ('14:59:30_344583', ValueError), # Invalid microsecond separator
  405. ('24:01', ValueError), # 24 used for non-midnight time
  406. ('24:00:01', ValueError), # 24 used for non-midnight time
  407. ('24:00:00.001', ValueError), # 24 used for non-midnight time
  408. ('24:00:00.000001', ValueError), # 24 used for non-midnight time
  409. ])
  410. def test_isotime_raises(isostr, exception):
  411. iparser = isoparser()
  412. with pytest.raises(exception):
  413. iparser.parse_isotime(isostr)