edu.isi.nlp.strings.offsets.OffsetRange Maven / Gradle / Ivy
package edu.isi.nlp.strings.offsets;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Ordering;
import com.google.common.collect.Range;
import com.google.common.primitives.Ints;
import java.util.Comparator;
/** Represents an inclusive range of offsets. */
public class OffsetRange> {
private final OffsetType startInclusive;
private final OffsetType endInclusive;
@JsonCreator
public static >
OffsetRange fromInclusiveEndpoints(
@JsonProperty("start") OffsetType startInclusive,
@JsonProperty("end") OffsetType endInclusive) {
checkArgument(startInclusive.asInt() <= endInclusive.asInt());
return new OffsetRange<>(startInclusive, endInclusive);
}
private OffsetRange(OffsetType startInclusive, OffsetType endInclusive) {
checkArgument(startInclusive.asInt() <= endInclusive.asInt());
this.startInclusive = checkNotNull(startInclusive);
this.endInclusive = checkNotNull(endInclusive);
}
@JsonProperty("start")
public OffsetType startInclusive() {
return startInclusive;
}
@JsonProperty("end")
public OffsetType endInclusive() {
return endInclusive;
}
public Range asRange() {
return Range.closed(startInclusive, endInclusive);
}
public int length() {
return endInclusive.asInt() - startInclusive().asInt() + 1;
}
@Override
public int hashCode() {
return Objects.hashCode(startInclusive, endInclusive);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final OffsetRange other = (OffsetRange) obj;
return Objects.equal(this.startInclusive, other.startInclusive)
&& Objects.equal(this.endInclusive, other.endInclusive);
}
public static > Ordering> byLengthOrdering() {
return new Ordering>() {
@Override
public int compare(final OffsetRange left, final OffsetRange right) {
return Ints.compare(left.length(), right.length());
}
};
}
private static > Function, T> toStartInclusiveFunction() {
return new Function, T>() {
@Override
public T apply(OffsetRange x) {
return x.startInclusive();
}
};
}
private static > Function, T> toEndInclusiveFunction() {
return new Function, T>() {
@Override
public T apply(OffsetRange x) {
return x.endInclusive();
}
};
}
/**
* Provides an {@link Ordering} of {@link OffsetRange}s by their start position. Note that this is
* not a total ordering because {@link OffsetRange}s with the same start position but different
* end positions will compare as equal.
*
* Consider producing a compound ordering with {@link #byEndOrdering()} using {@link
* Ordering#compound(Comparator)} or using one of the predefined total orderings.
*/
public static > Ordering> byStartOrdering() {
return Ordering.natural().onResultOf(OffsetRange.toStartInclusiveFunction());
}
/**
* Provides an {@link Ordering} of {@link OffsetRange}s by their end position. Note that this is
* not a total ordering because {@link OffsetRange}s with the same end position but different
* start positions will compare as equal.
*
* Consider producing a compound ordering with {@link #byStartOrdering()} using {@link
* Ordering#compound(Comparator)} or using one of the predefined total orderings.
*/
public static > Ordering> byEndOrdering() {
return Ordering.natural().onResultOf(OffsetRange.toEndInclusiveFunction());
}
/**
* Provides a total {@link Ordering} over {@link OffsetRange}s by their start position, breaking
* ties by placing the earlier end position first.
*/
public static > Ordering> byEarlierStartEarlierEndOrdering() {
return Ordering.natural()
.onResultOf(OffsetRange.toStartInclusiveFunction())
.compound(Ordering.natural().onResultOf(OffsetRange.toEndInclusiveFunction()));
}
/**
* Provides a total {@link Ordering} over {@link OffsetRange}s by their start position, breaking
* ties by placing the later end position first.
*/
public static > Ordering> byEarlierStartLaterEndOrdering() {
return Ordering.natural()
.onResultOf(OffsetRange.toStartInclusiveFunction())
.compound(
Ordering.natural().onResultOf(OffsetRange.toEndInclusiveFunction()).reverse());
}
public static OffsetRange inclusiveCharOffsetRange(
int startInclusive, int endInclusive) {
return fromInclusiveEndpoints(
CharOffset.asCharOffset(startInclusive), CharOffset.asCharOffset(endInclusive));
}
/** Prefer {@link #inclusiveCharOffsetRange(int, int)} */
public static OffsetRange charOffsetRange(int startInclusive, int endInclusive) {
return inclusiveCharOffsetRange(startInclusive, endInclusive);
}
public static OffsetRange byteOffsetRange(
final int startInclusive, final int endInclusive) {
return OffsetRange.fromInclusiveEndpoints(
ByteOffset.asByteOffset(startInclusive), ByteOffset.asByteOffset(endInclusive));
}
/** This returns optional because it is not possible to represent an empty offset span */
public static Optional> charOffsetsOfWholeString(String s) {
if (s.isEmpty()) {
return Optional.absent();
}
return Optional.of(charOffsetRange(0, s.length() - 1));
}
/**
* Returns a new {@code OffsetRange} of the same type with the start and end points shifted by the
* specified amount.
*/
public OffsetRange shiftedCopy(final int shiftAmount) {
return new OffsetRange<>(
startInclusive().shiftedCopy(shiftAmount), endInclusive().shiftedCopy(shiftAmount));
}
public boolean overlaps(OffsetRange other) {
return asRange().isConnected(other.asRange());
}
public Optional> intersection(OffsetRange other) {
final Range meAsRange = asRange();
final Range otherAsRange = other.asRange();
if (meAsRange.isConnected(otherAsRange)) {
final Range intersectionRange = meAsRange.intersection(otherAsRange);
return Optional.of(
fromInclusiveEndpoints(
intersectionRange.lowerEndpoint(), intersectionRange.upperEndpoint()));
} else {
return Optional.absent();
}
}
/** Returns if the given offset is within the inclusive bounds of this range. */
public boolean contains(final OffsetType x) {
return x.asInt() >= startInclusive().asInt() && x.asInt() <= endInclusive().asInt();
}
/** Returns if the given inclusive offset range is within the inclusive bounds of this range. */
public boolean contains(OffsetRange other) {
return asRange().encloses(other.asRange());
}
public static >
Predicate> containedInPredicate(
final OffsetRange container) {
return new Predicate>() {
@Override
public boolean apply(OffsetRange input) {
return container.contains(input);
}
};
}
/**
* Returns this range with its start and end positions clipped to fit within {@code bounds}. If
* this does not intersect {@code bounds}, returns {@link
* com.google.common.base.Optional#absent()} .
*/
public Optional> clipToBounds(OffsetRange bounds) {
if (bounds.contains(this)) {
return Optional.of(this);
}
if (!bounds.overlaps(this)) {
return Optional.absent();
}
final OffsetType newLowerBound;
final OffsetType newUpperBound;
if (bounds.startInclusive().asInt() > startInclusive().asInt()) {
newLowerBound = bounds.startInclusive();
} else {
newLowerBound = startInclusive();
}
if (bounds.endInclusive().asInt() < endInclusive().asInt()) {
newUpperBound = bounds.endInclusive();
} else {
newUpperBound = endInclusive();
}
return Optional.of(OffsetRange.fromInclusiveEndpoints(newLowerBound, newUpperBound));
}
public static Function, Integer> lengthFunction() {
return LengthFunction.INSTANCE;
}
private enum LengthFunction implements Function, Integer> {
INSTANCE {
@Override
public Integer apply(final OffsetRange> input) {
return input.length();
}
}
}
@Override
public String toString() {
return "[" + startInclusive().toString() + "-" + endInclusive().toString() + "]";
}
}