Repository: unt-libraries/edtf-validate

Issue:
`is_valid` errors on an interval with a season
Example is `is_valid('2012/2019-22')` due to [`to_date = datetime.datetime.strptime(parts[1], "%Y-%m")`] not being able to parse a season (i.e. 21, 22, 23, or 24) in the month place. Though it is not completely clear to some of us in the draft specification we are following if a season in the interval should be valid, I think @htarver wants this to be valid assuming the interval makes sense as far as the end date coming after the start date. Valid or not, we should avoid the exception.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ad73c9..8c1562d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@ Change Log
 ==========
 
 
+1.1.1
+=====
+
+* Fixed error on `is_valid` when checking an [interval with a season](https://github.com/unt-libraries/edtf-validate/issues/20).
+* In README.md, updated draft EDTF specification link to fix the 404.
+
 1.1.0
 =====
 
diff --git a/edtf_validate/valid_edtf.py b/edtf_validate/valid_edtf.py
index 8867384..b4702f8 100644
--- a/edtf_validate/valid_edtf.py
+++ b/edtf_validate/valid_edtf.py
@@ -73,7 +73,7 @@ LEVEL 1 GRAMMAR START
 UASymbol = oneOf("? ~ ?~")
 seasonNumber = oneOf("21 22 23 24")
 season = year + "-" + seasonNumber
-dateOrSeason = date | season
+dateOrSeason = season | date
 # uncertain Or Approximate Date
 uncertainOrApproxDate = date + UASymbol
 # unspecified
@@ -249,6 +249,28 @@ def replace_all(text, dic):
     return text
 
 
+# season start and end dates for use in interval checking
+# erring toward inclusivity
+seasons = {
+    '21': {'from': '01', 'to': '05'},
+    '22': {'from': '05', 'to': '08'},
+    '23': {'from': '08', 'to': '11'},
+    '24': {'from': '10', 'to': '12'},
+}
+
+
+def replace_season(season_date, marker):
+    """Replace a season with a month.
+
+    Keyword arguments:
+    season_date -- edtf candidate of form year-season
+    marker -- 'from' or 'to' representing earliest or latest season month
+    """
+    y_part, m_part = season_date.split('-')
+    m_part = seasons[m_part][marker]
+    return '-'.join([y_part, m_part])
+
+
 U_PATTERN = re.compile(r'(-?)([\du]{4})(-[\du]{2})?(-[\du]{2})?/'
                        r'(-?)([\du]{4})(-[\du]{2})?(-[\du]{2})?')
 
@@ -441,11 +463,28 @@ def is_valid_interval(edtf_candidate):
             from_date = datetime.datetime.strptime(parts[0], "%Y-%m-%d")
         if parts[1].count("-") == 2:
             to_date = datetime.datetime.strptime(parts[1], "%Y-%m-%d")
+        # handle special case of same year season/season range due to
+        # the overlap of the months we are designating for the seasons
+        if parts[0].count("-") == 1 and parts[1].count("-") == 1:
+            from_year, from_month = parts[0].split("-")
+            to_year, to_month = parts[1].split("-")
+            if from_year == to_year and from_month in seasons and to_month in seasons:
+                return from_month <= to_month
         # 1 '-' character means we are match year-month
         if parts[0].count("-") == 1:
-            from_date = datetime.datetime.strptime(parts[0], "%Y-%m")
+            try:
+                from_date = datetime.datetime.strptime(parts[0], "%Y-%m")
+            except ValueError:
+                # month is a season; use earliest possible month
+                replaced_season = replace_season(parts[0], 'from')
+                from_date = datetime.datetime.strptime(replaced_season, "%Y-%m")
         if parts[1].count("-") == 1:
-            to_date = datetime.datetime.strptime(parts[1], "%Y-%m")
+            try:
+                to_date = datetime.datetime.strptime(parts[1], "%Y-%m")
+            except ValueError:
+                # month is a season; use latest possible month
+                replaced_season = replace_season(parts[1], 'to')
+                to_date = datetime.datetime.strptime(replaced_season, "%Y-%m")
         # zero '-' characters means we are matching a year
         if parts[0].count("-") == 0:
             # if from_date is unknown, we can assume the lowest possible date


Test patch:
diff --git a/tests/test_valid_edtf.py b/tests/test_valid_edtf.py
index 94a77d0..1b496d0 100644
--- a/tests/test_valid_edtf.py
+++ b/tests/test_valid_edtf.py
@@ -42,11 +42,19 @@ class TestIsValid(object):
         '1111-01-01/1111',
         '0000-01/0000',
         '2000-uu/2012',
+        '1952-23~/1953',
+        '1923-21/1924',
+        '1981/1982-23',
+        '1623-11/1900-11',
+        '2003-22/2004-22',
+        '2003-22/2003-22',
+        '2003-22/2003-23',
         '2000-12-uu/2012',
         '2000-uu-10/2012',
         '-2000-uu-10/2012',
         '2000-uu-uu/2012',
         '2000/2000-uu-uu',
+        '1712/2012',
         '198u/199u',
         '198u/1999',
         '1987/199u',
@@ -87,6 +95,8 @@ class TestIsValid(object):
         '2012///4444',
         '2012\\2013',
         '2000/12-12',
+        '2012-24/2012-21',
+        '2012-23/2012-22',
         '0800/-0999',
         '-1000/-2000',
         '1000/-2000',