Skip to content

Commit be87139

Browse files
Merge pull request #22 from nickmaccarthy/issue_21
Issue 21 fix
2 parents b715364 + 69a313e commit be87139

File tree

6 files changed

+117
-46
lines changed

6 files changed

+117
-46
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
*.pyc
22
env
3+
venv
34
arrow_test.py
45
dm_pretty.py
56
ranges.py

CHANGELOG.md

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

3+
## 1.5.2 (2020-10-01)
4+
* [FIX] [Issue #21](https://github.com/nickmaccarthy/python-datemath/issues/21) - Fixed an issue where if timezone offset was in a datetime string (ISO8601), the timezone of the returned datemath object would be UTC and not the timezone as specified in the datetime string.
5+
36
## 1.5.1 (2020-03-25)
47

58
* [FIX] [Issue #15](https://github.com/nickmaccarthy/python-datemath/issues/15) - Fixed issue with parser finding invalid timeunits and throwing correct errors

README.md

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
[![Build Status](https://travis-ci.org/nickmaccarthy/python-datemath.svg?branch=master)](https://travis-ci.org/nickmaccarthy/python-datemath.svg?branch=master)
22

3+
# Python Datemath
4+
5+
## What?
36

4-
# What?
57
A date math (aka datemath) parser compatiable with the elasticsearch 'date math' format
68

7-
# Why?
8-
Working with date objects in python has always been interesting. Having a background in php, I have been looking for quite some time ( no pun intended ) for a way to do date time interpolation similar to php's ```strtotime()``` function. While the arrow module comes close, I needed something that could turn date math type strings into datetime objects for use in tattle.io and other projects I use in elasticsearch. I have found even more uses for it, including AWS cloudwatch and various other projects and hopefully you will too.
9+
## Why?
10+
11+
Working with date objects in python has always been interesting. Having a background in php, I have been looking for quite some time ( no pun intended ) for a way to do date time interpolation similar to php's ```strtotime()``` function. While the arrow module comes close, I needed something that could turn date math type strings into datetime objects for use in [tattle.io](http://tattle.io) and other projects I use in elasticsearch. I have found even more uses for it, including AWS cloudwatch and various other projects and hopefully you will too.
12+
13+
## What is date math?
914

10-
# What is date math ?
1115
Date Math is the short hand arithmetic to find relative time to fixed moments in date and time. Similar to the SOLR date math format, Elasticsearch has its own built in format for short hand date math and this module aims to support that same coverage in python.
1216

1317
Documentation from elasticsearch:
14-
http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-date-format.html#date-math
18+
[http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-date-format.html#date-math](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-date-format.html#date-math)
1519

1620
> The date type supports using date math expression when using it in a query/filter (mainly makes sense in range query/filter).
17-
21+
>
1822
> The expression starts with an "anchor" date, which can be either now or a date string (in the applicable format) ending with `||`.
19-
23+
>
2024
> It can then follow by a math expression, supporting `+`, `-` and `/` (rounding).
21-
25+
>
2226
> The units supported are `y` (year), `M` (month), `w` (week), `d` (day), `h` (hour), `m` (minute), and `s` (second).
23-
27+
>
2428
> Here are some samples: `now+1h`, `now+1h+1m`, `now+1h/d`, `2012-01-01||+1M/d`.
25-
29+
>
2630
> Note, when doing range type searches, and the upper value is inclusive, the rounding will properly be rounded to the ceiling instead of flooring it.
2731
28-
# Unit Maps
29-
```
32+
## Unit Maps
33+
34+
```yaml
3035
y or Y = 'year'
3136
M = 'month'
3237
m = 'minute'
@@ -36,13 +41,17 @@ h or H = 'hour'
3641
s or S = 'second'
3742
```
3843

39-
# Install
44+
## Install
45+
4046
```python
4147
pip install python-datemath
4248
```
43-
# Examples
44-
Assuming our datetime is currently: '2016-01-01T00:00:00-00:00'
45-
```
49+
50+
## Examples
51+
52+
Assuming our datetime is currently: `2016-01-01T00:00:00-00:00`
53+
54+
```yaml
4655
Expression: Result:
4756
now-1h 2015-12-31T23:00:00+00:00
4857
now-1y 2015-01-01T00:00:00+00:00
@@ -62,10 +71,11 @@ now/d 2016-01-01T23:59:59+00:00
6271
now/Y 2016-12-31T23:59:59+00:00
6372
```
6473

65-
# Usage
74+
## Usage
75+
6676
By default datemath return an arrow date object representing your timestamp.
6777

68-
```
78+
```python
6979
>>> from datemath import dm
7080
>>>
7181
>>> dm('now+1h')
@@ -92,7 +102,8 @@ By default datemath return an arrow date object representing your timestamp.
92102

93103
If you would rather have a string, you can use arrow's ```.format()``` method.
94104
> For for info on string formatting, check out arrows tokens section: http://crsmithdev.com/arrow/#tokens
95-
```
105+
106+
```python
96107
>>> from datemath import dm
97108
>>>
98109
>>> src_timestamp = dm('2016-01-01')
@@ -105,11 +116,11 @@ If you would rather have a string, you can use arrow's ```.format()``` method.
105116
>>>
106117
>>> new_timestamp.format('YYYY.MM.DD')
107118
u'2015.12.18'
108-
>>>
109119
```
110120

111121
Rather have a python datetime object instead? Just pass along the 'datetime' type
112-
```
122+
123+
```python
113124
from datemath import dm
114125
>>> dm('now', type='datetime')
115126
datetime.datetime(2016, 1, 22, 22, 58, 28, 338060, tzinfo=tzutc())
@@ -119,7 +130,8 @@ datetime.datetime(2016, 1, 24, 22, 57, 45, 394470, tzinfo=tzutc())
119130
```
120131

121132
Or you can just import the `datemath` module, this will always give us a native `datetime` object
122-
```
133+
134+
```python
123135
>>> from datemath import datemath
124136
>>>
125137
>>> datemath('2016-01-01T16:20:00||/d', roundDown=False)
@@ -129,20 +141,28 @@ datetime.datetime(2016, 1, 1, 23, 59, 59, 999999, tzinfo=tzutc())
129141
>>> # roundDown=True is default and implied
130142
>>> datemath('2016-01-01T16:20:00||/d')
131143
datetime.datetime(2016, 1, 1, 0, 0, tzinfo=tzutc())
132-
>>>
133144
```
134145

135146
If you want a Epoch timestamp back instead, we can do that.
147+
136148
```python
137149
>>> dm('now+2d-1m', type='timestamp')
138150
1453676321
139151
```
140152

141-
# What timezone are my objects in?
142-
By default all objects returned by datemath are in UTC. If you want them them in a different timezone, just pass along the ```tz``` argument.
143-
Timezone list can be found here: https://gist.github.com/pamelafox/986163
144-
```
145-
from datemath import dm
153+
## What timezone are my objects in?
154+
155+
By default all object returned by datemath are in UTC.
156+
157+
If you want them them back in a different timezone, just pass along the ```tz``` argument. Timezone list can be found here: [https://gist.github.com/pamelafox/986163](https://gist.github.com/pamelafox/986163)
158+
159+
If you provide a timezone offset in your timestring, datemath will return your time object as that timezone offset in the string.
160+
161+
Note - currently timestrings with a timezone offset and the usage of the ```tz``` argument will result in the time object being returned with the timezone of what was in the timezone offset in the original string
162+
163+
```python
164+
>>> from datemath import dm
165+
>>>
146166
>>> dm('now')
147167
<Arrow [2016-01-26T01:00:53.601088+00:00]>
148168
>>>
@@ -154,9 +174,26 @@ from datemath import dm
154174
>>>
155175
>>> dm('2017-10-20 09:15:20', tz='US/Pacific')
156176
<Arrow [2017-10-20T09:15:20.000000-08:00]>
177+
>>>
178+
>>> # Timestring with TZ offset in the string (ISO8601 format only)
179+
>>> dm('2016-01-01T00:00:00-05:00')
180+
<Arrow [2016-01-01T00:00:00-05:00]>
181+
>>>
182+
>>> # Timestring with TZ offset with datemath added (again, TS must be in ISO8601)
183+
>>> dm('2016-01-01T00:00:00-05:00||+2d+3h+5m')
184+
<Arrow [2016-01-03T03:05:00-05:00]>
185+
>>>
186+
>>> # Note, timestrings with TZ offsets will be returned as the timezone of the offset in the string even if the "tz" option is used.
187+
>>> dm('2016-01-01T00:00:00-05:00', tz='US/Central')
188+
<Arrow [2016-01-01T00:00:00-05:00]>
157189
```
158190

191+
## Debugging
192+
193+
If you would like more verbose output to debug the process of what datemath is doing, simply set `export DATEMATH_DEBUG=true` in your shell then run some datemath tests. To stop debugging, run `unset DATEMATH_DEBUG`.
194+
159195
## Changes
196+
160197
See CHANGELOG.md
161198

162199
# Happy date math'ing!

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.5.1
1+
1.5.2

datemath/helpers.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@
4242
import re
4343
import os
4444
from dateutil import tz
45+
import dateutil
4546
import sys
47+
from pprint import pprint
4648

47-
debug = True if os.environ.get('DEBUG') == 'true' else False
49+
debug = True if os.environ.get('DATEMATH_DEBUG') else False
4850

4951
class DateMathException(Exception):
5052
pass
@@ -75,7 +77,7 @@ def unitMap(c):
7577

7678
def as_datetime(expression, now, tz='UTC'):
7779
'''
78-
returs our datemath expression as a python datetime object
80+
returns our datemath expression as a python datetime object
7981
note: this has been deprecated and the 'type' argument in parse is the current way
8082
'''
8183
return parse(expression, now, tz)
@@ -89,26 +91,28 @@ def parse(expression, now=None, tz='UTC', type=None, roundDown=True):
8991
:param type - if we are dealing with a arrow or datetime object
9092
:param roundDown - wether or not we should round up or round down on this. default is roundDown=True, which means if it was 12:00:00, `/d` would be '00:00:00', and with roundDown=False, `/d` would be '29:59:59'
9193
'''
94+
if debug: print("parse() - starting for expression: {}".format(expression))
9295
if now is None:
96+
if debug: print("parse() - Now is None, setting now to utcnow()")
9397
now = arrow.utcnow()
9498

95-
if debug: print("Orig Expression: {0}".format(expression))
99+
if debug: print("parse() - Orig Expression: {0}".format(expression))
96100

97101
math = ''
98102
time = ''
99103

100104
if 'UTC' not in tz:
101-
if debug: print("will now convert tz to {0}".format(tz))
105+
if debug: print("parse() - will now convert tz to {0}".format(tz))
102106
now = now.to(tz)
103107

104108
if expression == 'now':
105-
if debug: print("Now, no dm: {0}".format(now))
109+
if debug: print("parse() - Now, no dm: {0}".format(now))
106110
if type:
107111
return getattr(now, type)
108112
else:
109113
return now
110114
elif re.match('\d{10,}', str(expression)):
111-
if debug: print('found an epoch timestamp')
115+
if debug: print('parse() - found an epoch timestamp')
112116
if len(str(expression)) == 13:
113117
raise DateMathException('Unable to parse epoch timestamps in millis, please convert to the nearest second to continue - i.e. 1451610061 / 1000')
114118
ts = arrow.get(int(expression))
@@ -118,7 +122,7 @@ def parse(expression, now=None, tz='UTC', type=None, roundDown=True):
118122
''' parse our standard "now+1d" kind of queries '''
119123
math = expression[3:]
120124
time = now
121-
if debug: print('now expression: {0}'.format(now))
125+
if debug: print('parse() - now expression: {0}'.format(now))
122126
else:
123127
''' parse out datemath with date, ex "2015-10-20||+1d" '''
124128
if '||' in expression:
@@ -137,24 +141,40 @@ def parse(expression, now=None, tz='UTC', type=None, roundDown=True):
137141

138142
if not math or math == '':
139143
rettime = time
144+
140145
rettime = evaluate(math, time, tz, roundDown)
141146

142147
if type:
143148
return getattr(rettime, type)
144149
else:
145150
return rettime
146151

147-
148152
def parseTime(timestamp, timezone='UTC'):
149153
'''
150-
parses a date/time stamp and returns and arrow object
154+
parses a datetime string and returns and arrow object
151155
'''
152156
if timestamp and len(timestamp) >= 4:
153157
ts = arrow.get(timestamp)
154-
ts = ts.replace(tzinfo=timezone)
155-
return ts
158+
if debug: print("parseTime() - ts = {} :: vars :: {}".format(ts, vars(ts)))
159+
if debug: print("parseTime() - ts timezone = {}".format(ts.tzinfo))
160+
if debug: print("parseTime() - tzinfo type = {}".format(type(ts.tzinfo)))
161+
if debug: print("parseTime() - timezone that came in = {}".format(timezone))
162+
163+
if ts.tzinfo:
164+
import dateutil
165+
if isinstance(ts.tzinfo, dateutil.tz.tz.tzoffset):
166+
# this means our TZ probably came in via our datetime string
167+
# then lets set our tz to whatever tzoffset is
168+
ts = ts.replace(tzinfo=ts.tzinfo)
169+
elif isinstance(ts.tzinfo, dateutil.tz.tz.tzutc):
170+
# otherwise if we are utc, then lets just set it to be as such
171+
ts = ts.replace(tzinfo=timezone)
172+
else:
173+
# otherwise lets just ensure its set to whatever timezone came in
174+
ts = ts.replace(tzinfo=timezone)
156175

157-
176+
return ts
177+
158178
def roundDate(now, unit, tz='UTC', roundDown=True):
159179
'''
160180
rounds our date object
@@ -163,7 +183,7 @@ def roundDate(now, unit, tz='UTC', roundDown=True):
163183
now = now.floor(unit)
164184
else:
165185
now = now.ceil(unit)
166-
if debug: print("roundDate Now: {0}".format(now))
186+
if debug: print("roundDate() Now: {0}".format(now))
167187
return now
168188

169189
def calculate(now, offsetval, unit):
@@ -175,7 +195,7 @@ def calculate(now, offsetval, unit):
175195
offsetval = int(offsetval)
176196
try:
177197
now = now.shift(**{unit: offsetval})
178-
if debug: print("Calculate called: now: {}, offsetval: {}, offsetval-type: {}, unit: {}".format(now, offsetval, type(offsetval), unit))
198+
if debug: print("calculate() called: now: {}, offsetval: {}, offsetval-type: {}, unit: {}".format(now, offsetval, type(offsetval), unit))
179199
return now
180200
except Exception as e:
181201
raise DateMathException('Unable to calculate date: now: {0}, offsetvalue: {1}, unit: {2} - reason: {3}'.format(now,offsetval,unit,e))
@@ -184,8 +204,8 @@ def evaluate(expression, now, timeZone='UTC', roundDown=True):
184204
'''
185205
evaluates our datemath style expression
186206
'''
187-
if debug: print('Expression: {0}'.format(expression))
188-
if debug: print('Now: {0}'.format(now))
207+
if debug: print('evaluate() - Expression: {0}'.format(expression))
208+
if debug: print('evaluate() - Now: {0}'.format(now))
189209
val = 0
190210
i = 0
191211
while i < len(expression):
@@ -221,7 +241,7 @@ def evaluate(expression, now, timeZone='UTC', roundDown=True):
221241
raise DateMathException(''''{}' is not a valid timeunit for expression: '{}' '''.format(char, expression))
222242

223243
i += 1
224-
if debug: print("Fin: {0}".format(now))
244+
if debug: print("evaluate() - Finished: {0}".format(now))
225245
if debug: print('\n\n')
226246
return now
227247

tests.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import unittest2 as unittest
66
import arrow
77
from datetime import datetime as pydatetime
8+
from datetime import timedelta
89
from datemath import dm, datemath
910
from datemath.helpers import DateMathException as DateMathException
1011
from dateutil import tz
@@ -40,6 +41,15 @@ def testParse(self):
4041
self.assertEqual(dm('2016-01-01', tz='US/Eastern'), pydatetime(2016, 1, 1, tzinfo=tz.gettz('US/Eastern')))
4142
self.assertEqual(datemath('2016-01-01T01:00:00', tz='US/Central'), pydatetime(2016, 1, 1, 1, 0, 0, tzinfo=tz.gettz('US/Central')))
4243
self.assertEqual(datemath('2016-01-01T02:00:00', tz='US/Eastern'), pydatetime(2016, 1, 1, 2, tzinfo=tz.gettz('US/Eastern')))
44+
# TZ offset inside of date string
45+
self.assertEqual(datemath('2016-01-01T16:20:00.5+12:00'), pydatetime(2016, 1, 1, 16, 20, 0, 500000, tzinfo=tz.tzoffset(None, timedelta(hours=12))))
46+
self.assertEqual(datemath('2016-01-01T16:20:00.5-05:00'), pydatetime(2016, 1, 1, 16, 20, 0, 500000, tzinfo=tz.tzoffset(None, timedelta(hours=-5))))
47+
self.assertEqual(datemath('2016-01-01T16:20:00.5-00:00'), pydatetime(2016, 1, 1, 16, 20, 0, 500000, tzinfo=tz.tzoffset(None, timedelta(hours=0))))
48+
# TZ offset inside of date string with datemath
49+
self.assertEqual(datemath('2016-01-01T16:20:00.5+12:00||+1d'), pydatetime(2016, 1, 2, 16, 20, 0, 500000, tzinfo=tz.tzoffset(None, timedelta(hours=12))))
50+
self.assertEqual(datemath('2016-01-01T16:20:00.6+12:00||+2d+1h'), pydatetime(2016, 1, 3, 17, 20, 0, 600000, tzinfo=tz.tzoffset(None, timedelta(hours=12))))
51+
# If a TZ offset is in a datetime string, and there is a tz param used, the TZ offset will take precedence for the returned timeobj
52+
self.assertEqual(datemath('2016-01-01T16:20:00.6+12:00||+2d+1h', tz='US/Eastern'), pydatetime(2016, 1, 3, 17, 20, 0, 600000, tzinfo=tz.tzoffset(None, timedelta(hours=12))))
4353

4454
# relitive formats
4555
# addition

0 commit comments

Comments
 (0)