Skip to content

Commit

Permalink
add default methods to Locatable (#443)
Browse files Browse the repository at this point in the history
* adding some useful default methods to Locatable
  • Loading branch information
lbergelson authored Sep 26, 2017
1 parent a35b420 commit 86a7417
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 0 deletions.
54 changes: 54 additions & 0 deletions src/main/java/htsjdk/samtools/util/Locatable.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package htsjdk.samtools.util;

import java.util.Objects;

/**
* Any class that has a single logical mapping onto the genome should implement Locatable
* positions should be reported as 1-based and closed at both ends
Expand All @@ -22,4 +24,56 @@ public interface Locatable {
* @return 1-based closed-ended position, undefined if getContig() == null
*/
int getEnd();

/**
* @return number of bases of reference covered by this interval
*/
default int getLengthOnReference() {
return CoordMath.getLength(getStart(), getEnd());
}

/**
* Determines whether this interval overlaps the provided locatable.
*
* @param other interval to check
* @return true if this interval overlaps other, otherwise false
*/
default boolean overlaps(Locatable other) {
return withinDistanceOf(other, 0);
}

/**
* Determines whether this interval comes within {@code distance} of overlapping the provided locatable.
* When distance = 0 this is equal to {@link #overlaps(Locatable)}
*
* @param other interval to check
* @param distance how many bases may be between the two intervals for us to still consider them overlapping.
* @return true if this interval overlaps other, otherwise false
*/
default boolean withinDistanceOf(Locatable other, int distance) {
return contigsMatch(other) &&
CoordMath.overlaps(getStart(), getEnd(), other.getStart()-distance, other.getEnd()+distance);
}

/**
* Determines whether this interval contains the entire region represented by other
* (in other words, whether it covers it).
*
*
* @param other interval to check
* @return true if this interval contains all of the base positions spanned by other, otherwise false
*/
default boolean contains(Locatable other) {
return contigsMatch(other) && CoordMath.encloses(getStart(), getEnd(), other.getStart(), other.getEnd());
}

/**
* Determine if this is on the same contig as other
* this must be equivalent to this.getContig().equals(other.getContig()) but may be implemented more efficiently
*
* @return true iff this.getContig().equals(other.getContig())
*/
default boolean contigsMatch(Locatable other) {
return getContig() != null && other != null && Objects.equals(this.getContig(), other.getContig());
}
}
160 changes: 160 additions & 0 deletions src/test/java/htsjdk/samtools/util/LocatableUnitTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package htsjdk.samtools.util;

import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class LocatableUnitTest {

private static Locatable getLocatable(final String contig, final int start, final int end) {
return new Locatable() {
@Override
public String getContig() {
return contig;
}

@Override
public int getStart() {
return start;
}

@Override
public int getEnd() {
return end;
}

@Override
public String toString() {
return String.format("%s:%s-%s", contig, start, end);
}
};
}

@DataProvider(name = "IntervalSizeData")
public Object[][] getIntervalSizeData() {
// Intervals + expected sizes
return new Object[][]{
{ getLocatable("1", 1, 1), 1 },
{ getLocatable("1", 1, 2), 2 },
{ getLocatable("1", 1, 10), 10 },
{ getLocatable("1", 2, 10), 9 },
{ getLocatable("1", 1,0), 0}
};
}

@Test(dataProvider = "IntervalSizeData")
public void testGetSize(final Locatable interval, final int expectedSize ) {
Assert.assertEquals(interval.getLengthOnReference(), expectedSize, "size() incorrect for interval " + interval);
}

@DataProvider(name = "IntervalOverlapData")
public static Object[][] getIntervalOverlapData() {
final Locatable standardInterval = getLocatable("1", 10, 20);
final Locatable oneBaseInterval = getLocatable("1", 10, 10);

return new Object[][] {
{ standardInterval, getLocatable("2", 10, 20), false },
{ standardInterval, getLocatable("1", 1, 5), false },
{ standardInterval, getLocatable("1", 1, 9), false },
{ standardInterval, getLocatable("1", 1, 10), true },
{ standardInterval, getLocatable("1", 1, 15), true },
{ standardInterval, getLocatable("1", 10, 10), true },
{ standardInterval, getLocatable("1", 10, 15), true },
{ standardInterval, getLocatable("1", 10, 20), true },
{ standardInterval, getLocatable("1", 15, 20), true },
{ standardInterval, getLocatable("1", 15, 25), true },
{ standardInterval, getLocatable("1", 20, 20), true },
{ standardInterval, getLocatable("1", 20, 25), true },
{ standardInterval, getLocatable("1", 21, 25), false },
{ standardInterval, getLocatable("1", 25, 30), false },
{ oneBaseInterval, getLocatable("2", 10, 10), false },
{ oneBaseInterval, getLocatable("1", 1, 5), false },
{ oneBaseInterval, getLocatable("1", 1, 9), false },
{ oneBaseInterval, getLocatable("1", 1, 10), true },
{ oneBaseInterval, getLocatable("1", 10, 10), true },
{ oneBaseInterval, getLocatable("1", 10, 15), true },
{ oneBaseInterval, getLocatable("1", 11, 15), false },
{ oneBaseInterval, getLocatable("1", 15, 20), false },
{ standardInterval, null, false },
{ standardInterval, standardInterval, true },
};
}

@Test(dataProvider = "IntervalOverlapData")
public void testOverlap(final Locatable firstInterval, final Locatable secondInterval, final boolean expectedOverlapResult ) {
Assert.assertEquals(firstInterval.overlaps(secondInterval), expectedOverlapResult,
"overlap() returned incorrect result for intervals " + firstInterval + " and " + secondInterval);
}

@DataProvider(name = "overlapsWithMargin")
public Object[][] overlapsWithMargin(){
final Locatable standardInterval = getLocatable("1", 10, 20);
final Locatable middleInterval = getLocatable("1", 100, 200);
final Locatable zeroLengthInterval = getLocatable("1", 1, 0);

return new Object[][] {
{ standardInterval, getLocatable("2", 10, 20), 100, false },
{ standardInterval, getLocatable("1", 1, 15), 0, true },
{ standardInterval, getLocatable("1", 30, 50), 9, false },
{ standardInterval, getLocatable("1", 30, 50), 10, true },
{ middleInterval, getLocatable("1", 50, 99), 0, false },
{ middleInterval, getLocatable("1", 50, 90), 9, false },
{ middleInterval, getLocatable("1", 50, 90), 10, true },
{ middleInterval, getLocatable("1", 150, 149), 0, true },
{ middleInterval, getLocatable("1", 100, 99), 0, true },
{ middleInterval, getLocatable("1", 99, 98), 0, false },
{ standardInterval, getLocatable(null, 10, 20), 100, false }
};
}

@Test(dataProvider = "overlapsWithMargin")
public void testOverlapWithMargin(final Locatable firstInterval, final Locatable secondInterval, int margin, final boolean expectedOverlapResult ) {
Assert.assertEquals(firstInterval.withinDistanceOf(secondInterval, margin), expectedOverlapResult,
"overlap() returned incorrect result for intervals " + firstInterval + " and " + secondInterval);
}

@DataProvider(name = "IntervalContainsData")
public Object[][] getIntervalContainsData() {
final Locatable containingInterval = getLocatable("1", 10, 20);
final Locatable zeroLengthIntervalBetween9And10 = getLocatable("1", 10, 9);
return new Object[][] {
{ containingInterval, getLocatable("2", 10, 20), false },
{ containingInterval, getLocatable("1", 1, 5), false },
{ containingInterval, getLocatable("1", 1, 10), false },
{ containingInterval, getLocatable("1", 5, 15), false },
{ containingInterval, getLocatable("1", 9, 10), false },
{ containingInterval, getLocatable("1", 9, 20), false },
{ containingInterval, getLocatable("1", 10, 10), true },
{ containingInterval, getLocatable("1", 10, 15), true },
{ containingInterval, getLocatable("1", 10, 20), true },
{ containingInterval, getLocatable("1", 10, 21), false },
{ containingInterval, getLocatable("1", 15, 25), false },
{ containingInterval, getLocatable("1", 20, 20), true },
{ containingInterval, getLocatable("1", 20, 21), false },
{ containingInterval, getLocatable("1", 20, 25), false },
{ containingInterval, getLocatable("1", 21, 25), false },
{ containingInterval, getLocatable("1", 25, 30), false },
{ containingInterval, null, false },
{ containingInterval, containingInterval, true },
{ containingInterval, getLocatable(null, 10, 20), false},
{ getLocatable(null, 10, 20), getLocatable(null, 10, 20), false},

//0 length intervals
{ containingInterval, zeroLengthIntervalBetween9And10, true},
{ containingInterval, getLocatable("1", 15, 14), true},
{ containingInterval, getLocatable("1", 21,20), true},
{ containingInterval, getLocatable("1", 25, 24), false},
{zeroLengthIntervalBetween9And10, getLocatable("1", 9, 8), false},
{zeroLengthIntervalBetween9And10, getLocatable("1", 11, 10), false},

//0 length interval is considered to contain itself
{zeroLengthIntervalBetween9And10, zeroLengthIntervalBetween9And10, true}
};
}

@Test(dataProvider = "IntervalContainsData")
public void testContains(final Locatable firstInterval, final Locatable secondInterval, final boolean expectedContainsResult ) {
Assert.assertEquals(firstInterval.contains(secondInterval), expectedContainsResult,
"contains() returned incorrect result for intervals " + firstInterval + " and " + secondInterval);
}
}

0 comments on commit 86a7417

Please sign in to comment.