mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-17 19:48:31 +02:00
Merge 92258f0b08
into 599c5a9ea1
This commit is contained in:
commit
7c6be4f6c0
9 changed files with 343 additions and 34 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -42,3 +42,6 @@ obj
|
|||
# virtaulenv
|
||||
env/
|
||||
env*/
|
||||
|
||||
#random extras
|
||||
extras/
|
||||
|
|
|
@ -10,7 +10,7 @@ install:
|
|||
# command to run tests
|
||||
script:
|
||||
- python --version
|
||||
- behave
|
||||
- "behave --tags ~wip"
|
||||
matrix:
|
||||
allow_failures: # python 3 support for travis is shaky....
|
||||
- python: 3.3
|
||||
|
|
14
features/data/configs/empty.json
Normal file
14
features/data/configs/empty.json
Normal 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": "@"
|
||||
}
|
0
features/data/journals/empty.journal
Normal file
0
features/data/journals/empty.journal
Normal file
114
features/dates.feature
Normal file
114
features/dates.feature
Normal 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
74
features/dates2.feature
Normal 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 |
|
|
@ -1,6 +1,7 @@
|
|||
from behave import *
|
||||
import shutil
|
||||
import os
|
||||
import time
|
||||
import jrnl
|
||||
try:
|
||||
from io import StringIO
|
||||
|
@ -17,14 +18,24 @@ def before_scenario(context, scenario):
|
|||
for folder in ("configs", "journals"):
|
||||
working_dir = os.path.join("features", folder)
|
||||
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"):
|
||||
original = os.path.join("features", "data", folder)
|
||||
working_dir = os.path.join("features", folder)
|
||||
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):
|
||||
source = os.path.join(original, filename)
|
||||
if os.path.isdir(source):
|
||||
|
@ -39,4 +50,9 @@ def after_scenario(context, scenario):
|
|||
for folder in ("configs", "journals"):
|
||||
working_dir = os.path.join("features", folder)
|
||||
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)
|
||||
|
|
132
jrnl/Journal.py
132
jrnl/Journal.py
|
@ -6,9 +6,12 @@ from . import Entry
|
|||
from . import util
|
||||
import codecs
|
||||
import os
|
||||
try: import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError: import parsedatetime as pdt
|
||||
try:
|
||||
import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError:
|
||||
import parsedatetime as pdt
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
import dateutil
|
||||
import time
|
||||
|
@ -209,8 +212,8 @@ class Journal(object):
|
|||
If strict is True, all tags must be present in an entry. If false, the
|
||||
entry is kept if any tag is present."""
|
||||
self.search_tags = set([tag.lower() for tag in tags])
|
||||
end_date = self.parse_date(end_date)
|
||||
start_date = self.parse_date(start_date)
|
||||
end_date = self.parse_date(end_date, end_flag="to")
|
||||
start_date = self.parse_date(start_date, end_flag="from")
|
||||
# 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
|
||||
result = [
|
||||
|
@ -236,40 +239,109 @@ class Journal(object):
|
|||
e.body = ''
|
||||
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"""
|
||||
if not date_str:
|
||||
return None
|
||||
elif isinstance(date_str, datetime):
|
||||
return date_str
|
||||
|
||||
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. Use the default time.
|
||||
date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute'])
|
||||
if re.match(r'^\d{4}$', date_str):
|
||||
# i.e. if we're just given a year
|
||||
if end_flag == "from":
|
||||
date = datetime(year=int(date_str), month=1, day=1, hour=0, minute=0)
|
||||
elif end_flag == "to":
|
||||
date = datetime(year=int(date_str), month=12, day=31, hour=23, minute=59, second=59)
|
||||
else:
|
||||
# Use the default time.
|
||||
date = datetime(year=int(date_str), month=1, day=1, hour=self.config['default_hour'], minute=self.config['default_minute'])
|
||||
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.
|
||||
# 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)
|
||||
# determine if we've been given just a month, or just a year and month
|
||||
replacements2 = ("january", "01"), ("february", "02"), ("march", "03"), \
|
||||
("april", "04"), ("may", "05"), ("june", "06"), \
|
||||
("july", "07"), ("august", "08"), \
|
||||
("october", "10"), ("november", "11"), ("december", "12"), \
|
||||
("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
|
||||
|
||||
|
|
16
jrnl/util.py
16
jrnl/util.py
|
@ -141,3 +141,19 @@ def slugify(string):
|
|||
slug = re.sub(r'[-\s]+', '-', no_punctuation)
|
||||
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)
|
||||
|
|
Loading…
Add table
Reference in a new issue