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 1 commit
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
16 changes: 16 additions & 0 deletions library/Helpers/CanValidateDateTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@

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
{
Expand Down Expand Up @@ -56,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
Contributor 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 Down
40 changes: 23 additions & 17 deletions library/Rules/DateTimeDiff.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Respect\Validation\Rules\Core\Standard;
use Respect\Validation\Validatable;

use function in_array;
use function is_scalar;

#[Template(
Expand All @@ -36,22 +35,20 @@ final class DateTimeDiff extends Standard
private readonly Validatable $rule;

/**
* @param string $type "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds"
* @param string|null $format Example: "Y-m-d H:i:s.u"
* @param string $type DateInterval format examples: (y, m, d, days, h, i, s, f)
Copy link
Member

@henriquemoody henriquemoody Nov 26, 2024

Choose a reason for hiding this comment

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

I do understand the appeal of using DateInterval formats directly. However, I think having more descriptive values is better as it's more understandable. With the literal constant, there's no reason to translate or document anything.

* @param DateTimeImmutable|null $now The value that will be compared to the input
Copy link
Member

Choose a reason for hiding this comment

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

I'm a against adding documentation to the code like that, I think the name of the parameter should be pretty obvious for what it does, otherwise we should do that for everything. I understand that it can be useful to add some documentation here and there, but for this specific method, I would rather not have it.

*/
public function __construct(
Validatable $rule,
private readonly string $type = 'years',
private readonly string $type = 'y',
private readonly ?string $format = null,
private readonly ?DateTimeImmutable $now = null,
) {
$availableTypes = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds'];
if (!in_array($this->type, $availableTypes, true)) {
if (!$this->isDateIntervalType($this->type)) {
throw new InvalidRuleConstructorException(
'"%s" is not a valid type of age (Available: %s)',
$this->type,
$availableTypes
['y', 'm', 'd', 'days', 'h', 'i', 's', 'f']
);
}
$this->rule = $this->extractSiblingSuitableRule(
Expand Down Expand Up @@ -82,22 +79,17 @@ public function evaluate(mixed $input): Result
->evaluate($this->comparisonValue($now, $compareTo))
->withNameIfMissing($input instanceof DateTimeInterface ? $input->format('c') : $input);

$parameters = ['type' => $this->type, 'now' => $this->nowParameter($now)];
$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 match ($this->type) {
'years' => $compareTo->diff($now)->y,
'months' => $compareTo->diff($now)->m,
'days' => $compareTo->diff($now)->days,
'hours' => $compareTo->diff($now)->h,
'minutes' => $compareTo->diff($now)->i,
'seconds' => $compareTo->diff($now)->s,
'microseconds' => $compareTo->diff($now)->f,
};
return $compareTo->diff($now)->{$this->type};
}

private function nowParameter(DateTimeInterface $now): string
Expand Down Expand Up @@ -135,4 +127,18 @@ private function createDateTimeObject(mixed $input): ?DateTimeInterface

return $dateTime;
}

private function getTranslatedType(string $type): string
{
return match ($type) {
'y' => 'years',
'm' => 'months',
'd' => 'days',
'days' => 'full days',
Copy link
Member

Choose a reason for hiding this comment

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

Very nice catch!

'h' => 'hours',
'i' => 'minutes',
's' => 'seconds',
'f' => 'microseconds',
};
}
}
33 changes: 21 additions & 12 deletions tests/integration/rules/dateTimeDiff.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ date_default_timezone_set('UTC');

run([
'Without customizations' => [v::dateTimeDiff(v::equals(2)), '1 year ago'],
'With $type = "months"' => [v::dateTimeDiff(v::equals(3), 'months'), '2 months ago'],
'With $type = "months"' => [v::dateTimeDiff(v::equals(3), 'm'), '2 months ago'],
'With $type = "days"' => [v::dateTimeDiff(v::equals(4), 'days'), '3 days ago'],
'With $type = "days" and difference of months' => [v::dateTimeDiff(v::not(v::lessThan(95)), 'days'), '3 months ago'],
'With $type = "hours"' => [v::dateTimeDiff(v::equals(5), 'hours'), '4 hours ago'],
'With $type = "minutes"' => [v::dateTimeDiff(v::equals(6), 'minutes'), '5 minutes ago'],
'With $type = "microseconds"' => [v::dateTimeDiff(v::equals(7), 'microseconds'), '6 microseconds ago'],
'With custom $format' => [v::dateTimeDiff(v::lessThan(8), 'years', 'd/m/Y'), '09/12/1988'],
'With custom $now' => [v::dateTimeDiff(v::lessThan(9), 'years', null, new DateTimeImmutable()), '09/12/1988'],
'With $type = "d"' => [v::dateTimeDiff(v::equals(4), 'd'), '3 days ago'],
'With $type = "hours"' => [v::dateTimeDiff(v::equals(5), 'h'), '4 hours ago'],
'With $type = "minutes"' => [v::dateTimeDiff(v::equals(6), 'i'), '5 minutes ago'],
'With $type = "microseconds"' => [v::dateTimeDiff(v::equals(7), 'f'), '6 microseconds ago'],
'With custom $format' => [v::dateTimeDiff(v::lessThan(8), 'y', 'd/m/Y'), '09/12/1988'],
'With custom $now' => [v::dateTimeDiff(v::lessThan(9), 'y', null, new DateTimeImmutable()), '09/12/1988'],
'Wrapped by "not"' => [v::not(v::dateTimeDiff(v::lessThan(8))), '7 year ago'],
'Wrapping "not"' => [v::dateTimeDiff(v::not(v::lessThan(9))), '8 year ago'],
]);
Expand All @@ -42,18 +43,26 @@ The number of months between now and 2 months ago must equal 3

With $type = "days"
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
The number of days between now and 3 days ago must equal 4
- The number of days between now and 3 days ago must equal 4
The number of full days between now and 3 days ago must equal 4
- The number of full days between now and 3 days ago must equal 4
[
'dateTimeDiff' => 'The number of days between now and 3 days ago must equal 4',
'dateTimeDiff' => 'The number of full days between now and 3 days ago must equal 4',
]

With $type = "days" and difference of months
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
The number of days between now and 3 months ago must not be less than 95
- The number of days between now and 3 months ago must not be less than 95
The number of full days between now and 3 months ago must not be less than 95
- The number of full days between now and 3 months ago must not be less than 95
[
'dateTimeDiff' => 'The number of full days between now and 3 months ago must not be less than 95',
]

With $type = "d"
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
The number of days between now and 3 days ago must equal 4
- The number of days between now and 3 days ago must equal 4
[
'dateTimeDiff' => 'The number of days between now and 3 months ago must not be less than 95',
'dateTimeDiff' => 'The number of days between now and 3 days ago must equal 4',
]

With $type = "hours"
Expand Down
15 changes: 8 additions & 7 deletions tests/unit/Rules/DateTimeDiffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,13 @@ public static function providerForValidInput(): array
{
return [
'years' => [new DateTimeDiff(Stub::pass(1)), new DateTimeImmutable()],
'months' => [new DateTimeDiff(Stub::pass(1), 'months'), new DateTimeImmutable()],
'days' => [new DateTimeDiff(Stub::pass(1), 'days'), new DateTimeImmutable()],
'hours' => [new DateTimeDiff(Stub::pass(1), 'hours'), new DateTimeImmutable()],
'minutes' => [new DateTimeDiff(Stub::pass(1), 'minutes'), new DateTimeImmutable()],
'seconds' => [new DateTimeDiff(Stub::pass(1), 'seconds'), new DateTimeImmutable()],
'microseconds' => [new DateTimeDiff(Stub::pass(1), 'microseconds'), new DateTimeImmutable()],
'months' => [new DateTimeDiff(Stub::pass(1), 'm'), new DateTimeImmutable()],
'total number of full days' => [new DateTimeDiff(Stub::pass(1), 'days'), new DateTimeImmutable()],
'number of days' => [new DateTimeDiff(Stub::pass(1), 'd'), new DateTimeImmutable()],
'hours' => [new DateTimeDiff(Stub::pass(1), 'h'), new DateTimeImmutable()],
'minutes' => [new DateTimeDiff(Stub::pass(1), 'i'), new DateTimeImmutable()],
'seconds' => [new DateTimeDiff(Stub::pass(1), 's'), new DateTimeImmutable()],
'microseconds' => [new DateTimeDiff(Stub::pass(1), 'f'), new DateTimeImmutable()],
];
}

Expand All @@ -96,7 +97,7 @@ public static function providerForInvalidInput(): array
{
return [
'valid date, with failing rule' => [
new DateTimeDiff(Stub::fail(1), 'years'),
new DateTimeDiff(Stub::fail(1), 'y'),
new DateTimeImmutable(),
],
] + array_map(
Expand Down