Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,49 @@ the locale:
{{ someDateTimeVariable|age(locale='es') }}
```

## Zodiac Sign

### Twig

```twig
{# With filter #}
{% set zodiac_sign = user.birthdate|zodiac_sign %}

{# ... or use the equivalent function: #}
{% set zodiac_sign = zodiac_sign(user.birthdate) %}

Your zodiac sign is {{ zodiac_sign.symbol }} {{ zodiac_sign|trans }}
{# outputs "Your zodiac sign is ♉ Taurus" #}

{# Get translated name with specific locale #}
{{ zodiac_sign|trans(locale='fr') }}
{# outputs "Taureau" #}
```

### Service

You can also format dates in your services/controllers by autowiring/injecting the
`Knp\Bundle\TimeBundle\DateTimeFormatter` service:

```php
use Knp\Bundle\TimeBundle\DateTimeFormatter;
// ...

public function yourAction(DateTimeFormatter $dateTimeFormatter)
{
$someDate = new \DateTimeImmutable('1990-08-01'); // or $entity->getBirthdate()

$zodiacSign = $dateTimeFormatter->calculateZodiacSign($someDate);

return $this->json([
// ...
'symbol' => $zodiacSign->getSymbol(), // ♌
'name' => $zodiacSign->trans($translator), // Leo
// ...
]);
}
```

## Tests

If you want to run tests, please check that you have installed dev dependencies.
Expand Down
23 changes: 23 additions & 0 deletions src/DateTimeFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ public function formatAge(
return $this->translator->trans('age', ['%count%' => $diff->y], 'time', $locale);
}

/**
* Returns the zodiac sign for the given date.
*/
public function calculateZodiacSign(\DateTimeInterface|string|int|float|null $date = null): ZodiacSign
{
$dateTime = match (true) {
null === $date => new \DateTimeImmutable(),
\is_int($date) => new \DateTimeImmutable('@'.$date),
\is_float($date) => new \DateTimeImmutable('@'.(string) (int) $date),
\is_string($date) => new \DateTimeImmutable($date),
$date instanceof \DateTimeImmutable => $date,
default => \DateTimeImmutable::createFromInterface($date),
};

foreach (ZodiacSign::cases() as $zodiacSign) {
if ($zodiacSign->contains($dateTime)) {
return $zodiacSign;
}
}

throw new \InvalidArgumentException(\sprintf('Unable to determine zodiac sign for date "%s".', $dateTime->format(\DateTimeInterface::RFC3339)));
}

private static function formatDateTime(int|string|\DateTimeInterface|null $value): \DateTimeInterface
{
if ($value instanceof \DateTimeInterface) {
Expand Down
8 changes: 8 additions & 0 deletions src/Twig/Extension/TimeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public function getFunctions(): array
[DateTimeFormatter::class, 'formatAge'],
['is_safe' => ['html']]
),
new TwigFunction(
'zodiac_sign',
[DateTimeFormatter::class, 'calculateZodiacSign'],
),
];
}

Expand All @@ -56,6 +60,10 @@ public function getFilters(): array
[DateTimeFormatter::class, 'formatAge'],
['is_safe' => ['html']]
),
new TwigFilter(
'zodiac_sign',
[DateTimeFormatter::class, 'calculateZodiacSign'],
),
];
}
}
77 changes: 77 additions & 0 deletions src/ZodiacSign.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Knp\Bundle\TimeBundle;

use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

enum ZodiacSign: string implements TranslatableInterface
{
case Aries = 'aries';
case Taurus = 'taurus';
case Gemini = 'gemini';
case Cancer = 'cancer';
case Leo = 'leo';
case Virgo = 'virgo';
case Libra = 'libra';
case Scorpio = 'scorpio';
case Sagittarius = 'sagittarius';
case Capricorn = 'capricorn';
case Aquarius = 'aquarius';
case Pisces = 'pisces';

public function trans(TranslatorInterface $translator, ?string $locale = null): string
{
return $translator->trans('zodiac_sign.'.$this->value, domain: 'zodiac_sign', locale: $locale);
}

public function getSymbol(): string
{
return match ($this) {
self::Aries => '♈',
self::Taurus => '♉',
self::Gemini => '♊',
self::Cancer => '♋',
self::Leo => '♌',
self::Virgo => '♍',
self::Libra => '♎',
self::Scorpio => '♏',
self::Sagittarius => '♐',
self::Capricorn => '♑',
self::Aquarius => '♒',
self::Pisces => '♓',
};
}

public function contains(\DateTimeImmutable $date): bool
{
$normalized = \DateTimeImmutable::createFromFormat('!m-d', $date->format('m-d'), new \DateTimeZone('UTC'));
$start = \DateTimeImmutable::createFromFormat('!m-d', $this->getDateRange()['start'], new \DateTimeZone('UTC'));
$end = \DateTimeImmutable::createFromFormat('!m-d', $this->getDateRange()['end'], new \DateTimeZone('UTC'));

if (self::Capricorn === $this) {
return $normalized >= $start || $normalized <= $end;
}

return $normalized >= $start && $normalized <= $end;
}

/** @return array{start: string, end: string} */
private function getDateRange(): array
{
return match ($this) {
self::Aquarius => ['start' => '01-20', 'end' => '02-18'],
self::Pisces => ['start' => '02-19', 'end' => '03-20'],
self::Aries => ['start' => '03-21', 'end' => '04-19'],
self::Taurus => ['start' => '04-20', 'end' => '05-20'],
self::Gemini => ['start' => '05-21', 'end' => '06-20'],
self::Cancer => ['start' => '06-21', 'end' => '07-22'],
self::Leo => ['start' => '07-23', 'end' => '08-22'],
self::Virgo => ['start' => '08-23', 'end' => '09-22'],
self::Libra => ['start' => '09-23', 'end' => '10-22'],
self::Scorpio => ['start' => '10-23', 'end' => '11-21'],
self::Sagittarius => ['start' => '11-22', 'end' => '12-21'],
self::Capricorn => ['start' => '12-22', 'end' => '01-19'],
};
}
}
12 changes: 12 additions & 0 deletions tests/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ public function testTimeDiffLocalTranslation(): void
$this->assertStringContainsString('il y a 1 mois', $result);
}

public function testZodiacSignTwigFilter(): void
{
$result = $this->kernel->getContainer()->get('public.twig')->render('@integration_test/zodiac_sign.twig', [
'date' => new \DateTimeImmutable('1990-08-01'),
]);

$this->assertStringContainsString('Filter value: leo', $result);
$this->assertStringContainsString('Filter symbol: ♌', $result);
$this->assertStringContainsString('Function value: leo', $result);
$this->assertStringContainsString('Function symbol: ♌', $result);
}

protected function setUp(): void
{
$this->kernel = new TimeBundleIntegrationTestKernel();
Expand Down
159 changes: 159 additions & 0 deletions tests/ZodiacSignCalculatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

namespace Knp\Bundle\TimeBundle\Tests;

use Knp\Bundle\TimeBundle\DateTimeFormatter;
use Knp\Bundle\TimeBundle\ZodiacSign;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatorInterface;

final class ZodiacSignCalculatorTest extends TestCase
{
private DateTimeFormatter $formatter;

protected function setUp(): void
{
$translator = $this->getMockBuilder(TranslatorInterface::class)
->disableOriginalConstructor()
->getMock();

$this->formatter = new DateTimeFormatter($translator);
}

/**
* @dataProvider zodiacSignProvider
*/
public function testCalculateZodiacSign(string $date, ZodiacSign $expectedSign): void
{
$inputs = [
'string' => $date,
'DateTime' => new \DateTime($date),
'DateTimeImmutable' => new \DateTimeImmutable($date),
];

foreach ($inputs as $type => $input) {
$result = $this->formatter->calculateZodiacSign($input);
$this->assertSame($expectedSign, $result, \sprintf('Failed for input type: %s', $type));
}
}

/**
* @dataProvider zodiacSignTimestampProvider
*/
public function testCalculateZodiacSignFromTimestamp(int $timestamp, ZodiacSign $expectedSign): void
{
$this->assertSame($expectedSign, $this->formatter->calculateZodiacSign($timestamp));
}

public function testCalculateZodiacSignFromFloat(): void
{
$this->assertSame(ZodiacSign::Aries, $this->formatter->calculateZodiacSign(954028800.5));
}

public function testCalculateZodiacSignWithNullDefaultsToNow(): void
{
$this->assertContains($this->formatter->calculateZodiacSign(null), ZodiacSign::cases());
}

public function testCalculateZodiacSignWithoutParameterDefaultsToNow(): void
{
$this->assertContains($this->formatter->calculateZodiacSign(), ZodiacSign::cases());
}

public function testCalculateZodiacSignWithInvalidDateThrows(): void
{
$this->expectException(\Exception::class);
$this->formatter->calculateZodiacSign('not-a-date');
}

public function testBoundaryDates(): void
{
$this->assertSame(ZodiacSign::Pisces, $this->formatter->calculateZodiacSign('2000-03-20'));
$this->assertSame(ZodiacSign::Aries, $this->formatter->calculateZodiacSign('2000-03-21'));
$this->assertSame(ZodiacSign::Aries, $this->formatter->calculateZodiacSign('2000-04-19'));
$this->assertSame(ZodiacSign::Taurus, $this->formatter->calculateZodiacSign('2000-04-20'));
}

public function testLeapYearDate(): void
{
$this->assertSame(ZodiacSign::Pisces, $this->formatter->calculateZodiacSign('2000-02-29'));
}

public function testSameSignAcrossDifferentYears(): void
{
$this->assertSame(ZodiacSign::Leo, $this->formatter->calculateZodiacSign('1990-08-01'));
$this->assertSame(ZodiacSign::Leo, $this->formatter->calculateZodiacSign('2000-08-01'));
$this->assertSame(ZodiacSign::Leo, $this->formatter->calculateZodiacSign('2050-08-01'));
}

public static function zodiacSignProvider(): array
{
return [
'Aries - start (March 21)' => ['2000-03-21', ZodiacSign::Aries],
'Aries - middle (April 1)' => ['2000-04-01', ZodiacSign::Aries],
'Aries - end (April 19)' => ['2000-04-19', ZodiacSign::Aries],

'Taurus - start (April 20)' => ['2000-04-20', ZodiacSign::Taurus],
'Taurus - middle (May 1)' => ['2000-05-01', ZodiacSign::Taurus],
'Taurus - end (May 20)' => ['2000-05-20', ZodiacSign::Taurus],

'Gemini - start (May 21)' => ['2000-05-21', ZodiacSign::Gemini],
'Gemini - middle (June 1)' => ['2000-06-01', ZodiacSign::Gemini],
'Gemini - end (June 20)' => ['2000-06-20', ZodiacSign::Gemini],

'Cancer - start (June 21)' => ['2000-06-21', ZodiacSign::Cancer],
'Cancer - middle (July 1)' => ['2000-07-01', ZodiacSign::Cancer],
'Cancer - end (July 22)' => ['2000-07-22', ZodiacSign::Cancer],

'Leo - start (July 23)' => ['2000-07-23', ZodiacSign::Leo],
'Leo - middle (August 1)' => ['2000-08-01', ZodiacSign::Leo],
'Leo - end (August 22)' => ['2000-08-22', ZodiacSign::Leo],

'Virgo - start (August 23)' => ['2000-08-23', ZodiacSign::Virgo],
'Virgo - middle (September 1)' => ['2000-09-01', ZodiacSign::Virgo],
'Virgo - end (September 22)' => ['2000-09-22', ZodiacSign::Virgo],

'Libra - start (September 23)' => ['2000-09-23', ZodiacSign::Libra],
'Libra - middle (October 1)' => ['2000-10-01', ZodiacSign::Libra],
'Libra - end (October 22)' => ['2000-10-22', ZodiacSign::Libra],

'Scorpio - start (October 23)' => ['2000-10-23', ZodiacSign::Scorpio],
'Scorpio - middle (November 1)' => ['2000-11-01', ZodiacSign::Scorpio],
'Scorpio - end (November 21)' => ['2000-11-21', ZodiacSign::Scorpio],

'Sagittarius - start (November 22)' => ['2000-11-22', ZodiacSign::Sagittarius],
'Sagittarius - middle (December 1)' => ['2000-12-01', ZodiacSign::Sagittarius],
'Sagittarius - end (December 21)' => ['2000-12-21', ZodiacSign::Sagittarius],

'Capricorn - start (December 22)' => ['2000-12-22', ZodiacSign::Capricorn],
'Capricorn - middle (January 1)' => ['2000-01-01', ZodiacSign::Capricorn],
'Capricorn - end (January 19)' => ['2000-01-19', ZodiacSign::Capricorn],

'Aquarius - start (January 20)' => ['2000-01-20', ZodiacSign::Aquarius],
'Aquarius - middle (February 1)' => ['2000-02-01', ZodiacSign::Aquarius],
'Aquarius - end (February 18)' => ['2000-02-18', ZodiacSign::Aquarius],

'Pisces - start (February 19)' => ['2000-02-19', ZodiacSign::Pisces],
'Pisces - middle (March 1)' => ['2000-03-01', ZodiacSign::Pisces],
'Pisces - end (March 20)' => ['2000-03-20', ZodiacSign::Pisces],
];
}

public static function zodiacSignTimestampProvider(): array
{
return [
'Aries (March 25, 2000)' => [954028800, ZodiacSign::Aries],
'Taurus (May 1, 2000)' => [957139200, ZodiacSign::Taurus],
'Gemini (June 1, 2000)' => [959817600, ZodiacSign::Gemini],
'Cancer (July 1, 2000)' => [962409600, ZodiacSign::Cancer],
'Leo (August 1, 2000)' => [965088000, ZodiacSign::Leo],
'Virgo (September 1, 2000)' => [967766400, ZodiacSign::Virgo],
'Libra (October 1, 2000)' => [970358400, ZodiacSign::Libra],
'Scorpio (November 1, 2000)' => [973036800, ZodiacSign::Scorpio],
'Sagittarius (December 1, 2000)' => [975628800, ZodiacSign::Sagittarius],
'Capricorn (January 1, 2000)' => [946684800, ZodiacSign::Capricorn],
'Aquarius (February 1, 2000)' => [949363200, ZodiacSign::Aquarius],
'Pisces (March 1, 2000)' => [951868800, ZodiacSign::Pisces],
];
}
}
4 changes: 4 additions & 0 deletions tests/fixtures/zodiac_sign.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Filter value: {{ date|zodiac_sign.value }}
Filter symbol: {{ date|zodiac_sign.symbol }}
Function value: {{ zodiac_sign(date).value }}
Function symbol: {{ zodiac_sign(date).symbol }}
Loading
Loading