Fixed bug #16611: [scheduler] tx_scheduler_CronCmd needs refactoring
authorChristian Kuhn <lolli@schwarzbu.ch>
Sun, 26 Dec 2010 20:35:39 +0000 (20:35 +0000)
committerChristian Kuhn <lolli@schwarzbu.ch>
Sun, 26 Dec 2010 20:35:39 +0000 (20:35 +0000)
git-svn-id: https://svn.typo3.org/TYPO3v4/Core/trunk@9901 709f56b5-9817-0410-a4d7-c38de5d9e867

ChangeLog
NEWS.txt
typo3/sysext/scheduler/class.tx_scheduler_croncmd.php
typo3/sysext/scheduler/class.tx_scheduler_croncmd_normalize.php [new file with mode: 0644]
typo3/sysext/scheduler/class.tx_scheduler_execution.php
typo3/sysext/scheduler/ext_autoload.php
typo3/sysext/scheduler/tests/tx_scheduler_croncmdTest.php
typo3/sysext/scheduler/tests/tx_scheduler_croncmd_normalizeTest.php [new file with mode: 0644]

index b38e9b5..82c44fd 100755 (executable)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2010-12-26  Christian Kuhn  <lolli@schwarzbu.ch>
+
+       * Fixed bug #16611: [scheduler] tx_scheduler_CronCmd needs refactoring
+
 2010-12-26  Francois Suter  <francois.suter@typo3.org>
 
        * Fixed bug #16838: Scheduler: Use new CSH rendering API
index 3d8798a..e7dc193 100644 (file)
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -78,6 +78,9 @@ Backend
          checkbox (#13797). In User-TS, you can set:
          options.enableShowPalettes=0 to see this feature in action.
          Default is still "1" like it used to be.
+       * The cron syntax interpreter of the Scheduler was entirely refactored.
+         It now supports the full range of cron syntax features as can be found
+         in Unix manual pages.
 
 
 Frontend
index 74895b2..3597497 100644 (file)
 <?php
 /***************************************************************
-*  Copyright notice
-*
-*  (c) 2008-2010 Markus Friedrich (markus.friedrich@dkd.de)
-*  All rights reserved
-*
-*  This script is part of the TYPO3 project. The TYPO3 project is
-*  free software; you can redistribute it and/or modify
-*  it under the terms of the GNU General Public License as published by
-*  the Free Software Foundation; either version 2 of the License, or
-*  (at your option) any later version.
-*
-*  The GNU General Public License can be found at
-*  http://www.gnu.org/copyleft/gpl.html.
-*
-*  This script is distributed in the hope that it will be useful,
-*  but WITHOUT ANY WARRANTY; without even the implied warranty of
-*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-*  GNU General Public License for more details.
-*
-*  This copyright notice MUST APPEAR in all copies of the script!
-***************************************************************/
-
+ *  Copyright notice
+ *
+ *  (c) 2008-2010 Markus Friedrich (markus.friedrich@dkd.de)
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
 
 /**
- * This class provides calulations for the cron command format
- *
- * @author             Markus Friedrich <markus.friedrich@dkd.de>
- * @package            TYPO3
- * @subpackage tx_scheduler
+ * This class provides calulations for the cron command format.
  *
- * $Id$
+ * @author Markus Friedrich <markus.friedrich@dkd.de>
+ * @author Christian Kuhn <lolli@schwarzbu.ch>
+ * @package TYPO3
+ * @subpackage tx_scheduler
  */
 class tx_scheduler_CronCmd {
 
        /**
-        * Sections of the cron command
+        * Normalized sections of the cron command.
+        * Allowed are comma separated lists of integers and the character '*'
         *
-        *      field          allowed values
+        *      field          lower and upper bound
         *      -----          --------------
         *      minute         0-59
         *      hour           0-23
         *      day of month   1-31
-        *      month          1-12 (or names, see below)
-        *      day of week    0-7 (0 or 7 is Sun, or use names)
+        *      month          1-12
+        *      day of week    1-7
         *
-        * @var array           $cmd_sections
+        * @var array $cronCommandSections
         */
-       public $cmd_sections;
+       protected $cronCommandSections;
 
        /**
-        * Valid values for each part
-        *
-        * @var array           $valid_values
+        * Timestamp of next execution date.
+        * This value starts with 'now + 1 minute' if not set externally
+        * by unit tests. After a call to calculateNextValue() it holds the timestamp of
+        * the next execution date which matches the cron command restrictions.
         */
-       public $valid_values;
-
-       /**
-        * Array containing the values to build the new execution date
-        *
-        * 0    =>      minute
-        * 1    =>      hour
-        * 2    =>      day
-        * 3    =>      month
-        * 4    =>      year
-        *
-        * @var array           $values
-        */
-       public $values;
+       protected $timestamp;
 
        /**
         * Constructor
         *
-        * @param       string          $cmd: the cron command
-        * @param       integer         $tstamp: optional start time
-        * @return      void
+        * @api
+        * @param string $cronCommand: The cron command can hold any combination documented as valid
+        *              expression in usual unix like crons like vixiecron. Special commands like @weekly,
+        *              ranges, steps and three letter month and weekday abbreviations are allowed.
+        * @param integer $timestamp: optional start time, used in unit tests
+        * @return void
         */
-       public function __construct($cmd, $tstamp = FALSE) {
-                       // Explode cmd in sections
-               $this->cmd_sections = t3lib_div::trimExplode(' ', $cmd);
+       public function __construct($cronCommand, $timestamp = FALSE) {
+               $cronCommand = tx_scheduler_CronCmd_Normalize::normalize($cronCommand);
+
+                       // Explode cron command to sections
+               $this->cronCommandSections = t3lib_div::trimExplode(' ', $cronCommand);
 
                        // Initialize the values with the starting time
                        // This takes care that the calculated time is always in the future
-               if ($tstamp === FALSE) {
-                       $tstamp = strtotime('+1 minute');
+               if ($timestamp === FALSE) {
+                       $timestamp = strtotime('+1 minute');
+               } else {
+                       $timestamp += 60;
                }
-               $this->values = array(
-                               // Minute
-                       intval(date('i', $tstamp)),
-                               // Hour
-                       intval(date('G', $tstamp)),
-                               // Day
-                       intval(date('j', $tstamp)),
-                               // Month
-                       intval(date('n', $tstamp)),
-                               // Year
-                       intval(date('Y', $tstamp))
-               );
-
-                       // Set valid values
-               $this->valid_values = array(
-                       $this->getList($this->cmd_sections[0], 0, 59),
-                       $this->getList($this->cmd_sections[1], 0, 23),
-                       $this->getDayList($this->values[3], $this->values[4]),
-                       $this->getList($this->cmd_sections[3], 1, 12),
-                       $this->getList('*', intval(date('Y', $tstamp)), intval(date('Y', $tstamp)) + 1)
-               );
+               $this->timestamp = $this->roundTimestamp($timestamp);
        }
 
        /**
-        * Calulates the next execution
+        * Calulates the date of the next execution.
         *
-        * @param       integer         $level: number of the current level, e.g. 2 is the day level
-        * @return      void
+        * @api
+        * @param integer $level (Deprecated) Number of the current level, e.g. 2 is the day level
+        * @return void
         */
-       public function calculateNextValue($level) {
-               if (isset($this->values[$level])) {
-                       $current_value = &$this->values[$level];
-                       $next_level = $level + 1;
-
-                       if (in_array($current_value, $this->valid_values[$level])) {
-                               $this->calculateNextValue($next_level);
-                       } else {
-                               $next_value = $this->getNextValue($this->values[$level], $this->valid_values[$level]);
-                               if ($next_value === false) {
-                                               // Set this value and prior values to the start value
-                                       for ($i = $level; $i >= 0; $i--) {
-                                               $this->values[$i] = $this->valid_values[$i][0];
-
-                                                       // Update day list if month was changed
-                                               if ($i == 3) {
-                                                       $this->valid_values[2] = $this->getDayList($this->values[3], $this->values[4]);
-                                               }
-                                       }
-
-                                               // Calculate next value for the next value
-                                       for ($i = $next_level; $i <= count($this->values); $i++) {
-                                               if (isset($this->values[$i])) {
-                                                       $increased_value = $this->getNextValue($this->values[$i], $this->valid_values[$i]);
-
-                                                       if ($increased_value !== false) {
-                                                               $this->values[$i] = $increased_value;
-
-                                                                       // Update day list if month was changed
-                                                               if ($i == 3) {
-                                                                       $this->valid_values[2] = $this->getDayList($this->values[3], $this->values[4]);
-
-                                                                               // Check if day had already a valid start value, if not set a new one
-                                                                       if (!$this->values[2] || !in_array($this->values[2], $this->valid_values[2])) {
-                                                                               $this->values[2] = $this->valid_values[2][0];
-                                                                       }
-                                                               }
-
-                                                               break;
-                                                       } else {
-                                                               $this->values[$i] = $this->valid_values[$i][0];
-
-                                                                       // Update day list if month was changed
-                                                               if ($i == 3) {
-                                                                       $this->valid_values[2] = $this->getDayList($this->values[3], $this->values[4]+1);
-                                                               }
-                                                       }
-                                               }
-                                       }
-
-                                       $this->calculateNextValue($next_level);
-                               } else {
-                                       if ($level == 3) {
-                                                       // Update day list if month was changed
-                                               $this->valid_values[2] = $this->getDayList($this->values[3], $this->values[4]);
-                                       }
-
-                                       $current_value = $next_value;
-                                       $this->calculateNextValue($next_level);
-                               }
+       public function calculateNextValue($level = NULL) {
+               if (!is_null($level)) {
+                       t3lib_div::deprecationLog('The parameter $level is deprecated since TYPO3 version 4.5.');
+               }
+
+               $newTimestamp = $this->getTimestamp();
+
+                       // Calculate next minute and hour field
+               $loopCount = 0;
+               while (TRUE) {
+                       $loopCount ++;
+                               // If there was no match within two days, cron command is invalid.
+                               // The second day is needed to catch the summertime leap in some countries.
+                       if ($loopCount > 2880) {
+                               throw new RuntimeException(
+                                       'Unable to determine next execution timestamp: Hour and minute combination is invalid.',
+                                       1291494126
+                               );
+                       }
+                       if ($this->minuteAndHourMatchesCronCommand($newTimestamp)) {
+                               break;
                        }
+                       $newTimestamp += 60;
                }
+
+               $loopCount = 0;
+               while (TRUE) {
+                       $loopCount ++;
+                               // A date must match within the next 4 years, this high number makes
+                               // sure leap year cron command configuration are caught.
+                               // If the loop runs longer than that, the cron command is invalid.
+                       if ($loopCount > 1464) {
+                               throw new RuntimeException(
+                                       'Unable to determine next execution timestamp: Day of month, month and day of week combination is invalid.',
+                                       1291501280
+                               );
+                       }
+                       if ($this->dayMatchesCronCommand($newTimestamp)) {
+                               break;
+                       }
+                       $newTimestamp += $this->numberOfSecondsInDay($newTimestamp);
+               }
+
+               $this->timestamp = $newTimestamp;
+       }
+
+       /*
+        * Get next timestamp
+        *
+        * @api
+        * @return integer Unix timestamp
+        */
+       public function getTimestamp() {
+               return $this->timestamp;
        }
 
        /**
-        * Builds a list of days for a certain month
+        * Get cron command sections. Array of strings, each containing either
+        * a list of comma seperated integers or *
         *
-        * @param       integer         $currentMonth: number of a month
-        * @param       integer         $currentYear: a year
-        * @return      array           list of days
+        * @return array command sections:
+        *      0 => minute
+        *      1 => hour
+        *      2 => day of month
+        *      3 => month
+        *      4 => day of week
         */
-       protected function getDayList($currentMonth, $currentYear) {
-                       // Create a dummy timestamp at 6:00 of the first day of the current month and year
-                       // to get the number of days in the month using date()
-               $dummyTimestamp = mktime(6, 0, 0, $currentMonth, 1, $currentYear);
-               $max_days = date('t', $dummyTimestamp);
-               $validDays = $this->getList($this->cmd_sections[2], 1, $max_days);
-
-                       // Consider special field 'day of week'
-                       // @TODO: Implement lists and ranges for day of week (2,3 and 1-5 and */2,3 and 1,1-5/2)
-                       // @TODO: Support usage of day names in day of week field ("mon", "sun", etc.)
-               if ((strpos($this->cmd_sections[4], '*') === FALSE && preg_match('/[0-7]{1}/', $this->cmd_sections[4]) !== FALSE)) {
-                               // Unset days from 'day of month' if * is given
-                               // * * * * 1 results to every monday of this month
-                               // * * 1,2 * 1 results to every monday, plus first and second day of month
-                       if ($this->cmd_sections[2] == '*') {
-                               $validDays = array();
-                       }
+       public function getCronCommandSections() {
+               return $this->cronCommandSections;
+       }
 
-                               // Allow 0 as representation for sunday and convert to 7
-                       $dayOfWeek = $this->cmd_sections[4];
-                       if ($dayOfWeek === '0') {
-                               $dayOfWeek = '7';
-                       }
+       /**
+        * Determine if current timestamp matches minute and hour cron command restriction.
+        *
+        * @param integer $timestamp to test
+        * @return boolean TRUE if cron command conditions are met
+        */
+       protected function minuteAndHourMatchesCronCommand($timestamp) {
+               $minute = intval(date('i', $timestamp));
+               $hour = intval(date('G', $timestamp));
+
+               $commandMatch = FALSE;
+               if (
+                       $this->isInCommandList($this->cronCommandSections[0], $minute)
+                       && $this->isInCommandList($this->cronCommandSections[1], $hour)
+               ) {
+                       $commandMatch = TRUE;
+               }
+
+               return $commandMatch;
+       }
 
-                               // Get list
-                       for ($i = 1; $i <= $max_days; $i++) {
-                               if (strftime('%u', mktime(0, 0, 0, $currentMonth, $i, $currentYear)) == $dayOfWeek) {
-                                       if (!in_array($i, $validDays)) {
-                                               $validDays[] = $i;
-                                       }
-                               }
+       /**
+        * Determine if current timestamp matches day of month, month and day of week
+        * cron command restriction
+        *
+        * @param integer $timestamp to test
+        * @return boolean TRUE if cron command conditions are met
+        */
+       protected function dayMatchesCronCommand($timestamp) {
+               $dayOfMonth = date('j', $timestamp);
+               $month = date('n', $timestamp);
+               $dayOfWeek = date('N', $timestamp);
+
+               $isInDayOfMonth = $this->isInCommandList($this->cronCommandSections[2], $dayOfMonth);
+               $isInMonth = $this->isInCommandList($this->cronCommandSections[3], $month);
+               $isInDayOfWeek = $this->isInCommandList($this->cronCommandSections[4], $dayOfWeek);
+
+                       // Quote from vixiecron:
+                       // Note: The day of a command's execution can be specified by two fields — day of month, and day of week.
+                       // If both fields are restricted (i.e., aren't  *),  the  command will be run when either field
+                       // matches the current time.  For example, `30 4 1,15 * 5' would cause
+                       // a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
+
+               $isDayOfMonthRestricted = (string)$this->cronCommandSections[2] ===  '*' ? FALSE : TRUE;
+               $isDayOfWeekRestricted = (string)$this->cronCommandSections[4] === '*' ? FALSE : TRUE;
+
+               $commandMatch = FALSE;
+               if ($isInMonth) {
+                       if (
+                               ($isInDayOfMonth && $isDayOfMonthRestricted)
+                               || ($isInDayOfWeek && $isDayOfWeekRestricted)
+                               || ($isInDayOfMonth && !$isDayOfMonthRestricted && $isInDayOfWeek && !$isDayOfWeekRestricted)
+                       ) {
+                               $commandMatch = TRUE;
                        }
                }
-               sort($validDays);
 
-               return $validDays;
+               return $commandMatch;
        }
 
        /**
-        * Builds a list of possible values from a cron command.
+        * Determine if a given number validates a cron command section. The given cron
+        * command must be a 'normalized' list with only comma separated integers or '*'
         *
-        * @param       string          $definition: the command e.g. "2-8,14,0-59/20"
-        * @param       integer         $min: minimum allowed value, greater or equal zero
-        * @param       integer         $max: maximum allowed value, greater than $min
-        * @return      array           list with possible values
+        * @param string $commandExpression: cron command
+        * @param integer $numberToMatch: number to look up
+        * @return boolean TRUE if number is in list
         */
-       protected function getList($definition, $min, $max) {
-               $possibleValues = array();
-
-               $listParts = t3lib_div::trimExplode(',', $definition, TRUE);
-               foreach ($listParts as $part) {
-                       $possibleValues = array_merge($possibleValues, $this->getListPart($part, $min, $max));
+       protected function isInCommandList($commandExpression, $numberToMatch) {
+               $inList = FALSE;
+               if ((string)$commandExpression === '*') {
+                       $inList = TRUE;
+               } else {
+                       $inList = t3lib_div::inList($commandExpression, $numberToMatch);
                }
 
-               sort($possibleValues);
-               return $possibleValues;
+               return $inList;
        }
 
        /**
-        * Builds a list of possible values from a single part of a cron command.
-        * Parses asterisk (*), ranges (2-4) and steps (2-10/2).
+        * Helper method to calculate number of seconds in a day.
+        *
+        * This is not always 86400 (60*60*24) and depends on the timezone:
+        * Some countries like Germany have a summertime / wintertime switch,
+        * on every last sunday in march clocks are forwarded by one hour (set from 2:00 to 3:00),
+        * and on last sunday of october they are set back one hour (from 3:00 to 2:00).
+        * This shortens and lengthens the length of a day by one hour.
         *
-        * @param       string          $definition: a command part e.g. "2-8", "*", "0-59/20"
-        * @param       integer         $min: minimum allowed value, greater or equal zero
-        * @param       integer         $max: maximum allowed value, greater than $min
-        * @return      array           list with possible values or empty array
+        * @param integer timestamp
+        * @return integer Number of seconds of day
         */
-       protected function getListPart($definition, $min, $max) {
-               $possibleValues = array();
+       protected function numberOfSecondsInDay($timestamp) {
+               $now = mktime(0, 0, 0, date('n', $timestamp), date('j', $timestamp), date('Y', $timestamp));
+                       // Make sure to be in next day, even if day has 25 hours
+               $nextDay = $now + 60*60*25;
+               $nextDay = mktime(0, 0, 0, date('n', $nextDay), date('j', $nextDay), date('Y', $nextDay));
 
-               if ($definition == '*') {
-                               // Get list for the asterisk
-                       for ($value = $min; $value <= $max; $value++) {
-                               $possibleValues[] = $value;
-                       }
-               } else if (strpos($definition, '/') !== false) {
-                               // Get list for step values
-                       list($listPart, $stepPart) = t3lib_div::trimExplode('/', $definition);
-                       $tempList = $this->getListPart($listPart, $min, $max);
-                       foreach ($tempList as $tempListValue) {
-                               if ($tempListValue % $stepPart == 0) {
-                                       $possibleValues[] = $tempListValue;
-                               }
-                       }
-               } else if (strpos($definition, '-') !== false) {
-                               // Get list for range definitions
-                               // Get list definition parts
-                       list($minValue, $maxValue) = t3lib_div::trimExplode('-', $definition);
-                       if ($minValue < $min) {
-                               $minValue = $min;
-                       }
-                       if ($maxValue > $max) {
-                               $maxValue = $max;
-                       }
-                       $possibleValues = $this->getListPart('*', $minValue, $maxValue);
-               } else if (is_numeric($definition) && $definition >= $min && $definition <= $max) {
-                               // Get list for single values
-                       $possibleValues[] = intval($definition);
-               }
+               return ($nextDay - $now);
+       }
 
-               sort($possibleValues);
-               return $possibleValues;
+       /**
+        * Round a timestamp down to full minute.
+        *
+        * @param integer timestamp
+        * @return integer Rounded timestamp
+        */
+       protected function roundTimestamp($timestamp) {
+               return mktime(date('H', $timestamp), date('i', $timestamp), 0, date('n', $timestamp), date('j', $timestamp), date('Y', $timestamp));
        }
 
        /**
         * Returns the first value that is higher than the current value
-        * from a list of possible values
+        * from a list of possible values.
         *
+        * @deprecated since 4.5
         * @param       mixed   $currentValue: the value to be searched in the list
         * @param       array   $listArray: the list of values
         * @return      mixed   The value from the list right after the current value
         */
        public function getNextValue($currentValue, array $listArray) {
+               t3lib_div::deprecationLog('The method is deprecated since TYPO3 version 4.5.');
+
                $next_value = false;
 
                $numValues = count($listArray);
@@ -312,17 +296,17 @@ class tx_scheduler_CronCmd {
        }
 
        /**
-        * Returns the timestamp for the value parts in $this->values
+        * Returns the timestamp for the value parts in $this->values.
         *
-        * @return      integer         unix timestamp
+        * @deprecated since 4.5
+        * @return integer unix timestamp
         */
        public function getTstamp() {
-               return mktime($this->values[1], $this->values[0], 0, $this->values[3], $this->values[2], $this->values[4]);
+               t3lib_div::deprecationLog('The method is deprecated since TYPO3 version 4.5.');
+               return $this->getTimestamp();
        }
 }
 
 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['ext/scheduler/class.tx_scheduler_croncmd.php'])) {
        include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['ext/scheduler/class.tx_scheduler_croncmd.php']);
 }
-
-?>
\ No newline at end of file
diff --git a/typo3/sysext/scheduler/class.tx_scheduler_croncmd_normalize.php b/typo3/sysext/scheduler/class.tx_scheduler_croncmd_normalize.php
new file mode 100644 (file)
index 0000000..c9d04c7
--- /dev/null
@@ -0,0 +1,461 @@
+<?php
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) 2010 Christian Kuhn <lolli@schwarzbu.ch>
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+/**
+ * Validate and normalize a cron command.
+ *
+ * Special fields like three letter weekdays, ranges and steps are substituted
+ * to a comma separated list of integers. Example:
+ *     '2-4 10-40/10 * mar * fri'  will be nolmalized to '2,4 10,20,30,40 * * 3 1,2'
+ *
+ * @author Christian Kuhn <lolli@schwarzbu.ch>
+ *
+ * @package TYPO3
+ * @subpackage scheduler
+ */
+final class tx_scheduler_CronCmd_Normalize {
+
+       /**
+        * Main API method: Get the cron command and normalize it.
+        *
+        * If no exception is thrown, the resulting cron command is validated
+        * and consists of five whitespace separated fields, which are either
+        * the letter '*' or a sorted, unique comma separated list of integers.
+        *
+        * @api
+        * @throws InvalidArgumentException cron command is invalid or out of bounds
+        * @param string $cronCommand The cron command to normalize
+        * @return string Normalized cron command
+        */
+       public static function normalize($cronCommand) {
+               $cronCommand = trim($cronCommand);
+               $cronCommand = self::convertKeywordsToCronCommand($cronCommand);
+               $cronCommand = self::normalizeFields($cronCommand);
+               return $cronCommand;
+       }
+
+       /**
+        * Accept special cron command keywords and convert to standard cron syntax.
+        * Allowed keywords: @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly
+        *
+        * @params string $cronCommand cron command
+        * @return string Normalized cron command if keyword was found, else unchanged cron command
+        */
+       public static function convertKeywordsToCronCommand($cronCommand) {
+               switch ($cronCommand) {
+                       case '@yearly':
+                       case '@annually':
+                               $cronCommand = '0 0 1 1 *';
+                       break;
+                       case '@monthly':
+                               $cronCommand = '0 0 1 * *';
+                       break;
+                       case '@weekly':
+                               $cronCommand = '0 0 * * 0';
+                       break;
+                       case '@daily':
+                       case '@midnight':
+                               $cronCommand = '0 0 * * *';
+                       break;
+                       case '@hourly':
+                               $cronCommand = '0 * * * *';
+                       break;
+               }
+
+               return $cronCommand;
+       }
+
+       /**
+        * Normalize cron command field to list of integers or *
+        *
+        * @param string $cronCommand cron command
+        * @return string Normalized cron command
+        */
+       public static function normalizeFields($cronCommand) {
+               $fieldArray = self::splitFields($cronCommand);
+
+               $fieldArray[0] = self::normalizeIntegerField($fieldArray[0], 0, 59);
+               $fieldArray[1] = self::normalizeIntegerField($fieldArray[1], 0, 23);
+               $fieldArray[2] = self::normalizeIntegerField($fieldArray[2], 1, 31);
+               $fieldArray[3] = self::normalizeMonthAndWeekdayField($fieldArray[3], TRUE);
+               $fieldArray[4] = self::normalizeMonthAndWeekdayField($fieldArray[4], FALSE);
+
+               $normalizedCronCommand = implode(' ', $fieldArray);
+               return $normalizedCronCommand;
+       }
+
+       /**
+        * Split a given cron command like '23 * * * *' to an array with five fields.
+        *
+        * @throws InvalidArgumentException If splitted array does not contain five entries
+        * @param string $cronCommand cron command
+        * @return array
+        *              0 => minute field
+        *              1 => hour field
+        *              2 => day of month field
+        *              3 => month field
+        *              4 => day of week field
+        */
+       public static function splitFields($cronCommand) {
+               $fields = explode(' ', $cronCommand);
+
+               if (count($fields) !== 5) {
+                       throw new InvalidArgumentException(
+                               'Unable to split given cron command to five fields.',
+                               1291227373
+                       );
+               }
+
+               return $fields;
+       }
+
+       /**
+        * Normalize month field.
+        *
+        * @param string $expression Month field expression
+        * @param boolean $isMonthField True if month field is handled, false for weekday field
+        * @return string Normalized expression
+        */
+       public static function normalizeMonthAndWeekdayField($expression, $isMonthField = TRUE) {
+               if ((string)$expression === '*') {
+                       $fieldValues = '*';
+               } else {
+                               // Fragment espression by , / and - and substitute three letter code of month and weekday to numbers
+                       $listOfCommaValues = explode(',', $expression);
+                       $fieldArray = array();
+                       foreach ($listOfCommaValues as $listElement) {
+                               if (strpos($listElement, '/') !== FALSE) {
+                                       list($left, $right) = explode('/', $listElement);
+                                       if (strpos($left, '-') !== FALSE) {
+                                               list($leftBound, $rightBound) = explode('-', $left);
+                                               $leftBound = self::normalizeMonthAndWeekday($leftBound, $isMonthField);
+                                               $rightBound = self::normalizeMonthAndWeekday($rightBound, $isMonthField);
+                                               $left = $leftBound . '-' . $rightBound;
+                                       } else {
+                                               if ((string)$left !== '*') {
+                                                       $left = self::normalizeMonthAndWeekday($left, $isMonthField);
+                                               }
+                                       }
+                                       $fieldArray[] = $left . '/' . $right;
+                               } elseif (strpos($listElement, '-') !== FALSE) {
+                                       list($left, $right) = explode('-', $listElement);
+                                       $left = self::normalizeMonthAndWeekday($left, $isMonthField);
+                                       $right = self::normalizeMonthAndWeekday($right, $isMonthField);
+                                       $fieldArray[] = $left . '-' . $right;
+                               } else {
+                                       $fieldArray[] = self::normalizeMonthAndWeekday($listElement, $isMonthField);
+                               }
+                       }
+                       $fieldValues = implode(',', $fieldArray);
+               }
+
+               return $isMonthField ? self::normalizeIntegerField($fieldValues, 1, 12) : self::normalizeIntegerField($fieldValues, 1, 7);
+       }
+
+       /**
+        * Normalize integer field.
+        *
+        * @throws InvalidArgumentException If field is invalid or out of bounds
+        * @param string $expression Expression
+        * @param integer $lowerBound Lower limit of result list
+        * @param integer $upperBound Upper limit of result list
+        * @return string Normalized expression
+        */
+       public static function normalizeIntegerField($expression, $lowerBound = 0, $upperBound = 59) {
+               if ((string)$expression === '*') {
+                       $fieldValues = '*';
+               } else {
+                       $listOfCommaValues = explode(',', $expression);
+                       $fieldArray = array();
+                       foreach ($listOfCommaValues as $listElement) {
+                               if (strpos($listElement, '/') !== FALSE) {
+                                       list($left, $right) = explode('/', $listElement);
+                                       if ((string)$left === '*') {
+                                               $leftList = self::convertRangeToListOfValues($lowerBound . '-' . $upperBound);
+                                       } else {
+                                               $leftList = self::convertRangeToListOfValues($left);
+                                       }
+                                       $fieldArray[] = self::reduceListOfValuesByStepValue($leftList . '/' . $right);
+                               } elseif (strpos($listElement, '-') !== FALSE) {
+                                       $fieldArray[] = self::convertRangeToListOfValues($listElement);
+                               } elseif (strcmp(intval($listElement), $listElement) === 0) {
+                                       $fieldArray[] = $listElement;
+                               } else {
+                                       throw new InvalidArgumentException(
+                                               'Unable to normalize integer field.',
+                                               1291429389
+                                       );
+                               }
+                       }
+                       $fieldValues = implode(',', $fieldArray);
+               }
+
+               if (strlen($fieldValues) === 0) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert integer field to list of values: Result list empty.',
+                               1291422012
+                       );
+               }
+
+               if ((string)$fieldValues !== '*') {
+                       $fieldList = explode(',', $fieldValues);
+
+                       sort($fieldList);
+                       $fieldList = array_unique($fieldList);
+
+                       if (current($fieldList) < $lowerBound) {
+                               throw new InvalidArgumentException(
+                                       'Lowest element in list is smaller than allowed.',
+                                       1291470084
+                               );
+                       }
+
+                       if (end($fieldList) > $upperBound) {
+                               throw new InvalidArgumentException(
+                                       'An element in the list is higher than allowed.',
+                                       1291470170
+                               );
+                       }
+
+                       $fieldValues = implode(',', $fieldList);
+               }
+
+               return (string)$fieldValues;
+       }
+
+
+       /**
+        * Convert a range of integers to a list: 4-6 results in a string '4,5,6'
+        *
+        * @throws InvalidArgumentException If range can not be coverted to list
+        * @params string $range integer-integer
+        * @return array
+        */
+       public static function convertRangeToListOfValues($range) {
+               if (strlen($range) === 0) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert range to list of values with empty string.',
+                               1291234985
+                       );
+               }
+
+               $rangeArray = explode('-', $range);
+
+                       // Sanitize fields and cast to integer
+               foreach ($rangeArray as $fieldNumber => $fieldValue) {
+                       if (strcmp(intval($fieldValue), $fieldValue) !== 0) {
+                               throw new InvalidArgumentException(
+                                       'Unable to convert value to integer.',
+                                       1291237668
+                               );
+                       }
+                       $rangeArray[$fieldNumber] = (int)$fieldValue;
+               }
+
+               $resultList = '';
+               if (count($rangeArray) === 1) {
+                       $resultList = $rangeArray[0];
+               } elseif (count($rangeArray) === 2) {
+                       $left = $rangeArray[0];
+                       $right = $rangeArray[1];
+
+                       if ($left > $right) {
+                               throw new InvalidArgumentException(
+                                       'Unable to convert range to list: Left integer must not be greather than right integer.',
+                                       1291237145
+                               );
+                       }
+
+                       $resultListArray = array();
+                       for ($i = $left; $i <= $right; $i++) {
+                               $resultListArray[] = $i;
+                       }
+
+                       $resultList = implode(',', $resultListArray);
+               } else {
+                       throw new InvalidArgumentException(
+                               'Unable to convert range to list of values.',
+                               1291234985
+                       );
+               }
+
+               return (string)$resultList;
+       }
+
+       /**
+        * Reduce a given list of values by step value.
+        * Following a range with ``/<number>'' specifies skips of the number's value through the range.
+        *      1-5/2 -> 1,3,5
+        *      2-10/3 -> 2,5,8
+        *
+        * @throws Exception if step value is invalid or if resulting list is empty
+        * @param string #stepExpression stepvalue expression
+        * @return string Comma separated list of valid values
+        */
+       public static function reduceListOfValuesByStepValue($stepExpression) {
+               if (strlen($stepExpression) === 0) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert step values.',
+                               1291234985
+                       );
+               }
+
+               $stepValuesAndStepArray = explode('/', $stepExpression);
+
+               if (count($stepValuesAndStepArray) < 1 || count($stepValuesAndStepArray) > 2) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert step values: Multiple slashes found.',
+                               1291242168
+                       );
+               }
+
+               $left = $stepValuesAndStepArray[0];
+               $right = $stepValuesAndStepArray[1];
+
+               if (strlen($stepValuesAndStepArray[0]) === 0) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert step values: Left part of / is empty.',
+                               1291414955
+                       );
+               }
+
+               if (strlen($stepValuesAndStepArray[1]) === 0) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert step values: Right part of / is empty.',
+                               1291414956
+                       );
+               }
+
+               if (strcmp(intval($right), $right) !== 0) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert step values: Right part must be a single integer.',
+                               1291414957
+                       );
+               }
+
+               $right = (int)$right;
+               $leftArray = explode(',', $left);
+
+               $validValues = array();
+               $currentStep = $right;
+               foreach ($leftArray as $leftValue) {
+                       if (strcmp(intval($leftValue), $leftValue) !== 0) {
+                               throw new InvalidArgumentException(
+                                       'Unable to convert step values: Left part must be a single integer or comma separated list of integers.',
+                                       1291414958
+                               );
+                       }
+
+                       if ($currentStep === 0) {
+                               $currentStep = $right;
+                       }
+
+                       if ($currentStep === $right) {
+                               $validValues[] = (int)$leftValue;
+                       }
+
+                       $currentStep --;
+               }
+
+               if (count($validValues) === 0) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert step values: Result value list is empty.',
+                               1291414958
+                       );
+               }
+
+               return implode(',', $validValues);
+       }
+
+       /**
+        * Dispatcher method for normalizeMonth and normalizeWeekday
+        *
+        * @param string $expression Month or weekday to be normalized
+        * @param boolean $isMonth TRUE if a month is handled, FALSE for weekday
+        * @return string normalized month or weekday
+        */
+       public static function normalizeMonthAndWeekday($expression, $isMonth = TRUE) {
+               $expression = $isMonth ? self::normalizeMonth($expression) : self::normalizeWeekday($expression);
+
+               return (string)$expression;
+       }
+
+       /**
+        * Accept a string representation or integer number of a month like
+        * 'jan', 'February', 01, ... and convert to normalized integer value 1 .. 12
+        *
+        * @throws InvalidArgumentException If month string can not be converted to integer
+        * @param string $month Month representation
+        * @return integer month integer representation between 1 and 12
+        */
+       public static function normalizeMonth($month) {
+               $timestamp = strtotime('2010-' . $month . '-01');
+
+                       // timestamp must be >= 2010-01-01 and <= 2010-12-01
+               if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-12-01')) {
+                       throw new InvalidArgumentException(
+                               'Unable to convert given month name.',
+                               1291083486
+                       );
+               }
+
+               return (int)date('n', $timestamp);
+       }
+
+       /**
+        * Accept a string representation or integer number of a weekday like
+        * 'mon', 'Friday', 3, ... and convert to normalized integer value 1 .. 7
+        *
+        * @throws InvalidArgumentException If weekday string can not be converted
+        * @param string $weekday Weekday representation
+        * @return integer weekday integer representation between 1 and 7
+        */
+       public static function normalizeWeekday($weekday) {
+               $normalizedWeekday = FALSE;
+
+                       // 0 (sunday) -> 7
+               if ((string)$weekday === '0') {
+                       $weekday = 7;
+               }
+
+               if ($weekday >= 1 && $weekday <= 7) {
+                       $normalizedWeekday = (int)$weekday;
+               }
+
+               if (!$normalizedWeekday) {
+                               // Convert string representation like 'sun' to integer
+                       $timestamp = strtotime('next ' . $weekday, mktime(0, 0, 0, 1, 1, 2010));
+                       if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-01-08')) {
+                               throw new InvalidArgumentException(
+                                       'Unable to convert given weekday name.',
+                                       1291163589
+                               );
+                       }
+                       $normalizedWeekday = (int)date('N', $timestamp);
+               }
+
+               return $normalizedWeekday;
+       }
+}
+?>
index 0b10970..95dabc5 100644 (file)
@@ -263,9 +263,9 @@ class tx_scheduler_Execution {
         */
        public function getNextCronExecution() {
                $cronCmd = t3lib_div::makeInstance('tx_scheduler_CronCmd', $this->getCronCmd());
-               $cronCmd->calculateNextValue(0);
+               $cronCmd->calculateNextValue();
 
-               return $cronCmd->getTstamp();
+               return $cronCmd->getTimestamp();
        }
 
        /**
index a378d25..cf82d1d 100644 (file)
@@ -8,6 +8,7 @@ $extensionPath = t3lib_extMgm::extPath('scheduler');
 return array(
        'tx_scheduler' => $extensionPath . 'class.tx_scheduler.php',
        'tx_scheduler_croncmd' => $extensionPath . 'class.tx_scheduler_croncmd.php',
+       'tx_scheduler_croncmd_normalize' => $extensionPath . 'class.tx_scheduler_croncmd_normalize.php',
        'tx_scheduler_execution' => $extensionPath . 'class.tx_scheduler_execution.php',
        'tx_scheduler_failedexecutionexception' => $extensionPath . 'class.tx_scheduler_failedexecutionexception.php',
        'tx_scheduler_task' => $extensionPath . 'class.tx_scheduler_task.php',
index f14292a..988db3c 100644 (file)
@@ -39,204 +39,230 @@ class tx_scheduler_croncmdTest extends tx_phpunit_testcase {
        /**
         * @test
         */
-       public function validValuesContainsIntegersForListOfMinutes() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '23 * * * *', self::TIMESTAMP);
-               $this->assertType('integer', $cronCmdInstance->valid_values[0][0]);
+       public function constructorSetsNormalizedCronCommandSections() {
+               $instance = new tx_scheduler_CronCmd('2-3 * * * *');
+               $this->assertSame($instance->getCronCommandSections(), array('2,3', '*', '*', '*', '*'));
        }
 
        /**
         * @test
+        * @expectedException InvalidArgumentException
         */
-       public function validValuesContainsIntegersForListOfHours() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* 23 * * *', self::TIMESTAMP);
-               $this->assertType('integer', $cronCmdInstance->valid_values[1][0]);
+       public function constructorThrowsExceptionForInvalidCronCommand() {
+               new tx_scheduler_CronCmd('61 * * * *');
        }
 
        /**
         * @test
         */
-       public function validValuesContainsIntegersForListOfDays() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * 3 * *', self::TIMESTAMP);
-               $this->assertType('integer', $cronCmdInstance->valid_values[2][0]);
+       public function constructorSetsTimestampToNowPlusOneMinuteRoundedDownToSixtySeconds() {
+               $instance = new tx_scheduler_CronCmd('* * * * *');
+               $this->assertSame($instance->getTimestamp(), $GLOBALS['ACCESS_TIME'] + 60);
        }
 
        /**
         * @test
         */
-       public function validValuesContainsIntegersForListOfMonth() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * * 7 *', self::TIMESTAMP);
-               $this->assertType('integer', $cronCmdInstance->valid_values[3][0]);
+       public function constructorSetsTimestampToGivenTimestampPlusSixtySeconds() {
+               $instance = new tx_scheduler_CronCmd('* * * * *', self::TIMESTAMP);
+               $this->assertSame($instance->getTimestamp(), self::TIMESTAMP + 60);
        }
 
        /**
         * @test
         */
-       public function validValuesContainsIntegersForListOfYear() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * * * 2010', self::TIMESTAMP);
-               $this->assertType('integer', $cronCmdInstance->valid_values[4][0]);
+       public function constructorSetsTimestampToGiveTimestampRoundedDownToSixtySeconds() {
+               $instance = new tx_scheduler_CronCmd('* * * * *', self::TIMESTAMP + 1);
+               $this->assertSame($instance->getTimestamp(), self::TIMESTAMP + 60);
        }
 
        /**
-        * Tests wether step values are correctly parsed for minutes
-        *
-        * @test
+        * @return array
+        *      0 => cron command
+        *      1 => start timestamp
+        *      2 => expected timestamp after first calculateNextValue()
+        *      3 => expected timestamp after second calculateNextValue()
         */
-       public function minutePartUsesStepValuesWithinRange() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '0-20/10 * * * *');
-               $expectedResult = array(
-                       '0' => 0,
-                       '1' => 10,
-                       '2' => 20,
+       public static function expectedTimestampDataProvider() {
+               return array(
+                       'every minute' => array(
+                               '* * * * *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60,
+                               self::TIMESTAMP + 120,
+                       ),
+                       'once an hour at 1' => array(
+                               '1 * * * *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60,
+                               self::TIMESTAMP + 60 + 60*60,
+                       ),
+                       'once an hour at 0' => array(
+                               '0 * * * *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60,
+                               self::TIMESTAMP + 60*60 + 60*60,
+                       ),
+                       'once a day at 1:00' => array(
+                               '0 1 * * *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60,
+                               self::TIMESTAMP + 60*60 + 60*60*24,
+                       ),
+                       'once a day at 0:00' => array(
+                               '0 0 * * *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60*24,
+                               self::TIMESTAMP + 60*60*24*2,
+                       ),
+                       'every first day of month' => array(
+                               '0 0 1 * *',
+                               self::TIMESTAMP,
+                               strtotime('01-02-2010'),
+                               strtotime('01-03-2010'),
+                       ),
+                       'once a month' => array(
+                               '0 0 4 * *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60*24*3,
+                               self::TIMESTAMP + 60*60*24*3 + 60*60*24*31,
+                       ),
+                       'once every Saturday' => array(
+                               '0 0 * * sat',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60*24,
+                               self::TIMESTAMP + 60*60*24 + 60*60*24*7,
+                       ),
+                       'once every day in February' => array(
+                               '0 0 * feb *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60*24*31,
+                               self::TIMESTAMP + 60*60*24*31 + 60*60*24
+                       ),
+                       'once every February' => array(
+                               '0 0 1 feb *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60*24*31,
+                               strtotime('01-02-2011'),
+                       ),
+                       'once every Friday February' => array(
+                               '0 0 * feb fri',
+                               self::TIMESTAMP,
+                               strtotime('05-02-2010'),
+                               strtotime('12-02-2010'),
+                       ),
+                       'first day in February and every Friday' => array(
+                               '0 0 1 feb fri',
+                               self::TIMESTAMP,
+                               strtotime('01-02-2010'),
+                               strtotime('05-02-2010'),
+                       ),
+                       'day of week and day of month restricted, next match in day of month field' => array(
+                               '0 0 2 * sun',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60*24,
+                               self::TIMESTAMP + 60*60*24 + 60*60*24,
+                       ),
+                       'day of week and day of month restricted, next match in day of week field' => array(
+                               '0 0 3 * sat',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60*24,
+                               self::TIMESTAMP + 60*60*24 + 60*60*24,
+                       ),
+                       '29th February leap year' => array(
+                               '0 0 29 feb *',
+                               self::TIMESTAMP,
+                               strtotime('29-02-2012'),
+                               strtotime('29-02-2016'),
+                       ),
+                       'list of minutes' => array(
+                               '2,4 * * * *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 120,
+                               self::TIMESTAMP + 240,
+                       ),
+                       'list of hours' => array(
+                               '0 2,4 * * *',
+                               self::TIMESTAMP,
+                               self::TIMESTAMP + 60*60*2,
+                               self::TIMESTAMP + 60*60*4,
+                       ),
+                       'list of days in month' => array(
+                               '0 0 2,4 * *',
+                               self::TIMESTAMP,
+                               strtotime('02-01-2010'),
+                               strtotime('04-01-2010'),
+                       ),
+                       'list of month' => array(
+                               '0 0 1 2,3 *',
+                               self::TIMESTAMP,
+                               strtotime('01-02-2010'),
+                               strtotime('01-03-2010'),
+                       ),
+                       'list of days of weeks' => array(
+                               '0 0 * * 2,4',
+                               self::TIMESTAMP,
+                               strtotime('05-01-2010'),
+                               strtotime('07-01-2010'),
+                       ),
                );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[0]);
        }
 
        /**
-        * Tests whether dayList is correctly calculated for a single day of month
-        *
         * @test
+        * @dataProvider expectedTimestampDataProvider
         */
-       public function dayPartUsesSingleDay() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * 2 * *');
-               $expectedResult = array(
-                       '0' => 2,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
+       public function calculateNextValueDeterminesCorrectNextTimestamp($cronCommand, $startTimestamp, $expectedTimestamp) {
+               $instance = new tx_scheduler_CronCmd($cronCommand, $startTimestamp);
+               $instance->calculateNextValue();
+               $this->assertSame($instance->getTimestamp(), $expectedTimestamp);
        }
 
        /**
-        * Tests whether dayList is correctly calculated for a comma separated list of month days
-        *
         * @test
+        * @dataProvider expectedTimestampDataProvider
         */
-       public function dayPartUsesLists() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * 2,7 * *');
-               $expectedResult = array(
-                       '0' => 2,
-                       '1' => 7,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
+       public function calculateNextValueDeterminesCorrectNextTimestampOnConsecutiveCall($cronCommand, $startTimestamp, $firstTimestamp, $secondTimestamp) {
+               $instance = new tx_scheduler_CronCmd($cronCommand, $firstTimestamp);
+               $instance->calculateNextValue();
+               $this->assertSame($instance->getTimestamp(), $secondTimestamp);
        }
 
        /**
-        * Tests whether dayList is correctly calculated for a range of month days
-        *
         * @test
         */
-       public function dayPartUsesRangesWithLists() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * 2-4,10 * *');
-               $expectedResult = array(
-                       '0' => 2,
-                       '1' => 3,
-                       '2' => 4,
-                       '3' => 10,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
+       public function calculateNextValueDeterminesCorrectNextTimestampOnChangeToSummertime() {
+               $backupTimezone = date_default_timezone_get();
+               date_default_timezone_set('Europe/Berlin');
+               $instance = new tx_scheduler_CronCmd('* 3 28 mar *', self::TIMESTAMP);
+               $instance->calculateNextValue();
+               date_default_timezone_set($backupTimezone);
+               $this->assertSame($instance->getTimestamp(), 1269741600);
        }
 
        /**
-        * Tests whether dayList is correctly calculated for stops of month days
-        *
         * @test
+        * @expectedException RuntimeException
         */
-       public function dayPartUsesStepValues() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * */14 * *');
-               $expectedResult = array(
-                       '0' => 14,
-                       '1' => 28,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
+       public function calculateNextValueThrowsExceptionWithImpossibleCronCommand() {
+               $instance = new tx_scheduler_CronCmd('* * 31 apr *', self::TIMESTAMP);
+               $instance->calculateNextValue();
        }
 
        /**
-        * Tests whether dayList is correctly calculated for stops of month days combined with ranges and lists
-        *
         * @test
         */
-       public function dayPartUsesListsWithRangesAndSteps() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * 2,4-6/2,*/14 * *');
-               $expectedResult = array(
-                       '0' => 2,
-                       '1' => 4,
-                       '2' => 6,
-                       '3' => 14,
-                       '4' => 28,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
+       public function getTimestampReturnsInteger() {
+               $instance = new tx_scheduler_CronCmd('* * * * *');
+               $this->assertType('integer', $instance->getTimestamp());
        }
 
        /**
-        * Tests whether dayList is correctly calculated for a single day of week
-        *
         * @test
         */
-       public function weekdayPartUsesSingleDay() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * * * 1', self::TIMESTAMP);
-               $expectedResult = array(
-                       '0' => 4,
-                       '1' => 11,
-                       '2' => 18,
-                       '3' => 25,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
-       }
-
-       /**
-        * @test
-        */
-       public function weekdayPartCorrectlyParsesZeroAsSunday() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '0 0 * * 0', self::TIMESTAMP);
-               $expectedResult = array(
-                       '0' => 3,
-                       '1' => 10,
-                       '2' => 17,
-                       '3' => 24,
-                       '4' => 31,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
-       }
-
-       /**
-        * @test
-        */
-       public function weekdayPartCorrectlyParsesSevenAsSunday() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '0 0 * * 7', self::TIMESTAMP);
-               $expectedResult = array(
-                       '0' => 3,
-                       '1' => 10,
-                       '2' => 17,
-                       '3' => 24,
-                       '4' => 31,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
-       }
-
-       /**
-        * Tests whether dayList is correctly calculated for a combination of day of month and day of weeks
-        *
-        * @test
-        */
-       public function dayListUsesListOfDayOfMonthWithSingleDayOfWeek() {
-               $cronCmdInstance = t3lib_div::makeInstance('tx_scheduler_cronCmd', '* * 1,2 * 1', self::TIMESTAMP);
-               $expectedResult = array(
-                       '0' => 1,
-                       '1' => 2,
-                       '2' => 4,
-                       '3' => 11,
-                       '4' => 18,
-                       '5' => 25,
-               );
-               $actualResult = $cronCmdInstance->valid_values;
-               $this->assertEquals($expectedResult, $actualResult[2]);
+       public function getCronCommandSectionsReturnsArray() {
+               $instance = new tx_scheduler_CronCmd('* * * * *');
+               $this->assertType('array', $instance->getCronCommandSections());
        }
 }
-?>
\ No newline at end of file
+?>
diff --git a/typo3/sysext/scheduler/tests/tx_scheduler_croncmd_normalizeTest.php b/typo3/sysext/scheduler/tests/tx_scheduler_croncmd_normalizeTest.php
new file mode 100644 (file)
index 0000000..ad9513d
--- /dev/null
@@ -0,0 +1,568 @@
+<?php
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) 2010 Christian Kuhn <lolli@schwarzbu.ch>
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+/**
+ * Test case for class "tx_scheduler_CronCmd_Normalize"
+ *
+ * @author Christian Kuhn <lolli@schwarzbu.ch>
+ *
+ * @package TYPO3
+ * @subpackage tx_scheduler
+ */
+class tx_scheduler_CronCmd_NormalizeTest extends tx_phpunit_testcase {
+
+       /**
+        * @return array
+        */
+       public static function normalizeValidDataProvider() {
+               return array(
+                       '@weekly' => array('@weekly', '0 0 * * 7'),
+                       ' @weekly ' => array(' @weekly ', '0 0 * * 7'),
+                       '* * * * *' => array('* * * * *', '* * * * *'),
+                       '30 4 1,15 * 5' => array('30 4 1,15 * 5', '30 4 1,15 * 5'),
+                       '5 0 * * *' => array('5 0 * * *', '5 0 * * *'),
+                       '15 14 1 * *' => array('15 14 1 * *', '15 14 1 * *'),
+                       '0 22 * * 1-5' => array('0 22 * * 1-5', '0 22 * * 1,2,3,4,5'),
+                       '23 0-23/2 * * *' => array('23 0-23/2 * * *', '23 0,2,4,6,8,10,12,14,16,18,20,22 * * *'),
+                       '5 4 * * sun' => array('5 4 * * sun', '5 4 * * 7'),
+                       '0-3/2,7 0,4 20-22, feb,mar-jun/2,7 1-3,sun' => array('0-3/2,7 0,4 20-22 feb,mar-jun/2,7 1-3,sun', '0,2,7 0,4 20,21,22 2,3,5,7 1,2,3,7'),
+                       '0-20/10 * * * *' => array('0-20/10 * * * *', '0,10,20 * * * *'),
+                       '* * 2 * *' => array('* * 2 * *', '* * 2 * *'),
+                       '* * 2,7 * *' => array('* * 2,7 * *', '* * 2,7 * *'),
+                       '* * 2-4,10 * *' => array('* * 2-4,10 * *', '* * 2,3,4,10 * *'),
+                       '* * */14 * *' => array('* * */14 * *', '* * 1,15,29 * *'),
+                       '* * 2,4-6/2,*/14 * *' => array('* * 2,4-6/2,*/14 * *', '* * 1,2,4,6,15,29 * *'),
+                       '* * * * 1' => array('* * * * 1', '* * * * 1'),
+                       '0 0 * * 0' => array('0 0 * * 0', '0 0 * * 7'),
+                       '0 0 * * 7' => array('0 0 * * 7', '0 0 * * 7'),
+                       '* * 1,2 * 1' => array('* * 1,2 * 1', '* * 1,2 * 1'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider normalizeValidDataProvider
+        */
+       public function normalizeConvertsCronCommand($expression, $expected) {
+               $result = tx_scheduler_CronCmd_Normalize::normalize($expression);
+               $this->assertEquals($expected, $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function validSpecialKeywordsDataProvider() {
+               return array(
+                       '@yearly' => array('@yearly', '0 0 1 1 *'),
+                       '@annually' => array('@annually', '0 0 1 1 *'),
+                       '@monthly' => array('@monthly', '0 0 1 * *'),
+                       '@weekly' => array('@weekly', '0 0 * * 0'),
+                       '@daily' => array('@daily', '0 0 * * *'),
+                       '@midnight' => array('@midnight', '0 0 * * *'),
+                       '@hourly' => array('@hourly', '0 * * * *'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider validSpecialKeywordsDataProvider
+        */
+       public function convertKeywordsToCronCommandConvertsValidKeywords($keyword, $exptedCronCommand) {
+               $result = tx_scheduler_CronCmd_Normalize::convertKeywordsToCronCommand($keyword);
+               $this->assertEquals($exptedCronCommand, $result);
+       }
+
+       /**
+        * @test
+        */
+       public function convertKeywordsToCronCommandReturnsUnchangedCommandIfKeywordWasNotFound() {
+               $invalidKeyword = 'foo';
+               $result = tx_scheduler_CronCmd_Normalize::convertKeywordsToCronCommand($invalidKeyword);
+               $this->assertEquals($invalidKeyword, $result);
+       }
+
+       /**
+        * @return array
+        */
+       public function normalizeFieldsValidDataProvider() {
+               return array(
+                       '1-2 * * * *' => array('1-2 * * * *', '1,2 * * * *'),
+                       '* 1-2 * * *' => array('* 1-2 * * *', '* 1,2 * * *'),
+                       '* * 1-2 * *' => array('* * 1-2 * *', '* * 1,2 * *'),
+                       '* * * 1-2 *' => array('* * * 1-2 *', '* * * 1,2 *'),
+                       '* * * * 1-2' => array('* * * * 1-2', '* * * * 1,2'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider normalizeFieldsValidDataProvider
+        */
+       public function normalizeFieldsConvertsField($expression, $expected) {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeFields($expression);
+               $this->assertEquals($expected, $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function normalizeMonthAndWeekdayFieldValidDataProvider() {
+               return array(
+                       '*' => array('*', TRUE, '*'),
+                       'string 1' => array('1', TRUE, '1'),
+                       'jan' => array('jan', TRUE, '1'),
+                       'feb/2' => array('feb/2', TRUE, '2'),
+                       'jan-feb/2' => array('jan-feb/2', TRUE, '1'),
+                       '1-2' => array('1-2', TRUE, '1,2'),
+                       '1-3/2,feb,may,6' => array('1-3/2,feb,may,6', TRUE, '1,2,3,5,6'),
+                       '*/4' => array('*/4', TRUE, '1,5,9'),
+                       '*' => array('*', FALSE, '*'),
+                       'string 1' => array('1', FALSE, '1'),
+                       'fri' => array('fri', FALSE, '5'),
+                       'sun' => array('sun', FALSE, '7'),
+                       'string 0 for sunday' => array('0', FALSE, '7'),
+                       '0,1' => array('0,1', FALSE, '1,7'),
+                       '*/3' => array('*/3', FALSE, '1,4,7'),
+                       'tue/2' => array('tue/2', FALSE, '2'),
+                       '1-2' => array('1-2', FALSE, '1,2'),
+                       'tue-fri/2' => array('tue-fri/2', FALSE, '2,4'),
+                       '1-3/2,tue,fri,6' => array('1-3/2,tue,fri,6', FALSE, '1,2,3,5,6'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider normalizeMonthAndWeekdayFieldValidDataProvider
+        */
+       public function normalizeMonthAndWeekdayFieldReturnsNormalizedListForValidExpression($expression, $isMonthField, $expected) {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeMonthAndWeekdayField($expression, $isMonthField);
+               $this->assertSame($expected, $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function normalizeMonthAndWeekdayFieldInvalidDataProvider() {
+               return array(
+                       'mon' => array('mon', TRUE),
+                       '1-2/mon' => array('1-2/mon', TRUE),
+                       '0,1' => array('0,1', TRUE),
+                       'feb' => array('feb', FALSE),
+                       '1-2/feb' => array('1-2/feb', FALSE),
+                       '0-fri/2,7' => array('0-fri/2,7', FALSE, '2,4,7'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider normalizeMonthAndWeekdayFieldInvalidDataProvider
+        * @expectedException InvalidArgumentException
+        */
+       public function normalizeMonthAndWeekdayFieldThrowsExceptionForInvalidExpression($expression, $isMonthField) {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeMonthAndWeekdayField($expression, $isMonthField);
+       }
+
+       /**
+        * @return array
+        */
+       public static function normalizeIntegerFieldValidDataProvider() {
+               return array(
+                       '*' => array('*', '*'),
+                       'string 2' => array('2', '2'),
+                       'integer 3' => array(3, '3'),
+                       'list of values' => array('1,2,3', '1,2,3'),
+                       'unsorted list of values' => array('3,1,5', '1,3,5'),
+                       'duplicate values' => array('0-2/2,2', '0,2'),
+                       'additional field between steps' => array('1-3/2,2', '1,2,3'),
+                       '2-4' => array('2-4', '2,3,4'),
+                       'simple step 4/4' => array('4/4', '4'),
+                       'step 2-7/5' => array('2-7/5', '2,7'),
+                       'steps 4-12/4' => array('4-12/4', '4,8,12'),
+                       '0-59/20' => array('0-59/20', '0,20,40'),
+                       '*/20' => array('*/20', '0,20,40'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider normalizeIntegerFieldValidDataProvider
+        */
+       public function normalizeIntegerFieldReturnsNormalizedListForValidExpression($expression, $expected) {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeIntegerField($expression);
+               $this->assertSame($expected, $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function normalizeIntegerFieldInvalidDataProvider() {
+               return array(
+                       'string foo' => array('foo', 0, 59),
+                       'empty string' => array('', 0, 59),
+                       '4-3' => array('4-3', 0, 59),
+                       '/2' => array('/2', 0, 59),
+                       '/' => array('/', 0, 59),
+                       'string foo' => array('foo', 0, 59),
+                       'left bound too low' => array('2-4', 3, 4),
+                       'right bound too high' => array('2-4', 2, 3),
+                       'left and right bound' => array('2-5', 2, 4),
+                       'element in list is lower than allowed' => array('2,1,4', 2, 4),
+                       'element in list is higher than allowed' => array('2,5,4', 1, 4),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider normalizeIntegerFieldInvalidDataProvider
+        * @expectedException InvalidArgumentException
+        */
+       public function normalizeIntegerFieldThrowsExceptionForInvalidExpressions($expression, $lowerBound, $upperBound) {
+               tx_scheduler_CronCmd_Normalize::normalizeIntegerField($expression, $lowerBound, $upperBound);
+       }
+
+       /**
+        * @test
+        */
+       public function splitFieldsReturnsIntegerArrayWithFieldsSplitByWhitespace() {
+               $result = tx_scheduler_CronCmd_Normalize::splitFields('12,13 * 1-12/2,14 jan fri');
+               $expectedResult = array(
+                       0 => '12,13',
+                       1 => '*',
+                       2 => '1-12/2,14',
+                       3 => 'jan',
+                       4 => 'fri',
+               );
+               $this->assertSame($expectedResult, $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function invalidCronCommandFieldsDataProvider() {
+               return array(
+                       'empty string' => array(''),
+                       'foo' => array('foo'),
+                       'integer 4' => array(4),
+                       'four fields' => array('* * * *'),
+                       'six fields' => array('* * * * * *'),
+               );
+       }
+
+       /**
+        * @test
+        * @expectedException InvalidArgumentException
+        * @dataProvider invalidCronCommandFieldsDataProvider
+        */
+       public function splitFieldsThrowsExceptionIfCronCommandDoesNotContainFiveFields($cronCommand) {
+               tx_scheduler_CronCmd_Normalize::splitFields($cronCommand);
+       }
+
+       /**
+        * @return array
+        */
+       public static function validRangeDataProvider() {
+               return array(
+                       'single value' => array('3', '3'),
+                       'integer 3' => array(3, '3'),
+                       '0-0' => array('0-0', '0'),
+                       '4-4' => array('4-4', '4'),
+                       '0-3' => array('0-3', '0,1,2,3'),
+                       '4-5' => array('4-5', '4,5'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider validRangeDataProvider
+        */
+       public function convertRangeToListOfValuesReturnsCorrectListForValidRanges($range, $expected) {
+               $result = tx_scheduler_CronCmd_Normalize::convertRangeToListOfValues($range);
+               $this->assertSame($expected, $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function invalidRangeDataProvider() {
+               return array(
+                       'empty string' => array(''),
+                       'string' => array('foo'),
+                       'single dash' => array('-'),
+                       'left part is string' => array('foo-5'),
+                       'right part is string' => array('5-foo'),
+                       'range of strings' => array('foo-bar'),
+                       'string five minus' => array('5-'),
+                       'string minus five' => array('-5'),
+                       'more than one dash' => array('2-3-4'),
+                       'left part bigger than right part' => array('6-3'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider invalidRangeDataProvider
+        * @expectedException InvalidArgumentException
+        */
+       public function convertRangeToListOfValuesThrowsExceptionForInvalidRanges($range) {
+               tx_scheduler_CronCmd_Normalize::convertRangeToListOfValues($range);
+       }
+
+       /**
+        * @return array
+        */
+       public static function validStepsDataProvider() {
+               return array(
+                       '2/2' => array('2/2', '2'),
+                       '2,3,4/2' => array('2,3,4/2', '2,4'),
+                       '1,2,3,4,5,6,7/3' => array('1,2,3,4,5,6,7/3', '1,4,7'),
+                       '0,1,2,3,4,5,6/3' => array('0,1,2,3,4,5,6/3', '0,3,6'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider validStepsDataProvider
+        */
+       public function reduceListOfValuesByStepValueReturnsCorrectListOfValues($stepExpression, $expected) {
+               $result = tx_scheduler_CronCmd_Normalize::reduceListOfValuesByStepValue($stepExpression);
+               $this->assertSame($expected, $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function invalidStepsDataProvider() {
+               return array(
+                       'empty string' => array(''),
+                       'slash only' => array('/'),
+                       'left part empty' => array('/2'),
+                       'right part empty' => array('2/'),
+                       'multiples slashes' => array('1/2/3'),
+                       '2-2' => array('2-2'),
+                       '2.3/2' => array('2.3/2'),
+                       '2,3,4/2.3' => array('2,3,4/2.3'),
+                       '2,3,4/2,3' => array('2,3,4/2,3'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider invalidStepsDataProvider
+        * @expectedException InvalidArgumentException
+        */
+       public function reduceListOfValuesByStepValueThrowsExceptionForInvalidStepExpressions($stepExpression) {
+               $result = tx_scheduler_CronCmd_Normalize::reduceListOfValuesByStepValue($stepExpression);
+       }
+
+       /**
+        * @test
+        */
+       public function normalizeMonthAndWeekdayNormalizesAMonth() {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeMonthAndWeekday('feb', TRUE);
+               $this->assertSame('2', $result);
+       }
+
+       /**
+        * @test
+        */
+       public function normalizeMonthAndWeekdayNormalizesAWeekday() {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeMonthAndWeekday('fri', FALSE);
+               $this->assertSame('5', $result);
+       }
+
+       /**
+        * @test
+        */
+       public function normalizeMonthAndWeekdayLeavesValueUnchanged() {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeMonthAndWeekday('2');
+               $this->assertSame('2', $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function validMonthNamesDataProvider() {
+               return array(
+                       'jan' => array('jan', 1),
+                       'feb' => array('feb', 2),
+                       'MaR' => array('MaR', 3),
+                       'aPr' => array('aPr', 4),
+                       'MAY' => array('MAY', 5),
+                       'jun' => array('jun', 6),
+                       'jul' => array('jul', 7),
+                       'aug' => array('aug', 8),
+                       'sep' => array('sep', 9),
+                       'September' => array('September', 9),
+                       'oct' => array('oct', 10),
+                       'nov' => array('nov', 11),
+                       'dec' => array('dec', 12),
+                       'string 7' => array('7', 7),
+                       'integer 7' => array(7, 7),
+                       'string 07' => array('07', 7),
+                       'integer 07' => array(07, 7),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider validMonthNamesDataProvider
+        */
+       public function normalizeMonthConvertsName($monthName, $expectedInteger) {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeMonth($monthName);
+               $this->assertEquals($expectedInteger, $result);
+       }
+
+       /**
+        * @test
+        * @dataProvider validMonthNamesDataProvider
+        */
+       public function normalizeMonthReturnsInteger($monthName, $expectedInteger) {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeMonth($monthName);
+               $this->assertType('integer', $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function invalidMonthNamesDataProvider() {
+               return array(
+                       'sep-' => array('sep-'),
+                       '-September-' => array('-September-'),
+                       ',sep' => array(',sep'),
+                       ',September,' => array(',September,'),
+                       'sep/' => array('sep/'),
+                       '/sep' => array('/sep'),
+                       '/September/' => array('/September/'),
+                       'foo' => array('foo'),
+                       'Tuesday' => array('Tuesday'),
+                       'Tue' => array('Tue'),
+                       'string 0' => array('0'),
+                       'integer 0' => array(0),
+                       'string seven' => array('seven'),
+                       'string 13' => array('13'),
+                       'integer 13' => array(13),
+                       'integer 100' => array(100),
+                       'integer 2010' => array(2010),
+                       'string minus 7' => array('-7'),
+                       'negative integer 7' => array(-7),
+               );
+       }
+
+       /**
+        * @test
+        * @expectedException InvalidArgumentException
+        * @dataProvider invalidMonthNamesDataProvider
+        */
+       public function normalizeMonthThrowsExceptionForInvalidMonthRepresentation($invalidMonthName) {
+               tx_scheduler_CronCmd_Normalize::normalizeMonth($invalidMonthName);
+       }
+
+       /**
+        * @return array
+        */
+       public static function validWeekdayDataProvider() {
+               return array(
+                       'string 1' => array('1', 1),
+                       'string 2' => array('2', 2),
+                       'string 02' => array('02', 2),
+                       'integer 02' => array(02, 2),
+                       'string 3' => array('3', 3),
+                       'string 4' => array('4', 4),
+                       'string 5' => array('5', 5),
+                       'integer 5' => array(5, 5),
+                       'string 6' => array('6', 6),
+                       'string 7' => array('7', 7),
+                       'string 0' => array('0', 7),
+                       'integer 0' => array(0, 7),
+                       'mon' => array('mon', 1),
+                       'monday' => array('monday', 1),
+                       'tue' => array('tue', 2),
+                       'tuesday' => array('tuesday', 2),
+                       'WED' => array('WED', 3),
+                       'WEDnesday' => array('WEDnesday', 3),
+                       'tHu' => array('tHu', 4),
+                       'Thursday' => array('Thursday', 4),
+                       'fri' => array('fri', 5),
+                       'friday' => array('friday', 5),
+                       'sat' => array('sat', 6),
+                       'saturday' => array('saturday', 6),
+                       'sun' => array('sun', 7),
+                       'sunday' => array('sunday', 7),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider validWeekdayDataProvider
+        */
+       public function normalizeWeekdayConvertsName($weekday, $expectedInteger) {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeWeekday($weekday);
+               $this->assertEquals($expectedInteger, $result);
+       }
+
+       /**
+        * @test
+        * @dataProvider validWeekdayDataProvider
+        */
+       public function normalizeWeekdayReturnsInteger($weekday, $expectedInteger) {
+               $result = tx_scheduler_CronCmd_Normalize::normalizeWeekday($weekday);
+               $this->assertType('integer', $result);
+       }
+
+       /**
+        * @return array
+        */
+       public static function invalidWeekdayDataProvider() {
+               return array(
+                       '-fri' => array('-fri'),
+                       'fri-' => array('fri-'),
+                       '-friday-' => array('-friday-'),
+                       '/fri' => array('/fri'),
+                       'fri/' => array('fri/'),
+                       '/friday/' => array('/friday/'),
+                       ',fri' => array(',fri'),
+                       ',friday,' => array(',friday,'),
+                       'string minus 1' => array('-1'),
+                       'integer -1' => array(-1),
+                       'string seven' => array('seven'),
+                       'string 8' => array('8'),
+                       'string 8' => array('8'),
+                       'string 29' => array('29'),
+                       'string 2010' => array('2010'),
+                       'Jan' => array('Jan'),
+                       'January' => array('January'),
+                       'MARCH' => array('MARCH'),
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider invalidWeekdayDataProvider
+        * @expectedException InvalidArgumentException
+        */
+       public function normalizeWeekdayThrowsExceptionForInvalidWeekdayRepresentation($weekday) {
+               tx_scheduler_CronCmd_Normalize::normalizeWeekday($weekday);
+       }
+}
+?>