Skip to content
This repository has been archived by the owner on Jan 31, 2020. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Ocramius committed Jan 12, 2015
5 parents f1236dd + 5c93f1b + ef9e46c + f775454 + 779752f commit cf458e4
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 25 deletions.
182 changes: 157 additions & 25 deletions src/DateStep.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,16 @@ public function isValid($value)
// Handle intervals of just one date or time unit.
$intervalParts = explode('|', $step->format('%y|%m|%d|%h|%i|%s'));
$partCounts = array_count_values($intervalParts);

$unitKeys = array('years', 'months', 'days', 'hours', 'minutes', 'seconds');
$intervalParts = array_combine($unitKeys, $intervalParts);

// Get absolute time difference
$timeDiff = $valueDate->diff($baseDate, true);
$diffParts = array_combine($unitKeys, explode('|', $timeDiff->format('%y|%m|%d|%h|%i|%s')));

if (5 === $partCounts["0"]) {
// Find the unit with the non-zero interval
$unitKeys = array('years', 'months', 'days', 'hours', 'minutes', 'seconds');
$intervalParts = array_combine($unitKeys, $intervalParts);

$intervalUnit = null;
$stepValue = null;
foreach ($intervalParts as $key => $value) {
Expand All @@ -226,11 +231,6 @@ public function isValid($value)
}
}

// Get absolute time difference
$timeDiff = $valueDate->diff($baseDate, true);
$diffParts = explode('|', $timeDiff->format('%y|%m|%d|%h|%i|%s'));
$diffParts = array_combine($unitKeys, $diffParts);

// Check date units
if (in_array($intervalUnit, array('years', 'months', 'days'))) {
switch ($intervalUnit) {
Expand Down Expand Up @@ -282,6 +282,10 @@ public function isValid($value)
} elseif ('seconds' === $intervalUnit) {
return true;
}

$this->error(self::NOT_STEP);

return false;
}

// Simple test for same day, when using default baseDate
Expand All @@ -305,7 +309,7 @@ public function isValid($value)
}
break;
case 'seconds':
$seconds = ($diffParts['hours'] * 60)
$seconds = ($diffParts['hours'] * 60 * 60)
+ ($diffParts['minutes'] * 60)
+ $diffParts['seconds'];
if (($seconds % $stepValue) === 0) {
Expand All @@ -319,26 +323,154 @@ public function isValid($value)
}
}

// Fall back to slower (but accurate) method for complex intervals.
// Keep adding steps to the base date until a match is found
// or until the value is exceeded.
if ($baseDate < $valueDate) {
while ($baseDate < $valueDate) {
$baseDate->add($step);
if ($baseDate == $valueDate) {
return true;
}
}
} else {
while ($baseDate > $valueDate) {
$baseDate->sub($step);
if ($baseDate == $valueDate) {
return true;
}
return $this->fallbackIncrementalIterationLogic($baseDate, $valueDate, $intervalParts, $diffParts, $step);
}

/**
* Fall back to slower (but accurate) method for complex intervals.
* Keep adding steps to the base date until a match is found
* or until the value is exceeded.
*
* This is really slow if the interval is small, especially if the
* default base date of 1/1/1970 is used. We can skip a chunk of
* iterations by starting at the lower bound of steps needed to reach
* the target
*
* @param DateTime $baseDate
* @param DateTime $valueDate
* @param int[] $intervalParts
* @param int[] $diffParts
* @param DateInterval $step
*
* @return bool
*/
private function fallbackIncrementalIterationLogic(
DateTime $baseDate,
DateTime $valueDate,
array $intervalParts,
array $diffParts,
DateInterval $step
) {
list($minSteps, $requiredIterations) = $this->computeMinStepAndRequiredIterations($intervalParts, $diffParts);
$minimumInterval = $this->computeMinimumInterval($intervalParts, $minSteps);
$isIncrementalStepping = $baseDate < $valueDate;
$dateModificationOperation = $isIncrementalStepping ? 'add' : 'sub';

for ($offsetIterations = 0; $offsetIterations < $requiredIterations; $offsetIterations += 1) {
$baseDate->{$dateModificationOperation}($minimumInterval);
}

while (($isIncrementalStepping && $baseDate < $valueDate)
|| (! $isIncrementalStepping && $baseDate > $valueDate)
) {
$baseDate->{$dateModificationOperation}($step);

if ($baseDate == $valueDate) {
return true;
}
}

$this->error(self::NOT_STEP);

return false;
}

/**
* Computes minimum interval to use for iterations while checking steps
*
* @param int[] $intervalParts
* @param int $minSteps
*
* @return DateInterval
*/
private function computeMinimumInterval(array $intervalParts, $minSteps)
{
return new DateInterval(sprintf(
'P%dY%dM%dDT%dH%dM%dS',
$intervalParts['years'] * $minSteps,
$intervalParts['months'] * $minSteps,
$intervalParts['days'] * $minSteps,
$intervalParts['hours'] * $minSteps,
$intervalParts['minutes'] * $minSteps,
$intervalParts['seconds'] * $minSteps
));
}

/**
* @param int[] $intervalParts
* @param int[] $diffParts
*
* @return int[] (ordered tuple containing minimum steps and required step iterations
*/
private function computeMinStepAndRequiredIterations(array $intervalParts, array $diffParts)
{
$minSteps = $this->computeMinSteps($intervalParts, $diffParts);

// If we use PHP_INT_MAX DateInterval::__construct falls over with a bad format error
// before we reach the max on 64 bit machines
$maxInteger = min(pow(2, 31), PHP_INT_MAX);
// check for integer overflow and split $minimum interval if needed
$maximumInterval = max($intervalParts);
$requiredStepIterations = 1;

if (($minSteps * $maximumInterval) > $maxInteger) {
$requiredStepIterations = ceil(($minSteps * $maximumInterval) / $maxInteger);
$minSteps = floor($minSteps / $requiredStepIterations);
}

return array($minSteps, $minSteps ? $requiredStepIterations : 0);
}

/**
* Multiply the step interval by the lower bound of steps to reach the target
*
* @param int[] $intervalParts
* @param int[] $diffParts
*
* @return int
*/
private function computeMinSteps(array $intervalParts, array $diffParts)
{
$intervalMaxSeconds = $this->computeIntervalMaxSeconds($intervalParts);

return (0 == $intervalMaxSeconds)
? 0
: max(floor($this->computeDiffMinSeconds($diffParts) / $intervalMaxSeconds) - 1, 0);
}

/**
* Get upper bound of the given interval in seconds
* Converts a given `$intervalParts` array into seconds
*
* @param int[] $intervalParts
*
* @return int
*/
private function computeIntervalMaxSeconds(array $intervalParts)
{
return ($intervalParts['years'] * 60 * 60 * 24 * 366)
+ ($intervalParts['months'] * 60 * 60 * 24 * 31)
+ ($intervalParts['days'] * 60 * 60 * 24)
+ ($intervalParts['hours'] * 60 * 60)
+ ($intervalParts['minutes'] * 60)
+ $intervalParts['seconds'];
}

/**
* Get lower bound of difference in secondss
* Converts a given `$diffParts` array into seconds
*
* @param int[] $diffParts
*
* @return int
*/
private function computeDiffMinSeconds(array $diffParts)
{
return ($diffParts['years'] * 60 * 60 * 24 * 365)
+ ($diffParts['months'] * 60 * 60 * 24 * 28)
+ ($diffParts['days'] * 60 * 60 * 24)
+ ($diffParts['hours'] * 60 * 60)
+ ($diffParts['minutes'] * 60)
+ $diffParts['seconds'];
}
}
3 changes: 3 additions & 0 deletions test/DateStepTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ public function stepTestsDataProvider()
// complex
array('P2M2DT12H', DateTime::ISO8601, '1970-01-01T00:00:00Z', '1970-03-03T12:00:00Z', true ),
array('P2M2DT12M', DateTime::ISO8601, '1970-01-01T00:00:00Z', '1970-03-03T12:00:00Z', false),
// long interval
array('PT1M20S', DateTime::ISO8601, '1970-01-01T00:00:00Z', '2020-09-13T12:26:40Z', true), // 20,000,000 steps
array('PT1M20S', DateTime::ISO8601, '1970-01-01T00:00:00Z', '2020-09-13T12:26:41Z', false),
);

// bug in DateTime fixed in 5.3.7
Expand Down

0 comments on commit cf458e4

Please sign in to comment.