diff --git a/docs/08-list-of-rules-by-category.md b/docs/08-list-of-rules-by-category.md index 79e298b3f..8544ca839 100644 --- a/docs/08-list-of-rules-by-category.md +++ b/docs/08-list-of-rules-by-category.md @@ -75,6 +75,7 @@ - [Date](rules/Date.md) - [DateTime](rules/DateTime.md) +- [DateTimeDiff](rules/DateTimeDiff.md) - [LeapDate](rules/LeapDate.md) - [LeapYear](rules/LeapYear.md) - [MaxAge](rules/MaxAge.md) @@ -323,6 +324,7 @@ - [CurrencyCode](rules/CurrencyCode.md) - [Date](rules/Date.md) - [DateTime](rules/DateTime.md) +- [DateTimeDiff](rules/DateTimeDiff.md) - [Decimal](rules/Decimal.md) - [Digit](rules/Digit.md) - [Directory](rules/Directory.md) diff --git a/docs/rules/Date.md b/docs/rules/Date.md index 15b723a52..538901344 100644 --- a/docs/rules/Date.md +++ b/docs/rules/Date.md @@ -45,6 +45,7 @@ Version | Description See also: - [DateTime](DateTime.md) +- [DateTimeDiff](DateTimeDiff.md) - [LeapDate](LeapDate.md) - [LeapYear](LeapYear.md) - [MaxAge](MaxAge.md) diff --git a/docs/rules/DateTime.md b/docs/rules/DateTime.md index 1ef7662fb..e818cd018 100644 --- a/docs/rules/DateTime.md +++ b/docs/rules/DateTime.md @@ -74,6 +74,7 @@ See also: - [BetweenExclusive](BetweenExclusive.md) - [Callback](Callback.md) - [Date](Date.md) +- [DateTimeDiff](DateTimeDiff.md) - [LeapDate](LeapDate.md) - [LeapYear](LeapYear.md) - [MinAge](MinAge.md) diff --git a/docs/rules/DateTimeDiff.md b/docs/rules/DateTimeDiff.md new file mode 100644 index 000000000..d7025c8af --- /dev/null +++ b/docs/rules/DateTimeDiff.md @@ -0,0 +1,53 @@ +# DateTimeDiff + +- `DateTimeDiff(string $type, Validatable $rule)` +- `DateTimeDiff(string $type, Validatable $rule, 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()][]). + +```php +v::dateTimeDiff('years', v::equals(7))->validate('7 years ago'); // true +v::dateTimeDiff('years', v::equals(7))->validate('7 years ago + 1 minute'); // false + +v::dateTimeDiff('years', v::greaterThan(18), 'd/m/Y')->validate('09/12/1990'); // true +v::dateTimeDiff('years', v::greaterThan(18), 'd/m/Y')->validate('09/12/2023'); // false + +v::dateTimeDiff('months', v::between(1, 18))->validate('5 months ago'); // true +``` + +The supported types are: + +* `years` +* `months` +* `days` +* `hours` +* `minutes` +* `seconds` +* `microseconds` + +## 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 diff --git a/docs/rules/Max.md b/docs/rules/Max.md index e1210dd9d..1cba0a25d 100644 --- a/docs/rules/Max.md +++ b/docs/rules/Max.md @@ -39,6 +39,7 @@ See also: - [Between](Between.md) - [BetweenExclusive](BetweenExclusive.md) +- [DateTimeDiff](DateTimeDiff.md) - [GreaterThan](GreaterThan.md) - [GreaterThanOrEqual](GreaterThanOrEqual.md) - [IterableType](IterableType.md) diff --git a/docs/rules/Min.md b/docs/rules/Min.md index 80fce44c8..bc3a4e3ad 100644 --- a/docs/rules/Min.md +++ b/docs/rules/Min.md @@ -39,6 +39,7 @@ See also: - [Between](Between.md) - [BetweenExclusive](BetweenExclusive.md) +- [DateTimeDiff](DateTimeDiff.md) - [Each](Each.md) - [GreaterThan](GreaterThan.md) - [GreaterThanOrEqual](GreaterThanOrEqual.md) diff --git a/docs/rules/Time.md b/docs/rules/Time.md index 4a7db9768..a5255447d 100644 --- a/docs/rules/Time.md +++ b/docs/rules/Time.md @@ -49,5 +49,6 @@ See also: - [Date](Date.md) - [DateTime](DateTime.md) +- [DateTimeDiff](DateTimeDiff.md) - [LeapDate](LeapDate.md) - [LeapYear](LeapYear.md) diff --git a/library/Helpers/CanExtractRules.php b/library/Helpers/CanExtractRules.php index 56388511e..ac57b6031 100644 --- a/library/Helpers/CanExtractRules.php +++ b/library/Helpers/CanExtractRules.php @@ -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; @@ -37,6 +40,40 @@ private function extractSingle(Validatable $rule, string $class): Validatable 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; + } + } + /** * @param array $rules * diff --git a/library/Helpers/CanValidateDateTime.php b/library/Helpers/CanValidateDateTime.php index d916bb22d..aebc0776c 100644 --- a/library/Helpers/CanValidateDateTime.php +++ b/library/Helpers/CanValidateDateTime.php @@ -21,10 +21,7 @@ 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; @@ -75,4 +72,13 @@ private function isDateInformation(array $info): bool return checkdate($info['month'] ?: 1, 1, $info['year'] ?: 1); } + + /** @return array */ + private function getExceptionalFormats(): array + { + return [ + 'c' => 'Y-m-d\TH:i:sP', + 'r' => 'D, d M Y H:i:s O', + ]; + } } diff --git a/library/Mixins/ChainedKey.php b/library/Mixins/ChainedKey.php index fd775aa46..367ca7386 100644 --- a/library/Mixins/ChainedKey.php +++ b/library/Mixins/ChainedKey.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface ChainedKey @@ -107,6 +108,17 @@ public function keyDate(int|string $key, string $format = 'Y-m-d'): ChainedValid public function keyDateTime(int|string $key, ?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public function keyDateTimeDiff( + int|string $key, + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public function keyDecimal(int|string $key, int $decimals): ChainedValidator; public function keyDigit(int|string $key, string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/ChainedNot.php b/library/Mixins/ChainedNot.php index 7c3dea999..b65cce0c0 100644 --- a/library/Mixins/ChainedNot.php +++ b/library/Mixins/ChainedNot.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface ChainedNot @@ -91,6 +92,16 @@ public function notDate(string $format = 'Y-m-d'): ChainedValidator; public function notDateTime(?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public function notDateTimeDiff( + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public function notDecimal(int $decimals): ChainedValidator; public function notDigit(string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/ChainedNullOr.php b/library/Mixins/ChainedNullOr.php index 37cd0cc7a..b4ed10a0c 100644 --- a/library/Mixins/ChainedNullOr.php +++ b/library/Mixins/ChainedNullOr.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface ChainedNullOr @@ -91,6 +92,16 @@ public function nullOrDate(string $format = 'Y-m-d'): ChainedValidator; public function nullOrDateTime(?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public function nullOrDateTimeDiff( + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public function nullOrDecimal(int $decimals): ChainedValidator; public function nullOrDigit(string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/ChainedProperty.php b/library/Mixins/ChainedProperty.php index 834853eed..7851baffc 100644 --- a/library/Mixins/ChainedProperty.php +++ b/library/Mixins/ChainedProperty.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface ChainedProperty @@ -115,6 +116,17 @@ public function propertyDate(string $propertyName, string $format = 'Y-m-d'): Ch public function propertyDateTime(string $propertyName, ?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public function propertyDateTimeDiff( + string $propertyName, + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public function propertyDecimal(string $propertyName, int $decimals): ChainedValidator; public function propertyDigit(string $propertyName, string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/ChainedUndefOr.php b/library/Mixins/ChainedUndefOr.php index 53aaa2b8b..84957960d 100644 --- a/library/Mixins/ChainedUndefOr.php +++ b/library/Mixins/ChainedUndefOr.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface ChainedUndefOr @@ -95,6 +96,16 @@ public function undefOrDate(string $format = 'Y-m-d'): ChainedValidator; public function undefOrDateTime(?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public function undefOrDateTimeDiff( + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public function undefOrDecimal(int $decimals): ChainedValidator; public function undefOrDigit(string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/ChainedValidator.php b/library/Mixins/ChainedValidator.php index 772b8924b..8f44f96aa 100644 --- a/library/Mixins/ChainedValidator.php +++ b/library/Mixins/ChainedValidator.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface ChainedValidator extends @@ -100,6 +101,16 @@ public function date(string $format = 'Y-m-d'): ChainedValidator; public function dateTime(?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public function dateTimeDiff( + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public function decimal(int $decimals): ChainedValidator; public function digit(string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/StaticKey.php b/library/Mixins/StaticKey.php index e5de42807..a97263422 100644 --- a/library/Mixins/StaticKey.php +++ b/library/Mixins/StaticKey.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface StaticKey @@ -111,6 +112,17 @@ public static function keyDate(int|string $key, string $format = 'Y-m-d'): Chain public static function keyDateTime(int|string $key, ?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public static function keyDateTimeDiff( + int|string $key, + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public static function keyDecimal(int|string $key, int $decimals): ChainedValidator; public static function keyDigit(int|string $key, string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/StaticNot.php b/library/Mixins/StaticNot.php index ac67b783d..fe8260d98 100644 --- a/library/Mixins/StaticNot.php +++ b/library/Mixins/StaticNot.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface StaticNot @@ -95,6 +96,16 @@ public static function notDate(string $format = 'Y-m-d'): ChainedValidator; public static function notDateTime(?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public static function notDateTimeDiff( + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public static function notDecimal(int $decimals): ChainedValidator; public static function notDigit(string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/StaticNullOr.php b/library/Mixins/StaticNullOr.php index 8954fc49c..47c87487d 100644 --- a/library/Mixins/StaticNullOr.php +++ b/library/Mixins/StaticNullOr.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface StaticNullOr @@ -103,6 +104,16 @@ public static function nullOrDate(string $format = 'Y-m-d'): ChainedValidator; public static function nullOrDateTime(?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public static function nullOrDateTimeDiff( + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public static function nullOrDecimal(int $decimals): ChainedValidator; public static function nullOrDigit(string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/StaticProperty.php b/library/Mixins/StaticProperty.php index 764f7aa3e..7c6be9553 100644 --- a/library/Mixins/StaticProperty.php +++ b/library/Mixins/StaticProperty.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface StaticProperty @@ -127,6 +128,17 @@ public static function propertyDate(string $propertyName, string $format = 'Y-m- public static function propertyDateTime(string $propertyName, ?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public static function propertyDateTimeDiff( + string $propertyName, + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public static function propertyDecimal(string $propertyName, int $decimals): ChainedValidator; public static function propertyDigit(string $propertyName, string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/StaticUndefOr.php b/library/Mixins/StaticUndefOr.php index 2e9a07096..e3b6e7c8d 100644 --- a/library/Mixins/StaticUndefOr.php +++ b/library/Mixins/StaticUndefOr.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface StaticUndefOr @@ -103,6 +104,16 @@ public static function undefOrDate(string $format = 'Y-m-d'): ChainedValidator; public static function undefOrDateTime(?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public static function undefOrDateTimeDiff( + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public static function undefOrDecimal(int $decimals): ChainedValidator; public static function undefOrDigit(string ...$additionalChars): ChainedValidator; diff --git a/library/Mixins/StaticValidator.php b/library/Mixins/StaticValidator.php index fd24dd6ab..d25afa885 100644 --- a/library/Mixins/StaticValidator.php +++ b/library/Mixins/StaticValidator.php @@ -9,6 +9,7 @@ namespace Respect\Validation\Mixins; +use DateTimeImmutable; use Respect\Validation\Validatable; interface StaticValidator extends @@ -103,6 +104,16 @@ public static function date(string $format = 'Y-m-d'): ChainedValidator; public static function dateTime(?string $format = null): ChainedValidator; + /** + * @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type + */ + public static function dateTimeDiff( + string $type, + Validatable $rule, + ?string $format = null, + ?DateTimeImmutable $now = null, + ): ChainedValidator; + public static function decimal(int $decimals): ChainedValidator; public static function digit(string ...$additionalChars): ChainedValidator; diff --git a/library/Rules/Core/Wrapper.php b/library/Rules/Core/Wrapper.php index b6ce71f7d..78f1c0760 100644 --- a/library/Rules/Core/Wrapper.php +++ b/library/Rules/Core/Wrapper.php @@ -50,4 +50,9 @@ public function setTemplate(string $template): static return $this; } + + public function getRule(): Validatable + { + return $this->rule; + } } diff --git a/library/Rules/DateTimeDiff.php b/library/Rules/DateTimeDiff.php new file mode 100644 index 000000000..75bf2acd6 --- /dev/null +++ b/library/Rules/DateTimeDiff.php @@ -0,0 +1,117 @@ + + * 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\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 in_array; + +#[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 CanValidateDateTime; + use CanExtractRules; + + private readonly Validatable $rule; + + /** @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type */ + public function __construct( + private readonly string $type, + Validatable $rule, + 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)) { + throw new InvalidRuleConstructorException( + '"%s" is not a valid type of age (Available: %s)', + $this->type, + $availableTypes + ); + } + $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); + } + + $now = $this->now ?? new DateTimeImmutable(); + $nextSibling = $this->rule + ->evaluate($this->comparisonValue($now, $compareTo)) + ->withNameIfMissing($input instanceof DateTimeInterface ? $input->format('c') : $input); + + $parameters = ['type' => $this->type, 'now' => $this->nowParameter($now)]; + + return (new Result($nextSibling->isValid, $input, $this, $parameters))->withNextSibling($nextSibling); + } + + private function comparisonValue(DateTimeInterface $now, DateTimeInterface $compareTo): int|float + { + return match ($this->type) { + 'years' => $compareTo->diff($now)->y, + 'months' => $compareTo->diff($now)->m, + 'days' => $compareTo->diff($now)->d, + 'hours' => $compareTo->diff($now)->h, + 'minutes' => $compareTo->diff($now)->i, + 'seconds' => $compareTo->diff($now)->s, + 'microseconds' => $compareTo->diff($now)->f, + }; + } + + 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 ($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; + } + + return $dateTime; + } +} diff --git a/tests/integration/rules/dateTimeDiff.phpt b/tests/integration/rules/dateTimeDiff.phpt new file mode 100644 index 000000000..43883438f --- /dev/null +++ b/tests/integration/rules/dateTimeDiff.phpt @@ -0,0 +1,104 @@ +--FILE-- + [v::dateTimeDiff('years', v::equals(2)), '1 year ago'], + 'With $type = "months"' => [v::dateTimeDiff('months', v::equals(3)), '2 months ago'], + 'With $type = "days"' => [v::dateTimeDiff('days', v::equals(4)), '3 days ago'], + 'With $type = "hours"' => [v::dateTimeDiff('hours', v::equals(5)), '4 hours ago'], + 'With $type = "minutes"' => [v::dateTimeDiff('minutes', v::equals(6)), '5 minutes ago'], + 'With $type = "microseconds"' => [v::dateTimeDiff('microseconds', v::equals(7)), '6 microseconds ago'], + 'With custom $format' => [v::dateTimeDiff('years', v::lessThan(8), 'd/m/Y'), '09/12/1988'], + 'With custom $now' => [v::dateTimeDiff('years', v::lessThan(9), null, new DateTimeImmutable()), '09/12/1988'], + 'Wrapped by "not"' => [v::not(v::dateTimeDiff('years', v::lessThan(8))), '7 year ago'], + 'Wrapping "not"' => [v::dateTimeDiff('years', v::not(v::lessThan(9))), '8 year ago'], +]); +?> +--EXPECTF-- +With $type = "years" +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of years between now and 1 year ago must equal 2 +- The number of years between now and 1 year ago must equal 2 +[ + 'dateTimeDiff' => 'The number of years between now and 1 year ago must equal 2', +] + +With $type = "months" +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of months between now and 2 months ago must equal 3 +- The number of months between now and 2 months ago must equal 3 +[ + 'dateTimeDiff' => '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 +[ + 'dateTimeDiff' => 'The number of days between now and 3 days ago must equal 4', +] + +With $type = "hours" +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of hours between now and 4 hours ago must equal 5 +- The number of hours between now and 4 hours ago must equal 5 +[ + 'dateTimeDiff' => 'The number of hours between now and 4 hours ago must equal 5', +] + +With $type = "minutes" +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of minutes between now and 5 minutes ago must equal 6 +- The number of minutes between now and 5 minutes ago must equal 6 +[ + 'dateTimeDiff' => 'The number of minutes between now and 5 minutes ago must equal 6', +] + +With $type = "microseconds" +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of microseconds between now and 6 microseconds ago must equal 7 +- The number of microseconds between now and 6 microseconds ago must equal 7 +[ + 'dateTimeDiff' => 'The number of microseconds between now and 6 microseconds ago must equal 7', +] + +With custom $format +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of years between %d/%d/%d and 09/12/1988 must be less than 8 +- The number of years between %d/%d/%d and 09/12/1988 must be less than 8 +[ + 'dateTimeDiff' => 'The number of years between %d/%d/%d and 09/12/1988 must be less than 8', +] + +With custom $now +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of years between %d-%d-%d %d:%d:%d.%d and 09/12/1988 must be less than 9 +- The number of years between %d-%d-%d %d:%d:%d.%d and 09/12/1988 must be less than 9 +[ + 'dateTimeDiff' => 'The number of years between %d-%d-%d %d:%d:%d.%d and 09/12/1988 must be less than 9', +] + +Wrapped by "not" +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of years between now and 7 year ago must not be less than 8 +- The number of years between now and 7 year ago must not be less than 8 +[ + 'notDateTimeDiff' => 'The number of years between now and 7 year ago must not be less than 8', +] + +Wrapping "not" +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of years between now and 8 year ago must not be less than 9 +- The number of years between now and 8 year ago must not be less than 9 +[ + 'dateTimeDiff' => 'The number of years between now and 8 year ago must not be less than 9', +] diff --git a/tests/unit/Rules/DateTimeDiffTest.php b/tests/unit/Rules/DateTimeDiffTest.php new file mode 100644 index 000000000..87c4d9a32 --- /dev/null +++ b/tests/unit/Rules/DateTimeDiffTest.php @@ -0,0 +1,129 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Rules; + +use DateTimeImmutable; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use Respect\Validation\Exceptions\InvalidRuleConstructorException; +use Respect\Validation\Test\Rules\Stub; +use Respect\Validation\Test\RuleTestCase; +use Respect\Validation\Validatable; +use Respect\Validation\Validator; + +use function array_map; +use function iterator_to_array; + +#[Group('rule')] +#[CoversClass(DateTimeDiff::class)] +final class DateTimeDiffTest extends RuleTestCase +{ + #[Test] + public function isShouldThrowAnExceptionWhenTypeIsNotValid(): void + { + $this->expectException(InvalidRuleConstructorException::class); + $this->expectExceptionMessageMatches('/"invalid" is not a valid type of age \(Available: .+\)/'); + + // @phpstan-ignore-next-line + new DateTimeDiff('invalid', Stub::daze()); + } + + #[Test] + #[DataProvider('providerForSiblingSuitableRules')] + public function isShouldAcceptRulesThatCanBeAddedAsNextSibling(Validatable $rule): void + { + $this->expectNotToPerformAssertions(); + + new DateTimeDiff('years', $rule); + } + + #[Test] + #[DataProvider('providerForSiblingUnsuitableRules')] + public function isShouldNotAcceptRulesThatCanBeAddedAsNextSibling(Validatable $rule): void + { + $this->expectException(InvalidRuleConstructorException::class); + $this->expectExceptionMessage('DateTimeDiff must contain exactly one rule'); + + new DateTimeDiff('years', $rule); + } + + /** @return array */ + public static function providerForSiblingSuitableRules(): array + { + return [ + 'single' => [Stub::daze()], + 'single in validator' => [Validator::create(Stub::daze())], + 'single wrapped by "Not"' => [new Not(Stub::daze())], + 'validator wrapping not, wrapping single' => [Validator::create(new Not(Stub::daze()))], + 'not wrapping validator, wrapping single' => [new Not(Validator::create(Stub::daze()))], + ]; + } + + /** @return array */ + public static function providerForSiblingUnsuitableRules(): array + { + return [ + 'double wrapped by validator' => [Validator::create(Stub::daze(), Stub::daze())], + 'double wrapped by validator, wrapped by "Not"' => [new Not(Validator::create(Stub::daze(), Stub::daze()))], + ]; + } + + /** @return array */ + public static function providerForValidInput(): array + { + return [ + 'years + DateTime' => [new DateTimeDiff('years', Stub::pass(1)), new DateTimeImmutable()], + 'years + string' => [new DateTimeDiff('years', Stub::pass(1)), '2020-01-01'], + 'years + string + format' => [new DateTimeDiff('years', Stub::pass(1), 'Y-m-d'), '2020-01-01'], + 'years + string + format + now' => [ + new DateTimeDiff('years', Stub::pass(1), 'Y-m-d', new DateTimeImmutable()), + '2020-01-01', + ], + 'years + string + now' => [ + new DateTimeDiff('years', Stub::pass(1), null, new DateTimeImmutable()), + '2020-01-01', + ], + 'years + DateTime + now' => [ + new DateTimeDiff('years', Stub::pass(1), null, new DateTimeImmutable()), + new DateTimeImmutable(), + ], + 'months + DateTime' => [new DateTimeDiff('months', Stub::pass(1)), new DateTimeImmutable()], + 'days + DateTime' => [new DateTimeDiff('days', Stub::pass(1)), new DateTimeImmutable()], + 'hours + DateTime' => [new DateTimeDiff('hours', Stub::pass(1)), new DateTimeImmutable()], + 'minutes + DateTime' => [new DateTimeDiff('minutes', Stub::pass(1)), new DateTimeImmutable()], + 'seconds + DateTime' => [new DateTimeDiff('seconds', Stub::pass(1)), new DateTimeImmutable()], + 'microseconds + DateTime' => [new DateTimeDiff('microseconds', Stub::pass(1)), new DateTimeImmutable()], + ]; + } + + /** @return array */ + public static function providerForInvalidInput(): array + { + return [ + 'valid date, with failing rule' => [ + new DateTimeDiff('years', Stub::fail(1)), + new DateTimeImmutable(), + ], + 'invalid date, with passing rule' => [ + new DateTimeDiff('years', Stub::pass(1), 'Y-m-d'), + 'invalid date', + ], + 'invalid date, with failing rule' => [ + new DateTimeDiff('years', Stub::fail(1), 'Y-m-d'), + 'invalid date', + ], + ] + array_map( + static fn (array $args): array => [new DateTimeDiff('years', Stub::fail(1)), new DateTimeImmutable()], + iterator_to_array(self::providerForNonScalarValues()) + ); + } +}