Index: stamper/filters.py
===================================================================
--- stamper/filters.py	(revision 63)
+++ stamper/filters.py	(revision 63)
@@ -0,0 +1,175 @@
+
+import re
+from datetime import datetime, date, timedelta
+
+
+class DateFilter(object):
+
+    """
+    Filtering tools for the dates input provided by the user
+    """
+
+    def __init__(self, date_format):
+        self.date_format = date_format
+
+    def days(self, current_date, days, action='-'):
+        """
+        Add/substract the given number of days from the given current_date
+        """
+        delta = timedelta(days=days)
+        if action == '-':
+            new_date = current_date - delta
+        else:
+            new_date = current_date + delta
+        new_date = new_date.replace(hour=0, minute=0, second=0)
+        return new_date
+
+    def days_ago(self, current_date, days):
+        """
+        Return the date N days ago from the current date
+        """
+        return self.days(current_date, days, '-')
+
+    def days_future(self, current_date, days):
+        """
+        Return the date N days in the future from the current date
+        """
+        return self.days(current_date, days, '+')
+
+    def months(self, current_date, months, action='-'):
+        """
+        Add/substract the given number of months from the given current_date
+        """
+        if action == '-':
+            # start travelling in time, back to N months ago
+            for n in range(months):
+                current_date = current_date.replace(day=1) - timedelta(days=1)
+        else:
+            # start travelling in time, forward to N months
+            for n in range(months):
+                current_date = current_date.replace(day=1) + timedelta(days=1)
+
+        # Now use the new year/month values + the current day to set the proper
+        # date
+        new_date = datetime(
+            current_date.year, current_date.month, date.today().day)
+        return new_date
+
+    def months_ago(self, current_date, months):
+        """
+        Return the date N days ago from the current date
+        """
+        return self.months(current_date, months, '-')
+
+    def months_future(self, current_date, months):
+        """
+        Return the date N days in the future from the current date
+        """
+        return self.months(current_date, months, '+')
+
+    def parse_number_filter(self, number_filter, future=False):
+        """
+        If a filter contains a number of days/months/years, try to find
+        out which filter it is exactly to apply it, then calculate the
+        number of days and find
+        """
+        number = None
+        filter_from = None
+
+        if re.search(r'(\d+[dD]{1})', number_filter):
+            number = int(number_filter.lower().replace('d', ''))
+            if future:
+                filtered_date = self.days_future(datetime.today(), number)
+            else:
+                filtered_date = self.days_ago(datetime.today(), number)
+
+        elif re.search(r'(\d+[wW]{1})', number_filter):
+            number = int(number_filter.lower().replace('w', '')) * 7
+            if future:
+                filtered_date = self.days_future(datetime.today(), number)
+            else:
+                filtered_date = self.days_ago(datetime.today(), number)
+
+        elif re.search(r'(\d+[mM]{1})', number_filter):
+            number = int(number_filter.lower().replace('m', ''))
+            if future:
+                filtered_date = self.months_future(datetime.today(), number)
+            else:
+                filtered_date = self.months_ago(datetime.today(), number)
+
+        elif re.search(r'(\d+[yY]{1})', number_filter):
+            number = int(number_filter.lower().replace('y', ''))
+            today = date.today()
+            if future:
+                year = today.year + number
+            else:
+                year = today.year - number
+            filtered_date = datetime(year, today.month, today.day)
+
+        return number, filtered_date
+
+    def validate(self, stamp_filter):
+        """
+        Validate a given filter. Filters can have the following notation:
+
+        - %Y-%m-%d: Times recorded at a given date
+
+        - %Y-%m-%d--%Y-%m-%d: Times recorded between two dates
+
+        - *%Y-%m-%d: Times recorded up to a  given date
+
+        - %Y-%m-%d*: Times recorded from a given date
+
+        - %Y-%m-%d+sN[d|w|m|y]: Times recorded since the given date up to
+          N more days/weeks/months/years
+
+        - N...N[d|w|m|y]: Times recorded N...N days/weeks/months/years ago
+
+        Important: all date comparisons are made on datetime objects, using
+        00:00 as the time (first second of the given day). This means that
+        for range filters, the first day is included, but the second day is not
+        """
+        filter_from = None
+        filter_to = None
+
+        if stamp_filter is None:
+            return filter_from, filter_to
+
+        if '--' in stamp_filter:
+            filter_from, filter_to = stamp_filter.split('--')
+            filter_from = datetime.strptime(filter_from, self.date_format)
+            filter_to = datetime.strptime(filter_to, self.date_format)
+
+        elif stamp_filter.startswith('*'):
+            filter_to = datetime.strptime(stamp_filter, '*'+self.date_format)
+            filter_to = filter_to.replace(hour=0, minute=0, second=0)
+
+        elif stamp_filter.endswith('*'):
+            filter_from = datetime.strptime(stamp_filter, self.date_format+'*')
+            filter_from = filter_from.replace(hour=0, minute=0, second=0)
+
+        elif '+' in stamp_filter:
+            # "+" filtering works with a date following by "+", a number and
+            # a letter (d|w|m|y) which sets the number of days, months, years
+            # we would like to go into the future
+            filter_to, number = stamp_filter.split('+')
+            filter_to = datetime.strptime(filter_to, self.date_format)
+            number, filter_from = self.parse_number_filter(number)
+            filter_from = self.days_ago(filter_to, int(number))
+        else:
+            # Check if the user is asking for N days/weeks/months/years
+            number, filter_from = self.parse_number_filter(stamp_filter)
+            if number is None:
+                # no filtering found, maybe they are giving us a fixed date
+                try:
+                    filter_from = datetime.strptime(stamp_filter,
+                                                    self.date_format)
+                except:
+                    # nothing to be used as a filter, go on, printing a warning
+                    print('[warning] invalid date filter: ' + stamp_filter)
+                else:
+                    filter_from = filter_from.replace(hour=0, minute=0,
+                                                      second=0)
+                    filter_to = filter_from + timedelta(days=1)
+
+        return filter_from, filter_to
Index: stamper/stamper.py
===================================================================
--- stamper/stamper.py	(revision 61)
+++ stamper/stamper.py	(revision 63)
@@ -1,7 +1,6 @@
 
 import json
-import re
 import os.path
-from datetime import datetime, date, timedelta
+from datetime import datetime, timedelta
 from os import symlink, remove, makedirs
 from collections import OrderedDict
@@ -11,4 +10,5 @@
 from .http import HTTPClient
 from .config import Config
+from .filters import DateFilter
 
 
@@ -29,4 +29,5 @@
         self.ensure_charts_dir()
         self.stamps = []
+        self.date_filter = DateFilter(self.date_format)
 
     def __json_load(self, filename):
@@ -93,81 +94,4 @@
                     datetime.strptime(start, self.datetime_format))
         return worktime.seconds
-
-    def validate_filter(self, stamp_filter):
-        """
-        Validate a given filter. Filters can have the following notation:
-
-        - %Y-%m-%d: Times recorded at a given date
-
-        - %Y-%m-%d--%Y-%m-%d: Times recorded between two dates
-
-        - *%Y-%m-%d: Times recorded up to a  given date
-
-        - %Y-%m-%d*: Times recorded from a given date
-
-        - N...N[d|w|m|y]: Times recorded N...N days/weeks/months/years ago
-
-        Important: all date comparisons are made on datetime objects, using
-        00:00 as the time (first second of the given day). This means that
-        for range filters, the first day is included, but the second day is not
-        """
-        filter_from = None
-        filter_to = None
-
-        if stamp_filter is None:
-            return filter_from, filter_to
-
-        if '--' in stamp_filter:
-            filter_from, filter_to = stamp_filter.split('--')
-            filter_from = datetime.strptime(filter_from, self.date_format)
-            filter_to = datetime.strptime(filter_to, self.date_format)
-
-        elif stamp_filter.startswith('*'):
-            filter_to = datetime.strptime(stamp_filter, '*'+self.date_format)
-            filter_to = filter_to.replace(hour=0, minute=0, second=0)
-
-        elif stamp_filter.endswith('*'):
-            filter_from = datetime.strptime(stamp_filter, self.date_format+'*')
-            filter_from = filter_from.replace(hour=0, minute=0, second=0)
-
-        elif re.search(r'(\d+[dD]{1})', stamp_filter):
-            number = int(stamp_filter.lower().replace('d', ''))
-            delta = timedelta(days=number)
-            filter_from = datetime.today() - delta
-            filter_from = filter_from.replace(hour=0, minute=0, second=0)
-
-        elif re.search(r'(\d+[wW]{1})', stamp_filter):
-            number = int(stamp_filter.lower().replace('w', '')) * 7
-            delta = timedelta(days=number)
-            filter_from = datetime.today() - delta
-            filter_from = filter_from.replace(hour=0, minute=0, second=0)
-
-        elif re.search(r'(\d+[mM]{1})', stamp_filter):
-            number = int(stamp_filter.lower().replace('m', ''))
-            past = date.today()
-            # start travelling in time, back to N months ago
-            for n in range(number):
-                past = past.replace(day=1) - timedelta(days=1)
-            # Now use the year/month from the past + the current day to set
-            # the proper date
-            filter_from = datetime(past.year, past.month, date.today().day)
-
-        elif re.search(r'(\d+[yY]{1})', stamp_filter):
-            number = int(stamp_filter.lower().replace('y', ''))
-            today = date.today()
-            filter_from = datetime(today.year - number, today.month, today.day)
-
-        else:
-            # maybe they are giving us a fixed date
-            try:
-                filter_from = datetime.strptime(stamp_filter, self.date_format)
-            except:
-                # nothing to be used as a filter, go on, printing a warning
-                print('[warning] invalid date filter: ' + stamp_filter)
-            else:
-                filter_from = filter_from.replace(hour=0, minute=0, second=0)
-                filter_to = filter_from + timedelta(days=1)
-
-        return filter_from, filter_to
 
     @property
@@ -252,5 +176,5 @@
 
     def timeline(self, customer=None, stamp_filter=None, filter_descr=None):
-        filter_from, filter_to = self.validate_filter(stamp_filter)
+        filter_from, filter_to = self.date_filter.validate(stamp_filter)
         for stamp in self.stamps:
             start = datetime.strptime(stamp['start'], self.datetime_format)
@@ -283,5 +207,5 @@
         Generate charts with information from the stamps
         """
-        filter_from, filter_to = self.validate_filter(stamp_filter)
+        filter_from, filter_to = self.date_filter.validate(stamp_filter)
         chart = pygal.Bar(title='Work hours per day',
                           range=(0, self.hours_day),
@@ -333,5 +257,5 @@
     def show_stamps(self, customer=None, stamp_filter=None, verbose=False,
         sum=False, filter_descr=None):
-        filter_from, filter_to = self.validate_filter(stamp_filter)
+        filter_from, filter_to = self.date_filter.validate(stamp_filter)
 
         # If the user asks for verbose information, show it before the
@@ -441,5 +365,5 @@
 
     def push_stamps(self, customer=None, stamp_filter=None, filter_descr=None):
-        filter_from, filter_to = self.validate_filter(stamp_filter)
+        filter_from, filter_to = self.date_filter.validate(stamp_filter)
 
         stamps = []
