<?php
// *****************************************************************************
// Copyright 2003-2011 by A J Marston <http://www.tonymarston.net>
// Amended 2011 by A J Marston to replace ereg* functions with preg* functions
// Distributed under the GNU General Public Licence
// *****************************************************************************

class date_class {

    // private variables
    var $monthalpha;            // array of 3-character month names
    var $internaldate;          // date as held in the database (yyyymmdd)
    var $externaldate;          // date as shown to the user (dd Mmm yyyy)
    var $errors;                // error messages
    var $date_format = 'dmy';   // date format - 'dmy', 'mdy' or 'ymd'

    // ****************************************************************************
    // class constructor
    // ****************************************************************************
    function __construct()
    {
        $this->monthalpha = array(1 => 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');

        if (isset($GLOBALS['date_format'])) {
            $this->date_format = $GLOBALS['date_format'];
        } // if

    } // __construct

    // ****************************************************************************
    // accessor functions
    // ****************************************************************************
    function formatDate ($dd, $mm, $ccyy)
    // convert a date into the format required by the user
    {
        $format     = strtolower($this->date_format);
        $monthalpha = $this->monthalpha;

        switch ($format) {
            case 'dmy':
                $mm     = (int)$mm;
                $output = "$dd $monthalpha[$mm] $ccyy";
                break;

            case 'mdy':
                $mm     = (int)$mm;
                $output = "$monthalpha[$mm] $dd $ccyy";
                break;

            case 'dd/mm/yyyy':
                $output = "$dd/$mm/$ccyy";
                break;

            case 'dd.mm.yyyy':
                $output = "$dd.$mm.$ccyy";
                break;

            case 'dd/mm/yy':
                $yy = substr($ccyy, 2);
                $output = "$dd/$mm/$yy";
                break;

            case 'ymd':
            default:
                $mm     = (int)$mm;
                $output = "$ccyy $monthalpha[$mm] $dd";
                break;
        } // switch

        return $output;

    } // formatDate

    // ****************************************************************************
    function getInternalDate ($input)
    // convert date from external format (as input by user)
    // to internal format (as used in the database)
    {
        // look for d(d)?m(m)?(yyyy) format (may also be m(m)?d(d)?y(yyy) format)
        $pattern = '/'
                 . '(^[0-9]{1,2})'      // 1 or 2 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{1,2})'       // 1 or 2 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{0,4}$)'      // 0 to 4 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            if (preg_match('#^(dmy|dd/mm/yyyy|dd\.mm\.yyyy|dd/mm/yy)$#i', $this->date_format)) {
                $result = $this->verifyDate($regs[1], $regs[3], $regs[5]);
            } else { // assume 'mdy'
                $result = $this->verifyDate($regs[3], $regs[1], $regs[5]);
            } // if
            return $result;
        } // if

        // look for d(d)?MMM?(yyyy) format
        $pattern = '/'
                 . '(^[0-9]{1,2})'      // 1 or 2 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([a-zA-Z]{1,})'     // 1 or more alpha
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{0,4}$)'      // 0 to 4 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyDate($regs[1], $regs[3], $regs[5]);
            return $result;
        } // if

        // look for d(d)MMM(yyyy) format
        $pattern = '/'
                 . '(^[0-9]{1,2})'      // 1 or 2 digits
                 . '([a-zA-Z]{1,})'     // 1 or more alpha
                 . '([0-9]{0,4}$)'      // 0 to 4 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyDate($regs[1], $regs[2], $regs[3]);
            return $result;
        } // if

        // look for MMM?d(d)?(yyyy) format
        $pattern = '/'
                 . '(^[a-zA-Z]{1,})'    // 1 or more alpha
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{1,2})'       // 1 or 2 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{1,4}$)'      // 0 to 4 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyDate($regs[3], $regs[1], $regs[5]);
            return $result;
        } // if

        // look for MMMddyyyy format
        $pattern = '/'
                 . '(^[a-zA-Z]{1,})'    // 1 or more alpha
                 . '([0-9]{2})'         // 2 digits
                 . '([0-9]{4}$)'        // 4 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyDate($regs[2], $regs[1], $regs[3]);
            return $result;
        } // if

        // look for yyyy?m(m)?d(d) format
        $pattern = '/'
                 . '(^[0-9]{4})'        // 4 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{1,2})'       // 1 or 2 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{1,2}$)'      // 1 to 2 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyDate($regs[5], $regs[3], $regs[1]);
            return $result;
        } // if

        if (preg_match('/^(dmy|mdy)$/i', $this->date_format)) {
            // look for ddmmyyyy format (may also be mmddyyyy format)
            $pattern = '/'
                     . '(^[0-9]{2})'        // 2 digits
                     . '([0-9]{2})'         // 2 digits
                     . '([0-9]{4}$)'        // 4 digits
                     . '/';
            if (preg_match($pattern, $input, $regs)) {
                if (preg_match('/^(dmy)$/i', $this->date_format)) {
                    $result = $this->verifyDate($regs[1], $regs[2], $regs[3]);
                } else { // assume 'mdy'
                    $result = $this->verifyDate($regs[2], $regs[1], $regs[3]);
                } // if
                return $result;
            } // if
        } // if

        if (preg_match('/^(ymd)$/i', $this->date_format)) {
            // look for yyyymmdd format
            $pattern = '/'
                     . '(^[0-9]{4})'        // 4 digits
                     . '([0-9]{2})'         // 2 digits
                     . '([0-9]{2}$)'        // 2 digits
                     . '/';
            if (preg_match($pattern, $input, $regs)) {
                $result = $this->verifyDate($regs[3], $regs[2], $regs[1]);
                return $result;
            } // if
        } // if

        // look for yyyy?MMM?d(d) format
        $pattern = '/'
                 . '(^[0-9]{4})'        // 4 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([a-zA-Z]{1,})'     // 1 or more alpha
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{1,2}$)'      // 1 to 2 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyDate($regs[5], $regs[3], $regs[1]);
            return $result;
        } // if

        if (strlen($input) > 10) {
            // input is too long, so split into two pieces and process first piece
            list($date, $time) = explode(' ', $input);
            if (strlen($date) == strlen($input)) {
                // same length, so drop last character
                $date = substr($date, 0, strlen($date)-1);
            } // if
            $this->internaldate = $this->getInternalDate($date);
            return $this->internaldate;
        } // if

        $this->errors = 'This is not a valid date';

        return false;

    } // getInternalDate

    // ****************************************************************************
    function getInternalTime ($input)
    // convert time from external format (as input by user)
    // to internal format (as used in the database)
    {
        // look for HH?MM?SS format
        $pattern = '/'
                 . '(^[0-9]{2})'        // 2 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{2})'         // 2 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{2})'         // 2 digits
                 . '(\.[0-9]+)?$'       // optional '.nnn' microseconds (for SQL Server)
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyTime($regs[1], $regs[3], $regs[5]);
            return $result;
        } // if

        // look for HHMMSS format
        $pattern = '/'
                 . '(^[0-9]{2})'        // 2 digits
                 . '([0-9]{2})'         // 2 digits
                 . '([0-9]{2}$)'        // 2 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyTime($regs[1], $regs[2], $regs[3]);
            return $result;
        } // if

        // look for HH?MM format
        $pattern = '/'
                 . '(^[0-9]{2})'        // 2 digits
                 . '([^0-9a-zA-Z])'     // not alpha or numeric
                 . '([0-9]{2}$)'        // 2 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyTime($regs[1], $regs[3], '00');
            return $result;
        } // if

        // look for HHMM format
        $pattern = '/'
                 . '(^[0-9]{2})'        // 2 digits
                 . '([0-9]{2}$)'        // 2 digits
                 . '/';
        if (preg_match($pattern, $input, $regs)) {
            $result = $this->verifyTime($regs[1], $regs[2], '00');
            return $result;
        } // if

        $this->errors = 'This is not a valid time';

        return false;

    } // getInternalTime

    // ****************************************************************************
    function getInternalDateTime ($input)
    // convert datetime from external format (as input by user)
    // to internal format (as used in the database)
    {
        // look for last space as a delimiter between date and time portions
        $pos = strrpos($input, ' ');

        // now split the input into its two portions
        $date = substr($input, 0, $pos);
        $time = substr($input, $pos+1);

        // validate the separate portions
        if (!$internaldate = $this->getInternalDate(trim($date))) {
            // fall through
        } elseif (!$internaltime = $this->getInternalTime(trim($time))) {
            // fall through
        } else {
            // set datetime to internal format
            $result = trim($internaldate) . ' ' . trim($internaltime);
            return $result;
        } // if

        $this->errors = 'This is not a valid datetime';

        return false;

    } // getInternalDateTime

    // ****************************************************************************
    function verifyDate ($day, $month, $year)
    {
        if (preg_match('/([a-z]{3})/i', $month)) {
            // convert array from 'N=month' to 'month=N'
            $month_array = array_flip($this->monthalpha);
            // convert all month names to upper case
            $month_array = array_change_key_case($month_array, CASE_UPPER);

            $month = strtoupper($month);

            if (array_key_exists($month, $month_array)) {
                $month_n = $month_array[$month];
            } else {
                $this->errors = 'Month name is invalid';
                return false;
            } // if
        } else {
            $month_n = $month;
        } // if

        // ensure that year has 4 digits
        if (strlen($year) == 4) {
            // do nothing
        } elseif (strlen($year) == 0) {
            $year = date('Y');
        } elseif (strlen($year) == 1) {
            $year = '200' . $year;
        } elseif (strlen($year) == 2) {
            if ($year > 50) {
                $year = '19' . $year;
            } else {
                $year = '20' . $year;
            } // if
        } elseif (strlen($year) == 3) {
            $year = '2' . $year;
        } // if

        if (!checkdate($month_n, $day, $year)) {
            $this->errors = 'This is not a valid date';
            return false;
        } else {
            if (strlen($day) < 2) {
                $day = '0' . $day; // add leading zero
            } // if
            if (strlen($month_n) < 2) {
                $month_n = '0' . $month_n; // add leading zero
            } // if
            $this->internaldate = $year . '-' . $month_n . '-' . $day;
            return $this->internaldate;
        } // if

        return;

    } // verifyDate

    // ****************************************************************************
    function verifyTime ($hours, $minutes, $seconds)
    {
        if ($hours > 23) {
            $this->errors = 'Invalid HOURS';
            return false;
        } // if

        if ($minutes > 59) {
            $this->errors = 'Invalid MINUTES';
            return false;
        } // if

        if ($seconds > 59) {
            $this->errors = 'Invalid SECONDS';
            return false;
        } // if

        return "$hours:$minutes:$seconds";

    } // verifyTime

    // ****************************************************************************
    function getExternalDate ($input)
    // convert date from internal format (as used in the database)
    // to external format (as shown to the user))
    {
        $monthalpha = $this->monthalpha;

        // input may be 'yyyy-mm-dd' or 'yyyymmdd'  or 'dd-Mmm-yy', so
        // check the length and process accordingly

        if (strlen($input) == 8) {
            // test for 'yyyymmdd'
            $pattern = '/'
                     . '(^[0-9]{4})'    // 4 digits (yyyy)
                     . '([0-9]{2})'     // 2 digits (mm)
                     . '([0-9]{2}$)'    // 2 digits (dd)
                     . '/';
            if (preg_match($pattern, $input, $regs)) {
                if ($input == '00000000') {
                    return '';
                } elseif (!checkdate($regs[2], $regs[3], $regs[1])) {
                    $this->errors = 'This is not a valid date';
                    return false;
                } else {
                    $this->externaldate = $this->formatDate($regs[3], $regs[2], $regs[1]);
                    return $this->externaldate;
                } // if
            } // if

            $this->errors = "Invalid date format: expected 'yyyymmdd'";
            return false;
        } // if

        if (strlen($input) == 9) {
            // test for 'dd-Mmm-yy'
            $pattern = '/'
                     . '(^[0-9]{2})'    // 2 digits (dd)
                     . '([^0-9])'       // not a digit
                     . '([a-zA-Z]{3})'  // 3 alpha (Mmm)
                     . '([^0-9])'       // not a digit
                     . '([0-9]{2}$)'    // 2 digits (yy)
                     . '/';
            if (preg_match($pattern, $input, $regs)) {
                if ($result = $this->verifyDate($regs[1], $regs[3], $regs[5])) {
                    $this->externaldate = $this->getExternalDate($result);
                    return $this->externaldate;
                } // if
            } // if

            $this->errors = "Invalid date format: expected 'dd-Mmm-yy'";
            return false;
        } // if

        if (strlen($input) == 10) {
            // test for 'yyyy-mm-dd'
            $pattern = '/'
                     . '(^[0-9]{4})'    // 4 digits (yyyy)
                     . '([^0-9])'       // not a digit
                     . '([0-9]{2})'     // 2 digits (mm)
                     . '([^0-9])'       // not a digit
                     . '([0-9]{2}$)'    // 2 digits (dd)
                     . '/';
            if (preg_match($pattern, $input, $regs)) {
                if ($input == '0000-00-00') {
                    return '';
                } elseif (!checkdate($regs[3], $regs[5], $regs[1])) {
                    $this->errors = 'This is not a valid date';
                    return false;
                } else {
                    $this->externaldate = $this->formatDate($regs[5], $regs[3], $regs[1]);
                    return $this->externaldate;
                } // if
            } // if

            $this->errors = "Invalid date format: expected 'dd-mm-yyyy'";
            return false;
        } // if

        if (strlen($input) == 11) {
            // this could already be in external format, so leave it alone
            return $input;
        } // if

        if (strlen($input) > 11) {
            // input is too long, so split into two pieces (after last ' ') and process first piece
            $time = strrchr($input, ' ');
            $date = substr($input, 0, strlen($input)-strlen($time));
            $this->externaldate = $this->getExternalDate($date);
            return $this->externaldate;
        } // if

        $this->errors = 'This is not a valid date';

        return $input;

    } // getExternalDate

    // ****************************************************************************
    function addDays ($internaldate, $days)
    // add a number of days (may be negative) to $internaldate (YYYY-MM-DD)
    // and return the result in the same format
    {
        // ensure date is in internal format
        $internaldate = $this->getInternalDate($internaldate);

        // convert to the number of days since basedate (4714 BC)
        $julian = GregoriantoJD(substr($internaldate, 5, 2) , substr($internaldate, 8, 2) , substr($internaldate, 0, 4));

        $days = (int)$days;
        $julian = $julian + $days;

        // convert from Julian to Gregorian (format m/d/y)
        $gregorian = JDtoGregorian($julian);

        // split date into its component parts
        list ($month, $day, $year) = preg_split ('[/]', $gregorian);

        // convert back into standard format
        $result = $this->getInternaldate("$day/$month/$year");

        return $result;

    } // addDays

    // ****************************************************************************
    function addWeeks ($internaldate, $weeks)
    // add a number of days (may be negative) to $internaldate (YYYY-MM-DD)
    // and return the result in the same format
    {
        // multiply weeks by 7 to get days
        $result = $this->addDays($internaldate, $weeks*7);

        return $result;

    } // addWeeks

    // ****************************************************************************
    function addMonths ($internaldate, $months)
    // add a number of days (may be negative) to $internaldate (YYYY-MM-DD)
    // and return the result in the same format
    {
        // ensure date is in internal format
        $internaldate = $this->getInternalDate($internaldate);

        // adjust it by speciied number of months
        $timestamp = strtotime($internaldate .' + ' .$months .' months');

        // convert from unix timestamp into a human-readable date
        $result = date('Y-m-d', $timestamp);

        return $result;

    } // addMonths

    // ****************************************************************************
    function getErrors ()
    {
        return $this->errors;

    } // getErrors

// ****************************************************************************
} // end date_class
// ****************************************************************************

?>