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