Skip to content
Draft
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
27 changes: 27 additions & 0 deletions src/AlternateAllele.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types=1);

namespace MLL\Utils;

/**
* The ALT of a VCF record, where `.` means there is no alternate allele.
*/
class AlternateAllele extends DnaSequence
{
public const MISSING = '.';

public function __construct(string $sequence)
{
if (trim($sequence) === self::MISSING) {
$this->validatedSequence = self::MISSING;

return;
}

parent::__construct($sequence);
}

public function isMissing(): bool
{
return $this->validatedSequence === self::MISSING;
}
}
2 changes: 1 addition & 1 deletion src/DnaSequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class DnaSequence
public function __construct(string $sequence)
{
$sequence = trim(strtoupper($sequence));
if (\Safe\preg_match('/^[ATGC]*$/', $sequence) === 0) {
if (\Safe\preg_match('/^[ATGCN]*$/', $sequence) === 0) {
throw new \InvalidArgumentException('Invalid DNA sequence');
}
$this->validatedSequence = $sequence;
Expand Down
8 changes: 4 additions & 4 deletions src/VariantIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function toString(VariantIdentifierFormat $format, NamingConvention $nami

private static function parseCanonical(string $value): self
{
if (preg_match('/^(.+)-(\d+)-([ATGC]+)\/([ATGC]+)$/i', $value, $matches) === 0) {
if (preg_match('/^(.+)-(\d+)-([ATGCN]+)\/([ATGCN]+|\.)$/i', $value, $matches) === 0) {
throw new \InvalidArgumentException("Invalid canonical variant identifier format: {$value}.");
}

Expand All @@ -58,13 +58,13 @@ private static function parseCanonical(string $value): self
return new self(
new GenomicPosition(new Chromosome($matches[1]), NucleotidePosition::fromOneBased(SafeCast::toInt($matches[2]))),
new DnaSequence($matches[3]),
new DnaSequence($matches[4])
new AlternateAllele($matches[4])
);
}

private static function parseDash(string $value): self
{
if (preg_match('/^(.+)-(\d+)-([ATGC]+)-([ATGC]+)$/i', $value, $matches) === 0) {
if (preg_match('/^(.+)-(\d+)-([ATGCN]+)-([ATGCN]+|\.)$/i', $value, $matches) === 0) {
throw new \InvalidArgumentException("Invalid VCF variant identifier format: {$value}.");
}

Expand All @@ -73,7 +73,7 @@ private static function parseDash(string $value): self
return new self(
new GenomicPosition(new Chromosome($matches[1]), NucleotidePosition::fromOneBased(SafeCast::toInt($matches[2]))),
new DnaSequence($matches[3]),
new DnaSequence($matches[4])
new AlternateAllele($matches[4])
);
}
}
29 changes: 29 additions & 0 deletions tests/AlternateAlleleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);

namespace MLL\Utils\Tests;

use MLL\Utils\AlternateAllele;
use PHPUnit\Framework\TestCase;

final class AlternateAlleleTest extends TestCase
{
public function testMissingAllele(): void
{
$alternateAllele = new AlternateAllele('.');
self::assertTrue($alternateAllele->isMissing());
self::assertSame('.', $alternateAllele->toString());
}

public function testBases(): void
{
$alternateAllele = new AlternateAllele('acn');
self::assertFalse($alternateAllele->isMissing());
self::assertSame('ACN', $alternateAllele->toString());
}

public function testInvalidAlleleThrowsException(): void
{
$this->expectException(\InvalidArgumentException::class);
new AlternateAllele('..');
}
}
14 changes: 14 additions & 0 deletions tests/DnaSequenceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,18 @@ public function testLength(): void
$dnaSequence = new DnaSequence(' GCAT ');
self::assertSame(4, $dnaSequence->length());
}

public function testUnknownBase(): void
{
$dnaSequence = new DnaSequence('ANGT');
self::assertSame('ANGT', $dnaSequence->toString());
self::assertSame('TNCA', $dnaSequence->complement());
self::assertSame('ACNT', $dnaSequence->reverseComplement());
}

public function testMissingAlleleIsNoDnaSequence(): void
{
$this->expectException(\InvalidArgumentException::class);
new DnaSequence('.');
}
}
10 changes: 10 additions & 0 deletions tests/VariantIdentifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public static function parseVCFProvider(): iterable
yield 'insertion' => ['chrX-99999-A-ATCG', 'X', 99999, 'A', 'ATCG'];
yield 'chromosome 22' => ['chr22-100-G-C', '22', 100, 'G', 'C'];
yield 'mitochondrial' => ['chrM-8000-T-C', 'M', 8000, 'T', 'C'];
yield 'unknown base' => ['chr17-42288064-TCAGGC-TGGN', '17', 42288064, 'TCAGGC', 'TGGN'];
yield 'missing alternate' => ['chr11-5304332-A-.', '11', 5304332, 'A', '.'];
}

/** @dataProvider parseCanonicalProvider */
Expand All @@ -101,6 +103,8 @@ public static function parseCanonicalProvider(): iterable
yield 'SNV with chr prefix' => ['chr1-12345-A/T', '1', 12345, 'A', 'T'];
yield 'multi-base alternate' => ['chr1-12345-A/ATT', '1', 12345, 'A', 'ATT'];
yield 'without chr prefix' => ['12-500-G/CA', '12', 500, 'G', 'CA'];
yield 'unknown base' => ['chr19-13054588-AAGACAAGAA/ANT', '19', 13054588, 'AAGACAAGAA', 'ANT'];
yield 'missing alternate' => ['chr11-5304332-A/.', '11', 5304332, 'A', '.'];
}

public function testParseInvalidVCFThrows(): void
Expand All @@ -109,6 +113,12 @@ public function testParseInvalidVCFThrows(): void
VariantIdentifier::parse('invalid');
}

public function testParseMissingReferenceThrows(): void
{
$this->expectException(\InvalidArgumentException::class);
VariantIdentifier::parse('chr11-5304332-.-A');
}

public function testParseInvalidCanonicalThrows(): void
{
$this->expectException(\InvalidArgumentException::class);
Expand Down
Loading