Direkt zum Inhalt | Direkt zur Navigation

Benutzerspezifische Werkzeuge
Anmelden
Home Produkte

SaaS

Software as a Service

Hosting

schon ab €14,00!

Software-Entwicklung

Wir programmieren alles, was wir hosten.

Lösungen

Web & E-Commerce

Kollaboration

Hosting & Skalierbarkeit

Support Open Source

Python

PHP

Weitere

Über uns

Kontakt

Wir

Sie sind hier: Startseite Open Source PHP Cron.php5 Cron.php5 Sourcecode

Cron.php5 Sourcecode

<?php
/*
 * File: Cron.php
 *
 * Copyright (c) 2010 by Daniel Kraft <dk@d9t.de>
 *
 * GNU General Public License (GPL)
 * 
 * This program 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.
 *
 * This program 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 *
 * If you find bugs, please feel free to report them by mail or on
 * irc: freenode://#d9t
 *
 */

class CronException extends Exception {
}

class Cron {
    /*
     * Class to parse a single crontab entry and tell, if it matches a given timestamp.
     * PLEASE NOTE: It requires a timestamp object - not just an integer.
     */

    var $cron_entry;
    var $timestampObj;

    var $minuteField;
    var $hourField;
    var $domField;
    var $monthField;
    var $dowField;

    function __construct($cron_entry, $timestampObj=null) {
        /*
         * constructor method
         */

        $this->cron_entry = $cron_entry;
        if ($timestampObj == null) $timestampObj = new Timestamp(time());
        $this->timestampObj = $timestampObj;
        $this->loadFields();
    }

    function __toString() {
        return sprintf("<Cron Fields: (%s %s %s %s %s)>", $this->minuteField, $this->hourField, $this->domField, $this->monthField, $this->dowField);
    }

    function splitFields($fields) {
        /*
         * Split fields by tab+ or space+ with limit 5 (like cron wants it)
         */
        return preg_split('#(\t+|\s+)#', $fields,5);
    }

    function loadFields() {
        $fields = $this->splitFields($this->cron_entry);
        $this->minuteField = new MinuteField($fields[0]);
        $this->hourField = new HourField($fields[1]);
        $this->domField = new DomField($fields[2]);
        $this->monthField = new MonthField($fields[3]);
        $this->dowField = new DowField($fields[4]);
    }

    function hit() {
        /*
         * Checks, if $this->timestampObj is a hit for the cron_entry.
         */
        return $this->minuteField->hit($this->timestampObj)
            && $this->hourField->hit($this->timestampObj)
            && $this->domField->hit($this->timestampObj)
            && $this->monthField->hit($this->timestampObj)
            && $this->dowField->hit($this->timestampObj);
    }

    function jumpToTime($timestampObj) {
        /*
         * This accepts a timestampObj and sets the current entry to this time.
         * Do this if you want to test a specific time for a hit.
         */
        $this->timestampObj = $timestampObj;
    }

    function moveInTime($deltaTimestampObj) {
        $deltaTimestampObj->applyToTimestamp($this->timestampObj);
    }

    function _moveInTime($seconds) {
        $this->timestampObj->_moveInTime($seconds);
    }

    function getTimestamp() {
        return $this->timestampObj;
    }

    function debug() {
        /*
         * call debug on each field.
         */
        print "\nChecking Date: ".$this->timestampObj->rfc2822()."\n========================================\n\n";

        $this->minuteField->debug($this->timestampObj);
        $this->hourField->debug($this->timestampObj);
        $this->domField->debug($this->timestampObj);
        $this->monthField->debug($this->timestampObj);
        $this->dowField->debug($this->timestampObj);

        print "\n----\n";
        print "Sum hit: ".$this->hit()."\n";
    }
}

class Searcher {
    /*
     * This is a base class for all Searcher Classes.
     * Searcher Classes are a implementation of a searching algorithm
     * which tries to find the closest previous or next hit for a
     * Cron.
     *
     * The methods must return a Timestamp object.
     */

    var $cron;

    function __construct($cron) {
        $this->cron= $cron;
    }

    function previous() {
        // dummy implementation
        return $this->cron->getTimestamp();
    }

    function next() {
        // dummy implementation
        return $this->cron->getTimestamp();
    }
}

class DumbIterateSearcher extends Searcher {
    /*
     * This searcher simply checks from now on every minute
     * forward/backwards in time for a hit and returns the closest match.
     *
     * Stupid, simple, but correct.
     * Use this only as reference.
     */

    function __construct($cron) {
        parent::__construct($cron);
    }

    function _iterate($deltaTimestampObj) {
        $maxIterations = 4207550;
        $currentIteration = 0;
        while (!$this->cron->hit() && $maxIterations>$currentIteration) {
            $deltaTimestampObj->applyToTimestamp($this->cron->timestampObj);
            $currentIteration++;
        }
        $this->cron->timestampObj->zeroSeconds();
        return $this->cron->getTimestamp();
    }

    function previous() {
        $deltaTimestampObj = new DeltaTimestampObj();
        $deltaTimestampObj->minutes(-1);
        $timestampObj = $this->_iterate($deltaTimestampObj);
        return $timestampObj;
    }

    function next() {
        $deltaTimestampObj = new DeltaTimestampObj();
        $deltaTimestampObj->minutes(1);
        $timestampObj = $this->_iterate($deltaTimestampObj);
        return $timestampObj;
    }
}


class EliminateSearcher extends Searcher {
    /*
     * This Searcher checks for each field, if it hits.
     * If not, it asks this field for the amount of time we could
     * add/substract until it possibly would hit the next time.
     *
     * This looks very performant. Even in cases like this:
     * 0 0 29 2 3
     */

    function __construct($cron) {
        parent::__construct($cron);
    }

    function _iterate() {
        /*
         * Idea: Maybe this could perform a little better, if we tried the
         * algo always from start when successfully applying a delta to
         * anything except month. Because a delta so big until it hits the
         * next superior boundery is possible.
         * But this becomes academical here. My worst cpu-time for this
         * was below 0.25s when hopping over 20 years.
         */
        for ($i=0; $i<100000; $i++) {
            while (!$this->cron->monthField->hit($this->cron->timestampObj))
                $this->cron->monthField->timespan->applyToTimestamp($this->cron->timestampObj);
            while (!$this->cron->domField->hit($this->cron->timestampObj))
                $this->cron->domField->timespan->applyToTimestamp($this->cron->timestampObj);
            while (!$this->cron->dowField->hit($this->cron->timestampObj))
                $this->cron->dowField->timespan->applyToTimestamp($this->cron->timestampObj);
            while (!$this->cron->hourField->hit($this->cron->timestampObj))
                $this->cron->hourField->timespan->applyToTimestamp($this->cron->timestampObj);
            while (!$this->cron->minuteField->hit($this->cron->timestampObj))
                $this->cron->minuteField->timespan->applyToTimestamp($this->cron->timestampObj);
            if ($this->cron->hit()) return;
        }
        throw new CronException("No hit was found. Max iterations reached.");
    }

    function previous() {
        $this->cron->minuteField->timespan->negative();
        $this->cron->hourField->timespan->negative();
        $this->cron->domField->timespan->negative();
        $this->cron->monthField->timespan->negative();
        $this->cron->dowField->timespan->negative();

        $this->_iterate();

        // wipe the seconds to make it reproducable.
        $this->cron->timestampObj->zeroSeconds();
        return $this->cron->timestampObj;
    }

    function next() {
        $this->cron->minuteField->timespan->positive();
        $this->cron->hourField->timespan->positive();
        $this->cron->domField->timespan->positive();
        $this->cron->monthField->timespan->positive();
        $this->cron->dowField->timespan->positive();

        $this->_iterate();

        // wipe the seconds to make it reproducable.
        $this->cron->timestampObj->zeroSeconds();
        return $this->cron->timestampObj;
    }
}

class Timestamp {
    /*
     * This class is mainly a wrapper for a integer-timestamp with
     * some methods using the date() function. It's more convenient
     * and nicer to read if we use that class.
     */

    var $stamp;

    function __construct($stamp) {
        /*
         * recieves a integer timestamp in unix-format (seconds since 1970 or so).
         */
        $this->stamp = $stamp;
    }

    function __toString() {
        return $this->rfc2822();
    }

    function copy() {
        return new Timestamp($this->stamp);
    }

    function _date($str) {
        return intval(date($str, $this->stamp));
    }

    function day() {
        return $this->_date('j');
    }

    function hour() {
        return $this->_date('G');
    }

    function minute() {
        return $this->_date('i');
    }

    function second() {
        return $this->_date('s');
    }

    function dom() {
        return $this->_date('j');
    }

    function month() {
        return $this->_date('n');
    }

    function dow() {
        return $this->_date('w');
    }

    function daysInMonth() {
        return $this->_date('t');
    }

    function zeroSeconds() {
        /*
         * removes the second-information of the timestamp.
         */
        $this->stamp -= $this->second();
    }

    function _moveInTime($seconds) {
        $this->stamp += $seconds;
    }

    function rfc2822() {
        return date('r', $this->stamp);
    }

    function sqlDatetime() {
        return date('Y-m-d H:i:s', $this->stamp);
    }

    function diffToGmt() {
        /*
         * Returns the number of minutes of delta to gmt.
         */
        $o = date('O', $this->stamp);
        $sign = substr($o,0,1);
        $hours = intval($sign.substr($o,1,2));
        $minutes = intval($sign.substr($o,3,2));
        $diff = $hours*60+$minutes;
        return $diff;
    }
}

class DeltaTimestampObj {
    /*
     * This class represents a delta timstamp which can be positive
     * or negative.
     * You can set the delta convenient with several methods and after
     * you're done, apply the change to your favourite Timestamp object.
     *
     * This method returns the latest possible value for negative
     * calls and the earliest possible value for positive calls.
     */

    var $delta = array('seconds'=>0, 'minutes'=>0, 'hours'=>0, 'days'=>0, 'weeks'=>0, 'months'=>0);
    var $applyTimeZone = true;

    function __construct($months=0, $days=0, $hours=0, $minutes=0, $seconds=0) {
        $this->delta["months"] = $months;
        $this->delta["days"] = $days;
        $this->delta["hours"] = $hours;
        $this->delta["minutes"] = $minutes;
        $this->delta["seconds"] = $seconds;
    }

    function __toString() {
        return sprintf("%s months %s days %s hours %s minutes %s seconds", $this->delta["months"], $this->delta["days"], $this->delta["hours"], $this->delta["minutes"], $this->delta["seconds"]);
    }

    function negative() {
        /*
         * changes the sign of all values to negative.
         */
        foreach (array_keys($this->delta) as $index)
            if ($this->delta[$index]>0) $this->delta[$index] = -$this->delta[$index];
    }

    function positive() {
        /*
         * changes the sign of all values to positive.
         */
        foreach (array_keys($this->delta) as $index)
            if ($this->delta[$index]<0) $this->delta[$index] = -$this->delta[$index];
    }

    function applyToTimestamp(&$timestampObj) {
        /*
         * apply the changes in the following order:
         * months, days, hours, minutes, seconds
         *
         * This is no generic method. It's optimized for cron as it
         * returns always the first possible second or the last possible
         * second for a question and not the correct delta to a given
         * timestamp.
         */

        /*
        printf ("Applying: %6dm %6dd %6dh %6dm %6ds\n",
            $this->delta["months"],
            $this->delta["days"],
            $this->delta["hours"],
            $this->delta["minutes"],
            $this->delta["seconds"]);
        */

        if ($this->delta["months"] < 0) {
            for ($i=0; $i>$this->delta["months"]; $i--) {
                $days = -$timestampObj->day();
                $dt = new DeltaTimestampObj(0,$days,0,0,0);
                $dt->applyToTimestamp($timestampObj);
            }
        }
        if ($this->delta["months"] > 0) {
            for ($i=0; $i<$this->delta["months"]; $i++) {
                $days = $timestampObj->daysInMonth() - $timestampObj->day() + 1;
                $dt = new DeltaTimestampObj(0,$days,0,0,0);
                $dt->applyToTimestamp($timestampObj);
            }
        }

        if ($this->delta["days"] < 0) {
            $hours = $this->delta["days"]*24 + 23 - $timestampObj->hour();
            $dt = new DeltaTimestampObj(0,0,$hours,0,0);
            $dt->applyToTimestamp($timestampObj);
        }
        if ($this->delta["days"] > 0) {
            $hours = $this->delta["days"]*24 - $timestampObj->hour();
            $dt = new DeltaTimestampObj(0,0,$hours,0,0);
            $dt->applyToTimestamp($timestampObj);
        }

        if ($this->delta["hours"] < 0) {
            $minutes = $this->delta["hours"]*60 + 59 - $timestampObj->minute();
            $dt = new DeltaTimestampObj(0,0,0,$minutes,0);
            $dt->applyToTimestamp($timestampObj);
        }
        if ($this->delta["hours"] > 0) {
            $minutes = $this->delta["hours"]*60 - $timestampObj->minute();
            $dt = new DeltaTimestampObj(0,0,0,$minutes,0);
            $dt->applyToTimestamp($timestampObj);
        }

        if ($this->delta["minutes"] < 0) {
            $diffToGmtBefore = $timestampObj->diffToGmt();
            $seconds = $this->delta["minutes"]*60 + 59 - $timestampObj->second();
            $timestampObj->_moveInTime($seconds);
            $diffToGmtAfter = $timestampObj->diffToGmt();
            $this->applyTimeZone($timestampObj, $diffToGmtBefore, $diffToGmtAfter);
        }
        if ($this->delta["minutes"] > 0) {
            $diffToGmtBefore = $timestampObj->diffToGmt();
            $seconds = $this->delta["minutes"]*60 - $timestampObj->second();
            $timestampObj->_moveInTime($seconds);
            $diffToGmtAfter = $timestampObj->diffToGmt();
            $this->applyTimeZone($timestampObj, $diffToGmtBefore, $diffToGmtAfter);
        }

        $timestampObj->_moveInTime($this->delta["seconds"]);
    }

    function applyTimeZone(&$timestampObj, $diffToGmtBefore, $diffToGmtAfter) {
        // "correct" timezone changes (like summer/wintertime)
        if (!$this->applyTimeZone) return;
        $deltaMinutes = $diffToGmtBefore - $diffToGmtAfter;
        if ($deltaMinutes)
            $timestampObj->_moveInTime($deltaMinutes*60);
    }

    function seconds($val) {
        $this->delta["seconds"] += $val;
    }

    function minutes($val) {
        $this->delta["minutes"] += $val;
    }

    function hours($val) {
        $this->delta["hours"] += $val;
    }

    function days($val) {
        $this->delta["days"] += $val;
    }

    function weeks($val) {
        $this->delta["weeks"] += $val;
    }

    function months($val) {
        $this->delta["months"] += $val;
    }
}

class CronField {
    /*
     * Class to parse a cron field. They can contain ranges
     * or comma seperated lists of numbers and ranges.
     * From the crontab (5) manpage:
     * 
     * A field may be an asterisk (*), which always stands for ‘‘first-last’’.
     * Ranges  of  numbers are allowed.  Ranges are two numbers separated with
     * a hyphen.  The specified range is inclusive.  For example, 8-11 for an ‘‘hours’’
     * entry specifies execution at hours 8, 9, 10 and 11.
     * 
     * Lists are allowed.  A list is a set of numbers (or ranges) separated by commas.
     * Examples: ‘‘1,2,5,9’’, ‘‘0-4,8-12’’.
     * 
     * Step values can be used in conjunction with ranges.  Following a range with
     * ‘‘/<number>’’ specifies skips of  the  number’s  value  through  the  range.
     * For example, ‘‘0-23/2’’  can  be  used  in  the hours field to specify command
     * execution every other hour (the alternative in the V7 standard is ‘‘0,2,4,6,8,10,12,14,16,18,20,22’’).
     * Steps are also permitted after an asterisk, so if you want to say ‘‘every two hours’’, just use ‘‘* /2’’
     * Names can also be used for the ‘‘month’’ and ‘‘day of week’’ fields.  Use the
     * first three letters of the particular day or month (case doesn’t matter).  Ranges or lists
     * of names are not allowed.
     */

    var $field = "";
    var $activeValues = array();
    var $timespan;              # contains a DeltaTimestampObj which knows how long one iteration of this field is.

    function __construct($field, $possibleValues, $timespan) {
        /*
         * Constructor.
         * Initialize all arrays as needed.
         */

        // Just the contents of the field as string.
        $this->field = $field;

        /*
         * create a list with key is value and value is true or false - depending
         * on if it's active or not.
         */
        foreach ($possibleValues as $possibleValue) {
            $this->activeValues[$possibleValue] = false;
        }

        $this->parseField();

        $this->timespan = $timespan;
    }

    function __toString() {
        return sprintf("<%s value %s; timespan (%s)>", get_class($this), $this->field, $this->timespan);
    }

    function activate($value) {
        /*
         * activates a key in $activeValues
         */
        $value = $this->correctValue($value);
        if (isset($this->activeValues[$value]))
            $this->activeValues[$value] = true;
        else
            throw new CronException('Illegal value: '.$value);
    }

    function correctValue($val) {
        /*
         * This is a callback method that should be overwritten if you
         * need to support correction of entries like if you need
         * 0 or 7 for sunday, or if you want to use names.
         * Simply react on $val and return any valid int.
         */

        // Default: Do nothing
        return $val;
    }

    function activateRange($range) {
        /*
         * Activates a range. A range is a string which could contain following values:
         * 1-3
         * 1-4/2
         * *
         * * /3 (ignore the whitspace)
         * and also following legal values, which wrap over the bounds:
         * 5-1
         * 5-5
         * and also following illegal values.
         * 1-1/0.5
         */
        $matches = array();
        if (strpos($range, '*') === 0) {
            $tmpmatches = array();
            $valid = preg_match('#^\*/?([0-9]*)$#', $range, $tmpmatches);
            # modify it to the other kind of range:
            $values = $this->getPossibleValues();
            $matches[1] = array_shift($values);
            $matches[2] = array_pop($values);
            $matches[3] = $tmpmatches[1];
        } else {
            $valid = preg_match('#^([0-9a-zA-Z]+)-([0-9a-zA-Z]+)/?([0-9]*)$#', $range, $matches);
        }

        if (!$valid)
            throw new CronException("Invalid Range: ".$range);

        $begin = $this->correctValue($matches[1]);
        $end = $this->correctValue($matches[2]);
        $step = $matches[3] ? $matches[3] : 1;

        /*
         * Algorithm:
         * We get the list getPossibleValues twice and search for $begin and
         * remember the position.
         * Then we search for $end AFTER the position of $begin and also remember
         * the position.
         * Then we have two indices between which we only have to iterate with $step.
         */

        $values = $this->getPossibleValues();

        // special case: If step is bigger than the list is long, DON'T activate
        // anything. If you don't like this behavour, simply wipe out the following
        // line. Then the value in $begin is activated. I consider this a bug though.
        if ($step > count($values)) return;

        $doubleValues = array_merge($values, $values);
        $beginPos = array_search($begin, $doubleValues);
        $endPos = array_search($end, $doubleValues);
        if ($endPos <= $beginPos)
            $endPos += count($values);

        // iterate. And include edges.
        for ($i = $beginPos; $i <= $endPos; $i += $step) {
            $this->activate($doubleValues[$i]);
        }
    }

    function getActiveValues() {
        /*
         * Returns a list of values where active == true
         */
        $ret = array();
        foreach ($this->activeValues as $value => $active)
            if ($active) $ret[] = $value;
        sort($ret);
        return $ret;
    }

    function isActive($value) {
        /*
         * checks if a given value is active
         */
        return $this->activeValues[$value];
    }

    function getPossibleValues() {
        /*
         * Returns all possible values in a list.
         */
        $ret = array_keys($this->activeValues);
        sort($ret);
        return $ret;
    }

    function parseField() {
        /*
         * Parses the contents of the field and creates active values from it.
         * This method handles the following possibilities:
         * 
         * **range**
         * Something in the format #^([0-9]+)-([0-9]+)/?([0-9]*)$#
         *
         * **range with wildcard**
         * Something in the format #^* /?([0-9]*)$#
         *
         * **single value**
         * like 4 or Sun
         */
        $entries = explode(',',$this->field);
        foreach ($entries as $entry) {
            if (strval(intval($entry)) == $entry || (strpos($entry,"-")===false && strpos($entry,"*")===false)) {
                $this->activate($entry);
            } else {
                $this->activateRange($entry);
            }
        }
    }

    function hit($timestampObj) {
        /*
         * Checks, if the given timestamp-object is a hit for this field.
         * PLEASE NOTE: THIS IS NOT AN INTEGER!
         * returns true, if hit.
         */
        return false;
    }

    function debug($timestampObj) {
        /*
         * outputs the state of the object in a nice way
         */
        print get_class($this)."\n";
        print join(',', $this->getActiveValues());
        print "\n";
        print "Hit: ".$this->hit($timestampObj)."\n";
        print "\n";
    }

}

class MinuteField extends CronField {
    function __construct($field) {
        parent::__construct($field, range(0,59), new DeltaTimestampObj(0,0,0,1,0));
    }

    function hit($timestampObj) {
        return $this->isActive($timestampObj->minute());
    }
}

class HourField extends CronField {
    function __construct($field) {
        parent::__construct($field, range(0,23), new DeltaTimestampObj(0,0,1,0,0));
    }

    function correctValue($val) {
        if ($val == 24) return 0;
        return $val;
    }

    function hit($timestampObj) {
        return $this->isActive($timestampObj->hour());
    }
}

class DomField extends CronField {
    function __construct($field) {
        parent::__construct($field, range(0,31), new DeltaTimestampObj(0,1,0,0,0));
    }

    function hit($timestampObj) {
        return $this->isActive($timestampObj->dom());
    }
}

class MonthField extends CronField {
    function __construct($field) {
        parent::__construct($field, range(1,12), new DeltaTimestampObj(1,0,0,0,0));
    }

    function hit($timestampObj) {
        return $this->isActive($timestampObj->month());
    }
}

class DowField extends CronField {
    function __construct($field) {
        parent::__construct($field, range(0,6), new DeltaTimestampObj(0,1,0,0,0));
    }

    function correctValue($val) {
        if ($val == 7) return 0;

        $weekdays = array('sun','mon','tue','wed','thu','fri','sat');
        if ($pos = array_search(strtolower($val), $weekdays)) return $pos;

        return $val;
    }

    function hit($timestampObj) {
        return $this->isActive($timestampObj->dow());
    }
}

class ConvenientCron {
    /*
     * This wraps several methods into single, most-likely-usecases.
     */

    var $cron;
    var $searcher;
    var $error;

    function __construct($cron_entry, $timestamp=null) {
        /*
         * cron_entry is something like "30 7 0 * *"
         * timestamp is a integer unix-timestamp.
         */
        if (!$timestamp) $timestamp = time();
        $this->cron = new Cron($cron_entry, new Timestamp($timestamp));
    }

    function next() {
        /*
         * Returns a Timestamp object
         * It emulates an iterator so you can call it multiple times.
         */
        if ($this->error) return new Timestamp(0);	// sorry, w/o exception handling I don't know any better way.
        if (!$this->searcher) $this->searcher = new EliminateSearcher($this->cron);
        $to = $this->searcher->next();
        $ret_to = $to->copy();
        $this->searcher->cron->moveInTime(new DeltaTimestampObj(0,0,0,1,0));
        return $ret_to;
    }

    function previous() {
        /*
         * Returns a Timestamp object
         * It emulates an iterator so you can call it multiple times.
         */
        if ($this->error) return new Timestamp(0);
        if (!$this->searcher) $this->searcher = new EliminateSearcher($this->cron);
        $to = $this->searcher->previous();
        $ret_to = $to->copy();
        $this->searcher->cron->moveInTime(new DeltaTimestampObj(0,0,0,-1,0));
        return $ret_to;
    }
}

function test_Cron() {
    $cron = new Cron("30 9 * * 1");
    $expected = "<Cron Fields: (<MinuteField value 30; timespan (0 months 0 days 0 hours 1 minutes 0 seconds)> <HourField value 9; timespan (0 months 0 days 1 hours 0 minutes 0 seconds)> <DomField value *; timespan (0 months 1 days 0 hours 0 minutes 0 seconds)> <MonthField value *; timespan (1 months 0 days 0 hours 0 minutes 0 seconds)> <DowField value 1; timespan (0 months 1 days 0 hours 0 minutes 0 seconds)>)>";
    assert($cron == $expected);
    print ".";
}

function test_ConvenientCron() {
    $cron = new ConvenientCron("30 9 * * 1", 1276088500); # 2010-06-09 15:something
    $expected1 = "Mon, 14 Jun 2010 09:30:00 +0200";
    $expected2 = "Mon, 21 Jun 2010 09:30:00 +0200";
    $expected3 = "Mon, 28 Jun 2010 09:30:00 +0200";
    assert($cron->next()->rfc2822() == $expected1);
    assert($cron->next()->rfc2822() == $expected2);
    assert($cron->next()->rfc2822() == $expected3);
    print ".";
}


//test_Cron();
//test_ConvenientCron();

?>