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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
with 100 `NK1` parses in 8.8 ms instead of 11.5 ms under PHP 8.5 with opcache on the CLI; what is
left is field hydration. Parsed structures, ordering, and exception messages are unchanged.

### Fixed

- A field declared with `maxReps: N` now accepts exactly N repetitions; the (N+1)th throws
`OutOfBoundsException`. Previously a 0-based repetition index was compared to the maximum with
`>`, so every capped field accepted one repetition too many — each `maxReps: 1` field accepted
two. The exception message now reports the 1-based repetition number so it agrees with the
allowed count.

## [0.10.0] - 2026-08-10

### Changed
Expand Down
11 changes: 8 additions & 3 deletions src/AbstractSegment.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,22 @@ private function assertRepetitionAllowed(int $number, int $repetition): void
{
$total = $this->getLength($number);

// Repetition indexes are 0-based, but the message reports the 1-based ordinal.
$ordinal = $repetition + 1;

if ($repetition > $total) {
throw new OutOfBoundsException(
"Cannot get repetition #{$repetition} of {$this->getName()}.{$number}, there are only {$total} repetitions",
"Cannot get repetition #{$ordinal} of {$this->getName()}.{$number}, there are only {$total} repetitions",
);
}

$max = $this->maxRepetitions($number);

if ($max !== 0 && $repetition > $max) {
if ($max !== 0 && $repetition >= $max) {
throw new OutOfBoundsException(
"Cannot get repetition #{$repetition} of {$this->getName()}.{$number}, only {$max} repetitions are allowed",
"Cannot get repetition #{$ordinal} of {$this->getName()}.{$number}, only {$max} "
. ($max === 1 ? 'repetition is' : 'repetitions are')
. ' allowed',
);
}
}
Expand Down
15 changes: 13 additions & 2 deletions tests/AbstractSegmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,23 @@ public function testFieldRejectsRepetitionsBeyondItsMaxReps(): void
$segment->add(new TypeDefinition('Cap', maxReps: 2));
$segment->getFieldRepetition(1, 0);
$segment->getFieldRepetition(1, 1);

$this->expectException(OutOfBoundsException::class);
$this->expectExceptionMessageMatches('/repetition #3 .* only 2 repetitions are allowed/');

$segment->getFieldRepetition(1, 2);
}

public function testFieldWithMaxRepsOneAcceptsExactlyOneRepetition(): void
{
$segment = new FakeSegment();
$segment->add(new TypeDefinition('Cap', maxReps: 1));
$segment->getFieldRepetition(1, 0);

$this->expectException(OutOfBoundsException::class);
$this->expectExceptionMessageMatches('/only 2 repetitions are allowed/');
$this->expectExceptionMessageMatches('/repetition #2 .* only 1 repetition is allowed/');

$segment->getFieldRepetition(1, 3);
$segment->getFieldRepetition(1, 1);
}

public function testRepetitionsCannotSkipPastTheOnesAlreadyCreated(): void
Expand Down
12 changes: 12 additions & 0 deletions tests/Segment/PIDTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace RoundingWell\HL7\Tests\Segment;

use OutOfBoundsException;
use Override;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -188,4 +189,15 @@ public function testLastUpdateFacilityMapsToItsComponents(): void
$this->assertSame('FACILITY', $this->pid->getLastUpdateFacility()->getNamespaceId()->getValue());
$this->assertSame('ISO', $this->pid->getLastUpdateFacility()->getUniversalIdType()->getValue());
}

public function testParseRejectsASecondRepetitionOfASingleRepetitionField(): void
{
// PID.1 is maxReps: 1, so a repeated Set ID must not be accepted.
$pid = new PID();

$this->expectException(OutOfBoundsException::class);
$this->expectExceptionMessageMatches('/repetition #2 of PID\.1, only 1 repetition is allowed/');

$pid->parse(new Encoding(), 'PID|1~2||X');
}
}
Loading