← Back to team overview

gtg team mailing list archive

[Merge] lp:~gtg-contributors/gtg/new-date-class into lp:gtg

 

Paul Kishimoto has proposed merging lp:~gtg-contributors/gtg/new-date-class into lp:gtg.

Requested reviews:
  Gtg developers (gtg)


This branch almost entirely rewrites the various classes that were contained in GTG/tools/dates.py.

When the GTG UI and backend are separated over DBus, Python objects (including built-in and custom dates) cannot be passed directly. Strings can be used instead. The new Date class in this code is designed to always obey:

  Date(str(d)) = d

for any Date instance d, even for special dates ('soon', 'later', etc.). As a result, neither client nor server code need make any distinction between FuzzyDates or RealDates or so on; it can simply construct Date() with the information passed over DBus.

The class also follows some of the semantics from the Python datetime module; for example:

  d1 = datetime.date.today() # get a date instance representing today
  d2 = Date.soon() # get a Date instance representing the special date 'soon'

I have tested the branch in several ways, but some additional experimentation would be appreciated to see if any bugs were introduced.
-- 
https://code.launchpad.net/~gtg-contributors/gtg/new-date-class/+merge/28009
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-contributors/gtg/new-date-class into lp:gtg.
=== modified file 'GTG/core/filters_bank.py'
--- GTG/core/filters_bank.py	2010-06-14 19:30:50 +0000
+++ GTG/core/filters_bank.py	2010-06-20 04:28:25 +0000
@@ -23,8 +23,8 @@
 
 from datetime import datetime
 
-from GTG.core.task import Task
-from GTG.tools.dates  import date_today, no_date, Date
+from GTG.core.task    import Task
+from GTG.tools.dates  import *
 
 
 class Filter:
@@ -179,12 +179,12 @@
     def is_started(self,task,parameters=None):
         '''Filter for tasks that are already started'''
         start_date = task.get_start_date()
-        if start_date :
+        if start_date:
             #Seems like pylint falsely assumes that subtraction always results
             #in an object of the same type. The subtraction of dates 
             #results in a datetime.timedelta object 
             #that does have a 'days' member.
-            difference = date_today() - start_date
+            difference = Date.today() - start_date
             if difference.days == 0:
                 # Don't count today's tasks started until morning
                 return datetime.now().hour > 4
@@ -202,14 +202,14 @@
     def workdue(self,task):
         ''' Filter for tasks due within the next day '''
         wv = self.workview(task) and \
-             task.get_due_date() != no_date and \
+             task.get_due_date() != Date.no_date() and \
              task.get_days_left() < 2
         return wv
 
     def worklate(self,task):
         ''' Filter for tasks due within the next day '''
         wv = self.workview(task) and \
-             task.get_due_date() != no_date and \
+             task.get_due_date() != Date.no_date() and \
              task.get_days_late() > 0
         return wv
 

=== modified file 'GTG/core/requester.py'
--- GTG/core/requester.py	2010-06-12 13:59:24 +0000
+++ GTG/core/requester.py	2010-06-20 04:28:25 +0000
@@ -27,7 +27,6 @@
 from GTG.core.filters_bank import FiltersBank
 from GTG.core.task         import Task
 from GTG.core.tagstore     import Tag
-from GTG.tools.dates       import date_today
 from GTG.tools.logger      import Log
 
 class Requester(gobject.GObject):

=== modified file 'GTG/core/task.py'
--- GTG/core/task.py	2010-06-18 16:36:17 +0000
+++ GTG/core/task.py	2010-06-20 04:28:25 +0000
@@ -20,15 +20,15 @@
 """
 task.py contains the Task class which represents (guess what) a task
 """
-
+import cgi
+from datetime         import datetime
+import uuid
 import xml.dom.minidom
-import uuid
-import cgi
 import xml.sax.saxutils as saxutils
 
 from GTG              import _
-from GTG.tools.dates  import date_today, no_date, Date
-from datetime         import datetime
+from GTG.tools.dates  import *
+
 from GTG.core.tree    import TreeNode
 from GTG.tools.logger import Log
 
@@ -55,9 +55,9 @@
         self.title = _("My new task")
         #available status are: Active - Done - Dismiss - Note
         self.status = self.STA_ACTIVE
-        self.closed_date = no_date
-        self.due_date = no_date
-        self.start_date = no_date
+        self.closed_date = Date.no_date()
+        self.due_date = Date.no_date()
+        self.start_date = Date.no_date()
         self.can_be_deleted = newtask
         # tags
         self.tags = []
@@ -134,10 +134,10 @@
                         c.set_status(status, donedate=donedate)
                 #to the specified date (if any)
                 if donedate:
-                    self.closed_date = donedate
+                    self.closed_date = Date(donedate)
                 #or to today
                 else:
-                    self.closed_date = date_today()
+                    self.closed_date = Date.today()
             #If we mark a task as Active and that some parent are not
             #Active, we break the parent/child relation
             #It has no sense to have an active subtask of a done parent.
@@ -166,14 +166,13 @@
         return self.modified
 
     def get_modified_string(self):
-        return self.modified.strftime("%Y-%m-%dT%H:%M:%S")
+        return self.modified.isoformat()
 
     def set_modified(self, modified):
         self.modified = modified
 
     def set_due_date(self, fulldate):
-        assert(isinstance(fulldate, Date))
-        self.due_date = fulldate
+        self.due_date = Date(fulldate)
         self.sync()
 
     #Due date return the most urgent date of all parents
@@ -189,16 +188,14 @@
         return zedate
 
     def set_start_date(self, fulldate):
-        assert(isinstance(fulldate, Date))
-        self.start_date = fulldate
+        self.start_date = Date(fulldate)
         self.sync()
 
     def get_start_date(self):
         return self.start_date
 
     def set_closed_date(self, fulldate):
-        assert(isinstance(fulldate, Date))
-        self.closed_date = fulldate
+        self.closed_date = Date(fulldate)
         self.sync()
         
     def get_closed_date(self):
@@ -206,13 +203,13 @@
 
     def get_days_left(self):
         due_date = self.get_due_date()
-        if due_date == no_date:
+        if due_date == Date.no_date():
             return None
-        return due_date.days_left()
+        return (due_date - Date.today()).days
     
     def get_days_late(self):
         due_date = self.get_due_date()
-        if due_date == no_date:
+        if due_date == Date.no_date():
             return None
         closed_date = self.get_closed_date()
         return (closed_date - due_date).days

=== modified file 'GTG/gtk/browser/browser.py'
--- GTG/gtk/browser/browser.py	2010-06-17 08:58:32 +0000
+++ GTG/gtk/browser/browser.py	2010-06-20 04:28:25 +0000
@@ -45,10 +45,7 @@
                                         ClosedTaskTreeView
 from GTG.gtk.browser.tagtree     import TagTree
 from GTG.tools                   import openurl
-from GTG.tools.dates             import strtodate,\
-                                        no_date,\
-                                        FuzzyDate, \
-                                        get_canonical_date
+from GTG.tools.dates             import *
 from GTG.tools.logger            import Log
 #from GTG.tools                   import clipboard
 
@@ -607,13 +604,12 @@
                 return s
             else:
                 return -1 * s
-        
-        
+
         if sort == 0:
             # Put fuzzy dates below real dates
-            if isinstance(t1, FuzzyDate) and not isinstance(t2, FuzzyDate):
+            if t1.is_special and not t2.is_special:
                 sort = reverse_if_descending(1)
-            elif isinstance(t2, FuzzyDate) and not isinstance(t1, FuzzyDate):
+            elif t2.is_special and not t1.is_special:
                 sort = reverse_if_descending(-1)
         
         if sort == 0: # Group tasks with the same tag together for visual cleanness 
@@ -915,8 +911,8 @@
 
     def on_quickadd_activate(self, widget):
         text = self.quickadd_entry.get_text()
-        due_date = no_date
-        defer_date = no_date
+        due_date = Date.no_date()
+        defer_date = Date.no_date()
         if text:
             tags, notagonly = self.get_selected_tags()
             # Get tags in the title
@@ -940,12 +936,12 @@
                         tags.append(GTG.core.tagstore.Tag(tag, self.req))
                 elif attribute.lower() == "defer" or \
                      attribute.lower() == _("defer"):
-                    defer_date = get_canonical_date(args)
+                    defer_date = Date.parse(args)
                     if not defer_date:
                         valid_attribute = False
                 elif attribute.lower() == "due" or \
                      attribute.lower() == _("due"):
-                    due_date = get_canonical_date(args)
+                    due_date = Date.parse(args)
                     if not due_date:
                         valid_attribute = False
                 else:
@@ -1114,7 +1110,7 @@
         tasks = [self.req.get_task(uid) for uid in tasks_uid]
         tasks_status = [task.get_status() for task in tasks]
         for uid, task, status in zip(tasks_uid, tasks, tasks_status):
-            task.set_start_date(get_canonical_date(new_start_date))
+            task.set_start_date(Date.parse(new_start_date))
         #FIXME: If the task dialog is displayed, refresh its start_date widget
 
     def on_mark_as_started(self, widget):

=== modified file 'GTG/gtk/dbuswrapper.py'
--- GTG/gtk/dbuswrapper.py	2010-06-10 14:45:36 +0000
+++ GTG/gtk/dbuswrapper.py	2010-06-20 04:28:25 +0000
@@ -23,8 +23,8 @@
 import dbus.glib
 import dbus.service
 
-from GTG.core  import CoreConfig
-from GTG.tools import dates
+from GTG.core        import CoreConfig
+from GTG.tools.dates import *
 
 
 BUSNAME = CoreConfig.BUSNAME
@@ -169,10 +169,10 @@
         nt = self.req.new_task(tags=tags)
         for sub in subtasks:
             nt.add_child(sub)
-        nt.set_status(status, donedate=dates.strtodate(donedate))
+        nt.set_status(status, donedate=Date.parse(donedate))
         nt.set_title(title)
-        nt.set_due_date(dates.strtodate(duedate))
-        nt.set_start_date(dates.strtodate(startdate))
+        nt.set_due_date(Date.parse(duedate))
+        nt.set_start_date(Date.parse(startdate))
         nt.set_text(text)
         return task_to_dict(nt)
 
@@ -187,10 +187,10 @@
         via this function.        
         """
         task = self.req.get_task(tid)
-        task.set_status(task_data["status"], donedate=dates.strtodate(task_data["donedate"]))
+        task.set_status(task_data["status"], donedate=Date.parse(task_data["donedate"]))
         task.set_title(task_data["title"])
-        task.set_due_date(dates.strtodate(task_data["duedate"]))
-        task.set_start_date(dates.strtodate(task_data["startdate"]))
+        task.set_due_date(Date.parse(task_data["duedate"]))
+        task.set_start_date(Date.parse(task_data["startdate"]))
         task.set_text(task_data["text"])
 
         for tag in task_data["tags"]:

=== modified file 'GTG/gtk/editor/editor.py'
--- GTG/gtk/editor/editor.py	2010-06-07 21:14:45 +0000
+++ GTG/gtk/editor/editor.py	2010-06-20 04:28:25 +0000
@@ -45,7 +45,7 @@
 from GTG.core.plugins.engine import PluginEngine
 from GTG.core.plugins.api    import PluginAPI
 from GTG.core.task           import Task
-from GTG.tools               import dates
+from GTG.tools.dates         import *
 
 
 date_separator = "-"
@@ -307,13 +307,13 @@
              
         #refreshing the due date field
         duedate = self.task.get_due_date()
-        prevdate = dates.strtodate(self.duedate_widget.get_text())
+        prevdate = Date.parse(self.duedate_widget.get_text())
         if duedate != prevdate or type(duedate) is not type(prevdate):
             zedate = str(duedate).replace("-", date_separator)
             self.duedate_widget.set_text(zedate)
         # refreshing the closed date field
         closeddate = self.task.get_closed_date()
-        prevcldate = dates.strtodate(self.closeddate_widget.get_text())
+        prevcldate = Date.parse(self.closeddate_widget.get_text())
         if closeddate != prevcldate or type(closeddate) is not type(prevcldate):
             zecldate = str(closeddate).replace("-", date_separator)
             self.closeddate_widget.set_text(zecldate)
@@ -348,7 +348,7 @@
         self.dayleft_label.set_markup("<span color='"+color+"'>"+txt+"</span>")
 
         startdate = self.task.get_start_date()
-        prevdate = dates.strtodate(self.startdate_widget.get_text())
+        prevdate = Date.parse(self.startdate_widget.get_text())
         if startdate != prevdate or type(startdate) is not type(prevdate):
             zedate = str(startdate).replace("-",date_separator)
             self.startdate_widget.set_text(zedate) 
@@ -377,9 +377,9 @@
         validdate = False
         if not text :
             validdate = True
-            datetoset = dates.no_date
+            datetoset = Date.no_date()
         else :
-            datetoset = dates.strtodate(text)
+            datetoset = Date.parse(text)
             if datetoset :
                 validdate = True
                 
@@ -400,15 +400,15 @@
             widget.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#F88"))
 
     def _mark_today_in_bold(self):
-        today = dates.date_today()
+        today = Date.today()
         #selected is a tuple containing (year, month, day)
         selected = self.cal_widget.get_date()
         #the following "-1" is because in pygtk calendar the month is 0-based,
         # in gtg (and datetime.date) is 1-based.
-        if selected[1] == today.month() - 1 and selected[0] == today.year():
-            self.cal_widget.mark_day(today.day())
+        if selected[1] == today.month - 1 and selected[0] == today.year:
+            self.cal_widget.mark_day(today.day)
         else:
-            self.cal_widget.unmark_day(today.day())
+            self.cal_widget.unmark_day(today.day)
         
         
     def on_date_pressed(self, widget,data): 
@@ -440,15 +440,13 @@
         gdk.pointer_grab(self.calendar.window, True,gdk.BUTTON1_MASK|gdk.MOD2_MASK)
         #we will close the calendar if the user clicks outside
         
-        if not isinstance(toset, dates.FuzzyDate):
-            if not toset:
-                # we set the widget to today's date if there is not a date defined
-                toset = dates.date_today()
-
-            y = toset.year()
-            m = toset.month()
-            d = int(toset.day())
-            
+        if not toset:
+            # we set the widget to today's date if there is not a date defined
+            toset = Date.today()
+        elif not toset.is_special:
+            y = toset.year
+            m = toset.month
+            d = int(toset.day)
             #We have to select the day first. If not, we might ask for
             #February while still being on 31 -> error !
             self.cal_widget.select_day(d)
@@ -463,11 +461,11 @@
     def day_selected(self,widget) :
         y,m,d = widget.get_date()
         if self.__opened_date == "due" :
-            self.task.set_due_date(dates.strtodate("%s-%s-%s"%(y,m+1,d)))
+            self.task.set_due_date(Date.parse("%s-%s-%s"%(y,m+1,d)))
         elif self.__opened_date == "start" :
-            self.task.set_start_date(dates.strtodate("%s-%s-%s"%(y,m+1,d)))
+            self.task.set_start_date(Date.parse("%s-%s-%s"%(y,m+1,d)))
         elif self.__opened_date == "closed" :
-            self.task.set_closed_date(dates.strtodate("%s-%s-%s"%(y,m+1,d)))
+            self.task.set_closed_date(Date.parse("%s-%s-%s"%(y,m+1,d)))
         if self.close_when_changed :
             #When we select a day, we connect the mouse release to the
             #closing of the calendar.
@@ -497,16 +495,16 @@
         self.__close_calendar()
         
     def nodate_pressed(self,widget) : #pylint: disable-msg=W0613
-        self.set_opened_date(dates.no_date)
+        self.set_opened_date(Date.no_date())
         
     def set_fuzzydate_now(self, widget) : #pylint: disable-msg=W0613
-        self.set_opened_date(dates.NOW)
+        self.set_opened_date(Date.today())
         
     def set_fuzzydate_soon(self, widget) : #pylint: disable-msg=W0613
-        self.set_opened_date(dates.SOON)
+        self.set_opened_date(Date.soon())
         
     def set_fuzzydate_later(self, widget) : #pylint: disable-msg=W0613
-        self.set_opened_date(dates.LATER)
+        self.set_opened_date(Date.later())
         
     def dismiss(self,widget) : #pylint: disable-msg=W0613
         stat = self.task.get_status()

=== modified file 'GTG/plugins/evolution_sync/gtgTask.py'
--- GTG/plugins/evolution_sync/gtgTask.py	2010-03-16 02:16:14 +0000
+++ GTG/plugins/evolution_sync/gtgTask.py	2010-06-20 04:28:25 +0000
@@ -16,9 +16,10 @@
 
 import datetime
 
-from GTG.tools.dates import NoDate, RealDate
+from GTG.tools.dates                        import *
 from GTG.plugins.evolution_sync.genericTask import GenericTask
 
+
 class GtgTask(GenericTask):
 
     def __init__(self, gtg_task, plugin_api, gtg_proxy):
@@ -62,15 +63,15 @@
 
     def _get_due_date(self):
         due_date = self._gtg_task.get_due_date()
-        if due_date == NoDate():
+        if due_date == Date.no_date():
                 return None
-        return due_date.to_py_date()
+        return due_date._date
 
     def _set_due_date(self, due):
         if due == None:
-            gtg_due = NoDate()
+            gtg_due = Date.no_date()
         else:
-            gtg_due = RealDate(due)
+            gtg_due = Date(due)
         self._gtg_task.set_due_date(gtg_due)
 
     def _get_modified(self):

=== modified file 'GTG/plugins/rtm_sync/gtgTask.py'
--- GTG/plugins/rtm_sync/gtgTask.py	2010-05-05 21:54:17 +0000
+++ GTG/plugins/rtm_sync/gtgTask.py	2010-06-20 04:28:25 +0000
@@ -16,9 +16,10 @@
 
 import datetime
 
-from GTG.tools.dates import NoDate, RealDate
+from GTG.tools.dates                  import *
 from GTG.plugins.rtm_sync.genericTask import GenericTask
 
+
 class GtgTask(GenericTask):
     #GtgTask passes only datetime objects with the timezone loaded 
     # to talk about dates and times
@@ -76,15 +77,15 @@
 
     def _get_due_date(self):
         due_date = self._gtg_task.get_due_date()
-        if due_date == NoDate():
+        if due_date == Date.no_date():
                 return None
-        return due_date.to_py_date()
+        return due_date._date
 
     def _set_due_date(self, due):
         if due == None:
-            gtg_due = NoDate()
+            gtg_due = Date.no_date()
         else:
-            gtg_due = RealDate(due)
+            gtg_due = Date(due)
         self._gtg_task.set_due_date(gtg_due)
 
     def _get_modified(self):

=== modified file 'GTG/tools/dates.py'
--- GTG/tools/dates.py	2010-04-30 19:23:02 +0000
+++ GTG/tools/dates.py	2010-06-20 04:28:25 +0000
@@ -17,214 +17,240 @@
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 # -----------------------------------------------------------------------------
 
-from datetime import date, timedelta
+import calendar
+import datetime
 import locale
-import calendar
 from GTG import _, ngettext
 
-#setting the locale of gtg to the system locale 
-#locale.setlocale(locale.LC_TIME, '')
+
+__all__ = 'Date',
+
+
+## internal constants
+# integers for special dates
+TODAY, SOON, NODATE, LATER = range(4)
+# strings representing special dates
+STRINGS = {
+  TODAY: 'today',
+  SOON: 'soon',
+  NODATE: '',
+  LATER: 'later',
+  }
+# inverse of STRINGS
+LOOKUP = dict([(v, k) for (k, v) in STRINGS.iteritems()])
+# functions giving absolute dates for special dates
+FUNCS = {
+  TODAY: lambda: datetime.date.today(),
+  SOON: lambda: datetime.date.today() + datetime.timedelta(15),
+  NODATE: lambda: datetime.date.max - datetime.timedelta(1),
+  LATER: lambda: datetime.date.max,
+  }
+
+# ISO 8601 date format
+ISODATE = '%Y-%m-%d'
+
+
+locale.setlocale(locale.LC_TIME, '')
+
 
 class Date(object):
+    """A date class that supports fuzzy dates.
+    
+    Date supports all the methods of the standard datetime.date class. A Date
+    can be constructed with:
+      * the special strings 'today', 'soon', '' (no date, default), or 'later'
+      * a string containing an ISO format date: YYYY-MM-DD, or
+      * a datetime.date or Date instance.
+    
+    """
+    _date = None
+    _special = None
+
+    def __init__(self, value=''):
+        if isinstance(value, datetime.date):
+            self._date = value
+        elif isinstance(value, Date):
+            self._date = value._date
+            self._special = value._special
+        elif isinstance(value, str) or isinstance(value, unicode):
+            try: # an ISO 8601 date
+                self._date = datetime.datetime.strptime(value, ISODATE).date()
+            except ValueError:
+                try: # a special date
+                    self.__init__(LOOKUP[value])
+                except KeyError:
+                    raise ValueError
+        elif isinstance(value, int):
+            self._date = FUNCS[value]()
+            self._special = value
+        else:
+            raise ValueError
+        assert not (self._date is None and self._special is None)
+
+    def __add__(self, other):
+        """Addition, same usage as datetime.date."""
+        if isinstance(other, datetime.timedelta):
+            return Date(self._date + other)
+        else:
+            raise NotImplementedError
+    __radd__ = __add__
+
+    def __sub__(self, other):
+        """Subtraction, same usage as datetime.date."""
+        if hasattr(other, '_date'):
+            return self._date - other._date
+        else:
+            # if other is a datetime.date, this will work, otherwise let it
+            # raise a NotImplementedError
+            return self._date - other
+
+    def __rsub__(self, other):
+        """Subtraction, same usage as datetime.date."""
+        # opposite of __sub__
+        if hasattr(other, '_date'):
+            return other._date - self._date
+        else:
+            return other - self._date
+
     def __cmp__(self, other):
-        if other is None: return 1
-        return cmp(self.to_py_date(), other.to_py_date())
-    
-    def __sub__(self, other):
-        return self.to_py_date() - other.to_py_date()
-
-    def __get_locale_string(self):
-        return locale.nl_langinfo(locale.D_FMT)
-        
-    def xml_str(self): return str(self)
-        
-    def day(self):      return self.to_py_date().day
-    def month(self):    return self.to_py_date().month
-    def year(self):     return self.to_py_date().year
+        """Compare with other Date instance."""
+        if hasattr(other, '_date'):
+            return cmp(self._date, other._date)
+        elif isinstance(other, datetime.date):
+            return cmp(self._date, other)
+
+    def __str__(self):
+        """String representation.
+        
+        Date(str(d))) == d, always.
+        
+        """
+        if self._special:
+            return STRINGS[self._special]
+        else:
+            return self._date.isoformat()
+
+    def __getattr__(self, name):
+        """Provide access to the wrapped datetime.date."""
+        try:
+            return self.__dict__[name]
+        except KeyError:
+            return getattr(self._date, name)
+
+    @property
+    def is_special(self):
+        """True if the Date is one of the special values; False if it is an
+        absolute date."""
+        return not self._special
+
+    @classmethod
+    def today(cls):
+        """Return the special Date 'today'."""
+        return Date(TODAY)
+
+    @classmethod
+    def no_date(cls):
+        """Return the special Date '' (no date)."""
+        return Date(NODATE)
+
+    @classmethod
+    def soon(cls):
+        """Return the special Date 'soon'."""
+        return Date(SOON)
+
+    @classmethod
+    def later(cls):
+        """Return the special Date 'tomorrow'."""
+        return Date(LATER)
+
+    @classmethod
+    def parse(cls, string):
+        """Return a Date corresponding to *string*, or None.
+        
+        *string* may be in one of the following formats:
+         * YYYY/MM/DD, YYYYMMDD, MMDD (assumes the current year),
+         * any of the special values for Date, or
+         * 'today', 'tomorrow', 'next week', 'next month' or 'next year' in
+           English or the system locale.
+        
+        """
+        # sanitize input
+        if string is None:
+            string = ''
+        else:
+            sting = string.lower()
+        # try the default formats
+        try:
+            return Date(string)
+        except ValueError:
+            pass
+        today = datetime.date.today()
+        # accepted date formats
+        formats = {
+          '%Y/%m/%d': 0,
+          '%Y%m%d': 0,
+          '%m%d': 0,
+          _('today'): 0,
+          'tomorrow': 1,
+          _('tomorrow'): 1,
+          'next week': 7,
+          _('next week'): 7,
+          'next month': calendar.mdays[today.month],
+          _('next month'): calendar.mdays[today.month],
+          'next year': 365 + int(calendar.isleap(today.year)),
+          _('next year'): 365 + int(calendar.isleap(today.year)),
+          }
+        # add week day names in the current locale
+        for i in range(7):
+            formats[calendar.day_name[i]] = i + 7 - today.weekday()
+        result = None
+        # try all of the formats
+        for fmt, offset in formats.iteritems():
+            try: # attempt to parse the string with known formats
+                result = datetime.datetime.strptime(string, fmt)
+            except ValueError: # parsing didn't work
+                continue
+            else: # parsing did work
+                break
+        if result:
+            r = result.date()
+            if r == datetime.date(1900, 1, 1):
+                # a format like 'next week' was used that didn't get us a real
+                # date value. Offset from today.
+                result = today
+            elif r.year == 1900:
+                # a format like '%m%d' was used that got a real month and day,
+                # but no year. Assume this year, or the next one if the day has
+                # passed.
+                if r.month >= today.month and r.day >= today.day:
+                    result = datetime.date(today.year, r.month, r.day)
+                else:
+                    result = datetime.date(today.year + 1, r.month, r.day)
+            return Date(result + datetime.timedelta(offset))
+        else: # can't parse this string
+            raise ValueError("can't parse a valid date from %s" % string)
 
     def to_readable_string(self):
-        if self.to_py_date() == NoDate().to_py_date():
+        if self._special == NODATE:
             return None
-        dleft = (self.to_py_date() - date.today()).days
+        dleft = (self - datetime.date.today()).days
         if dleft == 0:
-            return _("Today")
+            return _('Today')
         elif dleft < 0:
             abs_days = abs(dleft)
-            return ngettext("Yesterday", "%(days)d days ago", abs_days) % \
-                                                           {"days": abs_days}
+            return ngettext('Yesterday', '%(days)d days ago', abs_days) % \
+              {'days': abs_days}
         elif dleft > 0 and dleft <= 15:
-            return ngettext("Tomorrow", "In %(days)d days", dleft) % \
-                                                           {"days": dleft}
+            return ngettext('Tomorrow', 'In %(days)d days', dleft) % \
+              {'days': dleft}
         else:
-            locale_format = self.__get_locale_string()
-            if calendar.isleap(date.today().year):
+            locale_format = locale.nl_langinfo(locale.D_FMT)
+            if calendar.isleap(datetime.date.today().year):
                 year_len = 366
             else:
                 year_len = 365
             if float(dleft) / year_len < 1.0:
                 #if it's in less than a year, don't show the year field
                 locale_format = locale_format.replace('/%Y','')
-            return  self.to_py_date().strftime(locale_format)
-
-
-class FuzzyDate(Date):
-    def __init__(self, offset, name):
-        super(FuzzyDate, self).__init__()
-        self.name=name
-        self.offset=offset
-        
-    def to_py_date(self):
-        return date.today()+timedelta(self.offset)
-        
-    def __str__(self):
-        return _(self.name)
-        
-    def to_readable_string(self):
-    	return _(self.name)
-        
-    def xml_str(self):
-    	return self.name
-        
-    def days_left(self):
-        return None
-        
-class FuzzyDateFixed(FuzzyDate):
-	def to_py_date(self):
-		return self.offset
-
-NOW = FuzzyDate(0, _('now'))
-SOON = FuzzyDate(15, _('soon'))
-LATER = FuzzyDateFixed(date.max, _('later'))
-
-class RealDate(Date):
-    def __init__(self, dt):
-        super(RealDate, self).__init__()
-        assert(dt is not None)
-        self.proto = dt
-        
-    def to_py_date(self):
-        return self.proto
-        
-    def __str__(self):
-        return str(self.proto)
-
-    def days_left(self):
-        return (self.proto - date.today()).days
-      
-DATE_MAX_MINUS_ONE = date.max-timedelta(1)  # sooner than 'later'
-class NoDate(Date):
-
-    def __init__(self):
-        super(NoDate, self).__init__()
-
-    def to_py_date(self):
-        return DATE_MAX_MINUS_ONE
-    
-    def __str__(self):
-        return ''
-        
-    def days_left(self):
-        return None
-        
-    def __nonzero__(self):
-        return False 
-no_date = NoDate()
-
-#function to convert a string of the form YYYY-MM-DD
-#to a date
-#If the date is not correct, the function returns None
-def strtodate(stri) :
-    if stri == _("now") or stri == "now":
-        return NOW
-    elif stri == _("soon") or stri == "soon":
-        return SOON
-    elif stri == _("later") or stri == "later":
-        return LATER
-        
-    toreturn = None
-    zedate = []
-    if stri :
-        if '-' in stri :
-            zedate = stri.split('-')
-        elif '/' in stri :
-            zedate = stri.split('/')
-            
-        if len(zedate) == 3 :
-            y = zedate[0]
-            m = zedate[1]
-            d = zedate[2]
-            if y.isdigit() and m.isdigit() and d.isdigit() :
-                yy = int(y)
-                mm = int(m)
-                dd = int(d)
-                # we catch exceptions here
-                try :
-                    toreturn = date(yy,mm,dd)
-                except ValueError:
-                    toreturn = None
-    
-    if not toreturn: return no_date
-    else: return RealDate(toreturn)
-    
-    
-def date_today():
-    return RealDate(date.today())
-
-def get_canonical_date(arg):
-    """
-    Transform "arg" in a valid yyyy-mm-dd date or return None.
-    "arg" can be a yyyy-mm-dd, yyyymmdd, mmdd, today, next week,
-    next month, next year, or a weekday name.
-    Literals are accepted both in english and in the locale language.
-    When clashes occur the locale takes precedence.
-    """
-    today = date.today()
-    #FIXME: there surely exist a way to get day names from the  datetime
-    #       or time module.
-    day_names = ["monday", "tuesday", "wednesday", \
-                 "thursday", "friday", "saturday", \
-                 "sunday"]
-    day_names_localized = [_("monday"), _("tuesday"), _("wednesday"), \
-                 _("thursday"), _("friday"), _("saturday"), \
-                 _("sunday")]
-    delta_day_names = {"today":      0, \
-                       "tomorrow":   1, \
-                       "next week":  7, \
-                       "next month": calendar.mdays[today.month], \
-                       "next year":  365 + int(calendar.isleap(today.year))}
-    delta_day_names_localized = \
-                      {_("today"):      0, \
-                       _("tomorrow"):   1, \
-                       _("next week"):  7, \
-                       _("next month"): calendar.mdays[today.month], \
-                       _("next year"):  365 + int(calendar.isleap(today.year))}
-    ### String sanitization
-    arg = arg.lower()
-    ### Conversion
-    #yyyymmdd and mmdd
-    if arg.isdigit():
-        if len(arg) == 4:
-            arg = str(date.today().year) + arg
-        assert(len(arg) == 8)
-        arg = "%s-%s-%s" % (arg[:4], arg[4:6], arg[6:])
-    #today, tomorrow, next {week, months, year}
-    elif arg in delta_day_names.keys() or \
-         arg in delta_day_names_localized.keys():
-        if arg in delta_day_names:
-            delta = delta_day_names[arg]
-        else:
-            delta = delta_day_names_localized[arg]
-        arg = (today + timedelta(days = delta)).isoformat()
-    elif arg in day_names or arg in day_names_localized:
-        if arg in day_names:
-            arg_day = day_names.index(arg)
-        else:
-            arg_day = day_names_localized.index(arg)
-        today_day = today.weekday()
-        next_date = timedelta(days = arg_day - today_day + \
-                          7 * int(arg_day <= today_day)) + today
-        arg = "%i-%i-%i" % (next_date.year,  \
-                            next_date.month, \
-                            next_date.day)
-    return strtodate(arg)
+            return  self._date.strftime(locale_format)
 

=== modified file 'GTG/tools/taskxml.py'
--- GTG/tools/taskxml.py	2010-06-18 16:36:17 +0000
+++ GTG/tools/taskxml.py	2010-06-20 04:28:25 +0000
@@ -21,8 +21,8 @@
 import xml.dom.minidom
 import xml.sax.saxutils as saxutils
 
-from GTG.tools import cleanxml
-from GTG.tools import dates
+from GTG.tools       import cleanxml
+from GTG.tools.dates import *
 
 #Take an empty task, an XML node and return a Task.
 def task_from_xml(task,xmlnode) :
@@ -31,7 +31,7 @@
     uuid = "%s" %xmlnode.getAttribute("uuid")
     cur_task.set_uuid(uuid)
     donedate = cleanxml.readTextNode(xmlnode,"donedate")
-    cur_task.set_status(cur_stat,donedate=dates.strtodate(donedate))
+    cur_task.set_status(cur_stat,donedate=Date.parse(donedate))
     #we will fill the task with its content
     cur_task.set_title(cleanxml.readTextNode(xmlnode,"title"))
     #the subtasks
@@ -54,9 +54,9 @@
             tas = "<content>%s</content>" %tasktext[0].firstChild.nodeValue
             content = xml.dom.minidom.parseString(tas)
             cur_task.set_text(content.firstChild.toxml()) #pylint: disable-msg=E1103 
-    cur_task.set_due_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"duedate")))
+    cur_task.set_due_date(Date.parse(cleanxml.readTextNode(xmlnode,"duedate")))
     cur_task.set_modified(cleanxml.readTextNode(xmlnode,"modified"))
-    cur_task.set_start_date(dates.strtodate(cleanxml.readTextNode(xmlnode,"startdate")))
+    cur_task.set_start_date(Date.parse(cleanxml.readTextNode(xmlnode,"startdate")))
     cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
     if "" in cur_tags: cur_tags.remove("")
     for tag in cur_tags: cur_task.tag_added(saxutils.unescape(tag))
@@ -74,10 +74,10 @@
         tags_str = tags_str + saxutils.escape(str(tag)) + ","
     t_xml.setAttribute("tags", tags_str[:-1])
     cleanxml.addTextNode(doc,t_xml,"title",task.get_title())
-    cleanxml.addTextNode(doc,t_xml,"duedate", task.get_due_date().xml_str())
+    cleanxml.addTextNode(doc,t_xml,"duedate", str(task.get_due_date()))
     cleanxml.addTextNode(doc,t_xml,"modified",task.get_modified_string())
-    cleanxml.addTextNode(doc,t_xml,"startdate", task.get_start_date().xml_str())
-    cleanxml.addTextNode(doc,t_xml,"donedate", task.get_closed_date().xml_str())
+    cleanxml.addTextNode(doc,t_xml,"startdate", str(task.get_start_date()))
+    cleanxml.addTextNode(doc,t_xml,"donedate", str(task.get_closed_date()))
     childs = task.get_children()
     for c in childs :
         cleanxml.addTextNode(doc,t_xml,"subtask",c)


Follow ups