diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e97b53..89990b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/AbstractSegment.php b/src/AbstractSegment.php index 1afd735..5573290 100644 --- a/src/AbstractSegment.php +++ b/src/AbstractSegment.php @@ -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', ); } } diff --git a/tests/AbstractSegmentTest.php b/tests/AbstractSegmentTest.php index bdb2012..2541968 100644 --- a/tests/AbstractSegmentTest.php +++ b/tests/AbstractSegmentTest.php @@ -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 diff --git a/tests/Segment/PIDTest.php b/tests/Segment/PIDTest.php index 977e19d..e8c9f59 100644 --- a/tests/Segment/PIDTest.php +++ b/tests/Segment/PIDTest.php @@ -4,6 +4,7 @@ namespace RoundingWell\HL7\Tests\Segment; +use OutOfBoundsException; use Override; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -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'); + } }