diff --git a/CHANGELOG.md b/CHANGELOG.md index 61cd7a7..1e9fa56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## v4.1.0 + +### Added + +- Add `isValidRawContent`-method to class `TecanScanner` + ## v4.0.0 ### Removed diff --git a/src/FluidXPlate/FluidXPlate.php b/src/FluidXPlate/FluidXPlate.php index 401a4d4..30fa36d 100644 --- a/src/FluidXPlate/FluidXPlate.php +++ b/src/FluidXPlate/FluidXPlate.php @@ -10,7 +10,8 @@ final class FluidXPlate { - public const FLUIDX_BARCODE_REGEX = /* @lang RegExp */ '/[A-Z]{2}(\d){8}/'; + public const FLUIDX_BARCODE_REGEX = /* @lang RegExp */ '/' . self::FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER . '/'; + public const FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER = '[A-Z]{2}(\d){8}'; public string $rackId; diff --git a/src/TecanScanner/TecanScanner.php b/src/TecanScanner/TecanScanner.php index 5daf0ed..e6d613c 100644 --- a/src/TecanScanner/TecanScanner.php +++ b/src/TecanScanner/TecanScanner.php @@ -14,6 +14,7 @@ final class TecanScanner { public const NO_READ = 'NO READ'; + public const RACKID_IDENTIFIER = 'rackid,'; public static function parseRawContent(string $rawContent): FluidXPlate { @@ -25,9 +26,10 @@ public static function parseRawContent(string $rawContent): FluidXPlate $firstLineWithRackId = $lines->shift(); - if (! is_string($firstLineWithRackId) || ! Str::startsWith($firstLineWithRackId, 'rackid,')) { + if (! is_string($firstLineWithRackId) || ! Str::startsWith($firstLineWithRackId, self::RACKID_IDENTIFIER)) { throw new NoRackIdException(); } + $rackId = Str::substr($firstLineWithRackId, strlen(self::RACKID_IDENTIFIER)); $expectedCount = FluidXPlate::coordinateSystem()->positionsCount(); $actualCount = $lines->count(); @@ -35,7 +37,7 @@ public static function parseRawContent(string $rawContent): FluidXPlate throw new WrongNumberOfWells($expectedCount, $actualCount); } - $plate = new FluidXPlate(Str::substr($firstLineWithRackId, 7)); + $plate = new FluidXPlate($rackId); foreach ($lines as $line) { $barcode = Str::after($line, ','); @@ -55,4 +57,26 @@ public static function parseRawContent(string $rawContent): FluidXPlate return $plate; } + + /** + * Checks if a string can be parsed into a FluidXPlate. + */ + public static function isValidRawContent(string $rawContent): bool + { + $lines = explode("\n", $rawContent); + + if (97 !== count($lines)) { + return false; + } + if (0 === \Safe\preg_match(/* @lang RegExp */ '/^' . self::RACKID_IDENTIFIER . FluidXPlate::FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER . '$/', array_shift($lines))) { + return false; + } + foreach ($lines as $line) { + if (1 !== \Safe\preg_match(/* @lang RegExp */ '/^[A-H][1-12],' . FluidXPlate::FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER . '|' . self::NO_READ . '$/', $line)) { + return false; + } + } + + return true; + } } diff --git a/tests/Unit/TecanScanner/TecanScannerTest.php b/tests/Unit/TecanScanner/TecanScannerTest.php index 41be02f..719243c 100644 --- a/tests/Unit/TecanScanner/TecanScannerTest.php +++ b/tests/Unit/TecanScanner/TecanScannerTest.php @@ -13,32 +13,45 @@ final class TecanScannerTest extends TestCase { public function testCreateFromStringEmpty(): void { + $rawContent = ''; + self::assertFalse(TecanScanner::isValidRawContent($rawContent)); + $this->expectExceptionObject(new TecanScanEmptyException()); - TecanScanner::parseRawContent(''); + TecanScanner::parseRawContent($rawContent); } public function testCreateFromUnexpectedLineCount(): void { + $rawContent = "rackid,SA00411242\nA1,FD13945423\nB1,FD32807353"; + self::assertFalse(TecanScanner::isValidRawContent($rawContent)); + $this->expectExceptionObject(new WrongNumberOfWells(96, 2)); - TecanScanner::parseRawContent("rackid,SA00411242\nA1,FD13945423\nB1,FD32807353"); + TecanScanner::parseRawContent($rawContent); } public function testMicroplateHandlesDuplicateCoordinate(): void { + $rawContent = "rackid,SA00411242\nA1,FD13945423\nA1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"; + self::assertTrue(TecanScanner::isValidRawContent($rawContent)); + $this->expectExceptionObject(new WellNotEmptyException('Well with coordinate "A1" is not empty. Use setWell() to overwrite the coordinate. Well content "s:10:"FD32807353";" was not added.')); - TecanScanner::parseRawContent("rackid,SA00411242\nA1,FD13945423\nA1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"); + TecanScanner::parseRawContent($rawContent); } public function testNoBarcode(): void { + $rawContent = "A1,FD13945423\nB1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"; + self::assertFalse(TecanScanner::isValidRawContent($rawContent)); + $this->expectExceptionObject(new NoRackIdException()); - TecanScanner::parseRawContent("A1,FD13945423\nB1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"); + TecanScanner::parseRawContent($rawContent); } public function testSuccess(): void { - $fluidXPlate = TecanScanner::parseRawContent("rackid,SA00411242\nA1,FD13945423\nB1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"); + $rawContent = "rackid,SA00411242\nA1,FD13945423\nB1,FD32807353\nC1,NO READ\nD1,NO READ\nE1,NO READ\nF1,NO READ\nG1,NO READ\nH1,NO READ\nA2,NO READ\nB2,NO READ\nC2,NO READ\nD2,NO READ\nE2,NO READ\nF2,NO READ\nG2,NO READ\nH2,NO READ\nA3,NO READ\nB3,NO READ\nC3,NO READ\nD3,NO READ\nE3,NO READ\nF3,NO READ\nG3,NO READ\nH3,NO READ\nA4,NO READ\nB4,NO READ\nC4,NO READ\nD4,NO READ\nE4,NO READ\nF4,NO READ\nG4,NO READ\nH4,NO READ\nA5,NO READ\nB5,NO READ\nC5,NO READ\nD5,NO READ\nE5,NO READ\nF5,NO READ\nG5,NO READ\nH5,NO READ\nA6,NO READ\nB6,NO READ\nC6,NO READ\nD6,NO READ\nE6,NO READ\nF6,NO READ\nG6,NO READ\nH6,NO READ\nA7,NO READ\nB7,NO READ\nC7,NO READ\nD7,NO READ\nE7,NO READ\nF7,NO READ\nG7,NO READ\nH7,NO READ\nA8,NO READ\nB8,NO READ\nC8,NO READ\nD8,NO READ\nE8,NO READ\nF8,NO READ\nG8,NO READ\nH8,NO READ\nA9,NO READ\nB9,NO READ\nC9,NO READ\nD9,NO READ\nE9,NO READ\nF9,NO READ\nG9,NO READ\nH9,NO READ\nA10,NO READ\nB10,NO READ\nC10,NO READ\nD10,NO READ\nE10,NO READ\nF10,NO READ\nG10,NO READ\nH10,NO READ\nA11,NO READ\nB11,NO READ\nC11,NO READ\nD11,NO READ\nE11,NO READ\nF11,NO READ\nG11,NO READ\nH11,NO READ\nA12,NO READ\nB12,NO READ\nC12,NO READ\nD12,NO READ\nE12,NO READ\nF12,NO READ\nG12,NO READ\nH12,NO READ"; + $fluidXPlate = TecanScanner::parseRawContent($rawContent); self::assertCount(96, $fluidXPlate->wells()); self::assertCount(94, $fluidXPlate->freeWells()); self::assertSame('SA00411242', $fluidXPlate->rackId); @@ -47,5 +60,6 @@ public function testSuccess(): void 'A1' => 'FD13945423', 'B1' => 'FD32807353', ], $fluidXPlate->filledWells()->toArray()); + self::assertTrue(TecanScanner::isValidRawContent($rawContent)); } }