Skip to content

Commit

Permalink
Merge pull request #33 from hammerlab/intervals
Browse files Browse the repository at this point in the history
Add Interval classes
  • Loading branch information
danvk committed Mar 13, 2015
2 parents b304832 + 44e5a80 commit 8fccf98
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 0 deletions.
1 change: 1 addition & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[ignore]
.*node_modules/flow-bin.*
.*node_modules/jsxhint.*
.*build.*

[include]
Expand Down
41 changes: 41 additions & 0 deletions src/ContigInterval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* @flow */

var Interval = require('./Interval');

/**
* Class representing a closed interval on the genome: contig:start-stop.
*
* The contig may be either a string ("chr22") or a number (in case the contigs
* are indexed, for example).
*/
class ContigInterval {
contig: string|number;
interval: Interval;

constructor(contig: string|number, start: number, stop: number) {
this.contig = contig;
this.interval = new Interval(start, stop);
}

// TODO: make these getter methods & switch to Babel.
start(): number {
return this.interval.start;
}
stop(): number {
return this.interval.stop;
}
length(): number {
return this.interval.length();
}

intersects(other: ContigInterval): boolean {
return (this.contig === other.contig &&
this.interval.intersects(other.interval));
}

toString(): string {
return `${this.contig}:${this.start()}-${this.stop()}`;
}
}

module.exports = ContigInterval;
69 changes: 69 additions & 0 deletions src/Interval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Class representing a closed numeric interval, [start, stop].
*
* @flow
*/
class Interval {
start: number;
stop: number;

// Represents [start, stop] -- both ends are inclusive.
// If stop < start, then this is an empty interval.
constructor(start: number, stop: number) {
this.start = start;
this.stop = stop;
}

// TODO: make this a getter method & switch to Babel.
length(): number {
return Math.max(0, this.stop - this.start + 1);
}

intersect(other: Interval): Interval {
return new Interval(Math.max(this.start, other.start),
Math.min(this.stop, other.stop));
}

intersects(other: Interval): boolean {
return this.start <= other.stop && other.start <= this.stop;
}

contains(value: number): boolean {
return value >= this.start && value <= this.stop;
}

clone(): Interval {
return new Interval(this.start, this.stop);
}

static intersectAll(intervals: Array<Interval>): Interval {
if (!intervals.length) {
throw new Error('Tried to intersect zero intervals');
}
var result = intervals[0].clone();
intervals.slice(1).forEach(function({start, stop}) {
result.start = Math.max(start, result.start);
result.stop = Math.min(stop, result.stop);
});
return result;
}

// Returns an interval which contains all the given intervals.
static boundingInterval(intervals: Array<Interval>): Interval {
if (!intervals.length) {
throw new Error('Tried to bound zero intervals');
}
var result = intervals[0].clone();
intervals.slice(1).forEach(function({start, stop}) {
result.start = Math.min(start, result.start);
result.stop = Math.max(stop, result.stop);
});
return result;
}

toString(): string {
return `[${this.start}, ${this.stop}]`;
}
}

module.exports = Interval;
34 changes: 34 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Grab-bag of utility functions.
* @flow
*/


// Compare two tuples of equal length. Is t1 <= t2?
// TODO: make this tupleLessOrEqual<T> -- it works with strings or booleans, too.
function tupleLessOrEqual(t1: Array<number>, t2: Array<number>): boolean {
if (t1.length != t2.length) throw new Error('Comparing non-equal length tuples');
for (var i = 0; i < t1.length; i++) {
if (t1[i] > t2[i]) {
return false;
} else if (t1[i] < t2[i]) {
return true;
}
}
return true;
}

// Do two ranges of tuples overlap?
// TODO: make this tupleRangeOverlaps<T> -- it works with strings or booleans, too.
function tupleRangeOverlaps(tupleRange1: Array<Array<number>>,
tupleRange2: Array<Array<number>>): boolean {
return (
// Are the ranges overlapping?
tupleLessOrEqual(tupleRange1[0], tupleRange2[1]) &&
tupleLessOrEqual(tupleRange2[0], tupleRange1[1]) &&
// ... and non-empty?
tupleLessOrEqual(tupleRange1[0], tupleRange1[1]) &&
tupleLessOrEqual(tupleRange2[0], tupleRange2[1]));
}

module.exports = {tupleLessOrEqual, tupleRangeOverlaps};
22 changes: 22 additions & 0 deletions test/ContigInterval-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
var chai = require('chai');
var expect = chai.expect;

var ContigInterval = require('../src/ContigInterval');

describe('ContigInterval', function() {
it('should have basic accessors', function() {
var tp53 = new ContigInterval(10, 7512444, 7531643);
expect(tp53.toString()).to.equal('10:7512444-7531643');
expect(tp53.contig).to.equal(10);
expect(tp53.start()).to.equal(7512444);
expect(tp53.stop()).to.equal(7531643);
expect(tp53.length()).to.equal(19200);
});

it('should determine intersections', function() {
var tp53 = new ContigInterval(10, 7512444, 7531643);
var other = new ContigInterval(10, 7512444, 7531642);

expect(tp53.intersects(other)).to.be.true;
});
});
79 changes: 79 additions & 0 deletions test/Interval-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
var chai = require('chai');
var expect = chai.expect;

var Interval = require('../src/Interval');

describe('Interval', function() {
it('should have start/stop/length', function() {
var x = new Interval(10, 20);
expect(x.start).to.equal(10);
expect(x.stop).to.equal(20);
expect(x.length()).to.equal(11);
expect(x.toString()).to.equal('[10, 20]');
});

it('should determine containment', function() {
var x = new Interval(-10, 10);
expect(x.contains(0)).to.be.true;
expect(x.contains(-10)).to.be.true;
expect(x.contains(+10)).to.be.true;
expect(x.contains(+11)).to.be.false;
expect(x.contains(-11)).to.be.false;
});

it('should work with empty intervals', function() {
var empty = new Interval(5, 0),
other = new Interval(-10, 10);
expect(empty.contains(0)).to.be.false;
expect(empty.length()).to.equal(0);
expect(empty.intersect(other).length()).to.equal(0);
});

it('should determine intersections', function() {
var tp53 = new Interval(7512444, 7531643);
var other = new Interval(7512444, 7531642);

expect(tp53.intersects(other)).to.be.true;
});

it('should clone', function() {
var x = new Interval(0, 5),
y = x.clone();

y.start = 1;
expect(x.start).to.equal(0);
expect(y.start).to.equal(1);
});

it('should intersect many intervals', function() {
var ivs = [
new Interval(0, 10),
new Interval(5, 15),
new Interval(-5, 5)
];

var intAll = Interval.intersectAll;
expect(intAll( ivs ).toString()).to.equal('[5, 5]');
expect(intAll([ivs[0], ivs[1]]).toString()).to.equal('[5, 10]');
expect(intAll([ivs[0], ivs[2]]).toString()).to.equal('[0, 5]');
expect(intAll([ivs[0] ]).toString()).to.equal('[0, 10]');

expect(() => intAll([])).to.throw(/intersect zero intervals/);
});

it('should construct bounding intervals', function() {
var ivs = [
new Interval(0, 10),
new Interval(5, 15),
new Interval(-5, 5)
];

var bound = Interval.boundingInterval;
expect(bound( ivs ).toString()).to.equal('[-5, 15]');
expect(bound([ivs[0], ivs[1]]).toString()).to.equal('[0, 15]');
expect(bound([ivs[0], ivs[2]]).toString()).to.equal('[-5, 10]');
expect(bound([ivs[0] ]).toString()).to.equal('[0, 10]');

expect(() => bound([])).to.throw(/bound zero intervals/);
});
});
62 changes: 62 additions & 0 deletions test/utils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
var chai = require('chai');
var expect = chai.expect;

var utils = require('../src/utils');

describe('utils', function() {
describe('tupleLessOrEqual', function() {
var lessEqual = utils.tupleLessOrEqual;

it('should work on 1-tuples', function() {
expect(lessEqual([0], [1])).to.be.true;
expect(lessEqual([1], [0])).to.be.false;
expect(lessEqual([0], [0])).to.be.true;
});

it('should work on 2-tuples', function() {
expect(lessEqual([0, 1], [0, 2])).to.be.true;
expect(lessEqual([0, 1], [0, 0])).to.be.false;
expect(lessEqual([0, 1], [1, 0])).to.be.true;
});
});

describe('tupleRangeOverlaps', function() {
var overlap = utils.tupleRangeOverlaps;
it('should work on 1-tuples', function() {
var ivs = [
[[0], [10]],
[[5], [15]],
[[-5], [5]],
[[-5], [4]]
];
var empty = [[4], [3]];
expect(overlap(ivs[0], ivs[1])).to.be.true;
expect(overlap(ivs[0], ivs[2])).to.be.true
expect(overlap(ivs[1], ivs[3])).to.be.false

expect(overlap(ivs[0], empty)).to.be.false
expect(overlap(ivs[1], empty)).to.be.false
expect(overlap(ivs[2], empty)).to.be.false
expect(overlap(ivs[3], empty)).to.be.false
});

it('should work on 2-tuples', function() {
expect(overlap([[0, 0], [0, 10]],
[[0, 5], [0, 15]])).to.be.true;
expect(overlap([[0, 0], [0, 10]],
[[-1, 15], [0, 0]])).to.be.true;
expect(overlap([[0, 0], [0, 10]],
[[-1, 15], [1, -15]])).to.be.true;
expect(overlap([[0, 0], [0, 10]],
[[-1, 15], [0, -1]])).to.be.false;
expect(overlap([[0, 0], [0, 10]],
[[-1, 15], [0, 0]])).to.be.true;
expect(overlap([[0, 0], [0, 10]],
[[0, 10], [0, 11]])).to.be.true;
expect(overlap([[1, 0], [3, 10]],
[[-1, 10], [2, 1]])).to.be.true;
expect(overlap([[3, 0], [3, 10]],
[[-1, 10], [2, 1]])).to.be.false;
});
});
});

0 comments on commit 8fccf98

Please sign in to comment.