1
\$\begingroup\$

This class is intended to fulfill these requirements:

  1. Output in Y-m-d H:i:s format
  2. Allow dates to be entered as m-d-Y or m-d-y
  3. Complain if an invalid birthdate is entered (i.e., in the future or otherwise)
  4. Allow simpler, friendlier coding without typing a lengthy DateTime instantiation
<?php
/**
 * CustomDateTime with historical conversion and statically called methods
 *
 * European order d-m-y is not supported, and assumed to be m-d-Y.  
 * (converted to American m/d/y)
 * 
 * Adds `historical()` method which throws a range exception if a date is not 
 * in the past. YY dates are forced to the past.
 * 
 * Adds several static methods for easier writing and less cognitive effort
 * (CustomDateTime::now() vs (new DateTim())->format('Y-m-d H:i:s'))
 *
 */
class CustomDateTime extends DateTime 
{

    /**
    * Holds the original constructor string
    * @var string
    */
    public $time;

    /**
     * __construct accepts exactly the same parameters as the DateTime object
     *
     * European order d-m-y is not supported, and m-d-Y is converted to m/d/Y.
     * 
     * @param string            $time     Just about any English textual datetime description
     * @param DateTimeZone|null $timezone A DateTimeZone object representing the timezone of $time.
     */
    public function __construct(string $time="now", DateTimeZone $timezone = null) 
    {
        $this->time = str_replace(
          ['-','.'], 
          '/', 
          substr($time, 0, 10)
        ) . substr($time, 10 );
        parent::__construct($this->time, $timezone);
    }

    /**
     * Convert to Historical Date (static method)
     *
     * Static method to access instantiated version (historical())
     * 
     * @param  string $date m/d/Y, Y-m-d, '', null
     * @return string       Y-m-d or null if no value given
     */
    public static function asHistorical($date) : ?string 
    {
        if(empty($date)) { return null; }
        return (new CustomDateTime($date))->historical()->format('Y-m-d');
    }

    /**
     * Historical (instantiated) 
     * 
     * Originally was intended to prevent birthdates in the future.
     *
     * Silently corrects 2 digit year to a date in the past
     * Throws `RangeException` if date is more than 100 years in the future.
     *
     * @return CustomDateTime instance, which allows chaining of methods
     */
    public function historical() : CustomDateTime
    {
        $now = (new DateTime())->format('Y-m-d');

        // if a two-digit year was converted to the future, subtract 100 years
        if(
            preg_match("@^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2}$@", $this->time) &&
            $this->format('Y-m-d') > $now
        ) {
            $this->modify('-100 years');
        }

        if( $this->format('Y-m-d') > $now) {
            throw new RangeException('Date is too far in the future');
        }

        return $this;
    }

    /**
     * Return Database Safe date
     * 
     * This avoids passing '' to database, replacing it with null
     *
     * @param  string $date m/d/Y, Y-m-d, '', null
     * @return string       Y-m-d or null if no value given
     */
    public static function dbSafe($date) : ?string
    {
        if(empty($date)) { return null; }
        return (new CustomDateTime($date))->format('Y-m-d');
    }


    public static function now() : string
    {
        return (new DateTime())->format('Y-m-d H:i:s');
    }

    public static function today() : string
    {
        return (new DateTime())->format('Y-m-d');
    }
}
?>
Examples:


Short version: historical date... 3-4-30 = <?= CustomDateTime::asHistorical('3-4-30') ?>

Long version: historical date.... 3-4-30 = <?= (new CustomDateTime('3-4-30'))->historical()->format('Y-m-d') ?>


DateTime uses European format.... 3-4-30 = <?= (new DateTime('3-4-30'))->format('m/d/Y') ?>

DateTime splits century.......... 3-4-30 = <?= (new DateTime('3/4/30'))->format('m/d/Y') ?>


Bonus:

Easily get "now"................. <?= CustomDateTime::now() ?>

\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

I don't know if I trust the string surgery being conducted in the constructor.

Comments that I have since deleted under the question:

  • Dates can be m-d-y like 12-31-20, so I assume 12-31-20 12:59:59 is okay too, but then the first 10 characters is 12-31-20 1 -- that's going to make the intended logic in the constructor fail, right?

  • And your preg_match() call looks like you are accommodating 2-2-22 dates, so adding time to that in the constructor and extracting the first 10 digits would be 2-2-22 12:.

Do I misunderstand the strings that your class is supposed to accommodate or is this a problem in your constructor?


I recommend declaring a default value for the $date argument of asHistorical() and dbSafe(). With the variable sure to have a declared value, then empty() becomes an unnecessary function calls and you can just use a function-less falsey check. (I mean empty() does two things, it checks if a variable is not set and checks if it is falsey.) A single return is often considered the cleanest design. Example of dbSafe() with a truthy ternary condition:

public static function dbSafe(?string $date = null) : ?string
{
    return $data ? (new CustomDateTime($date))->format('Y-m-d') : null;
}

When writing regex, I prefer the more concise \d over [0-9] character class.

The use of @ (non-slash character) is a good choice for pattern delimiter because it avoids the need for escaping slashes in the pattern.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for looking it over. You correctly understood what the constructor is expected to handle; I see that I had a bit of tunnel vision... I was counting a 4 digit year. smh. Thanks for that catch. Nice cleanup on the dbSafe method too; now that I see it, it seems so obvious... \$\endgroup\$
    – Tim Morton
    Jan 26, 2021 at 4:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.