digital-craftsman-de/date-time-precision

Streamline comparison methods that compare something other then the current type

Closed this issue · 5 comments

At the moment there is a isDateAfterInTimeZone in DateTime. To be streamlined it would need to exist for month, year and time as well. With all permutations. And it only works with a parameter of DateTime. Perhaps it should support DateTime, Time, Date, Month and Year as parameter. But not as separate methods, but as union types.

I really need to rethink this whole topic. Last thoughts:

  • *InTimeZone methods only make sense for Moment, as it's the only one including a timeZone.

After more inspection I think I know what the problem is: I've stored a lot of data as moments, when they are specific dates in a relevant timezone and therefore have to handle comparisons on a date level of two moments.
The correct approach would be to store the date time in a precision relevant to the use case and then compare it with a moment. This way there is no need to have a isDateAfterInTimeZone on the moment that also takes another moment as a parameter any more. Not to mention that it would need to be called isDateInTimeZoneAfter as a date only always exists in a time zone.

This would then mean all precision value objects like Time, Date, Month and Year need the following methods:

  • isEqualTo and isNotEqualTo
  • isAfter and isNotAfter
  • isBefore and isNotBefore
  • isBeforeOrEqualTo and isNotBeforeOrEqualTo
  • isAfterOrEqualTo and isNotAfterOrEqualTo

The moment class would then contain all those (10) methods x all combinations with all precision value objects (4) (always with InTimeZone). But only with the precision value objects as parameters and never with a moment as parameter (as they are currently). In total 50 comparison methods.

Which means I also need to adapt the docs to reflect the (now more obvious) value of the bundle:

  • Have more precise value objects to work with and store parts of a moment.
  • Have more readable comparisons. Like instead of !($time->setTimeZone($companyTimeZone) <= $commandExecutedAt) have $commandExecutedAt->isTimeInTimeZoneNotAfterOrEqualTo($time). Need better example then that.
  • Explain why precise value objects should be stored instead of moments.

Or even better: Take inspiration from moment.js where there's a isBefore that has a granularity parameter. Which we could skip and simply do the comparison depending on the parameter given.
This could even be extended with a granularity parameter to do an automatic conversion.

In comparison to moment.js we've got the advantage that we have type safety one runtime, so we could do something like this

/**
 * @param Moment|Time|Date|Month|Year $before   Comparison is done depending on the class of the passed object (converted with the $timeZone)
 * @param \DateTimeZone|null          $timeZone needs to be supplied when something other than a Moment is passed, null otherwise
 */
public function isBefore(
    Moment | Time | Date | Month | Year $before,
    ?\DateTimeZone $timeZone = null,
): bool {
    if ($before instanceof Moment
        && $timeZone !== null
    ) {
        // TODO: Throw custom exception
        throw new \InvalidArgumentException('Shit');
    }
    if (!$before instanceof Moment
        && $timeZone === null
    ) {
        // TODO: Throw custom exception
        throw new \InvalidArgumentException('Shit');
    }

    return match (true) {
        $before instanceof Moment => $this->dateTime < $before->dateTime,
        $before instanceof Time => $this->timeInTimeZone($timeZone)->isBefore($before),
        $before instanceof Date => $this->dateInTimeZone($timeZone)->isBefore($before),
        $before instanceof Month => $this->monthInTimeZone($timeZone)->isBefore($before),
        $before instanceof Year => $this->yearInTimeZone($timeZone)->isBefore($before),
    };
}

Resolved with #40