Source code for ged4py.date

"""Module for parsing and representing dates in gedcom format.
"""

__all__ = [
    "DateValueTypes", "DateValue",
    "DateValueAbout", "DateValueAfter", "DateValueBefore", "DateValueCalculated",
    "DateValueEstimated", "DateValueFrom", "DateValueInterpreted", "DateValuePeriod",
    "DateValuePhrase", "DateValueRange", "DateValueSimple", "DateValueTo",
    "DateValueVisitor"
]

import abc
import enum
import re

from .calendar import CalendarDate, GregorianDate, DATE

# DATE_VALUE := [
#     <DATE> |
#     <DATE_PERIOD> |
#     <DATE_RANGE>|
#     <DATE_APPROXIMATED> |
#     INT <DATE> (<DATE_PHRASE>) |
#     (<DATE_PHRASE>)
#     ]

# DATE_PERIOD:= [ FROM <DATE> | TO <DATE> | FROM <DATE> TO <DATE> ]
DATE_PERIOD_FROM = r"^FROM\s+(?P<date>" + DATE + ")$"
DATE_PERIOD_TO = r"^TO\s+(?P<date>" + DATE + ")$"
DATE_PERIOD = r"^FROM\s+(?P<date1>" + DATE + r")\s+TO\s+(?P<date2>" + \
              DATE + ")$"

# DATE_RANGE:= [ BEF <DATE> | AFT <DATE> | BET <DATE> AND <DATE> ]
DATE_RANGE_BEFORE = r"^BEF\s+(?P<date>" + DATE + ")$"
DATE_RANGE_AFTER = r"^AFT\s+(?P<date>" + DATE + ")$"
DATE_RANGE = r"^BET\s+(?P<date1>" + DATE + r")\s+AND\s+(?P<date2>" + \
             DATE + ")$"

# DATE_APPROXIMATED := [ ABT <DATE> | CAL <DATE> | EST <DATE> ]
DATE_APPROX_ABOUT = r"^ABT\s+(?P<date>" + DATE + ")$"
DATE_APPROX_CALC = r"^CAL\s+(?P<date>" + DATE + ")$"
DATE_APPROX_EST = r"^EST\s+(?P<date>" + DATE + ")$"

# INT <DATE> (<DATE_PHRASE>)
DATE_INTERP = r"^INT\s+(?P<date>" + DATE + r")\s+\((?P<phrase>.*)\)$"

# (<DATE_PHRASE>)
DATE_PHRASE = r"^\((?P<phrase>.*)\)$"

# <DATE>
DATE_SIMPLE = r"^(?P<date>" + DATE + ")$"

# plus/minus infinity (kinda)
_START_OF_TIME = GregorianDate(5000, bc=True)
_END_OF_TIME = GregorianDate(5000)


[docs]@enum.unique class DateValueTypes(enum.Enum): """Namespace for constants defining types of date values. The constants defined in this namespace are used for the values of the `DateValue.kind` attribute. Each separate class implementing `DateValue` interface uses distinct value for that attribute, and this value can be used to deduce actual type of the date `DateValue` instance. """ SIMPLE = "SIMPLE" """Date value consists of a single CalendarDate, corresponding implementation class is `DateValueSimple`. """ FROM = "FROM" """Period of dates starting at specified date, end date is unknown, corresponding implementation class is `DateValueFrom` """ TO = "TO" """Period of dates ending at specified date, start date is unknown, corresponding implementation class is `DateValueTo`. """ PERIOD = "PERIOD" """Period of dates starting at one date and ending at another, corresponding implementation class is `DateValuePeriod`. """ BEFORE = "BEFORE" """Date value for an event known to happen before given date, corresponding implementation class is `DateValueBefore`. """ AFTER = "AFTER" """Date value for an event known to happen after given date, corresponding implementation class is `DateValueAfter`. """ RANGE = "RANGE" """Date value for an event known to happen between given dates, corresponding implementation class is `DateValueRange`. """ ABOUT = "ABOUT" """Date value for an event known to happen at approximate date, corresponding implementation class is `DateValueAbout`. """ CALCULATED = "CALCULATED" """Date value for an event calculated from other known information, corresponding implementation class is `DateValueCalculated`. """ ESTIMATED = "ESTIMATED" """Date value for an event estimated from other known information, corresponding implementation class is `DateValueEstimated`. """ INTERPRETED = "INTERPRETED" """Date value for an event interpreted from a specified phrase, corresponding implementation class is `DateValueInterpreted`. """ PHRASE = "PHRASE" """Date value for an event is a phrase, corresponding implementation class is `DateValuePhrase`. """
[docs]class DateValue(metaclass=abc.ABCMeta): """Representation of the <DATE_VALUE>, can be exact date, range, period, etc. Parameters ---------- key : `object` Object that is used for ordering, usually it is a pair of `~ged4py.calendar.CalendarDate` instances but can be ``None``. Notes ----- ``DateValue`` is an abstract base class, for each separate kind of GEDCOM date there is a separate concrete class. Class method `parse` is used to parse a date string and return an instance of corresponding sub-class of ``DateValue`` type. There are presently 12 concrete classes implementing this interface (e.g. `DateValueSimple`, `DateValueRange`, etc.) Different types have somewhat different set of attributes, to implement type-specific code on client side one can use one of these approaches: - dispatch based on the value of `kind` attribute, it has one of the values defined in `DateValueTypes` namespace, and that value maps uniquely to a corresponding sub-class of `DateValue`; - dispatch based on the type of the instance using ``isinstance`` method to check the type (e.g. ``isinstance(date, DateValueRange)``); - double dispatch (visitor pattern) by implementing `DateValueVisitor` interface. """ def __init__(self, key): self._key = key
[docs] @classmethod def parse(cls, datestr): """Parse string <DATE_VALUE> string and make `DateValue` instance out of it. Parameters ---------- datestr : `str` String with GEDCOM date, range, period, etc. Returns ------- date_value : `DateValue` Object representing the date value. """ # In some cases date strings can have leadin/trailing spaces if datestr: datestr = datestr.strip() # some apps generate DATE recods without any value, which is # non-standard, return empty DateValue for those if not datestr: return DateValuePhrase(None) for regex, klass in DATES: m = regex.match(datestr) if m is not None: groups = {} for key, val in m.groupdict().items(): if key != 'phrase': val = CalendarDate.parse(val) groups[key] = val return klass(**groups) # if cannot parse string assume it is a phrase return DateValuePhrase(datestr)
@property @abc.abstractmethod def kind(self): """The type of GEDCOM date, one of the `DateValueTypes` enums (`DateValueTypes`). """ raise NotImplementedError()
[docs] def key(self): """Return ordering key for this instance. If this instance has a range of dates associated with it then this method returns the range as pair of dates. If this instance has a single date associated with it then this method returns pair which includes the date twice. For other dates (``PHRASE`` is the only instance without date) it returns a a pair of fixed but arbitrary dates in the future. Returns ------- key : `tuple` [ `~ged4py.calendar.CalendarDate` ] Key used for ordering. """ if self._key is None: # Use _END_OF_TIME so that it is ordered after all real dates return _END_OF_TIME, _END_OF_TIME return self._key
def __lt__(self, other): return self.key() < other.key() def __le__(self, other): return self.key() <= other.key() def __eq__(self, other): return self.key() == other.key() def __ne__(self, other): return self.key() != other.key() def __gt__(self, other): return self.key() > other.key() def __ge__(self, other): return self.key() >= other.key() def __hash__(self): return hash(self.key())
[docs] @abc.abstractmethod def accept(self, visitor): """Implementation of visitor pattern. Each concrete sub-class will implement this method by dispatching the call to corresponding visitor method. Parameters ---------- visitor : `DateValueVisitor` Visitor instance. Returns ------- value : `object` Value returned from a visitor method. """ raise NotImplementedError()
@abc.abstractmethod def __str__(self): raise NotImplementedError() @abc.abstractmethod def __repr__(self): raise NotImplementedError()
[docs]class DateValueSimple(DateValue): """Implementation of `DateValue` interface for simple single-value DATE. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. """ def __init__(self, date): DateValue.__init__(self, (date, date)) self._date = date @property def kind(self): """For DateValueSimple class this is always `DateValueTypes.SIMPLE`. """ return DateValueTypes.SIMPLE @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitSimple(self)
def __str__(self): return str(self.date) def __repr__(self): return "{}(date={})".format(self.__class__.__name__, self.date)
[docs]class DateValueFrom(DateValue): """Implementation of `DateValue` interface for FROM date. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. """ def __init__(self, date): DateValue.__init__(self, (date, _END_OF_TIME)) self._date = date @property def kind(self): """For DateValueFrom class this is always `DateValueTypes.FROM`. """ return DateValueTypes.FROM @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitFrom(self)
def __str__(self): return "FROM {}".format(self.date) def __repr__(self): return "{}(date={})".format(self.__class__.__name__, self.date)
[docs]class DateValueTo(DateValue): """Implementation of `DateValue` interface for TO date. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. """ def __init__(self, date): DateValue.__init__(self, (_START_OF_TIME, date)) self._date = date @property def kind(self): """For DateValueTo class this is always `DateValueTypes.TO`. """ return DateValueTypes.TO @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitTo(self)
def __str__(self): return "TO {}".format(self.date) def __repr__(self): return "{}(date={})".format(self.__class__.__name__, self.date)
[docs]class DateValuePeriod(DateValue): """Implementation of `DateValue` interface for FROM ... TO date. Parameters ---------- date1 : `~ged4py.calendar.CalendarDate` FROM date. date2 : `~ged4py.calendar.CalendarDate` TO date. """ def __init__(self, date1, date2): DateValue.__init__(self, (date1, date2)) self._date1 = date1 self._date2 = date2 @property def kind(self): """For DateValuePeriod class this is always `DateValueTypes.PERIOD`. """ return DateValueTypes.PERIOD @property def date1(self): "First Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date1 @property def date2(self): "Second Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date2
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitPeriod(self)
def __str__(self): return "FROM {} TO {}".format(self.date1, self.date2) def __repr__(self): return "{}(date1={}, date2={})".format(self.__class__.__name__, self.date1, self.date2)
[docs]class DateValueBefore(DateValue): """Implementation of `DateValue` interface for BEF date. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. """ def __init__(self, date): DateValue.__init__(self, (_START_OF_TIME, date)) self._date = date @property def kind(self): """For DateValueBefore class this is always `DateValueTypes.BEFORE`. """ return DateValueTypes.BEFORE @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitBefore(self)
def __str__(self): return "BEFORE {}".format(self.date) def __repr__(self): return "{}(date={})".format(self.__class__.__name__, self.date)
[docs]class DateValueAfter(DateValue): """Implementation of `DateValue` interface for AFT date. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. """ def __init__(self, date): DateValue.__init__(self, (date, _END_OF_TIME)) self._date = date @property def kind(self): """For DateValueAfter class this is always `DateValueTypes.AFTER`. """ return DateValueTypes.AFTER @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitAfter(self)
def __str__(self): return "AFTER {}".format(self.date) def __repr__(self): return "{}(date={})".format(self.__class__.__name__, self.date)
[docs]class DateValueRange(DateValue): """Implementation of `DateValue` interface for BET ... AND ... date. Parameters ---------- date1 : `~ged4py.calendar.CalendarDate` First date. date2 : `~ged4py.calendar.CalendarDate` Second date. """ def __init__(self, date1, date2): DateValue.__init__(self, (date1, date2)) self._date1 = date1 self._date2 = date2 @property def kind(self): """For DateValueRange class this is always `DateValueTypes.RANGE`. """ return DateValueTypes.RANGE @property def date1(self): "First Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date1 @property def date2(self): "Second Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date2
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitRange(self)
def __str__(self): return "BETWEEN {} AND {}".format(self.date1, self.date2) def __repr__(self): return "{}(date1={}, date2={})".format(self.__class__.__name__, self.date1, self.date2)
[docs]class DateValueAbout(DateValue): """Implementation of `DateValue` interface for ABT date. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. """ def __init__(self, date): DateValue.__init__(self, (date, date)) self._date = date @property def kind(self): """For DateValueAbout class this is always `DateValueTypes.ABOUT`. """ return DateValueTypes.ABOUT @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitAbout(self)
def __str__(self): return "ABOUT {}".format(self.date) def __repr__(self): return "{}(date={})".format(self.__class__.__name__, self.date)
[docs]class DateValueCalculated(DateValue): """Implementation of `DateValue` interface for CAL date. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. """ def __init__(self, date): DateValue.__init__(self, (date, date)) self._date = date @property def kind(self): """For DateValueCalculated class this is always `DateValueTypes.CALCULATED`. """ return DateValueTypes.CALCULATED @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitCalculated(self)
def __str__(self): return "CALCULATED {}".format(self.date) def __repr__(self): return "{}(date={})".format(self.__class__.__name__, self.date)
[docs]class DateValueEstimated(DateValue): """Implementation of `DateValue` interface for EST date. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. """ def __init__(self, date): DateValue.__init__(self, (date, date)) self._date = date @property def kind(self): """For DateValueEstimated class this is always `DateValueTypes.ESTIMATED`. """ return DateValueTypes.ESTIMATED @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitEstimated(self)
def __str__(self): return "ESTIMATED {}".format(self.date) def __repr__(self): return "{}(date={})".format(self.__class__.__name__, self.date)
[docs]class DateValueInterpreted(DateValue): """Implementation of `DateValue` interface for INT date. Parameters ---------- date : `~ged4py.calendar.CalendarDate` Corresponding date. phrase : `str` Phrase string associated with this date. """ def __init__(self, date, phrase): DateValue.__init__(self, (date, date)) self._date = date self._phrase = phrase @property def kind(self): """For DateValueInterpreted class this is always `DateValueTypes.INTERPRETED`. """ return DateValueTypes.INTERPRETED @property def date(self): "Calendar date corresponding to this instance (`~ged4py.calendar.CalendarDate`)" return self._date @property def phrase(self): """Phrase associated with this date (`str`) """ return self._phrase
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitInterpreted(self)
def __str__(self): return "INTERPRETED {} ({})".format(self.date, self.phrase) def __repr__(self): return "{}(date={}, phrase={})".format(self.__class__.__name__, self.date, self.phrase)
[docs]class DateValuePhrase(DateValue): """Implementation of `DateValue` interface for phrase-date. Parameters ---------- phrase : `str` Phrase string associated with this date. """ def __init__(self, phrase): DateValue.__init__(self, None) self._phrase = phrase @property def kind(self): """For DateValuePhrase class this is always `DateValueTypes.PHRASE`. """ return DateValueTypes.PHRASE @property def phrase(self): """Phrase associated with this date (`str`) """ return self._phrase
[docs] def accept(self, visitor): # docstring inherited from DateValue class return visitor.visitPhrase(self)
def __str__(self): if self.phrase is None: return "" else: return "({})".format(self.phrase) def __repr__(self): return "{}(phrase={})".format(self.__class__.__name__, self.phrase)
DATES = ( (re.compile(DATE_PERIOD, re.X | re.I), DateValuePeriod), (re.compile(DATE_PERIOD_FROM, re.X | re.I), DateValueFrom), (re.compile(DATE_PERIOD_TO, re.X | re.I), DateValueTo), (re.compile(DATE_RANGE, re.X | re.I), DateValueRange), (re.compile(DATE_RANGE_BEFORE, re.X | re.I), DateValueBefore), (re.compile(DATE_RANGE_AFTER, re.X | re.I), DateValueAfter), (re.compile(DATE_APPROX_ABOUT, re.X | re.I), DateValueAbout), (re.compile(DATE_APPROX_CALC, re.X | re.I), DateValueCalculated), (re.compile(DATE_APPROX_EST, re.X | re.I), DateValueEstimated), (re.compile(DATE_INTERP, re.X | re.I), DateValueInterpreted), (re.compile(DATE_PHRASE, re.X | re.I), DateValuePhrase), (re.compile(DATE_SIMPLE, re.X | re.I), DateValueSimple), )
[docs]class DateValueVisitor(metaclass=abc.ABCMeta): """Interface for implementation of Visitor pattern for `DateValue` classes. One can easily extend behavior of the `DateValue` class hierarchy without modifying classes themselves. Clients need to implement new behavior by sub-classing ``DateValueVisitor`` and calling `DateValue.accept` method, e.g.:: class FormatterVisitor(DateValueVisitor): def visitSimple(self, date): return "Simple date: " + str(date.date) # and so on for each date type visitor = FormatterVisitor() date = DateValue.parse(date_string) formatted = date.accept(visitor) """
[docs] @abc.abstractmethod def visitSimple(self, date): """Visit an instance of `DateValueSimple` type. Parameters ---------- date : `DateValueSimple` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitPeriod(self, date): """Visit an instance of `DateValuePeriod` type. Parameters ---------- date : `DateValuePeriod` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitFrom(self, date): """Visit an instance of `DateValueFrom` type. Parameters ---------- date : `DateValueFrom` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitTo(self, date): """Visit an instance of `DateValueTo` type. Parameters ---------- date : `DateValueTo` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitRange(self, date): """Visit an instance of `DateValueRange` type. Parameters ---------- date : `DateValueRange` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitBefore(self, date): """Visit an instance of `DateValueBefore` type. Parameters ---------- date : `DateValueBefore` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitAfter(self, date): """Visit an instance of `DateValueAfter` type. Parameters ---------- date : `DateValueAfter` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitAbout(self, date): """Visit an instance of `DateValueAbout` type. Parameters ---------- date : `DateValueAbout` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitCalculated(self, date): """Visit an instance of `DateValueCalculated` type. Parameters ---------- date : `DateValueCalculated` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitEstimated(self, date): """Visit an instance of `DateValueEstimated` type. Parameters ---------- date : `DateValueEstimated` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitInterpreted(self, date): """Visit an instance of `DateValueInterpreted` type. Parameters ---------- date : `DateValueInterpreted` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()
[docs] @abc.abstractmethod def visitPhrase(self, date): """Visit an instance of `DateValuePhrase` type. Parameters ---------- date : `DateValuePhrase` Date value instance. Returns ------- value : `object` Implementation of this method can return anything, value will be returned from `DateValue.accept()` method. """ raise NotImplementedError()