Skip to content

Chronos fluent setters set the incorrect month under some scenarios. #486

@josep11

Description

@josep11

I have been trying to understand what is the issue why it is setting the wrong month when using the ->month() fluent setter.
I outline how to reproduce the issue, which can be seen at the composeDatetime method.
At the bottom of the bug, you can see a workaround that does work, for the provided scenarios.

Function Body (and dependencies):

class Service
{
    private function composeDatetime(?Chronos $eventDatetime, Chronos $eventSeriesDay): Chronos
    {
        if (null === $eventDatetime) {
            throw new LogicException();
        }

        $year = $eventSeriesDay->year;
        $month = $eventSeriesDay->month;
        $day = $eventSeriesDay->day;

// This does not work
        return Chronos::create()
            ->year($year)
            ->month($month)
            ->day($day)
            ->hour($eventDatetime->hour)
            ->minute($eventDatetime->minute)
            ->second($eventDatetime->second);
    }

    public function fixStartAndEndDatesInRequest(
        EventRequest $eventRequest,
        Chronos $eventSeriesDay,
    ): void
    {
        $datetimeStart = $eventRequest->getDatetimeStart();
        $datetimeEnd = $eventRequest->getDatetimeEnd();

        $eventDatetimeStart = $this->composeDatetime($datetimeStart, $eventSeriesDay);
        $eventDatetimeEnd = $this->composeDatetime($datetimeEnd, $eventSeriesDay);

        $eventRequest->setDatetimeStart($eventDatetimeStart);
        $eventRequest->setDatetimeEnd($eventDatetimeEnd);
    }
}
// dependencies for reproduction
class EventRequest {
    private ?Chronos $datetimeStart = null;
    private ?Chronos $datetimeEnd = null;
    
    public function setDatetimeStart(?Chronos $datetimeStart): self {
        $this->datetimeStart = $datetimeStart;
        
        return $this;
    }
    
    public function setDatetimeEnd(?Chronos $datetimeEnd): self {
        $this->datetimeEnd = $datetimeEnd;

        return $this;
    }
    
    public function getDatetimeStart(): ?Chronos {
        return $this->datetimeStart;
    }
    
    public function getDatetimeEnd(): ?Chronos {
        return $this->datetimeEnd;
    }
}

Unit Test:

class ChronosTest extends \PHPUnit\Framework\TestCase
{

    #[DataProvider('dataProviderForFixStartAndEndDatesInRequestDto')]
    public function testFixStartAndEndDatesInRequestDto(array $provider): void
    {
        $datetimeStart = $provider['datetimeStart'];
        $datetimeEnd = $provider['datetimeEnd'];
        $eventSeriesDay = $provider['eventSeriesDay'];

        $expectedDateTimeStart = $provider['expectedDateTimeStart'];
        $expectedDateTimeEnd = $provider['expectedDateTimeEnd'];

        $requestDto = new EventRequest()
            ->setDatetimeStart($datetimeStart)
            ->setDatetimeEnd($datetimeEnd);

        $this->service->fixStartAndEndDatesInRequest($requestDto, $eventSeriesDay);

        // Assert the date parts are updated correctly
        $this->assertEquals(
            $expectedDateTimeStart,
            $requestDto->getDatetimeStart()->toNative(),
            'Start date was not updated correctly'
        );
        $this->assertEquals(
            $expectedDateTimeEnd,
            $requestDto->getDatetimeEnd()->toNative(),
            'End date was not updated correctly'
        );

        // Assert the times are ok
        $this->assertEquals(
            $datetimeStart->toTimeString(),
            $requestDto->getDatetimeStart()->toTimeString(),
            'Start time was not updated correctly'
        );
        $this->assertEquals(
            $datetimeEnd->toTimeString(),
            $requestDto->getDatetimeEnd()->toTimeString(),
            'End time was not updated correctly'
        );
    }

    public static function dataProviderForFixStartAndEndDatesInRequestDto(): array
    {
        $datetimeStartOct = new Chronos('2025-10-27 09:00:00');
        $datetimeEndOct = new Chronos('2025-10-27 11:00:00');

        $datetimeStartJan = new Chronos('2026-01-29 09:00:00.000000');
        $datetimeEndJan = new Chronos('2026-01-29 10:00:00.000000');

        $datetimeStartOct2 = new Chronos('2025-10-29 09:00:00');
        $datetimeEndOct2 = new Chronos('2025-10-29 11:00:00');

        return [
            'Event start date is on October, series day is in November' => [
                [
                    'datetimeStart' => $datetimeStartOct,
                    'datetimeEnd' => $datetimeEndOct,
                    'eventSeriesDay' => new Chronos('2025-11-01 00:00:00'),

                    'expectedDateTimeStart' => $datetimeStartOct->setDate(2025, 11, 1)->toNative(),
                    'expectedDateTimeEnd' => $datetimeEndOct->setDate(2025, 11, 1)->toNative(),
                ],
            ],
            'Event start date is on October, series day is in February' => [
                [
                    'datetimeStart' => $datetimeStartOct,
                    'datetimeEnd' => $datetimeEndOct,
                    'eventSeriesDay' => new Chronos('2026-02-04 00:00:00'),

                    'expectedDateTimeStart' => $datetimeStartOct->setDate(2026, 2, 4)->toNative(),
                    'expectedDateTimeEnd' => $datetimeEndOct->setDate(2026, 2, 4)->toNative(),
                ],
            ],
            'Event start date is on October, series day is in February (2)' => [
                [
                    'datetimeStart' => $datetimeStartOct2,
                    'datetimeEnd' => $datetimeEndOct2,
                    'eventSeriesDay' => new Chronos('2026-02-04 00:00:00'),

                    'expectedDateTimeStart' => $datetimeStartOct2->setDate(2026, 2, 4)->toNative(),
                    'expectedDateTimeEnd' => $datetimeEndOct2->setDate(2026, 2, 4)->toNative(),
                ],
            ],
            'Event start date is on January, series day is in February' => [
                [
                    'datetimeStart' => $datetimeStartJan,
                    'datetimeEnd' => $datetimeEndJan,
                    'eventSeriesDay' => new Chronos('2026-02-04 00:00:00'),

                    'expectedDateTimeStart' => $datetimeStartJan->setDate(2026, 2, 4)->toNative(),
                    'expectedDateTimeEnd' => $datetimeEndJan->setDate(2026, 2, 4)->toNative(),
                ],
            ],
        ];
    }
}

Now, if using the Chronos::create with parameters, straight away, it does work.

private function composeDatetime(?Chronos $eventDatetime, Chronos $eventSeriesDay): Chronos
    {
        if (null === $eventDatetime) {
            throw new LogicException();
        }

        $year = $eventSeriesDay->year;
        $month = $eventSeriesDay->month;
        $day = $eventSeriesDay->day;

// works
        return Chronos::create(
            $year,
            $month,
            $day,
            $eventDatetime->hour,
            $eventDatetime->minute,
            $eventDatetime->second,
        );
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions