Index: Solr_Base_Query.php
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/apachesolr/Solr_Base_Query.php,v
retrieving revision 1.1.4.23
diff -u -p -r1.1.4.23 Solr_Base_Query.php
--- Solr_Base_Query.php	27 Mar 2009 09:54:21 -0000	1.1.4.23
+++ Solr_Base_Query.php	3 Apr 2009 14:32:08 -0000
@@ -10,7 +10,7 @@ class Solr_Base_Query {
     $queries = array();
     $values = array();
     // Range queries.  The "TO" is case-sensitive.
-    $patterns[] = '/(^| )'. $name .':(\[\S+ TO \S+\])/';
+    $patterns[] = '/(^| )'. $name .':([\[\{]\S+ TO \S+[\]\}])/';
     // Match quoted values.
     $patterns[] = '/(^| )'. $name .':"([^"]*)"/';
     // Match unquoted values.
@@ -33,7 +33,7 @@ class Solr_Base_Query {
   static function make_field(array $field) {
     // If the field value has spaces, or : in it, wrap it in double quotes.
     // unless it is a range query.
-    if (preg_match('/[ :]/', $field['#value']) && !preg_match('/\[\S+ TO \S+\]/', $field['#value'])) {
+    if (preg_match('/[ :]/', $field['#value']) && !preg_match('/[\[\{]\S+ TO \S+[\]\}]/', $field['#value'])) {
       $field['#value'] = '"'. $field['#value']. '"';
     }
     return $field['#name'] . ':' . $field['#value'];
Index: apachesolr.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/apachesolr/apachesolr.module,v
retrieving revision 1.1.2.12.2.122
diff -u -p -r1.1.2.12.2.122 apachesolr.module
--- apachesolr.module	3 Apr 2009 14:06:01 -0000	1.1.2.12.2.122
+++ apachesolr.module	3 Apr 2009 14:32:09 -0000
@@ -782,6 +782,230 @@ function apachesolr_facet_block($respons
 }
 
 /**
+ * Helper function for displaying a date facet block.
+ *
+ * TODO: Refactor with apachesolr_facet_block().
+ */
+function apachesolr_date_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) {
+  if (!empty($response->facet_counts->facet_dates->$facet_field)) {
+    $field = clone $response->facet_counts->facet_dates->$facet_field;
+
+    $end = $field->end;
+    unset($field->end);
+
+    $gap = $field->gap;
+    unset($field->gap);
+
+    // Treat each date facet as a range start, and use the next date
+    // facet as range end.  Use 'end' for the final end.
+    $range_end = array();
+    foreach ($field as $facet => $count) {
+      if (isset($prev_facet)) {
+        $range_end[$prev_facet] = $facet;
+      }
+      $prev_facet = $facet;
+    }
+    $range_end[$prev_facet] = $end;
+    
+    $contains_active = FALSE;
+    $items = array();
+    foreach ($field as $facet => $count) {
+      // Solr sends this back if it's empty.
+      if ($facet == '_empty_') {
+        continue;
+      }
+      $unclick_link = '';
+      unset($active);
+      if ($facet_callback && function_exists($facet_callback)) {
+        $facet_text = $facet_callback($facet);
+      }
+      else {
+        $facet_text = apachesolr_date_format_iso_by_gap(substr($gap, 2), $facet);
+      }
+      $new_query = clone $query;
+      $new_query->add_field($facet_field, "[$facet TO {$range_end[$facet]}]");
+      $path = 'search/'. arg(1) .'/'. $new_query->get_query_basic();
+      $querystring = $new_query->get_url_querystring();
+
+      $countsort = $count == 0 ? '' : 1 / $count;
+      // if numdocs == 1 and !active, don't add.
+      if ($count == 0 || ($response->numFound == 1 && !$active)) {
+        // skip
+      }
+      else {
+        $items[$active ? $countsort . $facet : 1 + $countsort . $facet] = theme('apachesolr_facet_item', $facet_text, $count, $path, $querystring, $active, $unclick_link, $response->numFound);
+      }
+    }
+    if (count($items) > 0) {
+      // ksort($items);
+      // Get information needed by the rest of the blocks about limits.
+      $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
+      $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
+      $output = theme('apachesolr_facet_list', $items, $limit);
+      return array('subject' => $filter_by, 'content' => $output);
+    }
+  }
+  return NULL;
+}
+
+/**
+ * Determine the gap in a date range query filter that we generated.
+ *
+ * This function assumes that the start and end dates are the
+ * beginning and end of a single period: 1 year, month, day, hour,
+ * minute, or second (all date range query filters we generate meet
+ * this criteria).  So, if the seconds are different, it is a second
+ * gap.  If the seconds are the same (incidentally, they will also be
+ * 0) but the minutes are different, it is a minute gap.  If the
+ * minutes are the same but hours are different, it's an hour gap.
+ * etc.
+ *
+ * @param $start
+ *   Start date as an ISO date string.
+ * @param $end
+ *   End date as an ISO date string.
+ * @return
+ *   YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND.
+ */
+function apachesolr_date_find_query_gap($start_iso, $end_iso) {
+  $gaps = array('SECOND' => 6, 'MINUTE' => 5, 'HOUR' => 4, 'DAY' => 3, 'MONTH' => 2, 'YEAR' => 1);
+  $re = '@(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})@';
+  if (preg_match($re, $start_iso, $start) && preg_match($re, $end_iso, $end)) {
+    foreach ($gaps as $gap => $idx) {
+      if ($start[$idx] != $end[$idx]) {
+        return $gap;
+      }
+    }
+  }
+  // can't tell
+  return 'YEAR';
+}
+
+/**
+ * Format an ISO date string based on the gap used to generate it.
+ * 
+ * This function assumes that gaps less than one day will be displayed
+ * in a search context in which a larger containing gap including a
+ * day is already displayed.  So, HOUR, MINUTE, and SECOND gaps only
+ * display time information, without date.
+ *
+ * @param $gap
+ *   A gap.
+ * @param $iso
+ *   An ISO date string.
+ * @return
+ *   A gap-appropriate formatted date.
+ */
+function apachesolr_date_format_iso_by_gap($gap, $iso) {
+  // TODO: If we assume that multiple search queries are formatted in
+  // order, we could store a static list of all gaps we've formatted.
+  // Then, if we format an HOUR, MINUTE, or SECOND without previously
+  // having formatted a DAY or later, we could include date
+  // information.  However, we'd need to do that per-field and I'm not
+  // our callers always have field information handy.
+  $unix = strtotime($iso);
+  if ($unix > 0) {
+    switch ($gap) {
+      case 'YEAR':
+        return gmdate('Y', $unix);
+      case 'MONTH':
+        return gmdate('F Y', $unix);
+      case 'DAY':
+        return gmdate('F j, Y', $unix);
+      case 'HOUR':
+        return gmdate('g A', $unix);
+      case 'MINUTE':
+        return gmdate('g:i A', $unix);
+      case 'SECOND':
+        return gmdate('g:i:s A', $unix);
+    }
+  }
+
+  return $iso;
+}
+
+/**
+ * Format the beginning of a date range query filter that we
+ * generated.
+ *
+ * @param $start_iso
+ *   The start date.
+ * @param $end_iso
+ *   The end date.
+ * @return
+ *   A display string reprepsenting the date range, such as "January
+ * 2009" for "2009-01-01T00:00:00Z TO 2009-02-01T00:00:00Z"
+ */
+function apachesolr_date_format_range($start_iso, $end_iso) {
+  $gap = apachesolr_date_find_query_gap($start_iso, $end_iso);
+  return apachesolr_date_format_iso_by_gap($gap, $start_iso);
+}
+
+/**
+ * Determine the best search gap to use for an arbitrary date range.
+ *
+ * Generally, we the maximum gap that fits between the start and end
+ * date.  If they are more than a year apart, 1 year; if they are more
+ * than a month apart, 1 month; etc.
+ *
+ * This function uses Unix timestamps for its computation and so is
+ * not useful for dates outside that range.
+ *
+ * @param $start
+ *   Start date as an ISO date string.
+ * @param $end
+ *   End date as an ISO date string.
+ * @return
+ *   YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND depending on how far
+ *   apart $start and $end are.
+ */
+function apachesolr_date_determine_gap($start, $end) {
+  $start = strtotime($start);
+  $end = strtotime($end);
+
+  if ($end - $start >= 86400*365) {
+    return 'YEAR';
+  }
+
+  if (date('m', $start) != date('m', $end)) {
+    return 'MONTH';
+  }
+
+  if ($end - $start > 86400) {
+    return 'DAY';
+  }
+  else if ($end - $start > 3600) {
+    return 'HOUR';
+  }
+  else if ($end - $start > 60) {
+    return 'MINUTE';
+  }
+
+  return 'SECOND';
+}
+
+/**
+ * Return the next smaller date gap.
+ *
+ * @param $gap
+ *   A gap.
+ * @return
+ *   The next smaller gap, or NULL if there is no smaller gap.
+ */
+function apachesolr_date_gap_drilldown($gap) {
+  $drill = array(
+    'YEAR' => 'MONTH',
+    'MONTH' => 'DAY',
+    'DAY' => 'HOUR',
+    // For now, HOUR is a reasonable smallest gap.
+    // 'HOUR' => 'MINUTE',
+    // 'MINUTE' => 'SECOND',
+  );
+  return isset($drill[$gap]) ? $drill[$gap] : NULL;
+}
+
+
+/**
  * Used by the 'configure' $op of hook_block so that modules can generically set
  * facet limits on their blocks.
  */
Index: apachesolr_search.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/apachesolr/apachesolr_search.module,v
retrieving revision 1.1.2.6.2.79
diff -u -p -r1.1.2.6.2.79 apachesolr_search.module
--- apachesolr_search.module	25 Mar 2009 01:56:01 -0000	1.1.2.6.2.79
+++ apachesolr_search.module	3 Apr 2009 14:32:09 -0000
@@ -128,16 +128,34 @@ function apachesolr_search_search($op = 
         }
 
         $facet_query_limits = variable_get('apachesolr_facet_query_limits', array());
-        // Request all enabled facets.
+        // Request all enabled facets.  We need $all_facets so we can
+        // check the facet_date property and callback, because
+        // get_enabled_facets() does not (currently) return all of that.
+        $all_facets = module_invoke_all('apachesolr_facets');
         foreach (apachesolr_get_enabled_facets() as $module => $module_facets) {
           foreach($module_facets as $delta => $facet_field) {
-            $params['facet.field'][] = $facet_field;
-            // Facet limits
-            if (isset($facet_query_limits[$module][$delta])) {
-              $params['f.' . $facet_field . '.facet.limit'] = $facet_query_limits[$module][$delta];
+            if (isset($all_facets[$delta]['facet_date'])) {
+              $facet_date = $all_facets[$delta]['facet_date'];
+              $params['facet.date'][] = $facet_date;
+
+              $callback = $all_facets[$delta]['facet_date_range_callback'];
+              list($start, $end, $gap) = $callback($query, $params, $delta);
+              if ($gap) {
+                $params['f.'. $facet_date .'.facet.date.start'] = $start;
+                $params['f.'. $facet_date .'.facet.date.end'] = $end;
+                $params['f.'. $facet_date .'.facet.date.gap'] = $gap;
+              }
+            }
+            else {
+              $params['facet.field'][] = $facet_field;
+              // Facet limits
+              if (isset($facet_query_limits[$module][$delta])) {
+                $params['f.' . $facet_field . '.facet.limit'] = $facet_query_limits[$module][$delta];
+              }
             }
           }
         }
+
         if (!empty($params['facet.field'])) {
           // Add a default limit for fields where no limit was set.
           $params['facet.limit'] = variable_get('apachesolr_facet_query_limit_default', 20);
@@ -281,6 +299,60 @@ function apachesolr_search_search($op = 
   } // switch
 }
 
+function _hack_parse_date_range($str) {
+  if (preg_match('/\[(\S+) TO (\S+)\]/', $str, $m)) {
+    return array($m[1], strtotime($m[1]), $m[2], strtotime($m[2]));
+  }
+  return array();
+}
+  
+function apachesolr_search_date_range($query, &$params, $delta) {
+  // Find the smallest filter range for $delta in the query.  $query
+  // has no method to retrieve filters for $delta, so we iterate
+  // through them all.
+  $fields = $query->get_fields();
+  foreach ($fields as $info) {
+    if ($info['#name'] == $delta) {
+      // $info['#value'] is the filter value.  Ideally it would also
+      // have #range, #from, and #to properties.  Until it does, we
+      // parse them out ourselves.
+      //
+      // Also, if we had an ISO date library we could use them
+      // directly.  Instead, we convert to Unix timestamps for comparison.
+      list($_start_iso, $_start, $_end_iso, $_end) = _hack_parse_date_range($info['#value']);
+      // Only use dates that we were able to parse into timestamps.
+      if ($_start > 0 && $_end > 0) {
+        if (!isset($start) || $_start > $start) {
+          $start = $_start;
+          $start_iso = $_start_iso;
+        }
+        if (!isset($end) || $_end < $end) {
+          $end = $_end;
+          $end_iso = $_end_iso;
+        }
+        
+        // Determine the drilldown gap for this range.
+        $gap = apachesolr_date_gap_drilldown(apachesolr_date_find_query_gap($start_iso, $end_iso));
+      }
+    }
+  }
+      
+  // If there is no $delta field in query object, get initial
+  // facet.date.* params from the DB and determine the best search
+  // gap to use.  This callback assumes $delta is 'changed' or 'created'.
+  if (!isset($start_iso)) {
+    $start_iso = apachesolr_date_iso(db_result(db_query("SELECT MIN($delta) FROM {node} WHERE status = 1")));
+    $end_iso = apachesolr_date_iso(db_result(db_query("SELECT MAX($delta) FROM {node} WHERE status = 1")));
+    $gap = apachesolr_date_determine_gap($start_iso, $end_iso);
+  }
+
+  // Return a query range from the beginning of a gap period to the
+  // beginning of the next gap period.  We ALWAYS generate query
+  // ranges of this form and the apachesolr_date_*() helper functions
+  // require it.
+  return array("$start_iso/$gap", "$end_iso+1$gap/$gap", "+1$gap");
+}
+
 /**
  * Implementation of hook_apachesolr_facets().
  *
@@ -302,6 +374,21 @@ function apachesolr_search_apachesolr_fa
     'facet_field' => 'language',
   );
 
+  // TODO bjaspan: I need to include facet_field or the entry is
+  // ignored, even though facet_date is the key element.
+  $facets['changed'] = array(
+    'info' => t('Apache Solr Search: Filter by updated date'),
+    'facet_field' => 'changed',
+    'facet_date' => 'changed',
+    'facet_date_range_callback' => 'apachesolr_search_date_range',
+  );
+  $facets['created'] = array(
+    'info' => t('Apache Solr Search: Filter by post date'),
+    'facet_field' => 'created',
+    'facet_date' => 'created',
+    'facet_date_range_callback' => 'apachesolr_search_date_range',
+  );
+
   // Get taxonomy vocabulary facets.
   if (module_exists('taxonomy')) {
     $vocabs = taxonomy_get_vocabularies();
@@ -449,6 +536,10 @@ function apachesolr_search_block($op = '
             return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by author'), 'apachesolr_search_get_username');
           case 'type':
             return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by type'), 'apachesolr_search_get_type');
+          case 'changed':
+            return apachesolr_date_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by modification date'));
+          case 'created':
+            return apachesolr_date_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by post date'));
 
           default:
            if ($fields = apachesolr_cck_fields()) {
@@ -585,6 +676,12 @@ function apachesolr_search_theme() {
     'apachesolr_breadcrumb_type' => array(
       'arguments' => array('type' => NULL),
     ),
+    'apachesolr_breadcrumb_changed' => array(
+      'arguments' => array('type' => NULL),
+    ),
+    'apachesolr_breadcrumb_created' => array(
+      'arguments' => array('type' => NULL),
+    ),
     'apachesolr_currentsearch' => array(
       'arguments' => array('total_found' => NULL, 'links' => NULL),
     ),
@@ -594,6 +691,24 @@ function apachesolr_search_theme() {
   );
 }
 
+function theme_apachesolr_breadcrumb_date_range($range) {
+  if (preg_match('@[\[\{](\S+) TO (\S+)[\]\}]@', $range, $m)) {
+    list($all, $start_iso, $end_iso) = $m;
+    return apachesolr_date_format_range($m[1], $m[2]);
+    $start_label = apachesolr_date_format_iso_by_gap($gap, $start_iso);
+    return "$start_label";
+  }
+  return $range;
+}
+
+function theme_apachesolr_breadcrumb_changed($range) {
+  return theme_apachesolr_breadcrumb_date_range($range);
+}
+
+function theme_apachesolr_breadcrumb_created($range) {
+  return theme_apachesolr_breadcrumb_date_range($range);
+}
+
 /**
  * Return the username from $uid
  */
