datetime_math.py 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024
  1. """
  2. Functions for mathematical datetime operations
  3. (C) 2011-2013 by the GRASS Development Team
  4. This program is free software under the GNU General Public
  5. License (>=v2). Read the file COPYING that comes with GRASS
  6. for details.
  7. :authors: Soeren Gebbert
  8. """
  9. import sys
  10. from datetime import datetime, timedelta
  11. from .core import get_tgis_message_interface
  12. import copy
  13. try:
  14. import dateutil.parser as parser
  15. has_dateutil = True
  16. except:
  17. has_dateutil = False
  18. if sys.version_info[0] >= 3:
  19. unicode = str
  20. DAY_IN_SECONDS = 86400
  21. SECOND_AS_DAY = 1.1574074074074073e-05
  22. ###############################################################################
  23. def relative_time_to_time_delta(value):
  24. """Convert the double value representing days
  25. into a timedelta object.
  26. """
  27. days = int(value)
  28. seconds = value % 1
  29. seconds = round(seconds * DAY_IN_SECONDS)
  30. return timedelta(days, seconds)
  31. ###############################################################################
  32. def time_delta_to_relative_time(delta):
  33. """Convert the time delta into a
  34. double value, representing days.
  35. """
  36. return float(delta.days) + float(delta.seconds * SECOND_AS_DAY)
  37. ###############################################################################
  38. def relative_time_to_time_delta_seconds(value):
  39. """Convert the double value representing seconds
  40. into a timedelta object.
  41. """
  42. days = value / 86400
  43. seconds = int(value % 86400)
  44. return timedelta(days, seconds)
  45. ###############################################################################
  46. def time_delta_to_relative_time_seconds(delta):
  47. """Convert the time delta into a
  48. double value, representing seconds.
  49. """
  50. return float(delta.days * DAY_IN_SECONDS) + float(delta.seconds)
  51. ###############################################################################
  52. def decrement_datetime_by_string(mydate, increment, mult=1):
  53. """Return a new datetime object decremented with the provided
  54. relative dates specified as string.
  55. Additional a multiplier can be specified to multiply the increment
  56. before adding to the provided datetime object.
  57. Usage:
  58. .. code-block:: python
  59. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  60. >>> string = "31 days"
  61. >>> decrement_datetime_by_string(dt, string)
  62. datetime.datetime(2000, 12, 1, 0, 0)
  63. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  64. >>> string = "1 month"
  65. >>> decrement_datetime_by_string(dt, string)
  66. datetime.datetime(2000, 12, 1, 0, 0)
  67. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  68. >>> string = "2 month"
  69. >>> decrement_datetime_by_string(dt, string)
  70. datetime.datetime(2000, 11, 1, 0, 0)
  71. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  72. >>> string = "24 months"
  73. >>> decrement_datetime_by_string(dt, string)
  74. datetime.datetime(1999, 1, 1, 0, 0)
  75. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  76. >>> string = "48 months"
  77. >>> decrement_datetime_by_string(dt, string)
  78. datetime.datetime(1997, 1, 1, 0, 0)
  79. >>> dt = datetime(2001, 6, 1, 0, 0, 0)
  80. >>> string = "5 months"
  81. >>> decrement_datetime_by_string(dt, string)
  82. datetime.datetime(2001, 1, 1, 0, 0)
  83. >>> dt = datetime(2001, 6, 1, 0, 0, 0)
  84. >>> string = "7 months"
  85. >>> decrement_datetime_by_string(dt, string)
  86. datetime.datetime(2000, 11, 1, 0, 0)
  87. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  88. >>> string = "1 year"
  89. >>> decrement_datetime_by_string(dt, string)
  90. datetime.datetime(2000, 1, 1, 0, 0)
  91. :param mydate: A datetime object to incremented
  92. :param increment: A string providing increment information:
  93. The string may include comma separated values of type
  94. seconds, minutes, hours, days, weeks, months and years
  95. Example: Increment the datetime 2001-01-01 00:00:00
  96. with "60 seconds, 4 minutes, 12 hours, 10 days,
  97. 1 weeks, 5 months, 1 years" will result in the
  98. datetime 2003-02-18 12:05:00
  99. :param mult: A multiplier, default is 1
  100. :return: The new datetime object or none in case of an error
  101. """
  102. return modify_datetime_by_string(mydate, increment, mult, sign=int(-1))
  103. ###############################################################################
  104. def increment_datetime_by_string(mydate, increment, mult=1):
  105. """Return a new datetime object incremented with the provided
  106. relative dates specified as string.
  107. Additional a multiplier can be specified to multiply the increment
  108. before adding to the provided datetime object.
  109. Usage:
  110. .. code-block:: python
  111. >>> dt = datetime(2001, 9, 1, 0, 0, 0)
  112. >>> string = "60 seconds, 4 minutes, 12 hours, 10 days, 1 weeks, 5 months, 1 years"
  113. >>> increment_datetime_by_string(dt, string)
  114. datetime.datetime(2003, 2, 18, 12, 5)
  115. >>> dt = datetime(2001, 11, 1, 0, 0, 0)
  116. >>> string = "1 months"
  117. >>> increment_datetime_by_string(dt, string)
  118. datetime.datetime(2001, 12, 1, 0, 0)
  119. >>> dt = datetime(2001, 11, 1, 0, 0, 0)
  120. >>> string = "13 months"
  121. >>> increment_datetime_by_string(dt, string)
  122. datetime.datetime(2002, 12, 1, 0, 0)
  123. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  124. >>> string = "72 months"
  125. >>> increment_datetime_by_string(dt, string)
  126. datetime.datetime(2007, 1, 1, 0, 0)
  127. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  128. >>> string = "72 months"
  129. >>> increment_datetime_by_string(dt, string)
  130. datetime.datetime(2007, 1, 1, 0, 0)
  131. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  132. >>> string = "5 minutes"
  133. >>> increment_datetime_by_string(dt, string)
  134. datetime.datetime(2001, 1, 1, 0, 5)
  135. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  136. >>> string = "49 hours"
  137. >>> increment_datetime_by_string(dt, string)
  138. datetime.datetime(2001, 1, 3, 1, 0)
  139. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  140. >>> string = "3600 seconds"
  141. >>> increment_datetime_by_string(dt, string)
  142. datetime.datetime(2001, 1, 1, 1, 0)
  143. >>> dt = datetime(2001, 1, 1, 0, 0, 0)
  144. >>> string = "30 days"
  145. >>> increment_datetime_by_string(dt, string)
  146. datetime.datetime(2001, 1, 31, 0, 0)
  147. :param mydate: A datetime object to incremented
  148. :param increment: A string providing increment information:
  149. The string may include comma separated values of type
  150. seconds, minutes, hours, days, weeks, months and years
  151. Example: Increment the datetime 2001-01-01 00:00:00
  152. with "60 seconds, 4 minutes, 12 hours, 10 days,
  153. 1 weeks, 5 months, 1 years" will result in the
  154. datetime 2003-02-18 12:05:00
  155. :param mult: A multiplier, default is 1
  156. :return: The new datetime object or none in case of an error
  157. """
  158. return modify_datetime_by_string(mydate, increment, mult, sign=int(1))
  159. ###############################################################################
  160. def modify_datetime_by_string(mydate, increment, mult=1, sign=1):
  161. """Return a new datetime object incremented with the provided
  162. relative dates specified as string.
  163. Additional a multiplier can be specified to multiply the increment
  164. before adding to the provided datetime object.
  165. :param mydate: A datetime object to incremented
  166. :param increment: A string providing increment information:
  167. The string may include comma separated values of type
  168. seconds, minutes, hours, days, weeks, months and years
  169. Example: Increment the datetime 2001-01-01 00:00:00
  170. with "60 seconds, 4 minutes, 12 hours, 10 days,
  171. 1 weeks, 5 months, 1 years" will result in the
  172. datetime 2003-02-18 12:05:00
  173. :param mult: A multiplier, default is 1
  174. :param sign: Choose 1 for positive sign (incrementing) or -1 for
  175. negative sign (decrementing).
  176. :return: The new datetime object or none in case of an error
  177. """
  178. sign = int(sign)
  179. if sign != 1 and sign != -1:
  180. return None
  181. if increment:
  182. seconds = 0
  183. minutes = 0
  184. hours = 0
  185. days = 0
  186. weeks = 0
  187. months = 0
  188. years = 0
  189. inclist = []
  190. # Split the increment string
  191. incparts = increment.split(",")
  192. for incpart in incparts:
  193. inclist.append(incpart.strip().split(" "))
  194. for inc in inclist:
  195. msgr = get_tgis_message_interface()
  196. if len(inc) < 2:
  197. msgr.error(_("Wrong increment format: %s") % (increment))
  198. return None
  199. if inc[1].find("seconds") >= 0 or inc[1].find("second") >= 0:
  200. seconds = sign * mult * int(inc[0])
  201. elif inc[1].find("minutes") >= 0 or inc[1].find("minute") >= 0:
  202. minutes = sign * mult * int(inc[0])
  203. elif inc[1].find("hours") >= 0 or inc[1].find("hour") >= 0:
  204. hours = sign * mult * int(inc[0])
  205. elif inc[1].find("days") >= 0 or inc[1].find("day") >= 0:
  206. days = sign * mult * int(inc[0])
  207. elif inc[1].find("weeks") >= 0 or inc[1].find("week") >= 0:
  208. weeks = sign * mult * int(inc[0])
  209. elif inc[1].find("months") >= 0 or inc[1].find("month") >= 0:
  210. months = sign * mult * int(inc[0])
  211. elif inc[1].find("years") >= 0 or inc[1].find("year") >= 0:
  212. years = sign * mult * int(inc[0])
  213. else:
  214. msgr.error(_("Wrong increment format: %s") % (increment))
  215. return None
  216. return modify_datetime(
  217. mydate, years, months, weeks, days, hours, minutes, seconds
  218. )
  219. return mydate
  220. ###############################################################################
  221. def modify_datetime(
  222. mydate, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0
  223. ):
  224. """Return a new datetime object incremented with the provided
  225. relative dates and times"""
  226. tdelta_seconds = timedelta(seconds=seconds)
  227. tdelta_minutes = timedelta(minutes=minutes)
  228. tdelta_hours = timedelta(hours=hours)
  229. tdelta_days = timedelta(days=days)
  230. tdelta_weeks = timedelta(weeks=weeks)
  231. tdelta_months = timedelta(0)
  232. tdelta_years = timedelta(0)
  233. if months > 0:
  234. # Compute the actual number of days in the month to add as timedelta
  235. year = mydate.year
  236. month = mydate.month
  237. all_months = int(months) + int(month)
  238. years_to_add = int(all_months / 12.001)
  239. residual_months = all_months - (years_to_add * 12)
  240. # Make a deep copy of the datetime object
  241. dt1 = copy.copy(mydate)
  242. # Make sure the month starts with a 1
  243. if residual_months == 0:
  244. residual_months = 1
  245. try:
  246. dt1 = dt1.replace(year=year + years_to_add, month=residual_months)
  247. except:
  248. raise
  249. tdelta_months = dt1 - mydate
  250. elif months < 0:
  251. # Compute the actual number of days in the month to add as timedelta
  252. year = mydate.year
  253. month = mydate.month
  254. years_to_remove = 0
  255. all_months = int(months) + int(month)
  256. if all_months <= 0:
  257. years_to_remove = abs(int(all_months / 12.001))
  258. residual_months = all_months + (years_to_remove * 12)
  259. years_to_remove += 1
  260. else:
  261. residual_months = all_months
  262. # Make a deep copy of the datetime object
  263. dt1 = copy.copy(mydate)
  264. # Correct the months
  265. if residual_months <= 0:
  266. residual_months += 12
  267. try:
  268. dt1 = dt1.replace(year=year - years_to_remove, month=residual_months)
  269. except:
  270. raise
  271. tdelta_months = dt1 - mydate
  272. if years != 0:
  273. # Make a deep copy of the datetime object
  274. dt1 = copy.copy(mydate)
  275. # Compute the number of days
  276. dt1 = dt1.replace(year=mydate.year + int(years))
  277. tdelta_years = dt1 - mydate
  278. return (
  279. mydate
  280. + tdelta_seconds
  281. + tdelta_minutes
  282. + tdelta_hours
  283. + tdelta_days
  284. + tdelta_weeks
  285. + tdelta_months
  286. + tdelta_years
  287. )
  288. ###############################################################################
  289. def adjust_datetime_to_granularity(mydate, granularity):
  290. """Modify the datetime object to fit the given granularity
  291. - Years will start at the first of Januar
  292. - Months will start at the first day of the month
  293. - Days will start at the first Hour of the day
  294. - Hours will start at the first minute of an hour
  295. - Minutes will start at the first second of a minute
  296. Usage:
  297. .. code-block:: python
  298. >>> dt = datetime(2001, 8, 8, 12,30,30)
  299. >>> adjust_datetime_to_granularity(dt, "5 seconds")
  300. datetime.datetime(2001, 8, 8, 12, 30, 30)
  301. >>> adjust_datetime_to_granularity(dt, "20 minutes")
  302. datetime.datetime(2001, 8, 8, 12, 30)
  303. >>> adjust_datetime_to_granularity(dt, "20 minutes")
  304. datetime.datetime(2001, 8, 8, 12, 30)
  305. >>> adjust_datetime_to_granularity(dt, "3 hours")
  306. datetime.datetime(2001, 8, 8, 12, 0)
  307. >>> adjust_datetime_to_granularity(dt, "5 days")
  308. datetime.datetime(2001, 8, 8, 0, 0)
  309. >>> adjust_datetime_to_granularity(dt, "2 weeks")
  310. datetime.datetime(2001, 8, 6, 0, 0)
  311. >>> adjust_datetime_to_granularity(dt, "6 months")
  312. datetime.datetime(2001, 8, 1, 0, 0)
  313. >>> adjust_datetime_to_granularity(dt, "2 years")
  314. datetime.datetime(2001, 1, 1, 0, 0)
  315. >>> adjust_datetime_to_granularity(dt, "2 years, 3 months, 5 days, 3 hours, 3 minutes, 2 seconds")
  316. datetime.datetime(2001, 8, 8, 12, 30, 30)
  317. >>> adjust_datetime_to_granularity(dt, "3 months, 5 days, 3 minutes")
  318. datetime.datetime(2001, 8, 8, 12, 30)
  319. >>> adjust_datetime_to_granularity(dt, "3 weeks, 5 days")
  320. datetime.datetime(2001, 8, 8, 0, 0)
  321. """
  322. if granularity:
  323. has_seconds = False
  324. has_minutes = False
  325. has_hours = False
  326. has_days = False
  327. has_weeks = False
  328. has_months = False
  329. has_years = False
  330. seconds = mydate.second
  331. minutes = mydate.minute
  332. hours = mydate.hour
  333. days = mydate.day
  334. weekday = mydate.weekday()
  335. months = mydate.month
  336. years = mydate.year
  337. granlist = []
  338. # Split the increment string
  339. granparts = granularity.split(",")
  340. for granpart in granparts:
  341. granlist.append(granpart.strip().split(" "))
  342. for inc in granlist:
  343. if inc[1].find("seconds") >= 0 or inc[1].find("second") >= 0:
  344. has_seconds = True
  345. elif inc[1].find("minutes") >= 0 or inc[1].find("minute") >= 0:
  346. has_minutes = True
  347. elif inc[1].find("hours") >= 0 or inc[1].find("hour") >= 0:
  348. has_hours = True
  349. elif inc[1].find("days") >= 0 or inc[1].find("day") >= 0:
  350. has_days = True
  351. elif inc[1].find("weeks") >= 0 or inc[1].find("week") >= 0:
  352. has_weeks = True
  353. elif inc[1].find("months") >= 0 or inc[1].find("month") >= 0:
  354. has_months = True
  355. elif inc[1].find("years") >= 0 or inc[1].find("year") >= 0:
  356. has_years = True
  357. else:
  358. msgr = get_tgis_message_interface()
  359. msgr.error(_("Wrong granularity format: %s") % (granularity))
  360. return None
  361. if has_seconds:
  362. pass
  363. elif has_minutes: # Start at 0 seconds
  364. seconds = 0
  365. elif has_hours: # Start at 0 minutes and seconds
  366. seconds = 0
  367. minutes = 0
  368. elif has_days: # Start at 0 hours, minutes and seconds
  369. seconds = 0
  370. minutes = 0
  371. hours = 0
  372. elif has_weeks: # Start at the first day of the week (Monday) at 00:00:00
  373. seconds = 0
  374. minutes = 0
  375. hours = 0
  376. if days > weekday:
  377. days = days - weekday # this needs to be fixed
  378. else:
  379. days = days + weekday # this needs to be fixed
  380. elif has_months: # Start at the first day of the month at 00:00:00
  381. seconds = 0
  382. minutes = 0
  383. hours = 0
  384. days = 1
  385. elif has_years: # Start at the first day of the first month at 00:00:00
  386. seconds = 0
  387. minutes = 0
  388. hours = 0
  389. days = 1
  390. months = 1
  391. dt = copy.copy(mydate)
  392. return dt.replace(
  393. year=years,
  394. month=months,
  395. day=days,
  396. hour=hours,
  397. minute=minutes,
  398. second=seconds,
  399. )
  400. ###############################################################################
  401. def compute_datetime_delta(start, end):
  402. """Return a dictionary with the accumulated delta in year, month, day,
  403. hour, minute and second
  404. Usage:
  405. .. code-block:: python
  406. >>> start = datetime(2001, 1, 1, 00,00,00)
  407. >>> end = datetime(2001, 1, 1, 00,00,00)
  408. >>> compute_datetime_delta(start, end)
  409. {'hour': 0, 'month': 0, 'second': 0, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 0}
  410. >>> start = datetime(2001, 1, 1, 00,00,14)
  411. >>> end = datetime(2001, 1, 1, 00,00,44)
  412. >>> compute_datetime_delta(start, end)
  413. {'hour': 0, 'month': 0, 'second': 30, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 0}
  414. >>> start = datetime(2001, 1, 1, 00,00,44)
  415. >>> end = datetime(2001, 1, 1, 00,01,14)
  416. >>> compute_datetime_delta(start, end)
  417. {'hour': 0, 'month': 0, 'second': 30, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 1}
  418. >>> start = datetime(2001, 1, 1, 00,00,30)
  419. >>> end = datetime(2001, 1, 1, 00,05,30)
  420. >>> compute_datetime_delta(start, end)
  421. {'hour': 0, 'month': 0, 'second': 300, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 5}
  422. >>> start = datetime(2001, 1, 1, 00,00,00)
  423. >>> end = datetime(2001, 1, 1, 00,01,00)
  424. >>> compute_datetime_delta(start, end)
  425. {'hour': 0, 'month': 0, 'second': 0, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 1}
  426. >>> start = datetime(2011,10,31, 00,45,00)
  427. >>> end = datetime(2011,10,31, 01,45,00)
  428. >>> compute_datetime_delta(start, end)
  429. {'hour': 1, 'second': 0, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 60}
  430. >>> start = datetime(2011,10,31, 00,45,00)
  431. >>> end = datetime(2011,10,31, 01,15,00)
  432. >>> compute_datetime_delta(start, end)
  433. {'hour': 1, 'second': 0, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 30}
  434. >>> start = datetime(2011,10,31, 00,45,00)
  435. >>> end = datetime(2011,10,31, 12,15,00)
  436. >>> compute_datetime_delta(start, end)
  437. {'hour': 12, 'second': 0, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 690}
  438. >>> start = datetime(2011,10,31, 00,00,00)
  439. >>> end = datetime(2011,10,31, 01,00,00)
  440. >>> compute_datetime_delta(start, end)
  441. {'hour': 1, 'second': 0, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 0}
  442. >>> start = datetime(2011,10,31, 00,00,00)
  443. >>> end = datetime(2011,11,01, 01,00,00)
  444. >>> compute_datetime_delta(start, end)
  445. {'hour': 25, 'second': 0, 'max_days': 1, 'year': 0, 'day': 1, 'minute': 0}
  446. >>> start = datetime(2011,10,31, 12,00,00)
  447. >>> end = datetime(2011,11,01, 06,00,00)
  448. >>> compute_datetime_delta(start, end)
  449. {'hour': 18, 'second': 0, 'max_days': 0, 'year': 0, 'day': 0, 'minute': 0}
  450. >>> start = datetime(2011,11,01, 00,00,00)
  451. >>> end = datetime(2011,12,01, 01,00,00)
  452. >>> compute_datetime_delta(start, end)
  453. {'hour': 721, 'month': 1, 'second': 0, 'max_days': 30, 'year': 0, 'day': 0, 'minute': 0}
  454. >>> start = datetime(2011,11,01, 00,00,00)
  455. >>> end = datetime(2011,11,05, 00,00,00)
  456. >>> compute_datetime_delta(start, end)
  457. {'hour': 0, 'second': 0, 'max_days': 4, 'year': 0, 'day': 4, 'minute': 0}
  458. >>> start = datetime(2011,10,06, 00,00,00)
  459. >>> end = datetime(2011,11,05, 00,00,00)
  460. >>> compute_datetime_delta(start, end)
  461. {'hour': 0, 'second': 0, 'max_days': 30, 'year': 0, 'day': 30, 'minute': 0}
  462. >>> start = datetime(2011,12,02, 00,00,00)
  463. >>> end = datetime(2012,01,01, 00,00,00)
  464. >>> compute_datetime_delta(start, end)
  465. {'hour': 0, 'second': 0, 'max_days': 30, 'year': 1, 'day': 30, 'minute': 0}
  466. >>> start = datetime(2011,01,01, 00,00,00)
  467. >>> end = datetime(2011,02,01, 00,00,00)
  468. >>> compute_datetime_delta(start, end)
  469. {'hour': 0, 'month': 1, 'second': 0, 'max_days': 31, 'year': 0, 'day': 0, 'minute': 0}
  470. >>> start = datetime(2011,12,01, 00,00,00)
  471. >>> end = datetime(2012,01,01, 00,00,00)
  472. >>> compute_datetime_delta(start, end)
  473. {'hour': 0, 'month': 1, 'second': 0, 'max_days': 31, 'year': 1, 'day': 0, 'minute': 0}
  474. >>> start = datetime(2011,12,01, 00,00,00)
  475. >>> end = datetime(2012,06,01, 00,00,00)
  476. >>> compute_datetime_delta(start, end)
  477. {'hour': 0, 'month': 6, 'second': 0, 'max_days': 183, 'year': 1, 'day': 0, 'minute': 0}
  478. >>> start = datetime(2011,06,01, 00,00,00)
  479. >>> end = datetime(2021,06,01, 00,00,00)
  480. >>> compute_datetime_delta(start, end)
  481. {'hour': 0, 'month': 120, 'second': 0, 'max_days': 3653, 'year': 10, 'day': 0, 'minute': 0}
  482. >>> start = datetime(2011,06,01, 00,00,00)
  483. >>> end = datetime(2012,06,01, 12,00,00)
  484. >>> compute_datetime_delta(start, end)
  485. {'hour': 8796, 'month': 12, 'second': 0, 'max_days': 366, 'year': 1, 'day': 0, 'minute': 0}
  486. >>> start = datetime(2011,06,01, 00,00,00)
  487. >>> end = datetime(2012,06,01, 12,30,00)
  488. >>> compute_datetime_delta(start, end)
  489. {'hour': 8796, 'month': 12, 'second': 0, 'max_days': 366, 'year': 1, 'day': 0, 'minute': 527790}
  490. >>> start = datetime(2011,06,01, 00,00,00)
  491. >>> end = datetime(2012,06,01, 12,00,05)
  492. >>> compute_datetime_delta(start, end)
  493. {'hour': 8796, 'month': 12, 'second': 31665605, 'max_days': 366, 'year': 1, 'day': 0, 'minute': 0}
  494. >>> start = datetime(2011,06,01, 00,00,00)
  495. >>> end = datetime(2012,06,01, 00,30,00)
  496. >>> compute_datetime_delta(start, end)
  497. {'hour': 0, 'month': 12, 'second': 0, 'max_days': 366, 'year': 1, 'day': 0, 'minute': 527070}
  498. >>> start = datetime(2011,06,01, 00,00,00)
  499. >>> end = datetime(2012,06,01, 00,00,05)
  500. >>> compute_datetime_delta(start, end)
  501. {'hour': 0, 'month': 12, 'second': 31622405, 'max_days': 366, 'year': 1, 'day': 0, 'minute': 0}
  502. :return: A dictionary with year, month, day, hour, minute and second as
  503. keys()
  504. """
  505. comp = {}
  506. day_diff = (end - start).days
  507. comp["max_days"] = day_diff
  508. # Date
  509. # Count full years
  510. d = end.year - start.year
  511. comp["year"] = d
  512. # Count full months
  513. if start.month == 1 and end.month == 1:
  514. comp["month"] = 0
  515. elif start.day == 1 and end.day == 1:
  516. d = end.month - start.month
  517. if d < 0:
  518. d = d + 12 * comp["year"]
  519. elif d == 0:
  520. d = 12 * comp["year"]
  521. comp["month"] = d
  522. # Count full days
  523. if start.day == 1 and end.day == 1:
  524. comp["day"] = 0
  525. else:
  526. comp["day"] = day_diff
  527. # Time
  528. # Hours
  529. if start.hour == 0 and end.hour == 0:
  530. comp["hour"] = 0
  531. else:
  532. d = end.hour - start.hour
  533. if d < 0:
  534. d = d + 24 + 24 * day_diff
  535. else:
  536. d = d + 24 * day_diff
  537. comp["hour"] = d
  538. # Minutes
  539. if start.minute == 0 and end.minute == 0:
  540. comp["minute"] = 0
  541. else:
  542. d = end.minute - start.minute
  543. if d != 0:
  544. if comp["hour"]:
  545. d = d + 60 * comp["hour"]
  546. else:
  547. d = d + 24 * 60 * day_diff
  548. elif d == 0:
  549. if comp["hour"]:
  550. d = 60 * comp["hour"]
  551. else:
  552. d = 24 * 60 * day_diff
  553. comp["minute"] = d
  554. # Seconds
  555. if start.second == 0 and end.second == 0:
  556. comp["second"] = 0
  557. else:
  558. d = end.second - start.second
  559. if d != 0:
  560. if comp["minute"]:
  561. d = d + 60 * comp["minute"]
  562. elif comp["hour"]:
  563. d = d + 3600 * comp["hour"]
  564. else:
  565. d = d + 24 * 60 * 60 * day_diff
  566. elif d == 0:
  567. if comp["minute"]:
  568. d = 60 * comp["minute"]
  569. elif comp["hour"]:
  570. d = 3600 * comp["hour"]
  571. else:
  572. d = 24 * 60 * 60 * day_diff
  573. comp["second"] = d
  574. return comp
  575. ###############################################################################
  576. def check_datetime_string(time_string, use_dateutil=True):
  577. """Check if a string can be converted into a datetime object and return the object
  578. In case datutil is not installed the supported ISO string formats are:
  579. - YYYY-mm-dd
  580. - YYYY-mm-dd HH:MM:SS
  581. - YYYY-mm-ddTHH:MM:SS
  582. - YYYY-mm-dd HH:MM:SS.s
  583. - YYYY-mm-ddTHH:MM:SS.s
  584. Time zones are not supported
  585. If dateutil is installed, all string formats of the dateutil module
  586. are supported, as well as time zones
  587. Time zones are not supported
  588. :param time_string: The time string to be checked for conversion
  589. :param use_dateutil: Use dateutil if available for datetime string parsing
  590. :return: datetime: object or an error message string in case of an error
  591. >>> s = "2000-01-01"
  592. >>> check_datetime_string(s)
  593. datetime.datetime(2000, 1, 1, 0, 0)
  594. >>> s = "2000-01-01T10:00:00"
  595. >>> check_datetime_string(s)
  596. datetime.datetime(2000, 1, 1, 10, 0)
  597. >>> s = "2000-01-01 10:00:00"
  598. >>> check_datetime_string(s)
  599. datetime.datetime(2000, 1, 1, 10, 0)
  600. >>> s = "2000-01-01T10:00:00.000001"
  601. >>> check_datetime_string(s)
  602. datetime.datetime(2000, 1, 1, 10, 0, 0, 1)
  603. >>> s = "2000-01-01 10:00:00.000001"
  604. >>> check_datetime_string(s)
  605. datetime.datetime(2000, 1, 1, 10, 0, 0, 1)
  606. # using native implementation, ignoring dateutil
  607. >>> s = "2000-01-01"
  608. >>> check_datetime_string(s, False)
  609. datetime.datetime(2000, 1, 1, 0, 0)
  610. >>> s = "2000-01-01T10:00:00"
  611. >>> check_datetime_string(s, False)
  612. datetime.datetime(2000, 1, 1, 10, 0)
  613. >>> s = "2000-01-01 10:00:00"
  614. >>> check_datetime_string(s, False)
  615. datetime.datetime(2000, 1, 1, 10, 0)
  616. >>> s = "2000-01-01T10:00:00.000001"
  617. >>> check_datetime_string(s, False)
  618. datetime.datetime(2000, 1, 1, 10, 0, 0, 1)
  619. >>> s = "2000-01-01 10:00:00.000001"
  620. >>> check_datetime_string(s, False)
  621. datetime.datetime(2000, 1, 1, 10, 0, 0, 1)
  622. """
  623. global has_dateutil
  624. if has_dateutil and use_dateutil is True:
  625. # First check if there is only a single number, which specifies
  626. # relative time. dateutil will interprete a single number as a valid
  627. # time string, so we have to catch this case beforehand
  628. try:
  629. value = int(time_string)
  630. return _("Time string seems to specify relative time")
  631. except ValueError:
  632. pass
  633. try:
  634. time_object = parser.parse(time_string)
  635. except Exception as inst:
  636. time_object = str(inst)
  637. return time_object
  638. # BC is not supported
  639. if "bc" in time_string > 0:
  640. return _("Dates Before Christ (BC) are not supported")
  641. # BC is not supported
  642. if "+" in time_string:
  643. return _("Time zones are not supported")
  644. if ":" in time_string or "T" in time_string:
  645. # Check for microseconds
  646. if "." in time_string:
  647. if "T" in time_string:
  648. time_format = "%Y-%m-%dT%H:%M:%S.%f"
  649. else:
  650. time_format = "%Y-%m-%d %H:%M:%S.%f"
  651. else:
  652. if "T" in time_string:
  653. time_format = "%Y-%m-%dT%H:%M:%S"
  654. else:
  655. time_format = "%Y-%m-%d %H:%M:%S"
  656. else:
  657. time_format = "%Y-%m-%d"
  658. try:
  659. return datetime.strptime(time_string, time_format)
  660. except:
  661. return _("Unable to parse time string: %s" % time_string)
  662. ###############################################################################
  663. def string_to_datetime(time_string):
  664. """Convert a string into a datetime object
  665. In case datutil is not installed the supported ISO string formats are:
  666. - YYYY-mm-dd
  667. - YYYY-mm-dd HH:MM:SS
  668. - YYYY-mm-ddTHH:MM:SS
  669. - YYYY-mm-dd HH:MM:SS.s
  670. - YYYY-mm-ddTHH:MM:SS.s
  671. Time zones are not supported
  672. If dateutil is installed, all string formats of the dateutil module
  673. are supported, as well as time zones
  674. :param time_string: The time string to convert
  675. :return: datetime object or None in case the string
  676. could not be converted
  677. """
  678. if not isinstance(time_string, unicode) and not isinstance(time_string, str):
  679. return None
  680. time_object = check_datetime_string(time_string)
  681. if not isinstance(time_object, datetime):
  682. msgr = get_tgis_message_interface()
  683. msgr.error(str(time_object))
  684. return None
  685. return time_object
  686. ###############################################################################
  687. def datetime_to_grass_datetime_string(dt):
  688. """Convert a python datetime object into a GRASS datetime string
  689. .. code-block:: python
  690. >>> import grass.temporal as tgis
  691. >>> import dateutil.parser as parser
  692. >>> dt = parser.parse("2011-01-01 10:00:00 +01:30")
  693. >>> tgis.datetime_to_grass_datetime_string(dt)
  694. '01 jan 2011 10:00:00 +0090'
  695. >>> dt = parser.parse("2011-01-01 10:00:00 +02:30")
  696. >>> tgis.datetime_to_grass_datetime_string(dt)
  697. '01 jan 2011 10:00:00 +0150'
  698. >>> dt = parser.parse("2011-01-01 10:00:00 +12:00")
  699. >>> tgis.datetime_to_grass_datetime_string(dt)
  700. '01 jan 2011 10:00:00 +0720'
  701. >>> dt = parser.parse("2011-01-01 10:00:00 -01:30")
  702. >>> tgis.datetime_to_grass_datetime_string(dt)
  703. '01 jan 2011 10:00:00 -0090'
  704. """
  705. # GRASS datetime month names
  706. month_names = [
  707. "",
  708. "jan",
  709. "feb",
  710. "mar",
  711. "apr",
  712. "may",
  713. "jun",
  714. "jul",
  715. "aug",
  716. "sep",
  717. "oct",
  718. "nov",
  719. "dec",
  720. ]
  721. if dt is None:
  722. raise Exception("Empty datetime object in datetime_to_grass_datetime_string")
  723. # Check for time zone info in the datetime object
  724. if dt.tzinfo is not None:
  725. tz = dt.tzinfo.utcoffset(0)
  726. if tz.seconds > 86400 / 2:
  727. tz = (tz.seconds - 86400) / 60
  728. else:
  729. tz = tz.seconds / 60
  730. string = "%.2i %s %.2i %.2i:%.2i:%.2i %+.4i" % (
  731. dt.day,
  732. month_names[dt.month],
  733. dt.year,
  734. dt.hour,
  735. dt.minute,
  736. dt.second,
  737. tz,
  738. )
  739. else:
  740. string = "%.2i %s %.4i %.2i:%.2i:%.2i" % (
  741. dt.day,
  742. month_names[dt.month],
  743. dt.year,
  744. dt.hour,
  745. dt.minute,
  746. dt.second,
  747. )
  748. return string
  749. ###############################################################################
  750. suffix_units = {
  751. "years": "%Y",
  752. "year": "%Y",
  753. "months": "%Y_%m",
  754. "month": "%Y_%m",
  755. "weeks": "%Y_%m_%d",
  756. "week": "%Y_%m_%d",
  757. "days": "%Y_%m_%d",
  758. "day": "%Y_%m_%d",
  759. "hours": "%Y_%m_%d_%H",
  760. "hour": "%Y_%m_%d_%H",
  761. "minutes": "%Y_%m_%d_%H_%M",
  762. "minute": "%Y_%m_%d_%H_%M",
  763. }
  764. def create_suffix_from_datetime(start_time, granularity):
  765. """Create a datetime string based on a datetime object and a provided
  766. granularity that can be used as suffix for map names.
  767. dateteime=2001-01-01 00:00:00, granularity="1 month" returns "2001_01"
  768. :param start_time: The datetime object
  769. :param granularity: The granularity for example "1 month" or "100 seconds"
  770. :return: A string
  771. """
  772. global suffix_units
  773. return start_time.strftime(suffix_units[granularity.split(" ")[1]])
  774. def create_time_suffix(mapp, end=False):
  775. """Create a datetime string based on a map datetime object
  776. :param mapp: a temporal map dataset
  777. :param end: True if you want add also end time to the suffix
  778. """
  779. start = mapp.temporal_extent.get_start_time()
  780. sstring = start.isoformat().replace(":", "_").replace("-", "_")
  781. if end:
  782. end = mapp.temporal_extent.get_end_time()
  783. estring = end.isoformat().replace(":", "_").replace("-", "_")
  784. return "{st}_{en}".format(st=sstring, en=estring)
  785. return sstring
  786. def create_numeric_suffix(base, count, zeros):
  787. """Create a string based on count and number of zeros decided by zeros
  788. :param base: the basename for new map
  789. :param count: a number
  790. :param zeros: a string containing the expected number, coming from suffix option like "%05"
  791. """
  792. spli = zeros.split("%")
  793. if len(spli) == 2:
  794. suff = spli[1]
  795. if suff.isdigit():
  796. if int(suff[0]) == 0:
  797. zero = suff
  798. else:
  799. zero = "0{nu}".format(nu=suff)
  800. else:
  801. zero = "05"
  802. else:
  803. zero = "05"
  804. s = "{ba}_{i:" + zero + "d}"
  805. return s.format(ba=base, i=count)
  806. if __name__ == "__main__":
  807. import doctest
  808. doctest.testmod()