This commit is contained in:
MinchinWeb 2014-03-28 20:37:24 +00:00
commit 7c6be4f6c0
9 changed files with 343 additions and 34 deletions

3
.gitignore vendored
View file

@ -42,3 +42,6 @@ obj
# virtaulenv # virtaulenv
env/ env/
env*/ env*/
#random extras
extras/

View file

@ -10,7 +10,7 @@ install:
# command to run tests # command to run tests
script: script:
- python --version - python --version
- behave - "behave --tags ~wip"
matrix: matrix:
allow_failures: # python 3 support for travis is shaky.... allow_failures: # python 3 support for travis is shaky....
- python: 3.3 - python: 3.3

View file

@ -0,0 +1,14 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "",
"journals": {
"default": "features/journals/empty.journal"
},
"tagsymbols": "@"
}

View file

114
features/dates.feature Normal file
View file

@ -0,0 +1,114 @@
@dates1 @dates @wip
Feature: Processing of (relative) dates and times
# all these test are 'brittle', in that they depend on the day it is run
# these results assume the test is run on Feb 8, 2014
Scenario Outline: no date
Given we use the config "empty.json"
When we run "jrnl I saw Elvis <entry no>. He's alive!"
Then we should get no error
Then the journal should contain "<date out> I saw Elvis <entry no>"
Examples: no date
| date in | date out | entry no |
| | 2014-02-08 13:24 | 1 |
Scenario Outline: Test all sorts of (non-fixed) dates
Given we use the config "empty.json"
When we run "jrnl <date in>: I saw Elvis <entry no>."
Then we should get no error
Then the journal should contain "<date out> I saw Elvis <entry no>"
Examples: strings
| date in | date out | entry no |
| today | 2014-02-08 09:00 | 2 |
| tomorrow | 2014-02-09 09:00 | 3 |
| yesterday | 2014-02-07 09:00 | 4 |
Examples: strings with times
| date in | date out | entry no |
| today 2pm | 2014-02-08 14:00 | 5 |
| today at 3pm | 2014-02-08 15:00 | 6 |
| today 8am | 2014-02-08 08:00 | 7 |
| today 16:27 | 2014-02-08 16:27 | 8 |
| today 5:18 | 2014-02-08 05:18 | 9 |
| today 6:47pm | 2014-02-08 18:47 | 10 |
Examples: days of the week
| date in | date out | entry no |
| monday | 2014-02-10 09:00 | 11 |
| tuesday | 2014-02-11 09:00 | 12 |
| wednesday | 2014-02-12 09:00 | 13 |
| thursday | 2014-02-13 09:00 | 14 |
| friday | 2014-02-14 09:00 | 15 |
| saturday | 2014-02-08 09:00 | 16 |
| sunday | 2014-02-09 09:00 | 17 |
Examples: days of the week
| date in | date out | entry no |
| mon | 2014-02-10 09:00 | 18 |
| tues | 2014-02-11 09:00 | 19 |
| wed | 2014-02-12 09:00 | 20 |
| thurs | 2014-02-13 09:00 | 21 |
| fri | 2014-02-14 09:00 | 22 |
| sat | 2014-02-08 09:00 | 23 |
| sun | 2014-02-09 09:00 | 24 |
| tue | 2014-02-11 09:00 | 25 |
| thu | 2014-02-13 09:00 | 26 |
Examples: days of the week with a time
| date in | date out | entry no |
| mon at 5am | 2014-02-10 05:00 | 27 |
Examples: Qualified days of the week
| date in | date out | entry no |
| last monday |2014-02-03 09:00 | 28 |
| next monday |2014-02-10 09:00 | 29 |
Examples: Just times
| date in | date out | entry no |
| at 8pm |2014-02-08 20:00 | 30 |
| noon |2014-02-08 12:00 | 31 |
| midnight |2014-02-08 00:00 | 32 |
| 2 o'clock |2014-02-08 02:00 | 32 bis |
Examples: short months
| date in | date out | entry no |
| jan | 2014-01-01 09:00 | 33 |
| feb | 2014-02-01 09:00 | 34 |
| mar | 2014-03-01 09:00 | 35 |
| apr | 2013-04-01 09:00 | 36 |
| may | 2013-05-01 09:00 | 37 |
| jun | 2013-06-01 09:00 | 38 |
| jul | 2013-07-01 09:00 | 39 |
| aug | 2013-08-01 09:00 | 40 |
| sep | 2013-09-01 09:00 | 41 |
| oct | 2013-10-01 09:00 | 42 |
| nov | 2013-11-01 09:00 | 43 |
| dec | 2013-12-01 09:00 | 44 |
| sept | 2013-09-01 09:00 | 45 |
Examples: long months
| date in | date out | entry no |
| january | 2014-01-01 09:00 | 46 |
| february | 2014-02-01 09:00 | 47 |
| march | 2014-03-01 09:00 | 48 |
| april | 2013-04-01 09:00 | 49 |
| june | 2013-06-01 09:00 | 50 |
| july | 2013-07-01 09:00 | 51 |
| august | 2013-08-01 09:00 | 52 |
| september | 2013-09-01 09:00 | 53 |
| october | 2013-10-01 09:00 | 54 |
| november | 2013-11-01 09:00 | 55 |
| december | 2013-12-01 09:00 | 56 |
Examples: month + day (no year)
# unless within 28 days, assumed to be the last occurance
# if in the next 28 days, assumed to be then
| date in | date out | entry no |
| 7 apr | 2013-04-07 09:00 | 57 |
| apr 8 | 2013-04-08 09:00 | 58 |
| 9 march | 2013-03-09 09:00 | 59 |
| march 10 | 2013-03-10 09:00 | 60 |
| march 7 | 2014-03-07 09:00 | 60 bis |

74
features/dates2.feature Normal file
View file

@ -0,0 +1,74 @@
@dates2 @dates
Feature: Processing of (fixed) dates and times
Scenario Outline: Test all sorts of (fixed) dates
Given we use the config "empty.json"
When we run "jrnl <date in>: I saw Elvis <entry no>. He's Alive!"
Then we should get no error
Then the journal should contain "<date out> I saw Elvis <entry no>"
Examples: year
| date in | date out | entry no |
| 1998 | 1998-01-01 09:00 | 61 |
| 2013 | 2013-01-01 09:00 | 62 |
| 2014 | 2014-01-01 09:00 | 63 |
| 2015 | 2015-01-01 09:00 | 64 |
| 2050 | 2050-01-01 09:00 | 64 bis |
| 2051 | 2051-01-01 09:00 | 65 |
Examples: year + month
| date in | date out | entry no |
| jun 2013 | 2013-06-01 09:00 | 66 |
| 2013 jul | 2013-07-01 09:00 | 67 |
| august 2013 | 2013-08-01 09:00 | 68 |
| 2013 september | 2013-09-01 09:00 | 69 |
Examples: 'YYYY-MM-DD' dates (with and without times)
| date in | date out | entry no |
| 2013-06-07 | 2013-06-07 09:00 | 70 |
| 2013-06-07 8:11 | 2013-06-07 08:11 | 71 |
| 2013-06-07 08:12 | 2013-06-07 08:12 | 72 |
| 2013-06-07 20:13 | 2013-06-07 20:13 | 73 |
Examples: 'YYYY-MMM-DD' dates (with and without times)
| date in | date out | entry no |
| 2013-may-07 | 2013-05-07 09:00 | 74 |
| 2013-may-07 8:11 | 2013-05-07 08:11 | 75 |
| 2013-may-07 08:12 | 2013-05-07 08:12 | 76 |
| 2013-may-07 20:13 | 2013-05-07 20:13 | 77 |
Examples: Full dates, with written months
| date in | date out | entry no |
| Feb 5, 2014 | 2014-02-05 09:00 | 78 |
| Feb 06, 2014 | 2014-02-06 09:00 | 79 |
| 9 Feb 2014 | 2014-02-09 09:00 | 82 |
| 01 Feb 2014 | 2014-02-01 09:00 | 83 |
Examples: 'YYYY/MM/DD' dates (with and without times)
| date in | date out | entry no |
| 2013/06/07 | 2013-06-07 09:00 | 86 |
| 2013/06/07 8:11 | 2013-06-07 08:11 | 87 |
| 2013/06/07 08:12 | 2013-06-07 08:12 | 88 |
| 2013/06/07 20:13 | 2013-06-07 20:13 | 89 |
Examples: 'DD/MM/YYYY' dates (with and without times)
| date in | date out | entry no |
| 13/06/2007 | 2007-06-13 09:00 | 90 |
| 13/06/2007 8:11 | 2007-06-13 08:11 | 91 |
| 13/06/2007 08:12 | 2007-06-13 08:12 | 92 |
| 13/06/2007 20:13 | 2007-06-13 20:13 | 93 |
@wip
Scenario Outline: Test all sorts of (fixed) dates with periods
Given we use the config "empty.json"
When we run "jrnl <date in>: I saw Elvis <entry no>. He's Alive!"
Then we should get no error
Then the journal should contain "<date out> I saw Elvis <entry no>"
Examples: Full dates, with written months, with periods
| date in | date out | entry no |
| Feb. 7, 2014 | 2014-02-07 09:00 | 80 |
| Feb. 08, 2014 | 2014-02-08 09:00 | 81 |
| 2 Feb. 2014 | 2014-02-02 09:00 | 84 |
| 03 Feb. 2014 | 2014-02-03 09:00 | 85 |

View file

@ -1,6 +1,7 @@
from behave import * from behave import *
import shutil import shutil
import os import os
import time
import jrnl import jrnl
try: try:
from io import StringIO from io import StringIO
@ -17,14 +18,24 @@ def before_scenario(context, scenario):
for folder in ("configs", "journals"): for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder) working_dir = os.path.join("features", folder)
if os.path.exists(working_dir): if os.path.exists(working_dir):
shutil.rmtree(working_dir) try:
shutil.rmtree(working_dir)
except:
# give it a second go at it...
time.sleep(0.5)
shutil.rmtree(working_dir)
for folder in ("configs", "journals"): for folder in ("configs", "journals"):
original = os.path.join("features", "data", folder) original = os.path.join("features", "data", folder)
working_dir = os.path.join("features", folder) working_dir = os.path.join("features", folder)
if not os.path.exists(working_dir): if not os.path.exists(working_dir):
os.mkdir(working_dir) try:
os.mkdir(working_dir)
except:
# give it a second go at it...
time.sleep(0.5)
os.mkdir(working_dir)
for filename in os.listdir(original): for filename in os.listdir(original):
source = os.path.join(original, filename) source = os.path.join(original, filename)
if os.path.isdir(source): if os.path.isdir(source):
@ -39,4 +50,9 @@ def after_scenario(context, scenario):
for folder in ("configs", "journals"): for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder) working_dir = os.path.join("features", folder)
if os.path.exists(working_dir): if os.path.exists(working_dir):
shutil.rmtree(working_dir) try:
shutil.rmtree(working_dir)
except:
# give it a second go at it...
time.sleep(0.5)
shutil.rmtree(working_dir)

View file

@ -6,9 +6,12 @@ from . import Entry
from . import util from . import util
import codecs import codecs
import os import os
try: import parsedatetime.parsedatetime_consts as pdt try:
except ImportError: import parsedatetime as pdt import parsedatetime.parsedatetime_consts as pdt
except ImportError:
import parsedatetime as pdt
import re import re
from datetime import timedelta
from datetime import datetime from datetime import datetime
import dateutil import dateutil
import time import time
@ -209,8 +212,8 @@ class Journal(object):
If strict is True, all tags must be present in an entry. If false, the If strict is True, all tags must be present in an entry. If false, the
entry is kept if any tag is present.""" entry is kept if any tag is present."""
self.search_tags = set([tag.lower() for tag in tags]) self.search_tags = set([tag.lower() for tag in tags])
end_date = self.parse_date(end_date) end_date = self.parse_date(end_date, end_flag="to")
start_date = self.parse_date(start_date) start_date = self.parse_date(start_date, end_flag="from")
# If strict mode is on, all tags have to be present in entry # If strict mode is on, all tags have to be present in entry
tagged = self.search_tags.issubset if strict else self.search_tags.intersection tagged = self.search_tags.issubset if strict else self.search_tags.intersection
result = [ result = [
@ -236,40 +239,109 @@ class Journal(object):
e.body = '' e.body = ''
self.entries = result self.entries = result
def parse_date(self, date_str): def parse_date(self, date_str, end_flag=None):
"""Parses a string containing a fuzzy date and returns a datetime.datetime object""" """Parses a string containing a fuzzy date and returns a datetime.datetime object"""
if not date_str: if not date_str:
return None return None
elif isinstance(date_str, datetime): elif isinstance(date_str, datetime):
return date_str return date_str
try: if re.match(r'^\d{4}$', date_str):
date = dateutil.parser.parse(date_str) # i.e. if we're just given a year
flag = 1 if date.hour == 0 and date.minute == 0 else 2 if end_flag == "from":
date = date.timetuple() date = datetime(year=int(date_str), month=1, day=1, hour=0, minute=0)
except: elif end_flag == "to":
date, flag = self.dateparse.parse(date_str) date = datetime(year=int(date_str), month=12, day=31, hour=23, minute=59, second=59)
else:
if not flag: # Oops, unparsable. # Use the default time.
try: # Try and parse this as a single year date = datetime(year=int(date_str), month=1, day=1, hour=self.config['default_hour'], minute=self.config['default_minute'])
year = int(date_str)
return datetime(year, 1, 1)
except ValueError:
return None
except TypeError:
return None
if flag is 1: # Date found, but no time. Use the default time.
date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute'])
else: else:
date = datetime(*date[:6]) # clean up some misunderstood dates
replacements = (u"september", u"sep"), (u"sept", u"sep"), (u"tuesday", u"tue"), \
(u"tues", u"tue"), (u"thursday", u"thu"), (u"thurs", u"thu"), \
(u" o'clock", u":00")
date_str = util.multiple_replace(date_str.lower(), *replacements)
# Ugly heuristic: if the date is more than 4 weeks in the future, we got the year wrong. # determine if we've been given just a month, or just a year and month
# Rather then this, we would like to see parsedatetime patched so we can tell it to prefer replacements2 = ("january", "01"), ("february", "02"), ("march", "03"), \
# past dates ("april", "04"), ("may", "05"), ("june", "06"), \
dt = datetime.now() - date ("july", "07"), ("august", "08"), \
if dt.days < -28: ("october", "10"), ("november", "11"), ("december", "12"), \
date = date.replace(date.year - 1) ("jan", "01"), ("feb", "02"), ("mar", "03"), ("apr", "04"), \
("jun", "06"), ("jul", "07"), ("aug", "08"), \
("sep", "09"), ("oct", "10"), ("nov", "11"), ("dec", "12")
date_str2 = util.multiple_replace(date_str.lower(), *replacements2)
year_month_only = False;
matches = re.match(r'^(\d{4})[ \\/-](\d{2})$', date_str2)
if matches:
myYear = matches.group(1)
myMonth = matches.group(2)
year_month_only = True
else:
matches2 = re.match(r'^(\d{2})[ \\/-](\d{4})$', date_str2)
if matches2:
myYear = matches2.group(2)
myMonth = matches2.group(1)
year_month_only = True
else:
matches3 = re.match(r'^(\d{2})$', date_str2)
if matches3:
myYear = datetime.today().year
myMonth = matches3.group(0)
# if given (just) a month and it's not this month or next, assume it was last year
dt = datetime.now() - datetime(year=int(myYear), month=int(myMonth), day=1)
if dt.days < -32:
myYear = myYear - 1
year_month_only = True
if year_month_only == True:
if end_flag == "from":
date = datetime(year=int(myYear), month=int(myMonth), day=1, hour=0, minute=0)
elif end_flag == "to":
# get the last day of the month
if myMonth == 12:
date = datetime(year=int(myYear), month=int(myMonth), day=31, hour=23, minute=59, second=59)
else:
date = datetime(year=int(myYear), month=int(myMonth)+1, day=1, hour=23, minute=59, second=59) - timedelta (days = 1)
else:
# Use the default time.
date = datetime(year=int(myYear), month=int(myMonth), day=1, hour=self.config['default_hour'], minute=self.config['default_minute'])
else:
try:
date = dateutil.parser.parse(date_str)
flag = 1 if date.hour == 0 and date.minute == 0 else 2
date = date.timetuple()
except:
date, flag = self.dateparse.parse(date_str)
if not flag: # Oops, unparsable.
try: # Try and parse this as a single year
year = int(date_str)
return datetime(year, 1, 1)
except ValueError:
return None
except TypeError:
return None
if flag is 1: # Date found, but no time.
if end_flag == "from":
date = datetime(*date[:3], hour=0, minute=0)
elif end_flag == "to":
date = datetime(*date[:3], hour=23, minute=59, second=59)
else:
# Use the default time.
date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute'])
else:
date = datetime(*date[:6])
# Ugly heuristic: if the date is more than 4 weeks in the future, we got the year wrong.
# Rather then this, we would like to see parsedatetime patched so we can tell it to prefer
# past dates
dt = datetime.now() - date
if dt.days < -28:
date = date.replace(date.year - 1)
return date return date

View file

@ -141,3 +141,19 @@ def slugify(string):
slug = re.sub(r'[-\s]+', '-', no_punctuation) slug = re.sub(r'[-\s]+', '-', no_punctuation)
return u(slug) return u(slug)
# Use the following two functions to do multiple replacements in one pass
# from http://stackoverflow.com/questions/6116978/python-replace-multiple-strings
#
# Useage:
# >>> replacements = (u"café", u"tea"), (u"tea", u"café"), (u"like", u"love")
# >>> print multiple_replace(u"Do you like café? No, I prefer tea.", *replacements)
# output: Do you love tea? No, I prefer café.
def multiple_replacer(*key_values):
replace_dict = dict(key_values)
replacement_function = lambda match: replace_dict[match.group(0)]
pattern = re.compile("|".join([re.escape(k) for k, v in key_values]), re.M | re.I)
return lambda string: pattern.sub(replacement_function, string)
def multiple_replace(string, *key_values):
return multiple_replacer(*key_values)(string)