Repository: akoumjian/datefinder

Issue:
Cannot find date in a particular type sentences
1) 'Dutta is the recipient of Femina Miss India Universe title in 2004.' => cannot find 2004
2) 'she said that she hit depression after being traumatized on the sets of "Horn OK" in 2008.' => cannot find 2008

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 7fca227..d6296a6 100755
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ venv/
 # due to using tox and pytest
 .tox
 .cache
+
+# IDE
+.vscode/
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index e0a1bb7..caec8e9 100755
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,17 @@
 language: python
 python:
-  # - "2.6"
-  - "2.7"
-  # - "3.2"
-  - "3.3"
   - "3.4"
   - "3.5"
-  - "3.5-dev" # 3.5 development branch
-  - "nightly" # currently points to 3.6-dev
+  - "3.6"
+  
+# Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs
+matrix:
+  include:
+    - python: 3.7
+      dist: xenial
+      sudo: true
+
+      
 # command to install dependencies
 install: "python setup.py install"
 # command to run tests
diff --git a/datefinder.py b/datefinder.py
deleted file mode 100644
index cfd1f74..0000000
--- a/datefinder.py
+++ /dev/null
@@ -1,303 +0,0 @@
-import copy
-import logging
-import regex as re
-from dateutil import tz, parser
-
-
-logger = logging.getLogger('datefinder')
-
-
-class DateFinder(object):
-    """
-    Locates dates in a text
-    """
-
-    DIGITS_MODIFIER_PATTERN = '\d+st|\d+th|\d+rd|first|second|third|fourth|fifth|sixth|seventh|eighth|nineth|tenth|next|last'
-    DIGITS_PATTERN = '\d+'
-    DAYS_PATTERN = 'monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|tue|tues|wed|thur|thurs|fri|sat|sun'
-    MONTHS_PATTERN = 'january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec'
-    TIMEZONES_PATTERN = 'ACDT|ACST|ACT|ACWDT|ACWST|ADDT|ADMT|ADT|AEDT|AEST|AFT|AHDT|AHST|AKDT|AKST|AKTST|AKTT|ALMST|ALMT|AMST|AMT|ANAST|ANAT|ANT|APT|AQTST|AQTT|ARST|ART|ASHST|ASHT|AST|AWDT|AWST|AWT|AZOMT|AZOST|AZOT|AZST|AZT|BAKST|BAKT|BDST|BDT|BEAT|BEAUT|BIOT|BMT|BNT|BORT|BOST|BOT|BRST|BRT|BST|BTT|BURT|CANT|CAPT|CAST|CAT|CAWT|CCT|CDDT|CDT|CEDT|CEMT|CEST|CET|CGST|CGT|CHADT|CHAST|CHDT|CHOST|CHOT|CIST|CKHST|CKT|CLST|CLT|CMT|COST|COT|CPT|CST|CUT|CVST|CVT|CWT|CXT|ChST|DACT|DAVT|DDUT|DFT|DMT|DUSST|DUST|EASST|EAST|EAT|ECT|EDDT|EDT|EEDT|EEST|EET|EGST|EGT|EHDT|EMT|EPT|EST|ET|EWT|FET|FFMT|FJST|FJT|FKST|FKT|FMT|FNST|FNT|FORT|FRUST|FRUT|GALT|GAMT|GBGT|GEST|GET|GFT|GHST|GILT|GIT|GMT|GST|GYT|HAA|HAC|HADT|HAE|HAP|HAR|HAST|HAT|HAY|HDT|HKST|HKT|HLV|HMT|HNA|HNC|HNE|HNP|HNR|HNT|HNY|HOVST|HOVT|HST|ICT|IDDT|IDT|IHST|IMT|IOT|IRDT|IRKST|IRKT|IRST|ISST|IST|JAVT|JCST|JDT|JMT|JST|JWST|KART|KDT|KGST|KGT|KIZST|KIZT|KMT|KOST|KRAST|KRAT|KST|KUYST|KUYT|KWAT|LHDT|LHST|LINT|LKT|LMT|LMT|LMT|LMT|LRT|LST|MADMT|MADST|MADT|MAGST|MAGT|MALST|MALT|MART|MAWT|MDDT|MDST|MDT|MEST|MET|MHT|MIST|MIT|MMT|MOST|MOT|MPT|MSD|MSK|MSM|MST|MUST|MUT|MVT|MWT|MYT|NCST|NCT|NDDT|NDT|NEGT|NEST|NET|NFT|NMT|NOVST|NOVT|NPT|NRT|NST|NT|NUT|NWT|NZDT|NZMT|NZST|OMSST|OMST|ORAST|ORAT|PDDT|PDT|PEST|PET|PETST|PETT|PGT|PHOT|PHST|PHT|PKST|PKT|PLMT|PMDT|PMMT|PMST|PMT|PNT|PONT|PPMT|PPT|PST|PT|PWT|PYST|PYT|QMT|QYZST|QYZT|RET|RMT|ROTT|SAKST|SAKT|SAMT|SAST|SBT|SCT|SDMT|SDT|SET|SGT|SHEST|SHET|SJMT|SLT|SMT|SRET|SRT|SST|STAT|SVEST|SVET|SWAT|SYOT|TAHT|TASST|TAST|TBIST|TBIT|TBMT|TFT|THA|TJT|TKT|TLT|TMT|TOST|TOT|TRST|TRT|TSAT|TVT|ULAST|ULAT|URAST|URAT|UTC|UYHST|UYST|UYT|UZST|UZT|VET|VLAST|VLAT|VOLST|VOLT|VOST|VUST|VUT|WARST|WART|WAST|WAT|WDT|WEDT|WEMT|WEST|WET|WFT|WGST|WGT|WIB|WIT|WITA|WMT|WSDT|WSST|WST|WT|XJT|YAKST|YAKT|YAPT|YDDT|YDT|YEKST|YEKST|YEKT|YEKT|YERST|YERT|YPT|YST|YWT|zzz'
-    ## explicit north american timezones that get replaced
-    NA_TIMEZONES_PATTERN = 'pacific|eastern|mountain|central'
-    ALL_TIMEZONES_PATTERN = TIMEZONES_PATTERN + '|' + NA_TIMEZONES_PATTERN
-    DELIMITERS_PATTERN = '[/\:\-\,\s\_\+\@]+'
-    TIME_PERIOD_PATTERN = 'a\.m\.|am|p\.m\.|pm'
-    ## can be in date strings but not recognized by dateutils
-    EXTRA_TOKENS_PATTERN = 'due|by|on|standard|daylight|savings|time|date|of|to|until|z|at|t'
-
-    ## TODO: Get english numbers?
-    ## http://www.rexegg.com/regex-trick-numbers-in-english.html
-
-    RELATIVE_PATTERN = 'before|after|next|last|ago'
-    TIME_SHORTHAND_PATTERN = 'noon|midnight|today|yesterday'
-    UNIT_PATTERN = 'second|minute|hour|day|week|month|year'
-
-    ## Time pattern is used independently, so specified here.
-    TIME_PATTERN = """
-    (?P<time>
-        ## Captures in format XX:YY(:ZZ) (PM) (EST)
-        (
-            (?P<hours>\d{{1,2}})
-            \:
-            (?P<minutes>\d{{1,2}})
-            (\:(?<seconds>\d{{1,2}}))?
-            ([\.\,](?<microseconds>\d{{1,6}}))?
-            \s*
-            (?P<time_periods>{time_periods})?
-            \s*
-            (?P<timezones>{timezones})?
-        )
-        |
-        ## Captures in format 11 AM (EST)
-        ## Note with single digit capture requires time period
-        (
-            (?P<hours>\d{{1,2}})
-            \s*
-            (?P<time_periods>{time_periods})
-            \s*
-            (?P<timezones>{timezones})*
-        )
-    )
-    """.format(
-        time_periods=TIME_PERIOD_PATTERN,
-        timezones=ALL_TIMEZONES_PATTERN
-    )
-
-    DATES_PATTERN = """
-    (
-        (
-            {time}
-            |
-            ## Grab any digits
-            (?P<digits_modifier>{digits_modifier})
-            |
-            (?P<digits>{digits})
-            |
-            (?P<days>{days})
-            |
-            (?P<months>{months})
-            |
-            ## Delimiters, ie Tuesday[,] July 18 or 6[/]17[/]2008
-            ## as well as whitespace
-            (?P<delimiters>{delimiters})
-            |
-            ## These tokens could be in phrases that dateutil does not yet recognize
-            ## Some are US Centric
-            (?P<extra_tokens>{extra_tokens})
-        ## We need at least three items to match for minimal datetime parsing
-        ## ie 10pm
-        ){{3,}}
-    )
-    """
-
-    DATES_PATTERN = DATES_PATTERN.format(
-        time=TIME_PATTERN,
-        digits=DIGITS_PATTERN,
-        digits_modifier=DIGITS_MODIFIER_PATTERN,
-        days=DAYS_PATTERN,
-        months=MONTHS_PATTERN,
-        delimiters=DELIMITERS_PATTERN,
-        extra_tokens=EXTRA_TOKENS_PATTERN
-    )
-
-    DATE_REGEX = re.compile(DATES_PATTERN, re.IGNORECASE | re.MULTILINE | re.UNICODE | re.DOTALL | re.VERBOSE)
-
-    TIME_REGEX = re.compile(TIME_PATTERN, re.IGNORECASE | re.MULTILINE | re.UNICODE | re.DOTALL | re.VERBOSE)
-
-    ## These tokens can be in original text but dateutil
-    ## won't handle them without modification
-    REPLACEMENTS = {
-        "standard": " ",
-        "daylight": " ",
-        "savings": " ",
-        "time": " ",
-        "date": " ",
-        "by": " ",
-        "due": " ",
-        "on": " ",
-        "to": " ",
-    }
-
-    TIMEZONE_REPLACEMENTS = {
-        "pacific": "PST",
-        "eastern": "EST",
-        "mountain": "MST",
-        "central": "CST",
-    }
-
-    ## Characters that can be removed from ends of matched strings
-    STRIP_CHARS = ' \n\t:-.,_'
-
-    def __init__(self, base_date=None):
-        self.base_date = base_date
-
-    def find_dates(self, text, source=False, index=False, strict=False):
-
-        for date_string, indices, captures in self.extract_date_strings(text, strict=strict):
-
-            as_dt = self.parse_date_string(date_string, captures)
-            if as_dt is None:
-                ## Dateutil couldn't make heads or tails of it
-                ## move on to next
-                continue
-
-            returnables = (as_dt,)
-            if source:
-                returnables = returnables + (date_string,)
-            if index:
-                returnables = returnables + (indices,)
-
-            if len(returnables) == 1:
-                returnables = returnables[0]
-            yield returnables
-
-    def _find_and_replace(self, date_string, captures):
-        """
-        :warning: when multiple tz matches exist the last sorted capture will trump
-        :param date_string:
-        :return: date_string, tz_string
-        """
-        # add timezones to replace
-        cloned_replacements = copy.copy(self.REPLACEMENTS)  # don't mutate
-        for tz_string in captures.get('timezones', []):
-            cloned_replacements.update({tz_string: ' '})
-
-        date_string = date_string.lower()
-        for key, replacement in cloned_replacements.items():
-            # we really want to match all permutations of the key surrounded by whitespace chars except one
-            # for example: consider the key = 'to'
-            # 1. match 'to '
-            # 2. match ' to'
-            # 3. match ' to '
-            # but never match r'(\s|)to(\s|)' which would make 'october' > 'ocber'
-            date_string = re.sub(r'(^|\s)' + key + '(\s|$)', replacement, date_string, flags=re.IGNORECASE)
-
-        return date_string, self._pop_tz_string(sorted(captures.get('timezones', [])))
-
-    def _pop_tz_string(self, list_of_timezones):
-        try:
-            tz_string = list_of_timezones.pop()
-            # make sure it's not a timezone we
-            # want replaced with better abbreviation
-            return self.TIMEZONE_REPLACEMENTS.get(tz_string, tz_string)
-        except IndexError:
-            return ''
-
-    def _add_tzinfo(self, datetime_obj, tz_string):
-        """
-        take a naive datetime and add dateutil.tz.tzinfo object
-
-        :param datetime_obj: naive datetime object
-        :return: datetime object with tzinfo
-        """
-        if datetime_obj is None:
-            return None
-
-        tzinfo_match = tz.gettz(tz_string)
-        return datetime_obj.replace(tzinfo=tzinfo_match)
-
-    def parse_date_string(self, date_string, captures):
-        # For well formatted string, we can already let dateutils parse them
-        # otherwise self._find_and_replace method might corrupt them
-        try:
-            as_dt = parser.parse(date_string, default=self.base_date)
-        except ValueError:
-            # replace tokens that are problematic for dateutil
-            date_string, tz_string = self._find_and_replace(date_string, captures)
-
-            ## One last sweep after removing
-            date_string = date_string.strip(self.STRIP_CHARS)
-            ## Match strings must be at least 3 characters long
-            ## < 3 tends to be garbage
-            if len(date_string) < 3:
-                return None
-
-            try:
-                logger.debug('Parsing {0} with dateutil'.format(date_string))
-                as_dt = parser.parse(date_string, default=self.base_date)
-            except Exception as e:
-                logger.debug(e)
-                as_dt = None
-            if tz_string:
-                as_dt = self._add_tzinfo(as_dt, tz_string)
-        return as_dt
-
-    def extract_date_strings(self, text, strict=False):
-        """
-        Scans text for possible datetime strings and extracts them
-
-        source: also return the original date string
-        index: also return the indices of the date string in the text
-        strict: Strict mode will only return dates sourced with day, month, and year
-        """
-        for match in self.DATE_REGEX.finditer(text):
-            match_str = match.group(0)
-            indices = match.span(0)
-
-            ## Get individual group matches
-            captures = match.capturesdict()
-            time = captures.get('time')
-            digits = captures.get('digits')
-            digits_modifiers = captures.get('digits_modifiers')
-            days = captures.get('days')
-            months = captures.get('months')
-            timezones = captures.get('timezones')
-            delimiters = captures.get('delimiters')
-            time_periods = captures.get('time_periods')
-            extra_tokens = captures.get('extra_tokens')
-
-            if strict:
-                complete = False
-                ## 12-05-2015
-                if len(digits) == 3:
-                    complete = True
-                ## 19 February 2013 year 09:10
-                elif (len(months) == 1) and (len(digits) == 2):
-                    complete = True
-
-                if not complete:
-                    continue
-
-            ## sanitize date string
-            ## replace unhelpful whitespace characters with single whitespace
-            match_str = re.sub('[\n\t\s\xa0]+', ' ', match_str)
-            match_str = match_str.strip(self.STRIP_CHARS)
-
-            ## Save sanitized source string
-            yield match_str, indices, captures
-
-
-def find_dates(
-    text,
-    source=False,
-    index=False,
-    strict=False,
-    base_date=None
-    ):
-    """
-    Extract datetime strings from text
-
-    :param text:
-        A string that contains one or more natural language or literal
-        datetime strings
-    :type text: str|unicode
-    :param source:
-        Return the original string segment
-    :type source: boolean
-    :param index:
-        Return the indices where the datetime string was located in text
-    :type index: boolean
-    :param strict:
-        Only return datetimes with complete date information. For example:
-        `July 2016` of `Monday` will not return datetimes.
-        `May 16, 2015` will return datetimes.
-    :type strict: boolean
-    :param base_date:
-        Set a default base datetime when parsing incomplete dates
-    :type base_date: datetime
-
-    :return: Returns a generator that produces :mod:`datetime.datetime` objects,
-        or a tuple with the source text and index, if requested
-    """
-    date_finder = DateFinder(base_date=base_date)
-    return date_finder.find_dates(text, source=source, index=index, strict=strict)
diff --git a/datefinder/__init__.py b/datefinder/__init__.py
new file mode 100644
index 0000000..eb294ed
--- /dev/null
+++ b/datefinder/__init__.py
@@ -0,0 +1,206 @@
+import copy
+import logging
+import regex as re
+from dateutil import tz, parser
+
+from .constants import REPLACEMENTS, TIMEZONE_REPLACEMENTS, STRIP_CHARS, DATE_REGEX, RANGE_REGEX
+
+logger = logging.getLogger('datefinder')
+
+
+class DateFinder(object):
+    """
+    Locates dates in a text
+    """
+
+    def __init__(self, base_date=None):
+        self.base_date = base_date
+
+    def find_dates(self, text, source=False, index=False, strict=False):
+
+        for date_string, indices, captures in self.extract_date_strings(text, strict=strict):
+
+            as_dt = self.parse_date_string(date_string, captures)
+            if as_dt is None:
+                ## Dateutil couldn't make heads or tails of it
+                ## move on to next
+                continue
+
+            returnables = (as_dt,)
+            if source:
+                returnables = returnables + (date_string,)
+            if index:
+                returnables = returnables + (indices,)
+
+            if len(returnables) == 1:
+                returnables = returnables[0]
+            yield returnables
+
+    def _find_and_replace(self, date_string, captures):
+        """
+        :warning: when multiple tz matches exist the last sorted capture will trump
+        :param date_string:
+        :return: date_string, tz_string
+        """
+        # add timezones to replace
+        cloned_replacements = copy.copy(REPLACEMENTS)  # don't mutate
+        for tz_string in captures.get('timezones', []):
+            cloned_replacements.update({tz_string: ' '})
+
+        date_string = date_string.lower()
+        for key, replacement in cloned_replacements.items():
+            # we really want to match all permutations of the key surrounded by whitespace chars except one
+            # for example: consider the key = 'to'
+            # 1. match 'to '
+            # 2. match ' to'
+            # 3. match ' to '
+            # but never match r'(\s|)to(\s|)' which would make 'october' > 'ocber'
+            date_string = re.sub(r'(^|\s)' + key + r'(\s|$)', replacement, date_string, flags=re.IGNORECASE)
+
+        return date_string, self._pop_tz_string(sorted(captures.get('timezones', [])))
+
+    def _pop_tz_string(self, list_of_timezones):
+        try:
+            tz_string = list_of_timezones.pop()
+            # make sure it's not a timezone we
+            # want replaced with better abbreviation
+            return TIMEZONE_REPLACEMENTS.get(tz_string, tz_string)
+        except IndexError:
+            return ''
+
+    def _add_tzinfo(self, datetime_obj, tz_string):
+        """
+        take a naive datetime and add dateutil.tz.tzinfo object
+
+        :param datetime_obj: naive datetime object
+        :return: datetime object with tzinfo
+        """
+        if datetime_obj is None:
+            return None
+
+        tzinfo_match = tz.gettz(tz_string)
+        return datetime_obj.replace(tzinfo=tzinfo_match)
+
+    def parse_date_string(self, date_string, captures):
+        # For well formatted string, we can already let dateutils parse them
+        # otherwise self._find_and_replace method might corrupt them
+        try:
+            as_dt = parser.parse(date_string, default=self.base_date)
+        except ValueError:
+            # replace tokens that are problematic for dateutil
+            date_string, tz_string = self._find_and_replace(date_string, captures)
+
+            ## One last sweep after removing
+            date_string = date_string.strip(STRIP_CHARS)
+            ## Match strings must be at least 3 characters long
+            ## < 3 tends to be garbage
+            if len(date_string) < 3:
+                return None
+
+            try:
+                logger.debug('Parsing {0} with dateutil'.format(date_string))
+                as_dt = parser.parse(date_string, default=self.base_date)
+            except Exception as e:
+                logger.debug(e)
+                as_dt = None
+            if tz_string:
+                as_dt = self._add_tzinfo(as_dt, tz_string)
+        return as_dt
+
+    def extract_date_strings(self, text, strict=False):
+        """
+        Scans text for possible datetime strings and extracts them
+        :param strict: Strict mode will only return dates sourced with day, month, and year
+        """
+
+        # Try to find ranges first
+        range_strings = list()
+        found_range = False
+        for range_match in RANGE_REGEX.finditer(text):
+            # Parse datetime 1 and datetime 2 recursively
+            range_captures = range_match.capturesdict()
+            dt1 = range_captures.get("dt1", [])
+            dt2 = range_captures.get("dt2", [])
+
+            for dt1_str in dt1:
+                range_strings.extend(self.extract_date_strings(dt1_str, strict=strict))
+
+            for dt2_str in dt2:
+                range_strings.extend(self.extract_date_strings(dt2_str, strict=strict))
+            
+            found_range = True
+
+        for range_string in range_strings:
+            yield range_string
+        
+        # Try to match regular datetimes if no ranges have been found
+        if not found_range:
+            for match in DATE_REGEX.finditer(text):
+                match_str = match.group(0)
+                indices = match.span(0)
+
+                ## Get individual group matches
+                captures = match.capturesdict()
+                # time = captures.get('time')
+                digits = captures.get('digits')
+                # digits_modifiers = captures.get('digits_modifiers')
+                # days = captures.get('days')
+                months = captures.get('months')
+                # timezones = captures.get('timezones')
+                # delimiters = captures.get('delimiters')
+                # time_periods = captures.get('time_periods')
+                # extra_tokens = captures.get('extra_tokens')
+
+                if strict:
+                    complete = False
+                    if len(digits) == 3:  # 12-05-2015
+                        complete = True
+                    elif (len(months) == 1) and (len(digits) == 2):  # 19 February 2013 year 09:10
+                        complete = True
+
+                    if not complete:
+                        continue
+
+                ## sanitize date string
+                ## replace unhelpful whitespace characters with single whitespace
+                match_str = re.sub(r'[\n\t\s\xa0]+', ' ', match_str)
+                match_str = match_str.strip(STRIP_CHARS)
+
+                ## Save sanitized source string
+                yield match_str, indices, captures            
+
+
+def find_dates(
+    text,
+    source=False,
+    index=False,
+    strict=False,
+    base_date=None
+    ):
+    """
+    Extract datetime strings from text
+
+    :param text:
+        A string that contains one or more natural language or literal
+        datetime strings
+    :type text: str|unicode
+    :param source:
+        Return the original string segment
+    :type source: boolean
+    :param index:
+        Return the indices where the datetime string was located in text
+    :type index: boolean
+    :param strict:
+        Only return datetimes with complete date information. For example:
+        `July 2016` of `Monday` will not return datetimes.
+        `May 16, 2015` will return datetimes.
+    :type strict: boolean
+    :param base_date:
+        Set a default base datetime when parsing incomplete dates
+    :type base_date: datetime
+
+    :return: Returns a generator that produces :mod:`datetime.datetime` objects,
+        or a tuple with the source text and index, if requested
+    """
+    date_finder = DateFinder(base_date=base_date)
+    return date_finder.find_dates(text, source=source, index=index, strict=strict)
diff --git a/datefinder/constants.py b/datefinder/constants.py
new file mode 100644
index 0000000..b643a00
--- /dev/null
+++ b/datefinder/constants.py
@@ -0,0 +1,154 @@
+import regex as re
+
+NUMBERS_PATTERN = r'first|second|third|fourth|fifth|sixth|seventh|eighth|nineth|tenth'
+POSITIONNAL_TOKENS= r'next|last'
+DIGITS_PATTERN = r'\d+'
+DIGITS_SUFFIXES= r'st|th|rd|nd'
+DAYS_PATTERN = 'monday|tuesday|wednesday|thursday|friday|saturday|sunday|mon|tue|tues|wed|thur|thurs|fri|sat|sun'
+MONTHS_PATTERN = r'january|february|march|april|may|june|july|august|september|october|november|december|jan\.?|feb\.?|mar\.?|apr\.?|may\.?|jun\.?|jul\.?|aug\.?|sep\.?|sept\.?|oct\.?|nov\.?|dec\.?'
+TIMEZONES_PATTERN = 'ACDT|ACST|ACT|ACWDT|ACWST|ADDT|ADMT|ADT|AEDT|AEST|AFT|AHDT|AHST|AKDT|AKST|AKTST|AKTT|ALMST|ALMT|AMST|AMT|ANAST|ANAT|ANT|APT|AQTST|AQTT|ARST|ART|ASHST|ASHT|AST|AWDT|AWST|AWT|AZOMT|AZOST|AZOT|AZST|AZT|BAKST|BAKT|BDST|BDT|BEAT|BEAUT|BIOT|BMT|BNT|BORT|BOST|BOT|BRST|BRT|BST|BTT|BURT|CANT|CAPT|CAST|CAT|CAWT|CCT|CDDT|CDT|CEDT|CEMT|CEST|CET|CGST|CGT|CHADT|CHAST|CHDT|CHOST|CHOT|CIST|CKHST|CKT|CLST|CLT|CMT|COST|COT|CPT|CST|CUT|CVST|CVT|CWT|CXT|ChST|DACT|DAVT|DDUT|DFT|DMT|DUSST|DUST|EASST|EAST|EAT|ECT|EDDT|EDT|EEDT|EEST|EET|EGST|EGT|EHDT|EMT|EPT|EST|ET|EWT|FET|FFMT|FJST|FJT|FKST|FKT|FMT|FNST|FNT|FORT|FRUST|FRUT|GALT|GAMT|GBGT|GEST|GET|GFT|GHST|GILT|GIT|GMT|GST|GYT|HAA|HAC|HADT|HAE|HAP|HAR|HAST|HAT|HAY|HDT|HKST|HKT|HLV|HMT|HNA|HNC|HNE|HNP|HNR|HNT|HNY|HOVST|HOVT|HST|ICT|IDDT|IDT|IHST|IMT|IOT|IRDT|IRKST|IRKT|IRST|ISST|IST|JAVT|JCST|JDT|JMT|JST|JWST|KART|KDT|KGST|KGT|KIZST|KIZT|KMT|KOST|KRAST|KRAT|KST|KUYST|KUYT|KWAT|LHDT|LHST|LINT|LKT|LMT|LMT|LMT|LMT|LRT|LST|MADMT|MADST|MADT|MAGST|MAGT|MALST|MALT|MART|MAWT|MDDT|MDST|MDT|MEST|MET|MHT|MIST|MIT|MMT|MOST|MOT|MPT|MSD|MSK|MSM|MST|MUST|MUT|MVT|MWT|MYT|NCST|NCT|NDDT|NDT|NEGT|NEST|NET|NFT|NMT|NOVST|NOVT|NPT|NRT|NST|NT|NUT|NWT|NZDT|NZMT|NZST|OMSST|OMST|ORAST|ORAT|PDDT|PDT|PEST|PET|PETST|PETT|PGT|PHOT|PHST|PHT|PKST|PKT|PLMT|PMDT|PMMT|PMST|PMT|PNT|PONT|PPMT|PPT|PST|PT|PWT|PYST|PYT|QMT|QYZST|QYZT|RET|RMT|ROTT|SAKST|SAKT|SAMT|SAST|SBT|SCT|SDMT|SDT|SET|SGT|SHEST|SHET|SJMT|SLT|SMT|SRET|SRT|SST|STAT|SVEST|SVET|SWAT|SYOT|TAHT|TASST|TAST|TBIST|TBIT|TBMT|TFT|THA|TJT|TKT|TLT|TMT|TOST|TOT|TRST|TRT|TSAT|TVT|ULAST|ULAT|URAST|URAT|UTC|UYHST|UYST|UYT|UZST|UZT|VET|VLAST|VLAT|VOLST|VOLT|VOST|VUST|VUT|WARST|WART|WAST|WAT|WDT|WEDT|WEMT|WEST|WET|WFT|WGST|WGT|WIB|WIT|WITA|WMT|WSDT|WSST|WST|WT|XJT|YAKST|YAKT|YAPT|YDDT|YDT|YEKST|YEKST|YEKT|YEKT|YERST|YERT|YPT|YST|YWT|zzz'
+## explicit north american timezones that get replaced
+NA_TIMEZONES_PATTERN = 'pacific|eastern|mountain|central'
+ALL_TIMEZONES_PATTERN = TIMEZONES_PATTERN + '|' + NA_TIMEZONES_PATTERN
+DELIMITERS_PATTERN = r'[/\:\-\,\s\_\+\@]+'
+
+# Allows for straightforward datestamps e.g 2017, 201712, 20171223. Created with:
+#  YYYYMM_PATTERN = '|'.join(['19\d\d'+'{:0>2}'.format(mon)+'|20\d\d'+'{:0>2}'.format(mon) for mon in range(1, 13)])
+#  YYYYMMDD_PATTERN = '|'.join(['19\d\d'+'{:0>2}'.format(mon)+'[0123]\d|20\d\d'+'{:0>2}'.format(mon)+'[0123]\d' for mon in range(1, 13)])
+YYYY_PATTERN = r'19\d\d|20\d\d'
+YYYYMM_PATTERN = r'19\d\d01|20\d\d01|19\d\d02|20\d\d02|19\d\d03|20\d\d03|19\d\d04|20\d\d04|19\d\d05|20\d\d05|19\d\d06|20\d\d06|19\d\d07|20\d\d07|19\d\d08|20\d\d08|19\d\d09|20\d\d09|19\d\d10|20\d\d10|19\d\d11|20\d\d11|19\d\d12|20\d\d12'
+YYYYMMDD_PATTERN = r'19\d\d01[0123]\d|20\d\d01[0123]\d|19\d\d02[0123]\d|20\d\d02[0123]\d|19\d\d03[0123]\d|20\d\d03[0123]\d|19\d\d04[0123]\d|20\d\d04[0123]\d|19\d\d05[0123]\d|20\d\d05[0123]\d|19\d\d06[0123]\d|20\d\d06[0123]\d|19\d\d07[0123]\d|20\d\d07[0123]\d|19\d\d08[0123]\d|20\d\d08[0123]\d|19\d\d09[0123]\d|20\d\d09[0123]\d|19\d\d10[0123]\d|20\d\d10[0123]\d|19\d\d11[0123]\d|20\d\d11[0123]\d|19\d\d12[0123]\d|20\d\d12[0123]\d'
+YYYYMMDDHHMMSS_PATTERN = '|'.join([r'19\d\d' + '{:0>2}'.format(mon) + r'[0-3]\d[0-5]\d[0-5]\d[0-5]\d|20\d\d' + '{:0>2}'.format(mon) + r'[0-3]\d[0-5]\d[0-5]\d[0-5]\d' for mon in range(1, 13)])
+ISO8601_PATTERN = r'(?P<years>-?(\:[1-9][0-9]*)?[0-9]{4})\-(?P<months>1[0-2]|0[1-9])\-(?P<days>3[01]|0[1-9]|[12][0-9])T(?P<hours>2[0-3]|[01][0-9])\:(?P<minutes>[0-5][0-9]):(?P<seconds>[0-5][0-9])(?:[\.,]+(?P<microseconds>[0-9]+))?(?P<offset>(?:Z|[+-](?:2[0-3]|[01][0-9])\:[0-5][0-9]))?'
+UNDELIMITED_STAMPS_PATTERN = '|'.join([YYYYMMDDHHMMSS_PATTERN, YYYYMMDD_PATTERN, YYYYMM_PATTERN, ISO8601_PATTERN])
+DELIMITERS_PATTERN = r'[/\:\-\,\.\s\_\+\@]+'
+TIME_PERIOD_PATTERN = r'a\.m\.|am|p\.m\.|pm'
+## can be in date strings but not recognized by dateutils
+EXTRA_TOKENS_PATTERN = r'due|by|on|during|standard|daylight|savings|time|date|dated|of|to|through|between|until|at|day'
+
+## TODO: Get english numbers?
+## http://www.rexegg.com/regex-trick-numbers-in-english.html
+
+RELATIVE_PATTERN = 'before|after|next|last|ago'
+TIME_SHORTHAND_PATTERN = 'noon|midnight|today|yesterday'
+UNIT_PATTERN = 'second|minute|hour|day|week|month|year'
+
+## Time pattern is used independently, so specified here.
+TIME_PATTERN = r"""
+(?P<time>
+    ## Captures in format XX:YY(:ZZ) (PM) (EST)
+    (
+        (?P<hours>\d{{1,2}})
+        \:
+        (?P<minutes>\d{{1,2}})
+        (\:(?<seconds>\d{{1,2}}))?
+        ([\.\,](?<microseconds>\d{{1,6}}))?
+        \s*
+        (?P<time_periods>{time_periods})?
+        \s*
+        (?P<timezones>{timezones})?
+    )
+    |
+    ## Captures in format 11 AM (EST)
+    ## Note with single digit capture requires time period
+    (
+        (?P<hours>\d{{1,2}})
+        \s*
+        (?P<time_periods>{time_periods})
+        \s*
+        (?P<timezones>{timezones})*
+    )
+)
+""".format(
+    time_periods=TIME_PERIOD_PATTERN,
+    timezones=ALL_TIMEZONES_PATTERN
+)
+
+DATES_PATTERN = """
+(
+    ## Undelimited datestamps (treated independently)
+    (?P<undelimited_stamps>{undelimited_stamps})
+    |
+    (
+        {time}
+        |
+        ## Grab any four digit years
+        (?P<years>{years})
+        |
+        ## Numbers
+        (?P<numbers>{numbers})
+        ## Grab any digits
+        |
+        (?P<digits>{digits})(?P<digits_suffixes>{digits_suffixes})?
+        |
+        (?P<days>{days})
+        |
+        (?P<months>{months})
+        |
+        ## Delimiters, ie Tuesday[,] July 18 or 6[/]17[/]2008
+        ## as well as whitespace
+        (?P<delimiters>{delimiters})
+        |
+        (?P<positionnal_tokens>{positionnal_tokens})
+        |
+        ## These tokens could be in phrases that dateutil does not yet recognize
+        ## Some are US Centric
+        (?P<extra_tokens>{extra_tokens})
+    ## We need at least three items to match for minimal datetime parsing
+    ## ie 10pm
+    ){{3,}}
+)
+"""
+
+DATES_PATTERN = DATES_PATTERN.format(
+    time=TIME_PATTERN,
+    undelimited_stamps=UNDELIMITED_STAMPS_PATTERN,
+    years=YYYY_PATTERN,
+    numbers=NUMBERS_PATTERN,
+    digits=DIGITS_PATTERN,
+    digits_suffixes=DIGITS_SUFFIXES,
+    days=DAYS_PATTERN,
+    months=MONTHS_PATTERN,
+    delimiters=DELIMITERS_PATTERN,
+    positionnal_tokens=POSITIONNAL_TOKENS,
+    extra_tokens=EXTRA_TOKENS_PATTERN
+)
+
+RANGE_PATTERN = r"""
+(?:
+    (?P<dt1>{date_pattern})
+    [\s]?(to|through)[\s]?
+    (?P<dt2>{date_pattern})
+)
+""".format(date_pattern=DATES_PATTERN)
+
+DATE_REGEX = re.compile(DATES_PATTERN, re.IGNORECASE | re.MULTILINE | re.UNICODE | re.DOTALL | re.VERBOSE)
+
+TIME_REGEX = re.compile(TIME_PATTERN, re.IGNORECASE | re.MULTILINE | re.UNICODE | re.DOTALL | re.VERBOSE)
+
+RANGE_REGEX = re.compile(RANGE_PATTERN, re.IGNORECASE | re.MULTILINE | re.UNICODE | re.DOTALL | re.VERBOSE)
+
+## These tokens can be in original text but dateutil
+## won't handle them without modification
+REPLACEMENTS = {
+    "standard": " ",
+    "daylight": " ",
+    "savings": " ",
+    "time": " ",
+    "date": " ",
+    "by": " ",
+    "due": " ",
+    "on": " ",
+    "to": " ",
+    "day": " ",
+}
+
+TIMEZONE_REPLACEMENTS = {
+    "pacific": "PST",
+    "eastern": "EST",
+    "mountain": "MST",
+    "central": "CST",
+}
+
+## Characters that can be removed from ends of matched strings
+STRIP_CHARS = ' \n\t:-.,_'
\ No newline at end of file
diff --git a/setup.py b/setup.py
index e0f564b..2aa926e 100755
--- a/setup.py
+++ b/setup.py
@@ -23,7 +23,7 @@ setup(
     # Versions should comply with PEP440.  For a discussion on single-sourcing
     # the version across setup.py and the project code, see
     # https://packaging.python.org/en/latest/single_source_version.html
-    version='0.6.1',
+    version='0.6.2',
 
     description='Extract datetime objects from strings',
     long_description=long_description,
@@ -58,11 +58,10 @@ setup(
         # 'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        # 'Programming Language :: Python :: 3.2',
-        'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
-
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
         # Topics
         'Natural Language :: English',
     ],
@@ -72,24 +71,24 @@ setup(
 
     # You can just specify the packages manually here if your project is
     # simple. Or you can use find_packages().
-    # packages=find_packages(exclude=['tests']),
+    packages=find_packages(exclude=['tests']),
 
     # Alternatively, if you want to distribute just a my_module.py, uncomment
     # this:
-    py_modules=['datefinder'],
+    # py_modules=['datefinder'],
 
     # List run-time dependencies here.  These will be installed by pip when
     # your project is installed. For an analysis of "install_requires" vs pip's
     # requirements files see:
     # https://packaging.python.org/en/latest/requirements.html
-    install_requires=['regex==2016.01.10', 'python-dateutil>=2.4.2', 'pytz'],
+    install_requires=['regex>=2017.02.08', 'python-dateutil>=2.4.2', 'pytz'],
 
     # List additional groups of dependencies here (e.g. development
     # dependencies). You can install these using the following syntax,
     # for example:
     # $ pip install -e .[dev,test]
     extras_require={
-        'dev': ['pytest>=2.8.5', 'mock', 'pytz>=2015.7'],
+        'dev': ['pytest>=2.8.5', 'mock', 'pytz>=2015.7', 'pylint==2.1.1'],
         'test': ['pytest>=2.8.5', 'mock', 'pytz>=2015.7'],
     },
 
diff --git a/tox.ini b/tox.ini
index ea55cc9..275e0d5 100755
--- a/tox.ini
+++ b/tox.ini
@@ -1,12 +1,12 @@
 [tox]
-envlist = py{27,33,34,35}
+envlist = py{34,35,36,37}
 
 [testenv]
 basepython =
-    py27: python2.7
-    py33: python3.3
     py34: python3.4
     py35: python3.5
+    py36: python3.6
+    py37: python3.7
 commands =
     pip install -e .[dev,test]
     py.test tests


Test patch:
diff --git a/tests/test_extract_date_strings.py b/tests/test_extract_date_strings.py
index 6c52c7c..83aee70 100644
--- a/tests/test_extract_date_strings.py
+++ b/tests/test_extract_date_strings.py
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
 def test_extract_date_strings(date_string, expected_match_date_string):
     dt = datefinder.DateFinder()
     for actual_date_string, indexes, captures in dt.extract_date_strings(date_string):
-        logger.debug("acutal={}  expected={}".format(actual_date_string, expected_match_date_string))
+        logger.debug("actual={}  expected={}".format(actual_date_string, expected_match_date_string))
         assert actual_date_string == expected_match_date_string
         assert len(captures.get('timezones',[])) > 0
 
@@ -27,7 +27,7 @@ def test_extract_date_strings(date_string, expected_match_date_string):
     ['They said it was on 01-03-2015', 'on 01-03-2015'], # 3 digits strict match
     ['May 20th 2015 is nowhere near the other date', 'May 20 2015'], # one month two digit match
 ])
-def test_extract_date_strings(date_string, expected_match_date_string):
+def test_extract_date_strings_with_strict_option(date_string, expected_match_date_string):
     """
     make sure that `strict` mode works for the dates we care about
     and doesn't work for others
@@ -38,5 +38,5 @@ def test_extract_date_strings(date_string, expected_match_date_string):
     """
     dt = datefinder.DateFinder()
     for actual_date_string, indexes, captures in dt.extract_date_strings(date_string,strict=True):
-        logger.debug("acutal={}  expected={}".format(actual_date_string, expected_match_date_string))
+        logger.debug("actual={}  expected={}".format(actual_date_string, expected_match_date_string))
         assert actual_date_string == expected_match_date_string
diff --git a/tests/test_find_and_replace.py b/tests/test_find_and_replace.py
index 3b97d37..1c11eed 100644
--- a/tests/test_find_and_replace.py
+++ b/tests/test_find_and_replace.py
@@ -2,6 +2,9 @@ import pytest
 import datefinder
 import copy
 import sys, logging
+
+from datefinder.constants import REPLACEMENTS
+
 logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
 logger = logging.getLogger(__name__)
 
@@ -14,11 +17,11 @@ logger = logging.getLogger(__name__)
 ])
 def test_find_and_replace(date_string, expected_replaced_string, captures, expected_tz_string):
     dt = datefinder.DateFinder()
-    expected_replacements = copy.copy(dt.REPLACEMENTS)
+    expected_replacements = copy.copy(REPLACEMENTS)
     actual_date_string, actual_tz_string = dt._find_and_replace(date_string, captures)
 
     # assert that dt._find_and_replace did not mutate dt.REPLACEMENTS
-    assert dt.REPLACEMENTS == expected_replacements
+    assert REPLACEMENTS == expected_replacements
 
     # assert the return values of dt._find_and_replace
     assert actual_date_string == expected_replaced_string
diff --git a/tests/test_find_dates.py b/tests/test_find_dates.py
index 86b684e..cbce01f 100644
--- a/tests/test_find_dates.py
+++ b/tests/test_find_dates.py
@@ -7,6 +7,8 @@ import logging
 logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
 logger = logging.getLogger(__name__)
 
+today = datetime.today()
+
 
 @pytest.mark.parametrize('input_text, expected_date', [
     ## English Dates
@@ -18,14 +20,16 @@ logger = logging.getLogger(__name__)
     ('December 13, 2014 at midnight', datetime(2014, 12, 13, 0, 0)),
     #('Nov 25 2014 10:17 pm EST', datetime(2014, 11, 26, 3, 17)),
     #('Wed Aug 05 12:00:00 EDT 2015', datetime(2015, 8, 5, 16, 0)),
-    #('April 9, 2013 at 6:11 a.m.', datetime(2013, 4, 9, 6, 11)),
-    #('Aug. 9, 2012 at 2:57 p.m.', datetime(2012, 8, 9, 14, 57)),
+    ('April 9, 2013 at 6:11 a.m.', datetime(2013, 4, 9, 6, 11)),
+    ('Aug. 9, 2012 at 2:57 p.m.', datetime(2012, 8, 9, 14, 57)),
     ('December 10, 2014, 11:02:21 pm', datetime(2014, 12, 10, 23, 2, 21)),
-    #('8:25 a.m. Dec. 12, 2014', datetime(2014, 12, 12, 8, 25)),
+    ('8:25 a.m. Dec. 12, 2014', datetime(2014, 12, 12, 8, 25)),
     ('2:21 p.m., December 11, 2014', datetime(2014, 12, 11, 14, 21)),
     ('Fri, 12 Dec 2014 10:55:50', datetime(2014, 12, 12, 10, 55, 50)),
     #('20 Mar 2013 10h11', datetime(2013, 3, 20, 10, 11)),
     ('10:06am Dec 11, 2014', datetime(2014, 12, 11, 10, 6)),
+    ('September 2nd, 1998', datetime(1998, 9, 2)),
+    ('May 5, 2010 to July 10, 2011', [datetime(2010, 5, 5), datetime(2011, 7, 10)]),
     #('19 February 2013 year 09:10', datetime(2013, 2, 19, 9, 10)),
 
     # Numeric dates
@@ -34,9 +38,13 @@ logger = logging.getLogger(__name__)
     ('2016-02-04T20:16:26+00:00', datetime(2016, 2, 4, 20, 16, 26, tzinfo=pytz.utc)),
     #('11. 12. 2014, 08:45:39', datetime(2014, 11, 12, 8, 45, 39)),
 
+    ("2017-02-03T09:04:08Z to 2017-02-03T09:04:09Z", [
+        datetime(2017, 2, 3, 9, 4, 8, tzinfo=pytz.utc),
+        datetime(2017, 2, 3, 9, 4, 9, tzinfo=pytz.utc)
+    ]),
+
     # dates from issue https://github.com/akoumjian/datefinder/issues/14
-    #("i am looking for a date june 4th 1996 to july 3rd 2013",[]), # this is wrong, but make it pass to show the issue
-    ("i am looking for a date june 4th 1996 so july 3rd 2013",[
+    ("i am looking for a date june 4th 1996 to july 3rd 2013",[
         datetime(1996, 6, 4),
         datetime(2013, 7, 3)
     ]),
@@ -44,10 +52,21 @@ logger = logging.getLogger(__name__)
         datetime(1994, 10, 27),
         datetime(1995,  6,  1)
     ]),
+    # Simple date range
+    ("31/08/2012 to 30/08/2013",[
+        datetime(2012, 8, 31),
+        datetime(2013,  8,  30)
+    ]),
     # Z dates with and without millis, from https://github.com/akoumjian/datefinder/issues/37
     ("2017-02-03T09:04:08.001Z", datetime(2017, 2, 3, 9, 4, 8, 1000, tzinfo=pytz.utc)),
     ("2017-02-03T09:04:08,00123Z", datetime(2017, 2, 3, 9, 4, 8, 1230, tzinfo=pytz.utc)),
     ("2017-02-03T09:04:08Z", datetime(2017, 2, 3, 9, 4, 8, tzinfo=pytz.utc)),
+    # Year only strings, from https://github.com/akoumjian/datefinder/issues/96
+    ("Dutta is the recipient of Femina Miss India Universe title in 2004.", datetime(2004, today.month, today.day)),
+    ("she said that she hit depression after being traumatized on the sets of \"Horn OK\" in 2008.", datetime(2008, today.month, today.day)),
+    # https://github.com/akoumjian/datefinder/issues/63
+    ("12th day of December, 2001", datetime(2001, 12, 12)),
+    
 ])
 def test_find_date_strings(input_text, expected_date):
     if isinstance(expected_date,list):
diff --git a/tests/test_parse_date_string.py b/tests/test_parse_date_string.py
index 076cf43..fe7eca9 100644
--- a/tests/test_parse_date_string.py
+++ b/tests/test_parse_date_string.py
@@ -1,4 +1,5 @@
 import pytest
+import warnings
 import datefinder
 from dateutil import tz, parser
 from datetime import datetime
@@ -11,22 +12,25 @@ logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
 logger = logging.getLogger(__name__)
 
 
-@pytest.mark.parametrize('date_string, expected_parse_arg, expected_captures, expected_date', [
+@pytest.mark.parametrize('date_string, expected_parse_args, expected_parse_kwargs, expected_captures, expected_date', [
     (
         'due on Tuesday Jul 22, 2014 eastern standard time',
-        'tuesday jul 22, 2014',
+        ['tuesday jul 22, 2014',],
+        {'default': None},
         { 'timezones': ['eastern'] },
         datetime(2014, 7, 22).replace(tzinfo=tz.gettz('EST'))
     ),
     (
         'Friday pAcific stanDard time',
-        'friday',
+        ['friday',],
+        {'default': None},
         { 'timezones': ['pacific'] },
         parser.parse('friday').replace(tzinfo=tz.gettz('PST'))
     ),
     (
         '13/03/2014 Central Daylight Savings Time',
-        '13/03/2014',
+        ['13/03/2014',],
+        {'default': None},
         { 'timezones': ['central'] },
         datetime(2014, 3, 13).replace(tzinfo=tz.gettz('CST'))
     ),
@@ -34,18 +38,19 @@ logger = logging.getLogger(__name__)
     # handles tz-naive date strings
     # and returns naive datetime objects
     (
-        ' on 12/24/2015 at 2pm ',
         '12/24/2015 at 2pm',
+        ['12/24/2015 at 2pm',],
+        {'default': None},
         { 'timezones': [] },
         datetime(2015, 12, 24, 14, 0)
     ),
 ])
-def test_parse_date_string_find_replace(date_string, expected_parse_arg, expected_captures, expected_date):
+def test_parse_date_string_find_replace(date_string, expected_parse_args, expected_parse_kwargs, expected_captures, expected_date):
     dt = datefinder.DateFinder()
     with mock.patch.object(parser, 'parse', wraps=parser.parse) as spy:
         actual_datetime = dt.parse_date_string(date_string, expected_captures)
-        spy.assert_called_with(expected_parse_arg)
-        logger.debug("acutal={}  expected={}".format(actual_datetime, expected_date))
+        spy.assert_called_with(*expected_parse_args, **expected_parse_kwargs)
+        logger.debug("actual={}  expected={}".format(actual_datetime, expected_date))
         assert actual_datetime == expected_date
 
 @pytest.mark.parametrize('date_string, expected_parse_arg, expected_captures, expected_date', [
@@ -70,9 +75,9 @@ def test_parse_date_string_find_replace(date_string, expected_parse_arg, expecte
 ])
 def test_parse_date_string_find_replace_nonexistent_tzinfo(date_string, expected_parse_arg, expected_captures, expected_date):
     '''
-    mimic what happens when dateutil.tz.gettz tries
-    to find a non-existent tzinfo string with mocks
-    because some operating systems might resolve 'CST' and 'IRST'
+    mimic what happens when dateutil tries
+    to find a non-existent tzinfo string
+    because some operating systems might resolve 'CST' and 'IRST' this should raise a warning.
 
     :param date_string:
     :param expected_parse_arg:
@@ -80,12 +85,11 @@ def test_parse_date_string_find_replace_nonexistent_tzinfo(date_string, expected
     :param expected_date:
     :return:
     '''
-    dt = datefinder.DateFinder()
-    with mock.patch.object(tz, 'gettz', wraps=tz.gettz) as mock_gettz:
-        mock_gettz.return_value = None
+    with pytest.warns(parser.UnknownTimezoneWarning):
+        warnings.simplefilter('always')
+        dt = datefinder.DateFinder()
         actual_datetime = dt.parse_date_string(date_string, expected_captures)
-        mock_gettz.assert_called_with(expected_captures['timezones'][0])
-        logger.debug("acutal={}  expected={}".format(actual_datetime, expected_date))
+        logger.debug("actual={}  expected={}".format(actual_datetime, expected_date))
         assert actual_datetime == expected_date
 
 # @pytest.mark.parametrize('date_string, expected_exception', [
diff --git a/tests/test_tz_gettz.py b/tests/test_tz_gettz.py
index 08b94a2..b35f1d9 100644
--- a/tests/test_tz_gettz.py
+++ b/tests/test_tz_gettz.py
@@ -1,6 +1,8 @@
-import datefinder
 from dateutil import tz
 import sys, logging
+
+from datefinder.constants import NA_TIMEZONES_PATTERN, TIMEZONES_PATTERN, TIMEZONE_REPLACEMENTS
+
 logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
 logger = logging.getLogger(__name__)
 
@@ -14,12 +16,11 @@ def test_tz_gettz_for_all_patterns():
     """
     bad_tz_strings = []
     good_tz_strings = []
-    finder = datefinder.DateFinder()
-    test_tz_strings = finder.NA_TIMEZONES_PATTERN.split('|') + finder.TIMEZONES_PATTERN.split('|\s')
+    test_tz_strings = NA_TIMEZONES_PATTERN.split('|') + TIMEZONES_PATTERN.split(r'|\s')
     for tz_string in test_tz_strings:
-        if tz_string in finder.TIMEZONE_REPLACEMENTS.keys():
-            tz_string = finder.TIMEZONE_REPLACEMENTS[tz_string]
-        tz_object = tz.gettz(tz_string.replace('\s',''))
+        if tz_string in TIMEZONE_REPLACEMENTS.keys():
+            tz_string = TIMEZONE_REPLACEMENTS[tz_string]
+        tz_object = tz.gettz(tz_string.replace(r'\s',''))
         if tz_object is None:
             bad_tz_strings.append(tz_string)
         else: