From 579a0d09a6e6001614ba9083d24a272b5dd57898 Mon Sep 17 00:00:00 2001 From: Dennis Haupt Date: Tue, 16 Jun 2026 16:30:59 +0200 Subject: [PATCH] feat: Prevent printing a column in BclConvertDataSection if no data exists --- .../V2/BclConvert/BclSample.php | 31 +++-- .../V2/Sections/BclConvertDataSection.php | 45 +++++- .../V2/BclConvertDataSectionTest.php | 130 ++++++++++++++++++ 3 files changed, 189 insertions(+), 17 deletions(-) create mode 100644 tests/IlluminaSampleSheet/V2/BclConvertDataSectionTest.php diff --git a/src/IlluminaSampleSheet/V2/BclConvert/BclSample.php b/src/IlluminaSampleSheet/V2/BclConvert/BclSample.php index a7b960cf..6660d82d 100644 --- a/src/IlluminaSampleSheet/V2/BclConvert/BclSample.php +++ b/src/IlluminaSampleSheet/V2/BclConvert/BclSample.php @@ -46,20 +46,27 @@ public function __construct( $this->barcodeMismatchesIndex2 = $barcodeMismatchesIndex2; } - public function toString(OverrideCycleCounter $overrideCycleCounter): string + public function toString(OverrideCycleCounter $overrideCycleCounter, bool $includeBarcodeMismatchesIndex2): string { $lines = array_map( - fn (int $lane): string => implode(',', [ - $lane, - $this->sampleID, - $this->indexRead1, - $this->indexRead2 ?? '', - $this->overrideCycles->toString($overrideCycleCounter), - $this->adapterRead1, - $this->adapterRead2, - $this->barcodeMismatchesIndex1, - $this->barcodeMismatchesIndex2 ?? '', - ]), + function (int $lane) use ($overrideCycleCounter, $includeBarcodeMismatchesIndex2): string { + $fields = [ + $lane, + $this->sampleID, + $this->indexRead1, + $this->indexRead2 ?? '', + $this->overrideCycles->toString($overrideCycleCounter), + $this->adapterRead1, + $this->adapterRead2, + $this->barcodeMismatchesIndex1, + ]; + + if ($includeBarcodeMismatchesIndex2) { + $fields[] = $this->barcodeMismatchesIndex2 ?? ''; + } + + return implode(',', $fields); + }, $this->flowcellType->lanes ); diff --git a/src/IlluminaSampleSheet/V2/Sections/BclConvertDataSection.php b/src/IlluminaSampleSheet/V2/Sections/BclConvertDataSection.php index eff4080f..605b5da0 100644 --- a/src/IlluminaSampleSheet/V2/Sections/BclConvertDataSection.php +++ b/src/IlluminaSampleSheet/V2/Sections/BclConvertDataSection.php @@ -11,9 +11,6 @@ class BclConvertDataSection implements Section { - /** @var string */ - public const HEADER_ROW = 'Lane,Sample_ID,Index,Index2,OverrideCycles,AdapterRead1,AdapterRead2,BarcodeMismatchesIndex1,BarcodeMismatchesIndex2'; - /** @var Collection */ public Collection $bclSampleList; @@ -31,14 +28,52 @@ public function __construct(Collection $bclSampleList) public function convertSectionToString(): string { $this->assertNotEmpty(); + $this->assertConsistentBarcodeMismatchesIndex2(); + + $includeBarcodeMismatchesIndex2 = $this->hasBarcodeMismatchesIndex2(); return - self::HEADER_ROW . PHP_EOL + self::headerRow($includeBarcodeMismatchesIndex2) . PHP_EOL . $this->bclSampleList - ->map(fn (BclSample $bclSample): string => $bclSample->toString($this->overrideCycleCounter)) + ->map(fn (BclSample $bclSample): string => $bclSample->toString($this->overrideCycleCounter, $includeBarcodeMismatchesIndex2)) ->join(PHP_EOL) . PHP_EOL; } + private function hasBarcodeMismatchesIndex2(): bool + { + return $this->bclSampleList->contains(fn (BclSample $bclSample): bool => $bclSample->barcodeMismatchesIndex2 !== null); + } + + private function assertConsistentBarcodeMismatchesIndex2(): void + { + $withIndex2 = $this->bclSampleList->contains(fn (BclSample $bclSample): bool => $bclSample->barcodeMismatchesIndex2 !== null); + $withoutIndex2 = $this->bclSampleList->contains(fn (BclSample $bclSample): bool => $bclSample->barcodeMismatchesIndex2 === null); + + if ($withIndex2 && $withoutIndex2) { + throw new IlluminaSampleSheetException('Either all or no samples must have a barcodeMismatchesIndex2.'); + } + } + + private static function headerRow(bool $includeBarcodeMismatchesIndex2): string + { + $columns = [ + 'Lane', + 'Sample_ID', + 'Index', + 'Index2', + 'OverrideCycles', + 'AdapterRead1', + 'AdapterRead2', + 'BarcodeMismatchesIndex1', + ]; + + if ($includeBarcodeMismatchesIndex2) { + $columns[] = 'BarcodeMismatchesIndex2'; + } + + return implode(',', $columns); + } + public function assertNotEmpty(): void { if ($this->bclSampleList->isEmpty()) { diff --git a/tests/IlluminaSampleSheet/V2/BclConvertDataSectionTest.php b/tests/IlluminaSampleSheet/V2/BclConvertDataSectionTest.php new file mode 100644 index 00000000..d212dee9 --- /dev/null +++ b/tests/IlluminaSampleSheet/V2/BclConvertDataSectionTest.php @@ -0,0 +1,130 @@ +convertSectionToString(); + + $expected = <<<'CSV' + Lane,Sample_ID,Index,Index2,OverrideCycles,AdapterRead1,AdapterRead2,BarcodeMismatchesIndex1 + 1,Sample1,Index1,Index2,Y151;I8;I8;Y151,Adapter1,Adapter2,0 + 2,Sample2,Index3,Index4,Y151;I8;I8;Y151,Adapter3,Adapter4,1 + + CSV; + self::assertSame($expected, $result); + } + + public function testIncludesBarcodeMismatchesIndex2ColumnWhenAllSet(): void + { + $indexOrientation = IndexOrientation::FORWARD(); + + $bclSample0 = new BclSample( + new NovaSeqX1_5B([1]), + 'Sample1', + 'Index1', + 'Index2', + OverrideCycles::fromString('Y151;I8;I8;Y151', $indexOrientation), + 'Adapter1', + 'Adapter2', + '0', + '0' + ); + + $bclSample1 = new BclSample( + new NovaSeqX1_5B([2]), + 'Sample2', + 'Index3', + 'Index4', + OverrideCycles::fromString('Y151;I8;I8;Y151', $indexOrientation), + 'Adapter3', + 'Adapter4', + '1', + '1' + ); + + $section = new BclConvertDataSection(new Collection([$bclSample0, $bclSample1])); + $result = $section->convertSectionToString(); + + $expected = <<<'CSV' + Lane,Sample_ID,Index,Index2,OverrideCycles,AdapterRead1,AdapterRead2,BarcodeMismatchesIndex1,BarcodeMismatchesIndex2 + 1,Sample1,Index1,Index2,Y151;I8;I8;Y151,Adapter1,Adapter2,0,0 + 2,Sample2,Index3,Index4,Y151;I8;I8;Y151,Adapter3,Adapter4,1,1 + + CSV; + self::assertSame($expected, $result); + } + + public function testThrowsWhenBarcodeMismatchesIndex2IsInconsistent(): void + { + $indexOrientation = IndexOrientation::FORWARD(); + + $bclSampleWithIndex2 = new BclSample( + new NovaSeqX1_5B([1]), + 'Sample1', + 'Index1', + 'Index2', + OverrideCycles::fromString('Y151;I8;I8;Y151', $indexOrientation), + 'Adapter1', + 'Adapter2', + '0', + '0' + ); + + $bclSampleWithoutIndex2 = new BclSample( + new NovaSeqX1_5B([2]), + 'Sample2', + 'Index3', + 'Index4', + OverrideCycles::fromString('Y151;I8;I8;Y151', $indexOrientation), + 'Adapter3', + 'Adapter4', + '1', + null + ); + + $section = new BclConvertDataSection(new Collection([$bclSampleWithIndex2, $bclSampleWithoutIndex2])); + + $this->expectException(IlluminaSampleSheetException::class); + $this->expectExceptionMessage('Either all or no samples must have a barcodeMismatchesIndex2.'); + $section->convertSectionToString(); + } +}