Skip to content

Commit

Permalink
Merge pull request #207 from ramchale/DateSelect
Browse files Browse the repository at this point in the history
Refactor DateSelect
  • Loading branch information
gsteel authored Dec 10, 2024
2 parents f7c2743 + e6fa450 commit cc21318
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 21 deletions.
18 changes: 18 additions & 0 deletions docs/book/v3/migration/v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ The following methods have been removed:

The constructor now only accepts an associative array of [documented options](../word.md#dashtoseparator).

#### `DateSelect`

The following methods have been removed:

- `setOptions`
- `getOptions`
- `setNullOnAllEmpty`
- `isNullOnAllEmpty`
- `setNullOnEmpty`
- `isNullOnEmpty`

The constructor now only accepts an associative array of [documented options](../standard-filters.md#dateselect).

RuntimeException are no longer thrown when the filter receives an array with the incorrect number of elements.

All invalid values passed to the filter, invalid calendar dates, will now return the original value.
Validators should be used to ensure the input has been filtered as expected, and to enforce any additional constraints.

#### `DenyList`

The following methods have been removed:
Expand Down
25 changes: 24 additions & 1 deletion docs/book/v3/standard-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,29 @@ $filter = new Laminas\Filter\CompressToArchive([
$archiveLocation = $filter->filter('/path/to/file.txt');
```

## DateSelect

`Laminas\Filter\DateSelect` allows you to filter a day, month, and year value into a dash separated string.

### Supported Options

The following options are supported for `Laminas\Filter\DateSelect`:

- `null_on_empty` => This defaults to `false`.
If set to `true`, the filter will return `null` if day, month, or year are empty.
- `null_on_all_empty` => This defaults to `false`.
If set to `true`, the filter will return `null` if day, month, and year are empty.

### Basic Usage

```php
$filter = new Laminas\Filter\DateSelect();

print $filter->filter(['day' => '1', 'month' => '2', 'year' => '2012']);
````

This will return '2012-02-01'.

## DateTimeFormatter

This filter formats either a `DateTimeInterface` object, a string, or integer that `DateTime` will understand to a date
Expand Down Expand Up @@ -706,7 +729,7 @@ This will return `-4.4` (as a float).

## MonthSelect

`Laminas\Filter\MonthSelect` allows you to filter a month and year value into a hyphen dash string.
`Laminas\Filter\MonthSelect` allows you to filter a month and year value into a dash separated string.

### Supported Options

Expand Down
97 changes: 85 additions & 12 deletions src/DateSelect.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,98 @@

namespace Laminas\Filter;

use DateTime;

use function is_array;
use function is_numeric;
use function sprintf;

/**
* @psalm-type Options = array{
* null_on_empty?: bool,
* null_on_all_empty?: bool,
* ...
* }
* @psalm-type InputArray = array{
* year: numeric,
* month: numeric,
* day: numeric,
* }
* @template TOptions of Options
* @template-extends AbstractDateDropdown<TOptions, InputArray>
* @implements FilterInterface<string|null>
*/
final class DateSelect extends AbstractDateDropdown
final class DateSelect implements FilterInterface
{
private readonly bool $returnNullIfAnyFieldEmpty;
private readonly bool $returnNullIfAllFieldsEmpty;

/** @param Options $options */
public function __construct(array $options = [])
{
$this->returnNullIfAnyFieldEmpty = $options['null_on_empty'] ?? false;
$this->returnNullIfAllFieldsEmpty = $options['null_on_all_empty'] ?? false;
}

public function __invoke(mixed $value): mixed
{
return $this->filter($value);
}

/**
* Year-Month-Day
* Returns the result of filtering $value
*
* @template T
* @param T $value
* @return string|null|T
*/
protected string $format = '%3$s-%2$s-%1$s';
protected int $expectedInputs = 3;
public function filter(mixed $value): mixed
{
if (! is_array($value)) {
return $value;
}

$day = $this->getValue($value, 'day');
$month = $this->getValue($value, 'month');
$year = $this->getValue($value, 'year');

if ($this->returnNullIfAnyFieldEmpty && ($day === null || $month === null || $year === null)) {
return null;
}

if ($this->returnNullIfAllFieldsEmpty && $day === null && $month === null && $year === null) {
return null;
}

if ($day === null || $month === null || $year === null) {
return $value;
}

if (! $this->isParsableAsDateValue($day, $month, $year)) {
/** @psalm-var T */
return $value;
}

return sprintf('%d-%02d-%02d', $year, $month, $day);
}

/**
* @psalm-assert-if-true int $day
* @psalm-assert-if-true int $month
* @psalm-assert-if-true int $year
*/
private function isParsableAsDateValue(mixed $day, mixed $month, mixed $year): bool
{
if (! is_numeric($day) || ! is_numeric($month) || ! is_numeric($year)) {
return false;
}

$date = DateTime::createFromFormat('Y-m-d', $year . '-' . $month . '-' . $day);

if (! $date || $date->format('Ymd') !== sprintf('%d%02d%02d', $year, $month, $day)) {
return false;
}

return true;
}

/** @param mixed[] $value */
private function getValue(array $value, string $string): mixed
{
/** @var mixed $result */
$result = $value[$string] ?? null;
return $result === '' ? null : $result;
}
}
51 changes: 43 additions & 8 deletions test/DateSelectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,71 @@
namespace LaminasTest\Filter;

use Laminas\Filter\DateSelect as DateSelectFilter;
use Laminas\Filter\Exception\RuntimeException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

/** @psalm-import-type Options from DateSelectFilter */
class DateSelectTest extends TestCase
{
/** @param Options $options */
#[DataProvider('provideFilter')]
public function testFilter(array $options, array $input, ?string $expected): void
{
$sut = new DateSelectFilter();
$sut->setOptions($options);
$sut = new DateSelectFilter($options);

self::assertSame($expected, $sut->filter($input));
}

/** @return list<array{0: array, 1: array, 2: string|null}> */
public static function provideFilter(): array
{
return [
[[], ['year' => '2014', 'month' => '2', 'day' => '7'], '2014-02-07'],
[[], ['year' => '2014', 'month' => '10', 'day' => '26'], '2014-10-26'],
[['nullOnEmpty' => true], ['year' => null, 'month' => '10', 'day' => '26'], null],
[[], ['year' => 2014, 'month' => 10, 'day' => 26], '2014-10-26'],
[['null_on_empty' => true], ['year' => null, 'month' => '10', 'day' => '26'], null],
[['nullOnAllEmpty' => true], ['year' => null, 'month' => null, 'day' => null], null],
[['null_on_empty' => true], ['month' => null], null],
[['null_on_empty' => true], ['year' => null], null],
[['null_on_empty' => true], ['day' => null], null],
[['null_on_all_empty' => true], ['year' => null, 'month' => null, 'day' => null], null],
[['null_on_all_empty' => true], [], null],
[['null_on_all_empty' => true], ['year' => '', 'month' => '', 'day' => ''], null],
];
}

public function testInvalidInput(): void
#[DataProvider('provideInvalidFilterValues')]
public function testInvalidInput(mixed $value): void
{
$this->expectException(RuntimeException::class);
$sut = new DateSelectFilter();
$sut->filter(['year' => '2120', 'month' => '07']);

self::assertSame($value, $sut->filter($value));
}

/** @return array<string, array{0: mixed}> */
public static function provideInvalidFilterValues(): array
{
return [
'empty array' => [[]],
'missing year' => [['day' => '2', 'month' => '10']],
'missing month' => [['day' => '2', 'year' => '2023']],
'missing day' => [['month' => '10', 'year' => '2023']],
'passed bool' => [true],
'passed string' => ['string'],
'passed int' => [10],
'passed float' => [10.5],
'invalid keys' => [['not year' => '2014', 'not month' => '10', 'not day' => '2']],
'year is invalid type' => [['year' => true, 'month' => '09', 'day' => '2']],
'year is float' => [['year' => '1.5', 'month' => '09', 'day' => '2']],
'year out of bounds' => [['year' => '-1', 'month' => '09', 'day' => '2']],
'month is too high' => [['year' => '2014', 'month' => '13', 'day' => '2']],
'month is low' => [['year' => '2014', 'month' => '0', 'day' => '2']],
'month is invalid type' => [['year' => '2014', 'month' => true, 'day' => '2']],
'day is too high' => [['year' => '2014', 'month' => '2', 'day' => '30']],
'day is low' => [['year' => '2014', 'month' => '0', 'day' => '2']],
'day is invalid type' => [['year' => '2014', 'month' => '09', 'day' => true]],
'invalid year' => [['year' => 'not a year', 'month' => '10', 'day' => '2']],
'invalid month' => [['year' => '2023', 'month' => 'not a month', 'day' => '2']],
'invalid day' => [['year' => '2023', 'month' => '10', 'day' => 'not a day']],
];
}
}

0 comments on commit cc21318

Please sign in to comment.