Skip to content

Commit

Permalink
Link date with timings + timings without dates
Browse files Browse the repository at this point in the history
  • Loading branch information
RomainMapado committed Sep 15, 2016
1 parent 5a7b182 commit c68fea0
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 20 deletions.
2 changes: 2 additions & 0 deletions datection/grammar/fr.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ def set_weekday(text, start_index, match):
('date_interval', DATE_INTERVAL),
]),

('time_pattern', TIME_PATTERN),

('exclusion', EXCLUSION),
]

Expand Down
5 changes: 4 additions & 1 deletion datection/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ def parse(text, lang, valid=True, reference=None):
excluded_tps = [tok.timepoint for tok in token_group[2:]]
# hack, transmit the span at the last minute so that it gets
# exported
token.timepoint.span = token.span[0], token_group[2].span[1]
token.timepoint.span = token.span[0], token_group[-1].span[1]
schedule.add(
timepoint=token.timepoint, excluded_tps=excluded_tps)

# Merge unassigned timings with dates that have no timings
schedule.complete_timings()

# remove any redundancy
timepoints = list(set(schedule._timepoints))

Expand Down
71 changes: 54 additions & 17 deletions datection/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from datection.timepoint import DatetimeInterval
from datection.timepoint import ContinuousDatetimeInterval
from datection.timepoint import WeeklyRecurrence
from datection.timepoint import has_no_timings
from datection.timepoint import enrich_with_timings
from datection.exclude import TimepointExcluder


Expand Down Expand Up @@ -208,32 +210,67 @@ def __init__(self):
self.date_lists = []
self.date_intervals = []
self._timepoints = [] # TEMPORARY
self.unassigned_timings = []

def add_exclusion(self, timepoint, excluded_tps):
"""
Adds the excluded timepoints to the given timepoint
@param timepoint(Timepoint)
@param excluded_tps(list(Timepoint) or None)
"""
if excluded_tps is not None:
for excluded in excluded_tps:
duration = excluded.duration
excluder = TimepointExcluder(timepoint, excluded)
excluded = excluder.exclude()
if excluded is not None:
timepoint.excluded.append(excluded)
timepoint.excluded_duration.append(duration)

def add(self, timepoint, excluded_tps=None):
"""Add the timepoint to the one of the schedule internal lists,
if its class is found in the schedule router.
"""
if type(timepoint) in self.router:
if not excluded_tps:
# Get the timepoint transformation method
container_name, constructor = self.router[type(timepoint)]
else:
# perform the exclusion bewteen the 'timepoint' and 'excluded'
# Timepoints
for excluded in excluded_tps:
duration = excluded.duration
excluder = TimepointExcluder(timepoint, excluded)
excluded = excluder.exclude()
if excluded is not None:
timepoint.excluded.append(excluded)
timepoint.excluded_duration.append(duration)

# Get the timepoint transformation method
container_name, constructor = self.router[
type(timepoint)]
# perform the exclusion bewteen the 'timepoint' and 'excluded'
# Timepoints
self.add_exclusion(timepoint, excluded_tps)

# Get the timepoint transformation method
container_name, constructor = self.router[type(timepoint)]

# add timepoint to the schedule
getattr(self, container_name).append(constructor(timepoint))
if timepoint not in self._timepoints:
self._timepoints.append(timepoint) # TEMPORARY

elif type(timepoint) in [Time, TimeInterval]:
self.unassigned_timings.append((timepoint, excluded_tps))

def complete_timings(self):
"""
Hanldes the unassigned timings by either merging them
to existing timepoi or by creating new timepoints
"""
if len(self.unassigned_timings) > 0:

# Create new timepoints from the timings
if len(self._timepoints) == 0:
for timing in self.unassigned_timings:
new_tp = WeeklyRecurrence.make_undefined(timing[0])
self.add_exclusion(new_tp, timing[1])
self._timepoints.append(new_tp)

# Complete dates without timings with the unassigned timings
elif any([tp for tp in self._timepoints if has_no_timings(tp)]):
no_timings = [tp for tp in self._timepoints if has_no_timings(tp)]
with_timings = [tp for tp in self._timepoints if not has_no_timings(tp)]
self._timepoints = with_timings

for tp in no_timings:
for timing in self.unassigned_timings:
new_tp = enrich_with_timings(tp, timing[0])
self.add_exclusion(new_tp, timing[1])
self._timepoints.append(new_tp)
29 changes: 27 additions & 2 deletions datection/test/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,9 @@ def test_date_interval_multi_weekdays(self):
"Du 05-02-2015 au 06-02-2015 - jeu. à 19h | ven. à 20h30",
"fr")
self.assertEqual(len(wks), 2)
self.assertEqual(wks[1].weekdays, [FR])
self.assertEqual(wks[0].weekdays, [TH])
self.assertNotEqual(wks[0].weekdays, wks[1].weekdays)
self.assertIn(wks[1].weekdays[0], [TH, FR])
self.assertIn(wks[0].weekdays[0], [TH, FR])

def test_datetime_pattern(self):
self.assert_generates(
Expand Down Expand Up @@ -378,6 +379,30 @@ def test_slash_separator(self):
self.assertEqual(len(res), 2)
self.assertIsInstance(res[0], Date)

def test_timing_alone(self):
text = u"ouvert de 10h à 18h."
res = parse(text, "fr")
self.assertEqual(len(res), 1)
self.assertTrue(isinstance(res[0], WeeklyRecurrence))
self.assertEqual(len(res[0].weekdays), 7)
self.assertEqual(res[0].time_interval.start_time, Time(10,00))

def test_timing_with_exclusion(self):
text = u"ouvert de 10h à 18h. Fermé le dimanche"
res = parse(text, "fr")
self.assertEqual(len(res), 1)
self.assertTrue(isinstance(res[0], WeeklyRecurrence))
self.assertEqual(res[0].time_interval.start_time, Time(10,00))
self.assertEqual(len(res[0].excluded), 1)

def test_timing_away_from_date(self):
text = u"Tous les jours.Blablablablablabla. De 9h45 à 13h."
res = parse(text, "fr")[0]
self.assertTrue(isinstance(res, WeeklyRecurrence))
self.assertEqual(len(res.weekdays), 7)
self.assertEqual(res.time_interval.start_time, Time(9,45))
self.assertEqual(res.time_interval.end_time, Time(13,00))


class TestYearLessExpressions(unittest.TestCase):

Expand Down
23 changes: 23 additions & 0 deletions datection/test/test_timepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from datection.timepoint import DatetimeInterval
from datection.timepoint import ContinuousDatetimeInterval
from datection.timepoint import WeeklyRecurrence
from datection.timepoint import enrich_with_timings


class CurrentDayMock(unittest.TestCase):
Expand Down Expand Up @@ -111,6 +112,15 @@ def test_future(self):
self.assertFalse(self.d.future())
self.assertTrue(self.d.future(reference=date(2014, 11, 12)))

def test_add_timings(self):
time_interval = TimeInterval(Time(12, 00), Time(20, 00))
date = Date(2015, 10, 12)
result = enrich_with_timings(date, time_interval)
self.assertTrue(isinstance(result, Datetime))
self.assertEqual(date, result.date)
self.assertEqual(time_interval.start_time, result.start_time)
self.assertEqual(time_interval.end_time, result.end_time)


class TestDateList(CurrentDayMock):

Expand Down Expand Up @@ -156,6 +166,14 @@ def test_future(self):
self.set_current_date(date(2013, 11, 12)) # today: in between
self.assertTrue(self.dl.future())

def test_add_timings(self):
time_interval = TimeInterval(Time(12, 00), Time(20, 00))
dl = DateList([Date(2013, 11, 12), Date(2013, 11, 13)])
result = enrich_with_timings(dl, time_interval)
self.assertTrue(isinstance(result, DatetimeList))
self.assertEqual(len(result.datetimes), 2)
self.assertEqual(result.datetimes[1].start_time, time_interval.start_time)


class TestDateInterval(CurrentDayMock):

Expand Down Expand Up @@ -203,6 +221,11 @@ def test_future(self):
self.set_current_date(date(2013, 3, 14)) # today: in between
self.assertTrue(self.di.future())

def test_add_timings(self):
time_interval = TimeInterval(Time(12, 00), Time(20, 00))
result = enrich_with_timings(self.di, time_interval)
self.assertTrue(isinstance(result, DatetimeInterval))
self.assertEqual(result.time_interval, time_interval)

class TestDatetime(CurrentDayMock):

Expand Down
1 change: 1 addition & 0 deletions datection/test/test_tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def test_search_context(self):
'date_interval', # du 5 au 29 mars 2015
'exclusion', # sauf
'weekly_rec', # les lundis
'time_pattern',
])

def assertTokenGroupEquals(self, tokens, expected_token_groups):
Expand Down
98 changes: 98 additions & 0 deletions datection/timepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from dateutil.rrule import WEEKLY
from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU
from operator import attrgetter
from copy import deepcopy

from datection.utils import get_current_date
from datection.utils import makerrulestr
Expand Down Expand Up @@ -58,6 +59,55 @@ def wrapper(*args, **kwargs):
return wrapper


def has_no_timings(tp):
"""
Function that checks if the given timepoint has some timings
defined
@param tp(Timepoint)
@return: True if the timepoints no timings
"""
if isinstance(tp, (Date, DateList, DateInterval)):
return True

if isinstance(tp, Datetime):
return tp.duration == ALL_DAY

if isinstance(tp, (DatetimeList, DatetimeInterval, WeeklyRecurrence)):
return tp.time_interval.undefined

return False


def enrich_with_timings(tp, timing):
"""
Returns a new timepoint based on the given one and
adds the given timing.
@param tp(Timepoint)
@param timing(TimeInterval)
@return: new Timepoint with timings
"""
if isinstance(tp, Date):
return Datetime.from_match(tp, timing)

elif isinstance(tp, DateList):
return DatetimeList.from_match(tp, timing)

elif isinstance(tp, DateInterval):
return DatetimeInterval.from_match(tp, timing)

elif isinstance(tp, (Datetime, DatetimeList,
DatetimeInterval, WeeklyRecurrence)):
new_tp = deepcopy(tp)
new_tp.set_time_interval(timing)
return new_tp

return tp


class YearDescriptor(object):

"""A descriptor of the year of a timepoint, whatever its class."""
Expand Down Expand Up @@ -608,6 +658,21 @@ def __unicode__(self):
str(self.end_time.minute).zfill(2)
) if self.start_time != self.end_time else '')

def set_time_interval(self, time_interval):
""" Sets the time interval """
self.start_time = time_interval.start_time
if not time_interval.end_time:
self.end_time = self.start_time
else:
self.end_time = time_interval.end_time

@classmethod
def from_match(cls, date, timing):
"""
Creates a Datetime from a Date and a TimeInterval
"""
return cls(date, timing.start_time, timing.end_time)

@classmethod
def combine(cls, date, start_time, end_time=None):
return Datetime(date, start_time, end_time)
Expand Down Expand Up @@ -695,6 +760,11 @@ def __iter__(self):
def __repr__(self):
return object.__repr__(self)

def set_time_interval(self, time_interval):
""" Sets the time interval """
for datetime in self.datetimes:
datetime.set_time_interval(time_interval)

@classmethod
# pragma: no cover
def from_match(cls, dates, time_interval, *args, **kwargs):
Expand Down Expand Up @@ -763,6 +833,20 @@ def __iter__(self):
yield current
current += timedelta(days=1)

def set_time_interval(self, time_interval):
""" Sets the time interval """
self.time_interval = time_interval

@classmethod
def from_match(cls, date_interval, time_interval):
"""
Creates a DatetimeInterval from a DateInterval and a TimeInterval
"""
datetime_interval = cls(date_interval, time_interval)
datetime_interval.excluded = date_interval.excluded
datetime_interval.excluded_duration = date_interval.excluded_duration
return datetime_interval

@property
def valid(self):
return all([self.date_interval.valid, self.time_interval.valid])
Expand Down Expand Up @@ -972,6 +1056,20 @@ def __repr__(self):
else " { EXCLUDED: " + str(self.excluded) + "}"),
)

@classmethod
def make_undefined(cls, time_interval):
"""
Creates a unbounded, every day, WeeklyRecurrence based
on the given TimeInterval
"""
date_interval = DateInterval.make_undefined()
weekdays = ORDERED_DAYS
return cls(date_interval, time_interval, weekdays)

def set_time_interval(self, time_interval):
""" Sets the time interval """
self.time_interval = time_interval

@property
def rrulestr(self):
""" Generate a full description of the recurrence rule"""
Expand Down

0 comments on commit c68fea0

Please sign in to comment.