Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rule/date time diff #1463

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions docs/rules/DateTimeDiff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# DateTimeDiff

- `DateTimeDiff(Validatable $rule)`
- `DateTimeDiff(Validatable $rule, string $type)`
- `DateTimeDiff(Validatable $rule, string $type, string $format)`

Validates the difference of date/time against a specific rule.

The `$format` argument should follow PHP's [date()][] function. When the `$format` is not given, this rule accepts
[Supported Date and Time Formats][] by PHP (see [strtotime()][]).

The `$type` argument should follow PHP's [DateInterval] properties. When the `$type` is not given, its default value is `y`.

```php
v::dateTimeDiff(v::equal(7))->validate('7 years ago - 1 minute'); // true
v::dateTimeDiff(v::equal(7))->validate('7 years ago + 1 minute'); // false

v::dateTimeDiff(v::greaterThan(18), 'y', 'd/m/Y')->validate('09/12/1990'); // true
v::dateTimeDiff(v::greaterThan(18), 'y', 'd/m/Y')->validate('09/12/2023'); // false

v::dateTimeDiff(v::between(1, 18), 'm')->validate('5 months ago'); // true
```

The supported types are:

* `years` as `y`
* `months` as `m`
* `days` as `days` and `d`
* `hours` as `h`
* `minutes` as `i`
* `seconds` as `s`
* `microseconds` as `f`

Difference between `d` and `days`

`d` (days): Represents the difference in days within the same month or year. For example, if the difference between two dates is 1 month and 10 days, the value of d will be 10.

`days` (full days): Represents the total difference in days between two dates, regardless of months or years. For example, if the difference between two dates is 1 month and 10 days, the value of days will be the total number of days between these dates.

## Categorization

- Date and Time

## Changelog

| Version | Description |
| ------: |--------------------------------------------|
| 3.0.0 | Created from `Age`, `MinAge`, and `MaxAge` |

***
See also:

- [Date](Date.md)
- [DateTime](DateTime.md)
- [Max](Max.md)
- [Min](Min.md)
- [Time](Time.md)

[date()]: http://php.net/date
[DateTimeInterface]: http://php.net/DateTimeInterface
[strtotime()]: http://php.net/strtotime
[Supported Date and Time Formats]: http://php.net/datetime.formats
[DateInterval]: https://www.php.net/manual/en/class.dateinterval.php
37 changes: 37 additions & 0 deletions library/Helpers/CanExtractRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
namespace Respect\Validation\Helpers;

use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Rules\Core\Composite;
use Respect\Validation\Rules\Not;
use Respect\Validation\Validatable;
use Respect\Validation\Validator;
use Throwable;

use function array_map;
use function count;
Expand All @@ -37,6 +40,40 @@
return $rule;
}

private function extractSiblingSuitableRule(Validatable $rule, Throwable $throwable): Validatable
{
$this->assertSingleRule($rule, $throwable);

if ($rule instanceof Validator) {
return $rule->getRules()[0];
}

return $rule;
}

private function assertSingleRule(Validatable $rule, Throwable $throwable): void
{
if ($rule instanceof Not) {
$this->assertSingleRule($rule->getRule(), $throwable);

return;
}

if ($rule instanceof Validator) {
if (count($rule->getRules()) !== 1) {
throw $throwable;
}

$this->assertSingleRule($rule->getRules()[0], $throwable);

return;
}

if ($rule instanceof Composite) {
throw $throwable;

Check warning on line 73 in library/Helpers/CanExtractRules.php

View check run for this annotation

Codecov / codecov/patch

library/Helpers/CanExtractRules.php#L73

Added line #L73 was not covered by tests
}
}

/**
* @param array<Validatable> $rules
*
Expand Down
30 changes: 26 additions & 4 deletions library/Helpers/CanValidateDateTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@

namespace Respect\Validation\Helpers;

use DateInterval;
use DateTime;
use DateTimeZone;

use function checkdate;
use function date_default_timezone_get;
use function date_parse_from_format;
use function preg_match;
use function array_keys;
use function in_array;
use function get_object_vars;


trait CanValidateDateTime
{
private function isDateTime(string $format, string $value): bool
{
$exceptionalFormats = [
'c' => 'Y-m-d\TH:i:sP',
'r' => 'D, d M Y H:i:s O',
];
$exceptionalFormats = $this->getExceptionalFormats();

$format = $exceptionalFormats[$format] ?? $format;

Expand Down Expand Up @@ -59,6 +61,17 @@ private function isDateTimeParsable(array $info): bool
return $info['error_count'] === 0 && $info['warning_count'] === 0;
}

/**
* Validates if the given string is a valid DateInterval type.
*
* @param string $age
* @return bool
*/
private function isDateIntervalType(string $age): bool
{
return in_array($age, array_keys(get_object_vars((new DateInterval('P1Y')))));
}
Comment on lines +66 to +69
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this seems weird, but i wanted to use those DateInterval vars. what do you guys think about it? How do we can improve this validation?


private function isDateFormat(string $format): bool
{
return preg_match('/[djSFmMnYy]/', $format) > 0;
Expand All @@ -75,4 +88,13 @@ private function isDateInformation(array $info): bool

return checkdate($info['month'] ?: 1, 1, $info['year'] ?: 1);
}

/** @return array<string, string> */
private function getExceptionalFormats(): array
{
return [
'c' => 'Y-m-d\TH:i:sP',
'r' => 'D, d M Y H:i:s O',
];
}
}
5 changes: 5 additions & 0 deletions library/Rules/Core/Wrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,9 @@ public function setTemplate(string $template): static

return $this;
}

public function getRule(): Validatable
{
return $this->rule;
}
}
152 changes: 152 additions & 0 deletions library/Rules/DateTimeDiff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

declare(strict_types=1);

namespace Respect\Validation\Rules;

use DateTimeImmutable;
use DateTimeInterface;
use Respect\Validation\Exceptions\InvalidRuleConstructorException;
use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Helpers\CanExtractRules;
use Respect\Validation\Helpers\CanValidateDateTime;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Standard;
use Respect\Validation\Validatable;

use function is_scalar;

#[Template(
'The number of {{type|raw}} between {{now|raw}} and',
'The number of {{type|raw}} between {{now|raw}} and',
)]
final class DateTimeDiff extends Standard
{
use CanBindEvaluateRule;
use CanValidateDateTime;
use CanExtractRules;

private readonly Validatable $rule;

/**
* @param string $type DateInterval format examples:
* - 'y': years
* - 'm': months
* - 'd': days (within the same month or year)
* - 'days': full days (total difference in days)
* - 'h': hours
* - 'i': minutes
* - 's': seconds
* - 'f': microseconds
* @param DateTimeImmutable|null $now The value that will be compared to the input
*/
public function __construct(
Validatable $rule,
private readonly string $type = 'y',
private readonly ?string $format = null,
private readonly ?DateTimeImmutable $now = null,
) {
if (!$this->isDateIntervalType($this->type)) {
throw new InvalidRuleConstructorException(
'"%s" is not a valid type of age (Available: %s)',
$this->type,
['y', 'm', 'd', 'days', 'h', 'i', 's', 'f']
);
}
$this->rule = $this->extractSiblingSuitableRule(
$rule,
new InvalidRuleConstructorException('DateTimeDiff must contain exactly one rule')
);
}

public function evaluate(mixed $input): Result
{
$compareTo = $this->createDateTimeObject($input);
if ($compareTo === null) {
return Result::failed($input, $this);

Check warning on line 72 in library/Rules/DateTimeDiff.php

View check run for this annotation

Codecov / codecov/patch

library/Rules/DateTimeDiff.php#L72

Added line #L72 was not covered by tests
}

$dateTimeResult = $this->bindEvaluate(
binded: new DateTime($this->format),
binder: $this,
input: $input
);
if (!$dateTimeResult->isValid) {
return $dateTimeResult;

Check warning on line 81 in library/Rules/DateTimeDiff.php

View check run for this annotation

Codecov / codecov/patch

library/Rules/DateTimeDiff.php#L81

Added line #L81 was not covered by tests
}

$now = $this->now ?? new DateTimeImmutable();

$nextSibling = $this->rule
->evaluate($this->comparisonValue($now, $compareTo))
->withNameIfMissing($input instanceof DateTimeInterface ? $input->format('c') : $input);

$parameters = [
'type' => $this->getTranslatedType($this->type),
'now' => $this->nowParameter($now)
];

return (new Result($nextSibling->isValid, $input, $this, $parameters))->withNextSibling($nextSibling);
}

private function comparisonValue(DateTimeInterface $now, DateTimeInterface $compareTo)
{
return $compareTo->diff($now)->{$this->type};
}

private function nowParameter(DateTimeInterface $now): string
{
if ($this->format === null && $this->now === null) {
return 'now';
}

if ($this->format === null) {
return $now->format('Y-m-d H:i:s.u');
}

return $now->format($this->format);
}

private function createDateTimeObject(mixed $input): ?DateTimeInterface
{
if ($input instanceof DateTimeInterface) {
return $input;
}

if (!is_scalar($input)) {
return null;

Check warning on line 123 in library/Rules/DateTimeDiff.php

View check run for this annotation

Codecov / codecov/patch

library/Rules/DateTimeDiff.php#L123

Added line #L123 was not covered by tests
}

if ($this->format === null) {
return new DateTimeImmutable((string) $input);
}

$format = $this->getExceptionalFormats()[$this->format] ?? $this->format;
$dateTime = DateTimeImmutable::createFromFormat($format, (string) $input);
if ($dateTime === false) {
return null;

Check warning on line 133 in library/Rules/DateTimeDiff.php

View check run for this annotation

Codecov / codecov/patch

library/Rules/DateTimeDiff.php#L133

Added line #L133 was not covered by tests
}

return $dateTime;
}

private function getTranslatedType(string $type): string
{
return match ($type) {
'y' => 'years',
'm' => 'months',
'd' => 'days',
'days' => 'full days',
'h' => 'hours',
'i' => 'minutes',
's' => 'seconds',
'f' => 'microseconds',
};
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to use this, I believe that we can improve to a Enum file. I didn't create because the project don't have any

Loading
Loading