--- date_repeat/date_repeat.module.OLD	2010-08-15 13:18:28.397960947 +1000
+++ date_repeat/date_repeat.module	2010-08-15 14:51:18.198459825 +1000
@@ -1,19 +1,19 @@
 <?php
 // $Id: date_repeat.module,v 1.30.4.13 2010/08/12 19:41:04 karens Exp $
+
 /**
  * @file
+ * This module creates a form element that allows users to select repeat rules
+ * for a date, and reworks the result into an iCal RRULE string that can be
+ * stored in the database.
  *
- * This module creates a form element that allows users to select
- * repeat rules for a date, and reworks the result into an iCal
- * RRULE string that can be stored in the database.
- *
- * The module also parses iCal RRULEs to create an array of dates
- * that meet their criteria.
- *
- * Other modules can use this API to add self-validating form elements
- * to their dates, and identify dates that meet the RRULE criteria.
+ * The module also parses iCal RRULEs to create an array of dates that meet
+ * their criteria.
  *
+ * Other modules can use this API to add self-validating form elements to their
+ * dates, and identify dates that meet the RRULE criteria.
  */
+
 /**
  * Implementation of hook_elements().
  */
@@ -28,8 +28,12 @@ function date_repeat_elements() {
 
 function date_repeat_theme() {
   return array(
-    'date_repeat' => array('arguments' => array('element' => NULL)),
-    'date_repeat_current_exceptions' => array('arguments' => array('element' => NULL)),
+    'date_repeat' => array(
+      'arguments' => array('element' => NULL),
+    ),
+    'date_repeat_current_exceptions' => array(
+      'arguments' => array('element' => NULL),
+    ),
   );
 }
 
@@ -60,9 +64,9 @@ function INTERVAL_options() {
 /**
  * Helper function for FREQ options.
  *
- * Translated and untranslated arrays of the iCal day of week names.
- * We need the untranslated values for date_modify(), translated
- * values when displayed to user.
+ * Translated and untranslated arrays of the iCal day of week names. We need the
+ * untranslated values for date_modify(), translated values when displayed to
+ * user.
  */
 function date_repeat_dow_day_options($translated = TRUE) {
   return array(
@@ -112,8 +116,8 @@ function date_repeat_dow_options() {
 /**
  * Translate a day of week position to the iCal day name.
  *
- * Used with date_format($date, 'w') or get_variable('date_first_day'),
- * which return 0 for Sunday, 1 for Monday, etc.
+ * Used with date_format($date, 'w') or get_variable('date_first_day'), which
+ * return 0 for Sunday, 1 for Monday, etc.
  *
  * dow 2 becomes 'TU', dow 3 becomes 'WE', and so on.
  */
@@ -123,8 +127,8 @@ function date_repeat_dow2day($dow) {
 }
 
 /**
- * Shift the array of iCal day names into the right order
- * for a specific week start day.
+ * Shift the array of iCal day names into the right order for a specific week
+ * start day.
  */
 function date_repeat_days_ordered($week_start_day) {
   $days = array_flip(array_keys(date_repeat_dow_day_options(FALSE)));
@@ -149,92 +153,156 @@ function date_repeat_rrule_description($
   if (empty($rrule) || !strstr($rrule, 'RRULE')) {
     return;
   }
-  
-  require_once('./'. drupal_get_path('module', 'date_api') .'/date_api_ical.inc');
-  require_once('./'. drupal_get_path('module', 'date_repeat') .'/date_repeat_calc.inc');
-  
+
+  module_load_include('inc', 'date_api', 'date_api_ical');
+  module_load_include('inc', 'date_repeat', 'date_repeat_calc');
+
   // Make sure there will be an empty description for any unused parts.
   $description = array(
-    '!interval' => '', 
-    '!byday' => '', 
-    '!bymonth' => '', 
+    '!interval' => '',
+    '!byday' => '',
+    '!bymonth' => '',
     '!count' => '',
-    '!until' => '', 
+    '!until' => '',
     '!except' => '',
     '!week_starts_on' => '',
-    );
+  );
   $parts = date_repeat_split_rrule($rrule);
   $exceptions = $parts[1];
   $rrule = $parts[0];
   $interval = INTERVAL_options();
-  switch ($rrule['FREQ']) {
-    case 'WEEKLY':
-      $description['!interval'] = format_plural($rrule['INTERVAL'], 'every week', 'every @count weeks') .' ';
-      break;
-    case 'MONTHLY':
-      $description['!interval'] = format_plural($rrule['INTERVAL'], 'every month', 'every @count months') .' ';
-      break;
-    case 'YEARLY':
-      $description['!interval'] = format_plural($rrule['INTERVAL'], 'every year', 'every @count years') .' ';
-      break;
-    default:
-      $description['!interval'] = format_plural($rrule['INTERVAL'], 'every day', 'every @count days') .' ';
-      break;
+
+  if ($rrule['INTERVAL'] < 1) {
+    $rrule['INTERVAL'] = 1;
   }
-  
+
+  if ((empty($rrule['BYDAY']) && empty($rrule['BYMONTHDAY']) && empty($rrule['BYMONTH']) || $rrule['FREQ'] == 'DAILY' || $rrule['FREQ'] == 'WEEKLY') || $rrule['INTERVAL'] > 1) {
+    switch ($rrule['FREQ']) {
+      case 'WEEKLY':
+        $description['!interval'] = format_plural($rrule['INTERVAL'], 'weekly', 'every @count weeks') .' ';
+        break;
+      case 'MONTHLY':
+        $description['!interval'] = format_plural($rrule['INTERVAL'], 'monthly', 'every @count months') .' ';
+        break;
+      case 'YEARLY':
+        $description['!interval'] = format_plural($rrule['INTERVAL'], 'annually', 'every @count years') .' ';
+        break;
+      default:
+        $description['!interval'] = format_plural($rrule['INTERVAL'], 'daily', 'every @count days') .' ';
+        break;
+    }
+  }
+
+  $days = date_repeat_dow_day_options();
+  $counts = date_repeat_dow_count_options();
+
+  // Sort the by-day results if any into two categories, those occurring weekly,
+  // and those occurring on the nth ordinal per month, grouped by ordinal
+  $ordinalResults = array();
+  $weeklyResults = array();
   if (!empty($rrule['BYDAY'])) {
-    $days = date_repeat_dow_day_options();
-    $counts = date_repeat_dow_count_options();
-    $results = array();
     foreach ($rrule['BYDAY'] as $byday) {
-      $day = substr($byday, -2);
-      $count = intval(str_replace(' '. $day, '', $byday));
-      if ($count = intval(str_replace(' ' . $day, '', $byday))) {
-        $results[] = trim(t('!repeats_every_interval on the !date_order !day_of_week', array('!repeats_every_interval ' => '', '!date_order' => strtolower($counts[substr($byday, 0, 2)]), '!day_of_week' => $days[$day])));
+      $day = drupal_substr($byday, -2);
+      $strCount = drupal_substr($byday, 0, 2);
+      if ($count = intval($strCount)) {
+        $instance = t($days[$day]);
+        if (empty($ordinalResults[$count])) {
+          $ordinalResults[$count] = array();
+        }
+        $ordinalResults[$count][] = $instance;
       }
       else {
-        $results[] = trim(t('!repeats_every_interval every !day_of_week', array('!repeats_every_interval ' => '', '!day_of_week' => $days[$day])));
+        $weeklyResults[] = t($days[$day]);
+      }
+    }
+
+    $byDayResults = array();
+    if (!empty($weeklyResults)) {
+      $byDayResults[] = t('on') .' '. t(implodeGrammar($weeklyResults));
+    }
+    if (!empty($ordinalResults)) {
+      $resultsByOrdinal = array();
+      // TODO: Put this instances in the correct sequence. Currently they are
+      // 'fifth from last' to 'last', followed by 'first' to 'fifth'. They
+      // should probably be in order from earliest to latest so that 'first'
+      // comes before 'last'.
+      for ($i = -5; $i <= 5; $i++) {
+        if (!empty($ordinalResults[''. $i])) {
+          $ordinal = $i < 0 ? $i : ($i > 0 ? '+'. $i : '');
+          $resultsByOrdinal[] = t('the') .' '. drupal_strtolower(t($counts[$ordinal])) .' '. implodeGrammar($ordinalResults[$i]);
+        }
       }
+      $byDayResults[] = t('on') .' '. t(implodeGrammar($resultsByOrdinal)) .' '. t('of the month');
     }
-    $description['!byday'] = implode(' '. t('and') .' ', $results);
+    $description['!byday'] = implodeGrammar($byDayResults);
   }
-  if (!empty($rrule['BYMONTH'])) {
+
+  if (!empty($rrule['BYMONTH']) || $rrule['FREQ'] == 'MONTHLY') {
     if (sizeof($rrule['BYMONTH']) < 12) {
       $results = array();
       $months = date_month_names();
       foreach ($rrule['BYMONTH'] as $month) {
         $results[] = $months[$month];
       }
-      if (!empty($rrule['BYMONTHDAY'])) {
-        $description['!bymonth'] = trim(t('!repeats_every_interval on the !month_days of !month_names', array('!repeats_every_interval ' => '', '!month_days' => implode(', ', $rrule['BYMONTHDAY']), '!month_names' => implode(', ', $results))));
+      $monthdays = $rrule['BYMONTHDAY'];
+      // Add ordinal suffix to each day
+      foreach ($monthdays as $id => $day) {
+        $monthdays[$id] = date_repeat_ordinal_suffix($day);
       }
-      else {
-        $description['!bymonth'] = trim(t('!repeats_every_interval on !month_names', array('!repeats_every_interval ' => '', '!month_names' => implode(', ', $results))));
+      if (!empty($monthdays)) {
+        if (empty($results)) {
+          $results[0] = "the month";
+        }
+        $description['!bymonth'] = t('on the !month_days of !month_names', array('!month_days' => implodeGrammar($monthdays), '!month_names' => implodeGrammar($results)));
+      }
+      elseif (!empty($rrule['BYMONTH'])) {
+        $description['!bymonth'] = t('during !month_names', array('!month_names' => implodeGrammar($results)));
       }
     }
   }
-  if ($rrule['INTERVAL'] < 1) {
-    $rrule['INTERVAL'] = 1;
-  }
   if (!empty($rrule['COUNT'])) {
-    $description['!count'] = trim(t('!repeats_every_interval !count times', array('!repeats_every_interval ' => '', '!count' => $rrule['COUNT'])));
+    $description['!count'] = t('!count times', array('!count' => $rrule['COUNT']));
   }
   if (!empty($rrule['UNTIL'])) {
     $until = date_ical_date($rrule['UNTIL']);
-    $description['!until'] = trim(t('!repeats_every_interval until !until_date', array('!repeats_every_interval ' => '', '!until_date' => date_format_date($until, 'custom', $format))));
+    $description['!until'] = t('until !until_date', array('!until_date' => date_format_date($until, 'custom', $format)));
   }
   if ($exceptions) {
     $values = array();
     foreach ($exceptions as $exception) {
       $values[] = date_format_date(date_ical_date($exception), 'custom', $format);
     }
-    $description['!except'] = trim(t('!repeats_every_interval except !except_dates', array('!repeats_every_interval ' => '', '!except_dates' => implode(', ', $values))));
+    $description['!except'] = t('except on !except_dates', array('!except_dates' => implodeGrammar($values)));
   }
   if (!empty($rrule['WKST'])) {
     $day_names = date_repeat_dow_day_options();
-    $description['!week_starts_on'] = trim(t('!repeats_every_interval where the week start on !day_of_week', array('!repeats_every_interval ' => '', '!day_of_week' => $day_names[trim($rrule['WKST'])])));
+    $description['!week_starts_on'] = t('where the week start on !day_of_week', array('!day_of_week' => $day_names[trim($rrule['WKST'])]));
+  }
+  return t('Repeats !interval !byday !count !bymonth !until !except', $description) .'.';
+}
+
+/**
+ * Implodes an array of strings using punctuation and/or a conjunction,
+ * depending on the number of items in the array.
+ */
+function implodeGrammar($terms, $punctuation = ',', $conjunction = 'and') {
+  $conjunction = t($conjunction);
+  $count = count($terms);
+  if ($count == 1) {
+    return $terms[0];
+  }
+  elseif ($count == 2) {
+    return implode(' '. $conjunction .' ', $terms);
   }
-  return t('Repeats !interval !bymonth !byday !count !until !except.', $description);
+  else {
+    $result = array();
+    for ($i = 0; $i < $count - 1; $i++) {
+      $result[] = $terms[$i];
+    }
+    $result[] = ' '. $conjunction .' '. $terms[$count - 1];
+    return implode($punctuation .' ', $result);
+  }
+  return '';
 }
 
 /**
@@ -262,7 +330,7 @@ function date_repeat_split_rrule($rrule)
  * Analyze a RRULE and return dates that match it.
  */
 function date_repeat_calc($rrule, $start, $end, $exceptions = array(), $timezone = NULL) {
-  require_once('./'. drupal_get_path('module', 'date_repeat') .'/date_repeat_calc.inc');
+  module_load_include('inc', 'date_repeat', 'date_repeat_calc');
   return _date_repeat_calc($rrule, $start, $end, $exceptions, $timezone);
 }
 
@@ -270,6 +338,22 @@ function date_repeat_calc($rrule, $start
  * Generate the repeat rule setting form.
  */
 function date_repeat_rrule_process($element, $edit, $form_state, $form) {
-  require_once('./'. drupal_get_path('module', 'date_repeat') .'/date_repeat_form.inc');
+  module_load_include('inc', 'date_repeat', 'date_repeat_form');
   return _date_repeat_rrule_process($element, $edit, $form_state, $form);
 }
+
+/**
+ * Adds an ordinal suffix to a number
+ */
+function date_repeat_ordinal_suffix($number) {
+  switch ($number % 10) {
+    case 1:
+      return $number .'st';
+    case 2:
+      return $number .'nd';
+    case 3:
+      return $number .'rd';
+    default:
+      return $number .'th';
+  }
+}
