From 5c99bb8c1f315151baaba0041151f325208e5865 Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Fri, 31 Oct 2014 10:32:00 -0400
Subject: [PATCH] Added "fullDateRange" facets for dealing with granular dates.

---
 config/vufind/facets.ini                      |  7 +-
 .../src/VuFind/Controller/AbstractSearch.php  | 80 +++++++++++-------
 .../src/VuFind/Recommend/SideFacets.php       | 29 ++++++-
 .../VuFind/src/VuFind/Search/Base/Params.php  | 70 ++++++++++++++--
 module/VuFind/src/VuFind/Solr/Utils.php       | 83 +++++++++++++++++++
 5 files changed, 229 insertions(+), 40 deletions(-)

diff --git a/config/vufind/facets.ini b/config/vufind/facets.ini
index 699a74f2216..954825e4b5d 100644
--- a/config/vufind/facets.ini
+++ b/config/vufind/facets.ini
@@ -26,8 +26,12 @@ topic_facet        = "Suggested Topics"
 ; This section is used to identify facets for special treatment by the SideFacets
 ; recommendations module.
 [SpecialFacets]
-; Any fields listed below will be treated as date ranges rather than plain facets:
+; Any fields listed below will be treated as year-based date ranges rather than plain
+; facets:
 dateRange[] = publishDate
+; Any fields listed below will be treated as year/month/day-based date ranges rather
+; than plain facets:
+;fullDateRange[] = example_field_date
 ; Any fields listed below will be treated as numeric ranges rather than plain facets:
 ;numericRange[] = example_field_str
 ; Any fields listed below will be treated as free-form ranges rather than plain
@@ -101,6 +105,7 @@ orFacets = *
 ;      [Special_Facets] above; if multiple fields are specified above but you
 ;      only want certain ones on the advanced screen, you can filter with a
 ;      colon separated list; e.g. "daterange:field1:field2:field3"
+; fulldaterange - just like daterange above, but for fullDateRange[] fields.
 ; genericrange - just like daterange above, but for genericRange[] fields.
 ; illustrated - for the "illustrated/not illustrated" radio button limiter
 ; numericrange - just like daterange above, but for numericRange[] fields.
diff --git a/module/VuFind/src/VuFind/Controller/AbstractSearch.php b/module/VuFind/src/VuFind/Controller/AbstractSearch.php
index 77f7c038e7e..79411fbe1c2 100644
--- a/module/VuFind/src/VuFind/Controller/AbstractSearch.php
+++ b/module/VuFind/src/VuFind/Controller/AbstractSearch.php
@@ -426,6 +426,30 @@ class AbstractSearch extends AbstractBase
         return $parts;
     }
 
+    /**
+     * Get the range facet configurations from the specified config section and
+     * filter them appropriately.
+     *
+     * @param string $config  Name of config file
+     * @param string $section Configuration section to check
+     * @param array  $filter  Whitelist of fields to include (if empty, all
+     * fields will be returned)
+     *
+     * @return array
+     */
+    protected function getRangeFieldList($config, $section, $filter)
+    {
+        $config = $this->getServiceLocator()->get('VuFind\Config')->get($config);
+        $fields = isset($config->SpecialFacets->$section)
+            ? $config->SpecialFacets->$section->toArray() : array();
+
+        if (!empty($filter)) {
+            $fields = array_intersect($fields, $filter);
+        }
+
+        return $fields;
+    }
+
     /**
      * Get the current settings for the date range facets, if set:
      *
@@ -439,19 +463,27 @@ class AbstractSearch extends AbstractBase
     protected function getDateRangeSettings($savedSearch = false, $config = 'facets',
         $filter = array()
     ) {
-        $config = $this->getServiceLocator()->get('VuFind\Config')->get($config);
-
-        $fields = isset($config->SpecialFacets->dateRange)
-            ? $config->SpecialFacets->dateRange->toArray()
-            : array();
-
-        if (!empty($filter)) {
-            $fields = array_intersect($fields, $filter);
-        }
-
+        $fields = $this->getRangeFieldList($config, 'dateRange', $filter);
         return $this->getRangeSettings($fields, 'date', $savedSearch);
     }
 
+    /**
+     * Get the current settings for the full date range facets, if set:
+     *
+     * @param object $savedSearch Saved search object (false if none)
+     * @param string $config      Name of config file
+     * @param array  $filter      Whitelist of fields to include (if empty, all
+     * fields will be returned)
+     *
+     * @return array
+     */
+    protected function getFullDateRangeSettings($savedSearch = false, $config = 'facets',
+        $filter = array()
+    ) {
+        $fields = $this->getRangeFieldList($config, 'fullDateRange', $filter);
+        return $this->getRangeSettings($fields, 'fulldate', $savedSearch);
+    }
+
     /**
      * Get the current settings for the generic range facets, if set:
      *
@@ -465,16 +497,7 @@ class AbstractSearch extends AbstractBase
     protected function getGenericRangeSettings($savedSearch = false,
         $config = 'facets', $filter = array()
     ) {
-        $config = $this->getServiceLocator()->get('VuFind\Config')->get($config);
-
-        $fields = isset($config->SpecialFacets->genericRange)
-            ? $config->SpecialFacets->genericRange->toArray()
-            : array();
-
-        if (!empty($filter)) {
-            $fields = array_intersect($fields, $filter);
-        }
-
+        $fields = $this->getRangeFieldList($config, 'genericRange', $filter);
         return $this->getRangeSettings($fields, 'generic', $savedSearch);
     }
 
@@ -491,16 +514,7 @@ class AbstractSearch extends AbstractBase
     protected function getNumericRangeSettings($savedSearch = false,
         $config = 'facets', $filter = array()
     ) {
-        $config = $this->getServiceLocator()->get('VuFind\Config')->get($config);
-
-        $fields = isset($config->SpecialFacets->numericRange)
-            ? $config->SpecialFacets->numericRange->toArray()
-            : array();
-
-        if (!empty($filter)) {
-            $fields = array_intersect($fields, $filter);
-        }
-
+        $fields = $this->getRangeFieldList($config, 'numericRange', $filter);
         return $this->getRangeSettings($fields, 'numeric', $savedSearch);
     }
 
@@ -523,6 +537,12 @@ class AbstractSearch extends AbstractBase
             );
             $result = array_merge($result, $dates);
         }
+        if (isset($specialFacets['fulldaterange'])) {
+            $fulldates = $this->getFullDateRangeSettings(
+                $savedSearch, $config, $specialFacets['fulldaterange']
+            );
+            $result = array_merge($result, $fulldates);
+        }
         if (isset($specialFacets['genericrange'])) {
             $generic = $this->getGenericRangeSettings(
                 $savedSearch, $config, $specialFacets['genericrange']
diff --git a/module/VuFind/src/VuFind/Recommend/SideFacets.php b/module/VuFind/src/VuFind/Recommend/SideFacets.php
index cc7faa10d52..885c63410b3 100644
--- a/module/VuFind/src/VuFind/Recommend/SideFacets.php
+++ b/module/VuFind/src/VuFind/Recommend/SideFacets.php
@@ -42,12 +42,19 @@ use VuFind\Solr\Utils as SolrUtils;
 class SideFacets extends AbstractFacets
 {
     /**
-     * Date facet configuration
+     * Year-only date facet configuration
      *
      * @var array
      */
     protected $dateFacets = array();
 
+    /**
+     * Day/month/year date facet configuration
+     *
+     * @var array
+     */
+    protected $fullDateFacets = array();
+
     /**
      * Generic range facet configuration
      *
@@ -115,6 +122,9 @@ class SideFacets extends AbstractFacets
         if (isset($config->SpecialFacets->dateRange)) {
             $this->dateFacets = $config->SpecialFacets->dateRange->toArray();
         }
+        if (isset($config->SpecialFacets->fullDateRange)) {
+            $this->fullDateFacets = $config->SpecialFacets->fullDateRange->toArray();
+        }
         if (isset($config->SpecialFacets->genericRange)) {
             $this->genericRangeFacets
                 = $config->SpecialFacets->genericRange->toArray();
@@ -182,7 +192,8 @@ class SideFacets extends AbstractFacets
     /**
      * getDateFacets
      *
-     * Return date facet information in a format processed for use in the view.
+     * Return year-based date facet information in a format processed for use in the
+     * view.
      *
      * @return array Array of from/to value arrays keyed by field.
      */
@@ -191,6 +202,19 @@ class SideFacets extends AbstractFacets
         return $this->getRangeFacets('dateFacets');
     }
 
+    /**
+     * getFullDateFacets
+     *
+     * Return year/month/day-based date facet information in a format processed for
+     * use in the view.
+     *
+     * @return array Array of from/to value arrays keyed by field.
+     */
+    public function getFullDateFacets()
+    {
+        return $this->getRangeFacets('fullDateFacets');
+    }
+
     /**
      * getGenericRangeFacets
      *
@@ -226,6 +250,7 @@ class SideFacets extends AbstractFacets
     {
         $raw = array(
             'date' => $this->getDateFacets(),
+            'fulldate' => $this->getFullDateFacets(),
             'generic' => $this->getGenericRangeFacets(),
             'numeric' => $this->getNumericRangeFacets()
         );
diff --git a/module/VuFind/src/VuFind/Search/Base/Params.php b/module/VuFind/src/VuFind/Search/Base/Params.php
index 301cb8a040e..a07ba1cb1f0 100644
--- a/module/VuFind/src/VuFind/Search/Base/Params.php
+++ b/module/VuFind/src/VuFind/Search/Base/Params.php
@@ -29,7 +29,7 @@ namespace VuFind\Search\Base;
 use Zend\ServiceManager\ServiceLocatorAwareInterface,
     Zend\ServiceManager\ServiceLocatorInterface;
 use VuFindSearch\Backend\Solr\LuceneSyntaxHelper, VuFindSearch\Query\Query;
-use VuFind\Search\QueryAdapter;
+use VuFind\Search\QueryAdapter, VuFind\Solr\Utils as SolrUtils;
 
 /**
  * Abstract parameters search model.
@@ -1166,13 +1166,14 @@ class Params implements ServiceLocatorAwareInterface
     protected function initRangeFilters($request)
     {
         $this->initDateFilters($request);
+        $this->initFullDateFilters($request);
         $this->initGenericRangeFilters($request);
         $this->initNumericRangeFilters($request);
     }
 
     /**
-     * Support method for initDateFilters() -- normalize a year for use in a date
-     * range.
+     * Support method for initDateFilters() -- normalize a year for use in a
+     * year-based date range.
      *
      * @param string $year Value to check for valid year.
      *
@@ -1193,6 +1194,21 @@ class Params implements ServiceLocatorAwareInterface
         return $year;
     }
 
+    /**
+     * Support method for initFullDateFilters() -- normalize a date for use in a
+     * year/month/day date range.
+     *
+     * @param string $date Value to check for valid date.
+     *
+     * @return string      Formatted date.
+     */
+    protected function formatDateForFullDateRange($date)
+    {
+        // Make sure date is valid; default to wildcard otherwise:
+        $date = SolrUtils::sanitizeDate($date);
+        return $date === null ? '*' : $date;
+    }
+
     /**
      * Support method for initNumericRangeFilters() -- normalize a year for use in
      * a date range.
@@ -1309,7 +1325,7 @@ class Params implements ServiceLocatorAwareInterface
 
     /**
      * Support method for initDateFilters() -- build a filter query based on a range
-     * of dates.
+     * of 4-digit years.
      *
      * @param string $field field to use for filtering.
      * @param string $from  year for start of range.
@@ -1324,9 +1340,31 @@ class Params implements ServiceLocatorAwareInterface
     }
 
     /**
-     * Support method for initFilters() -- initialize date-related filters.  Factored
-     * out as a separate method so that it can be more easily overridden by child
-     * classes.
+     * Support method for initFullDateFilters() -- build a filter query based on a
+     * range of dates.
+     *
+     * @param string $field field to use for filtering.
+     * @param string $from  year for start of range.
+     * @param string $to    year for end of range.
+     *
+     * @return string       filter query.
+     */
+    protected function buildFullDateRangeFilter($field, $from, $to)
+    {
+        // Make sure that $to is less than $from:
+        if ($to != '*' && $from!= '*' && strtotime($to) < strtotime($from)) {
+            $tmp = $to;
+            $to = $from;
+            $from = $tmp;
+        }
+
+        return $this->buildGenericRangeFilter($field, $from, $to);
+    }
+
+    /**
+     * Support method for initFilters() -- initialize year-based date filters.
+     * Factored out as a separate method so that it can be more easily overridden
+     * by child classes.
      *
      * @param \Zend\StdLib\Parameters $request Parameter object representing user
      * request.
@@ -1341,6 +1379,24 @@ class Params implements ServiceLocatorAwareInterface
         );
     }
 
+    /**
+     * Support method for initFilters() -- initialize year/month/day-based date
+     * filters. Factored out as a separate method so that it can be more easily
+     * overridden by child classes.
+     *
+     * @param \Zend\StdLib\Parameters $request Parameter object representing user
+     * request.
+     *
+     * @return void
+     */
+    protected function initFullDateFilters($request)
+    {
+        return $this->initGenericRangeFilters(
+            $request, 'fulldaterange', array($this, 'formatDateForFullDateRange'),
+            array($this, 'buildFullDateRangeFilter')
+        );
+    }
+
     /**
      * Support method for initFilters() -- initialize numeric range filters. Factored
      * out as a separate method so that it can be more easily overridden by child
diff --git a/module/VuFind/src/VuFind/Solr/Utils.php b/module/VuFind/src/VuFind/Solr/Utils.php
index 11d77a0222e..782f0cc2035 100644
--- a/module/VuFind/src/VuFind/Solr/Utils.php
+++ b/module/VuFind/src/VuFind/Solr/Utils.php
@@ -59,4 +59,87 @@ class Utils
         }
         return array('from' => trim($matches[1]), 'to' => trim($matches[2]));
     }
+
+    /**
+     * Convert a raw string date (as, for example, from a MARC record) into a legal
+     * Solr date string. Return null if conversion is impossible.
+     *
+     * @param string $date Date to convert.
+     *
+     * @return string|null
+     */
+    public static function sanitizeDate($date)
+    {
+        // Strip brackets; we'll assume guesses are correct.
+        $date = str_replace(array('[', ']'), '', $date);
+
+        // Special case -- first four characters are not a year:
+        if (!preg_match('/^[0-9]{4}/', $date)) {
+            // 'n.d.' means no date known -- give up!
+            if (preg_match('/n\.?\s*d\.?/', $date)) {
+                return null;
+            }
+
+            // strtotime can only handle a limited range of dates; let's extract
+            // a year from the string and temporarily replace it with a known
+            // good year; we'll swap it back after the conversion.
+            $year = preg_match('/[0-9]{4}/', $date, $matches) ? $matches[0] : false;
+            if ($year) {
+                $date = str_replace($year, '1999', $date);
+            }
+            $time = @strtotime($date);
+            if ($time) {
+                $date = @date("Y-m-d", $time);
+                if ($year) {
+                    $date = str_replace('1999', $year, $date);
+                }
+            } else {
+                return null;
+            }
+        }
+
+        // If we've gotten this far, we at least know that we have a valid year.
+        $year = substr($date, 0, 4);
+
+        // Let's get rid of punctuation and normalize separators:
+        $date = str_replace(array('.', ' ', '?'), '', $date);
+        $date = str_replace(array('/', '--', '-0'), '-', $date);
+
+        // If multiple dates are &'ed together, take just the first:
+        list($date) = explode('&', $date);
+
+        // Default to January 1 if no month/day present:
+        if (strlen($date) < 5) {
+            $month = $day = '01';
+        } else {
+            // If we have year + month, parse that out:
+            if (strlen($date) < 8) {
+                $day = '01';
+                if (preg_match('/^[0-9]{4}-([0-9]{1,2})/', $date, $matches)) {
+                    $month = str_pad($matches[1], 2, "0", STR_PAD_LEFT);
+                } else {
+                    $month = '01';
+                }
+            } else {
+                // If we have year + month + day, parse that out:
+                $ymdRegex = '/^[0-9]{4}-([0-9]{1,2})-([0-9]{1,2})/';
+                if (preg_match($ymdRegex, $date, $matches)) {
+                    $month = str_pad($matches[1], 2, "0", STR_PAD_LEFT);
+                    $day = str_pad($matches[2], 2, "0", STR_PAD_LEFT);
+                } else {
+                    $month = $day = '01';
+                }
+            }
+        }
+
+        // Make sure month/day/year combination is legal. Make it legal if it isn't.
+        if (!checkdate($month, $day, $year)) {
+            $day = '01';
+            if (!checkdate($month, $day, $year)) {
+                $month = '01';
+            }
+        }
+
+        return "{$year}-{$month}-{$day}T00:00:00Z";
+    }
 }
-- 
GitLab