Skip to content

Commit cf31218

Browse files
committed
PS-935 databox: factorize date normalization
1 parent 48782de commit cf31218

File tree

4 files changed

+126
-57
lines changed

4 files changed

+126
-57
lines changed

databox/api/src/Attribute/Type/DateTimeAttributeType.php

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use App\Elasticsearch\ESFacetInterface;
88
use App\Elasticsearch\SearchType;
9+
use App\Util\DateUtil;
910
use Symfony\Component\Validator\Context\ExecutionContextInterface;
1011

1112
class DateTimeAttributeType extends AbstractAttributeType
@@ -76,64 +77,18 @@ public function getElasticSearchTextSubField(): ?string
7677
public function normalizeValue($value): ?string
7778
{
7879
if (!$value instanceof \DateTimeInterface) {
79-
if (empty($value)) {
80-
return null;
81-
}
82-
83-
$value = trim((string) $value);
84-
if (empty($value)) {
85-
return null;
86-
}
87-
88-
foreach ([
89-
[
90-
'p' => '#^(\d{4})\D(\d{2})\D(\d{2})$#',
91-
'f' => '%04d-%02d-%02dT00:00:00Z',
92-
'm' => [1, 2, 3]],
93-
[
94-
'p' => '#^(\d{4})\D(\d{2})\D(\d{2})\D(\d{2})\D(\d{2})\D(\d{2})$#',
95-
'f' => '%04d-%02d-%02dT%02d:%02d:%02dZ',
96-
'm' => [1, 2, 3, 4, 5, 6]],
97-
[
98-
'p' => '#^(\d{4})\D(\d{2})\D(\d{2})T(\d{2})\D(\d{2})$#',
99-
'f' => '%04d-%02d-%02dT%02d:%02d:00Z',
100-
'm' => [1, 2, 3, 4, 5]],
101-
] as $tryout) {
102-
$matches = [];
103-
if (1 === preg_match($tryout['p'], $value, $matches)) {
104-
// m is the mapping from matches[x] to arg[i] for vsprintf(f, args)
105-
$args = array_map(fn ($a) => (int) $matches[$a], $tryout['m']);
106-
$value = vsprintf($tryout['f'], $args);
107-
break;
108-
}
109-
}
110-
if (false === $value = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $value)) {
111-
return null;
112-
}
80+
$value = DateUtil::normalizeDate($value);
11381
}
11482

115-
return $value->format(\DateTimeInterface::ATOM);
83+
return $value?->format(\DateTimeInterface::ATOM);
11684
}
11785

11886
/**
11987
* @return \DateTimeImmutable|null
12088
*/
12189
public function denormalizeValue(?string $value)
12290
{
123-
if (null === $value) {
124-
return null;
125-
}
126-
127-
try {
128-
$date = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $value);
129-
if (false === $date) {
130-
return null;
131-
}
132-
133-
return $date;
134-
} catch (\Throwable) {
135-
return null;
136-
}
91+
return DateUtil::normalizeDate($value);
13792
}
13893

13994
public function validate($value, ExecutionContextInterface $context): void

databox/api/src/Elasticsearch/AQL/Function/AbstractDateFunction.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@
22

33
namespace App\Elasticsearch\AQL\Function;
44

5+
use App\Util\DateUtil;
6+
57
abstract readonly class AbstractDateFunction implements AQLFunctionInterface
68
{
79
protected function normalizeDate(mixed $date): \DateTimeImmutable
810
{
9-
if ($date instanceof \DateTimeInterface) {
10-
return \DateTimeImmutable::createFromInterface($date);
11-
}
12-
13-
if (is_int($date)) {
14-
return new \DateTimeImmutable('@'.$date);
15-
} elseif (is_string($date)) {
16-
return new \DateTimeImmutable($date);
11+
$d = DateUtil::normalizeDate($date);
12+
if ($d instanceof \DateTimeImmutable) {
13+
return $d;
1714
}
1815

1916
throw new \InvalidArgumentException('Invalid date format %s', get_debug_type($date));

databox/api/src/Util/DateUtil.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\Util;
4+
5+
abstract class DateUtil
6+
{
7+
public static function normalizeDate(mixed $date): ?\DateTimeImmutable
8+
{
9+
if (empty($date)) {
10+
return null;
11+
}
12+
13+
if ($date instanceof \DateTimeInterface) {
14+
return \DateTimeImmutable::createFromInterface($date);
15+
} elseif (is_int($date)) {
16+
return new \DateTimeImmutable('@'.$date);
17+
} elseif (is_string($date)) {
18+
$value = $date;
19+
foreach ([
20+
[
21+
'p' => '#^(\d{4})\D(\d{2})\D(\d{2})$#',
22+
'f' => '%04d-%02d-%02dT00:00:00Z',
23+
'm' => [1, 2, 3]],
24+
[
25+
'p' => '#^(\d{4})\D(\d{2})\D(\d{2})\D(\d{2})\D(\d{2})\D(\d{2})$#',
26+
'f' => '%04d-%02d-%02dT%02d:%02d:%02dZ',
27+
'm' => [1, 2, 3, 4, 5, 6]],
28+
[
29+
'p' => '#^(\d{4})\D(\d{2})\D(\d{2})T(\d{2})\D(\d{2})$#',
30+
'f' => '%04d-%02d-%02dT%02d:%02d:00Z',
31+
'm' => [1, 2, 3, 4, 5]],
32+
] as $tryout) {
33+
$matches = [];
34+
if (1 === preg_match($tryout['p'], $date, $matches)) {
35+
$args = array_map(fn (string $a): string => (int) $matches[$a], $tryout['m']);
36+
$value = vsprintf($tryout['f'], $args);
37+
break;
38+
}
39+
}
40+
41+
if (false === $value = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $value)) {
42+
return null;
43+
}
44+
45+
return $value;
46+
}
47+
48+
return null;
49+
}
50+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace App\Tests\Util;
4+
5+
use App\Util\DateUtil;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class DateUtilTest extends TestCase
9+
{
10+
public function testNormalizeDateWithNull()
11+
{
12+
$this->assertNull(DateUtil::normalizeDate(null));
13+
}
14+
15+
public function testNormalizeDateWithEmptyString()
16+
{
17+
$this->assertNull(DateUtil::normalizeDate(''));
18+
}
19+
20+
public function testNormalizeDateWithDateTime()
21+
{
22+
$dt = new \DateTime('2024-01-01 12:00:00');
23+
$result = DateUtil::normalizeDate($dt);
24+
$this->assertInstanceOf(\DateTimeImmutable::class, $result);
25+
$this->assertEquals($dt->format(\DateTimeInterface::ATOM), $result->format(\DateTimeInterface::ATOM));
26+
}
27+
28+
public function testNormalizeDateWithTimestamp()
29+
{
30+
$timestamp = 1704067200; // 2024-01-01T00:00:00+00:00
31+
$result = DateUtil::normalizeDate($timestamp);
32+
$this->assertInstanceOf(\DateTimeImmutable::class, $result);
33+
$this->assertSame((string) $timestamp, $result->format('U'));
34+
}
35+
36+
public function testNormalizeDateWithDateString()
37+
{
38+
$result = DateUtil::normalizeDate('2024-01-02');
39+
$this->assertInstanceOf(\DateTimeImmutable::class, $result);
40+
$this->assertEquals('2024-01-02T00:00:00+00:00', $result->format('Y-m-d\TH:i:sP'));
41+
}
42+
43+
public function testNormalizeDateWithDateTimeString()
44+
{
45+
$result = DateUtil::normalizeDate('2024-01-02T13:45');
46+
$this->assertInstanceOf(\DateTimeImmutable::class, $result);
47+
$this->assertEquals('2024-01-02T13:45:00+00:00', $result->format('Y-m-d\TH:i:sP'));
48+
}
49+
50+
public function testNormalizeDateWithFullDateTimeString()
51+
{
52+
$result = DateUtil::normalizeDate('2024-01-02 13:45:59');
53+
$this->assertInstanceOf(\DateTimeImmutable::class, $result);
54+
$this->assertEquals('2024-01-02T13:45:59+00:00', $result->format('Y-m-d\TH:i:sP'));
55+
}
56+
57+
public function testNormalizeDateWithInvalidString()
58+
{
59+
$this->assertNull(DateUtil::normalizeDate('not-a-date'));
60+
}
61+
62+
public function testNormalizeDateWithUnsupportedType()
63+
{
64+
$this->assertNull(DateUtil::normalizeDate([]));
65+
$this->assertNull(DateUtil::normalizeDate(new \stdClass()));
66+
}
67+
}

0 commit comments

Comments
 (0)