Skip to content
Merged
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
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@
"tiny-blocks/value-object": "^5.0"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.52",
"infection/infection": "^0.34",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^13.2",
"ergebnis/composer-normalize": "^2.54",
"infection/infection": "^0.35",
"phpstan/phpstan": "^2.3",
"phpunit/phpunit": "^13.4",
"slevomat/coding-standard": "^8.31",
"squizlabs/php_codesniffer": "^4.0"
},
Expand Down Expand Up @@ -79,7 +79,7 @@
"test-file": "@php ./vendor/bin/phpunit --configuration phpunit.xml --no-coverage --filter",
"tests": [
"@php -d memory_limit=2G ./vendor/bin/phpunit --configuration phpunit.xml tests",
"@php ./vendor/bin/infection --threads=max --logger-html=reports/coverage/mutation-report.html --coverage=reports/coverage"
"@php ./vendor/bin/infection --threads=max --logger-html=reports/coverage/mutation-report.html"
]
}
}
2 changes: 1 addition & 1 deletion src/DayOfWeek.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function isWeekend(): bool
* - Monday->distanceTo(Monday) returns 0
*
* @param DayOfWeek $other The target day of the week.
* @return int The number of days forward from this day to the other (0–6).
* @return int The number of days forward from this day to the other (0 to 6).
*/
public function distanceTo(DayOfWeek $other): int
{
Expand Down
2 changes: 1 addition & 1 deletion src/Internal/Decoders/OffsetDateTimeDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
final readonly class OffsetDateTimeDecoder implements Decoder
{
private const string FORMAT = 'Y-m-d\TH:i:sP';
private const string FORMAT_MICRO = 'Y-m-d\TH:i:s.uP';
private const string PATTERN = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+\-]\d{2}:\d{2})$/';
private const string FORMAT_MICRO = 'Y-m-d\TH:i:s.uP';
private const string PATTERN_MICRO = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(?:Z|[+\-]\d{2}:\d{2})$/';

public function decode(string $value): ?DateTimeImmutable
Expand Down
2 changes: 1 addition & 1 deletion tests/Models/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

final readonly class Event
{
public function __construct(public Instant $occurredAt, public string $name)
public function __construct(public string $name, public Instant $occurredAt)
{
}
}
110 changes: 55 additions & 55 deletions tests/Unit/DurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ public function testFromMinutesWhenZeroThenIsZero(): void
self::assertTrue($duration->isZero());
}

public function testEqualsWhenSameAmountThenIsTrue(): void
{
/** @Given the same amount of time reached by two units */
$one = Duration::fromMinutes(minutes: 2);
$other = Duration::fromSeconds(seconds: 120);

/** @Then they are equal by value and share the hash code */
self::assertTrue($one->equals(other: $other));
self::assertSame($one->hashCode(), $other->hashCode());
}

public function testZeroThenHoldsNoSecondsAndIsZero(): void
{
/** @When creating a zero Duration */
Expand Down Expand Up @@ -55,6 +66,17 @@ public function testToDaysWhenHasRemainderThenTruncates(): void
self::assertSame(1, $result);
}

public function testEqualsWhenDifferentAmountThenIsFalse(): void
{
/** @Given two durations one second apart */
$one = Duration::fromSeconds(seconds: 120);
$other = Duration::fromSeconds(seconds: 121);

/** @Then they are not equal and the hash codes differ */
self::assertFalse($one->equals(other: $other));
self::assertNotSame($one->hashCode(), $other->hashCode());
}

public function testFromDaysWhenZeroThenHoldsZeroSeconds(): void
{
/** @When creating a Duration from zero days */
Expand Down Expand Up @@ -418,39 +440,6 @@ public function testIsGreaterThanWhenDurationsAreEqualThenReturnsFalse(): void
self::assertFalse($result);
}

public function testDivideWhenAppointmentSplitIntoSlotsThenSlotCountIsExact(): void
{
/** @Given a Duration of 90 minutes */
$appointmentDuration = Duration::fromMinutes(minutes: 90);

/** @And a slot size of 30 minutes */
$slotSize = Duration::fromMinutes(minutes: 30);

/** @When dividing the appointment by the slot size */
$slotCount = $appointmentDuration->divide(other: $slotSize);

/** @Then the slot count should be 3 */
self::assertSame(3, $slotCount);
}

public function testFromMinutesWhenReconstructedFromSlotCountThenMatchesOriginal(): void
{
/** @Given a Duration of 90 minutes */
$appointmentDuration = Duration::fromMinutes(minutes: 90);

/** @And a slot size of 30 minutes */
$slotSize = Duration::fromMinutes(minutes: 30);

/** @And the slot count obtained by dividing the appointment by the slot size */
$slotCount = $appointmentDuration->divide(other: $slotSize);

/** @When reconstructing a Duration from the slot count and slot size */
$reconstructed = Duration::fromMinutes(minutes: ($slotCount * $slotSize->toMinutes()));

/** @Then it should match the original appointment duration */
self::assertSame($appointmentDuration->toSeconds(), $reconstructed->toSeconds());
}

public function testPlusAndMinusWhenAppliedWithSameAmountThenAreInverse(): void
{
/** @Given a Duration of 45 minutes */
Expand Down Expand Up @@ -481,6 +470,21 @@ public function testMinusWhenResultWouldBeNegativeThenThrowsInvalidSeconds(): vo
$tenMinutes->minus(other: $thirtyMinutes);
}

public function testDivideWhenAppointmentSplitIntoSlotsThenSlotCountIsExact(): void
{
/** @Given a Duration of 90 minutes */
$appointmentDuration = Duration::fromMinutes(minutes: 90);

/** @And a slot size of 30 minutes */
$slotSize = Duration::fromMinutes(minutes: 30);

/** @When dividing the appointment by the slot size */
$slotCount = $appointmentDuration->divide(other: $slotSize);

/** @Then the slot count should be 3 */
self::assertSame(3, $slotCount);
}

public function testFromSecondsWhenPositiveValueThenHoldsThatNumberOfSeconds(): void
{
/** @When creating a Duration from 1800 seconds */
Expand All @@ -493,6 +497,24 @@ public function testFromSecondsWhenPositiveValueThenHoldsThatNumberOfSeconds():
self::assertFalse($duration->isZero());
}

public function testFromMinutesWhenReconstructedFromSlotCountThenMatchesOriginal(): void
{
/** @Given a Duration of 90 minutes */
$appointmentDuration = Duration::fromMinutes(minutes: 90);

/** @And a slot size of 30 minutes */
$slotSize = Duration::fromMinutes(minutes: 30);

/** @And the slot count obtained by dividing the appointment by the slot size */
$slotCount = $appointmentDuration->divide(other: $slotSize);

/** @When reconstructing a Duration from the slot count and slot size */
$reconstructed = Duration::fromMinutes(minutes: ($slotCount * $slotSize->toMinutes()));

/** @Then it should match the original appointment duration */
self::assertSame($appointmentDuration->toSeconds(), $reconstructed->toSeconds());
}

public function testDivideWhenDivisorIsZeroDurationFromSecondsThenThrowsInvalidSeconds(): void
{
/** @Given a Duration of 30 minutes */
Expand Down Expand Up @@ -527,26 +549,4 @@ public function testFactoriesWhenSameDurationExpressedDifferentlyThenProduceSame
self::assertSame($fromMinutes->toSeconds(), $fromHours->toSeconds());
self::assertSame($fromHours->toSeconds(), $fromDays->toSeconds());
}

public function testEqualsWhenSameAmountThenIsTrue(): void
{
/** @Given the same amount of time reached by two units */
$one = Duration::fromMinutes(minutes: 2);
$other = Duration::fromSeconds(seconds: 120);

/** @Then they are equal by value and share the hash code */
self::assertTrue($one->equals(other: $other));
self::assertSame($one->hashCode(), $other->hashCode());
}

public function testEqualsWhenDifferentAmountThenIsFalse(): void
{
/** @Given two durations one second apart */
$one = Duration::fromSeconds(seconds: 120);
$other = Duration::fromSeconds(seconds: 121);

/** @Then they are not equal and the hash codes differ */
self::assertFalse($one->equals(other: $other));
self::assertNotSame($one->hashCode(), $other->hashCode());
}
}
4 changes: 2 additions & 2 deletions tests/Unit/InstantSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ public function testToArrayWhenInstantFieldThenEmitsIso8601Seconds(): void
$mapper = Mapper::create();

/** @And an event carrying an instant and a name */
$event = new Event(occurredAt: Instant::fromString(value: '2026-02-17T10:30:00+00:00'), name: 'launch');
$event = new Event(name: 'launch', occurredAt: Instant::fromString(value: '2026-02-17T10:30:00+00:00'));

/** @When the event is serialized to an array */
$array = $mapper->toArray(source: $event);

/** @Then the instant is written as a second-precision ISO 8601 string */
self::assertSame(['occurredAt' => '2026-02-17T10:30:00+00:00', 'name' => 'launch'], $array);
self::assertSame(['name' => 'launch', 'occurredAt' => '2026-02-17T10:30:00+00:00'], $array);
}

public function testToObjectWhenInstantFromOffsetStringThenNormalizesToUtc(): void
Expand Down
66 changes: 33 additions & 33 deletions tests/Unit/InstantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ public function testNowThenTimezoneIsUtc(): void
self::assertSame('UTC', $instant->toDateTimeImmutable()->getTimezone()->getName());
}

public function testEqualsWhenSameMomentThenIsTrue(): void
{
/** @Given two Instants built separately from the same moment */
$one = Instant::fromString(value: '2026-02-17T08:27:21.106011+00:00');
$other = Instant::fromString(value: '2026-02-17T08:27:21.106011+00:00');

/** @Then they are equal by value and share the hash code */
self::assertTrue($one->equals(other: $other));
self::assertSame($one->hashCode(), $other->hashCode());
}

public function testIsAfterWhenLaterThenReturnsTrue(): void
{
/** @Given a later Instant */
Expand Down Expand Up @@ -131,6 +142,17 @@ public function testNowThenProducesOrderedUtcInstances(): void
);
}

public function testEqualsWhenDifferentMomentThenIsFalse(): void
{
/** @Given two Instants one microsecond apart */
$one = Instant::fromString(value: '2026-02-17T08:27:21.106011+00:00');
$other = Instant::fromString(value: '2026-02-17T08:27:21.106012+00:00');

/** @Then they are not equal and the hash codes differ */
self::assertFalse($one->equals(other: $other));
self::assertNotSame($one->hashCode(), $other->hashCode());
}

public function testNowThenIso8601HasNoFractionalSeconds(): void
{
/** @Given an Instant created from now */
Expand Down Expand Up @@ -436,6 +458,17 @@ public function testPlusWhenCrossesDayBoundaryThenReturnsNextDay(): void
self::assertSame('2026-02-18T00:30:00+00:00', $shifted->toIso8601());
}

public function testEqualsWhenSameMomentInAnotherOffsetThenIsTrue(): void
{
/** @Given two Instants for the same moment written in different offsets */
$one = Instant::fromString(value: '2026-02-17T08:27:21+00:00');
$other = Instant::fromString(value: '2026-02-17T05:27:21-03:00');

/** @Then they are equal by value, because the state is normalized to UTC */
self::assertTrue($one->equals(other: $other));
self::assertSame($one->hashCode(), $other->hashCode());
}

public function testFromStringWhenDatabaseFormatThenTimezoneIsUtc(): void
{
/** @Given a database date-time string */
Expand Down Expand Up @@ -1306,37 +1339,4 @@ public static function validDatabaseStringsDataProvider(): array
]
];
}

public function testEqualsWhenSameMomentThenIsTrue(): void
{
/** @Given two Instants built separately from the same moment */
$one = Instant::fromString(value: '2026-02-17T08:27:21.106011+00:00');
$other = Instant::fromString(value: '2026-02-17T08:27:21.106011+00:00');

/** @Then they are equal by value and share the hash code */
self::assertTrue($one->equals(other: $other));
self::assertSame($one->hashCode(), $other->hashCode());
}

public function testEqualsWhenSameMomentInAnotherOffsetThenIsTrue(): void
{
/** @Given two Instants for the same moment written in different offsets */
$one = Instant::fromString(value: '2026-02-17T08:27:21+00:00');
$other = Instant::fromString(value: '2026-02-17T05:27:21-03:00');

/** @Then they are equal by value, because the state is normalized to UTC */
self::assertTrue($one->equals(other: $other));
self::assertSame($one->hashCode(), $other->hashCode());
}

public function testEqualsWhenDifferentMomentThenIsFalse(): void
{
/** @Given two Instants one microsecond apart */
$one = Instant::fromString(value: '2026-02-17T08:27:21.106011+00:00');
$other = Instant::fromString(value: '2026-02-17T08:27:21.106012+00:00');

/** @Then they are not equal and the hash codes differ */
self::assertFalse($one->equals(other: $other));
self::assertNotSame($one->hashCode(), $other->hashCode());
}
}
Loading