def date(self, value): if self.start_time: self.start_time = hamsterday_time_to_datetime( value, self.start_time.time()) if self.end_time: self.end_time = hamsterday_time_to_datetime( value, self.end_time.time())
def test_roundtrips(self): for start_time in ( None, dt.time(12, 33), ): for end_time in ( None, dt.time(13, 34), ): for activity in ( "activity", "#123 with two #hash", "activity, with comma", ): for category in ( "", "category", ): for description in ( "", "description", "with #hash", "with, comma", "with @at", ): for tags in ( [], ["single"], ["with space"], ["two", "tags"], ["with @at"], ): start = hamsterday_time_to_datetime( hamster_today(), start_time) if start_time else None end = hamsterday_time_to_datetime( hamster_today(), end_time) if end_time else None if end and not start: # end without start is not parseable continue fact = Fact(start_time=start, end_time=end, activity=activity, category=category, description=description, tags=tags) for range_pos in ("head", "tail"): fact_str = fact.serialized( range_pos=range_pos) parsed = Fact.parse(fact_str, range_pos=range_pos) self.assertEqual(fact, parsed) self.assertEqual(parsed.activity, fact.activity) self.assertEqual(parsed.category, fact.category) self.assertEqual(parsed.description, fact.description) self.assertEqual(parsed.tags, fact.tags)
def date(self, value): if self.start_time: previous_start_time = self.start_time self.start_time = hamsterday_time_to_datetime(value, self.start_time.time()) if self.end_time: # start_time date prevails. # Shift end_time to preserve the fact duration. self.end_time += self.start_time - previous_start_time elif self.end_time: self.end_time = hamsterday_time_to_datetime(value, self.end_time.time())
def test_hamsterday_time_to_datetime(self): hamsterday = dt.date(2018, 8, 13) time = dt.time(23, 10) expected = dt.datetime(2018, 8, 13, 23, 10) # 2018-08-13 23:10 self.assertEqual(hamsterday_time_to_datetime(hamsterday, time), expected) hamsterday = dt.date(2018, 8, 13) time = dt.time(0, 10) expected = dt.datetime(2018, 8, 14, 0, 10) # 2018-08-14 00:10 self.assertEqual(hamsterday_time_to_datetime(hamsterday, time), expected)
def on_start_time_changed(self, widget): if not self.master_is_cmdline: # note: resist the temptation to preserve duration here; # for instance, end time might be at the beginning of next fact. new_time = self.start_time.time if new_time: if self.fact.start_time: new_start_time = dt.datetime.combine( self.fact.start_time.date(), new_time) else: # date not specified; result must fall in current hamster_day new_start_time = hamsterday_time_to_datetime( hamster_today(), new_time) else: new_start_time = None self.fact.start_time = new_start_time # let start_date extract date or handle None self.start_date.date = new_start_time self.validate_fields() self.update_cmdline()
def _extract_datetime(match, d="date", h="hour", m="minute", r="relative", default_day=None): """extract datetime from a dt_pattern match. h (str): name of the group containing the hour m (str): name of the group containing the minute r (str): name of the group containing the relative time default_day (dt.date): the datetime will belong to this hamster day if date is missing. """ time = _extract_time(match, h, m) if time: date_str = match.group(d) if date_str: date = parse_date(date_str) return dt.datetime.combine(date, time) else: return hamsterday_time_to_datetime(default_day, time) else: relative_str = match.group(r) if relative_str: return dt.timedelta(minutes=int(relative_str)) else: return None
def parse_fact(text, phase=None, res=None, date=None): """tries to extract fact fields from the string the optional arguments in the syntax makes us actually try parsing values and fallback to next phase start -> [end] -> activity[@category] -> tags Returns dict for the fact and achieved phase TODO - While we are now bit cooler and going recursively, this code still looks rather awfully spaghetterian. What is the real solution? Tentative syntax: [date] start_time[-end_time] activity[@category][, description]{[,] { })#tag} According to the legacy tests, # were allowed in the description """ now = dt.datetime.now() # determine what we can look for phases = [ "date", # hamster day "start_time", "end_time", "tags", "activity", "category", ] phase = phase or phases[0] phases = phases[phases.index(phase):] if res is None: res = {} text = text.strip() if not text: return res fragment = re.split("[\s|#]", text, 1)[0].strip() # remove a fragment assumed to be at the beginning of text remove_fragment = lambda text, fragment: text[len(fragment):] if "date" in phases: # if there is any date given, it must be at the front try: date = dt.datetime.strptime(fragment, DATE_FMT).date() remaining_text = remove_fragment(text, fragment) except ValueError: date = now.date() remaining_text = text return parse_fact(remaining_text, "start_time", res, date) if "start_time" in phases or "end_time" in phases: # -delta ? delta_re = re.compile("^-[0-9]{1,3}$") if delta_re.match(fragment): # TODO untested # delta_re was probably thought to be used # alone or together with a start_time # but using "now" prevents the latter res[phase] = now + dt.timedelta(minutes=int(fragment)) remaining_text = remove_fragment(text, fragment) return parse_fact(remaining_text, phases[phases.index(phase) + 1], res, date) # only starting time ? m = re.search(time_re, fragment) if m: time = extract_time(m) res[phase] = hamsterday_time_to_datetime(date, time) remaining_text = remove_fragment(text, fragment) return parse_fact(remaining_text, phases[phases.index(phase) + 1], res, date) # start-end ? start, __, end = fragment.partition("-") m_start = re.search(time_re, start) m_end = re.search(time_re, end) if m_start and m_end: start_time = extract_time(m_start) end_time = extract_time(m_end) res["start_time"] = hamsterday_time_to_datetime(date, start_time) res["end_time"] = hamsterday_time_to_datetime(date, end_time) remaining_text = remove_fragment(text, fragment) return parse_fact(remaining_text, "tags", res, date) if "tags" in phases: # Need to start from the end, because # the description can hold some '#' characters tags = [] remaining_text = text while True: m = re.search(tag_re, remaining_text) if not m: break tag = m.group(1) tags.append(tag) # strip the matched string (including #) remaining_text = remaining_text[:m.start()] res["tags"] = tags return parse_fact(remaining_text, "activity", res, date) if "activity" in phases: activity = re.split("[@|#|,]", text, 1)[0] if looks_like_time(activity): # want meaningful activities return res res["activity"] = activity remaining_text = remove_fragment(text, activity) return parse_fact(remaining_text, "category", res, date) if "category" in phases: category, _, description = text.partition(",") res["category"] = category.lstrip("@").strip() or None res["description"] = description.strip() or None return res return {}