
org.xmlcml.euclid.IntRange Maven / Gradle / Ivy
Show all versions of euclid Show documentation
/**
* Copyright 2011 Peter Murray-Rust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.xmlcml.euclid;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
/**
* maximum and minimum values
*
* Contains two ints representing the minimum and maximum of an allowed or
* observed range.
*
* Default is range with low {@literal >} high; this can be regarded as the uninitialised
* state. If points are added to a default IntRange it becomes initialised.
*
* @author (C) P. Murray-Rust, 1996
*/
public class IntRange implements EuclidConstants, Comparable {
private static final Logger LOG = Logger.getLogger(IntRange.class);
static {
LOG.setLevel(Level.DEBUG);
}
private final static Pattern CURLY_PATTERN1 = Pattern.compile("\\{([^,]+)\\}");
private final static Pattern CURLY_PATTERN2 = Pattern.compile("\\{([^,]+),([^,]+)\\}");
private final static String ANY = "*";
public final static IntRangeComparator ASCENDING_MIN_COMPARATOR = new IntRangeComparator();
/**
* maximum of range
*/
protected int maxval;
/**
* minimum of range
*/
protected int minval;
/**
* creates invalid range from MAX_VALUE to MIN_VALUE
*/
public IntRange() {
minval = Integer.MAX_VALUE;
maxval = Integer.MIN_VALUE;
}
/**
* initialise with min and max values; if minv {@literal >} maxv create inValid
* IntRange
*
* @param minv
* @param maxv
*/
public IntRange(int minv, int maxv) {
maxval = maxv;
minval = minv;
if (minval > maxval) {
minval = Integer.MAX_VALUE;
maxval = Integer.MIN_VALUE;
}
}
/**
* copy constructor
*
* @param r
*/
public IntRange(IntRange r) {
minval = r.minval;
maxval = r.maxval;
}
/**
*
* @param r
*/
public IntRange(RealRange r) {
minval = (int) Math.round(r.minval);
maxval = (int) Math.round(r.maxval);
}
/**
* a Range is only valid if its maxval is not less than its minval; this
* tests for uninitialised ranges
*
* @return valid
*/
public boolean isValid() {
return (minval <= maxval);
}
/**
* invalid ranges return false
*
* @param r
* @return equals
*
*/
public boolean isEqualTo(IntRange r) {
return (r != null && minval == r.minval && maxval == r.maxval && minval <= maxval);
}
@Override
public boolean equals(Object o) {
boolean equals = false;
if (o != null && o instanceof IntRange) {
IntRange ir =(IntRange) o;
equals = this.minval == ir.minval && this.maxval == ir.maxval;
}
return equals;
}
@Override
public int hashCode() {
return 17*minval + 31*maxval;
}
/**
* combine two ranges if both valid; takes greatest limits of both, else
* returns InValid
*
* @param r2
* @return range
*/
public IntRange plus(IntRange r2) {
if (!this.isValid()) {
if (r2 == null || !r2.isValid()) {
return new IntRange();
}
return new IntRange(r2);
}
IntRange temp = new IntRange();
temp = new IntRange(Math.min(minval, r2.minval), Math.max(maxval,
r2.maxval));
return temp;
}
public boolean intersectsWith(IntRange r2) {
IntRange r = this.intersectionWith(r2);
return r != null && r.isValid();
}
/**
* intersect two ranges and take the range common to both; return invalid
* range if no overlap
*
* @param r2
* @return range
*/
public IntRange intersectionWith(IntRange r2) {
if (!isValid() || r2 == null || !r2.isValid()) {
return new IntRange();
}
int minv = Math.max(minval, r2.minval);
int maxv = Math.min(maxval, r2.maxval);
return new IntRange(minv, maxv);
}
/**
* get minimum value (MAX_VALUE if inValid)
*
* @return min
*/
public int getMin() {
return minval;
}
/**
* get maximum value (MIN_VALUE if inValid)
*
* @return max
*
*/
public int getMax() {
return maxval;
}
/**
* get range (MIN_VALUE if invalid)
*
* @return range
*/
public int getRange() {
if (!isValid())
return Integer.MIN_VALUE;
return maxval - minval;
}
/**
* does one range include another
*
* @param r2
* @return includes
*/
public boolean includes(IntRange r2) {
return (r2 != null && r2.isValid() && this.includes(r2.getMin()) && this
.includes(r2.getMax()));
}
/**
* is a int within a IntRange
*
* @param f
* @return includes If inValid, return false
*/
public boolean includes(int f) {
return f >= minval && f <= maxval;
}
/**
* synonym for includes()
*
* @param f
* @return includes
*/
public boolean contains(int f) {
return includes(f);
}
/**
* add a value to a range
*
* @param x
*/
public void add(int x) {
maxval = Math.max(maxval, x);
minval = Math.min(minval, x);
}
/**
* to string
*
* @return string
*/
public String toString() {
return (minval > maxval) ? "NULL" : S_LBRAK + minval + S_COMMA + maxval + S_RBRAK;
}
/** comparees on min values
*
* @param intRange
* @return
*/
public int compareTo(IntRange intRange) {
if (intRange == null) {
return -1;
} else if (this.minval < intRange.minval) {
return -1;
} else if (this.minval > intRange.minval) {
return 1;
} else {
if (this.maxval < intRange.maxval) {
return -1;
} else if (this.maxval > intRange.maxval) {
return 1;
}
}
return 0;
}
/** makes new IntRange extended by deltaMin and deltaMax.
*
* the effect is for positive numbers to increase the range.
* if extensions are negative they are applied, but may result
* in invalid range (this is not checked at this stage).
*
* Does not alter this.
*
*
* @param minExtend subtracted from min
* @param maxExtend added to max
*/
public IntRange getRangeExtendedBy(int minExtend, int maxExtend) {
return new IntRange(minval - minExtend, maxval + maxExtend);
}
/** do ranges touch but not overlap?
*
* range from [a,b] inclusive touches [c,a-1] or [b+1,c]
*
* ([a,b] overlaps with [b,c])
* @param range
* @return
*/
public boolean touches(IntRange range) {
return range != null &&
(this.maxval + 1 == range.minval || range.maxval + 1 == this.minval);
}
/** mid point of range.
*
* @return
*/
public int getMidPoint() {
return (minval + maxval)/2;
}
/** interprets a String as an IntRange.
*
* {m,n} is interpreted as IntRange(m,n)
* {*,n} is interpreted as IntRange(any,n)
* {m,*} is interpreted as IntRange(m,any)
* {*,*} is interpreted as IntRange(any,any)
* {m} is interpreted as IntRange(m,m)
* {*} is interpreted as IntRange(any,any)
*
* @param token
* @return null if cannot create a valid IntRange
*/
public static IntRange parseCurlyBracketString(String token) {
IntRange intRange = null;
if (token != null) {
Integer min = null;
Integer max = null;
token = token.replaceAll("\\s+", ""); // strip spaces
Matcher matcher = CURLY_PATTERN2.matcher(token);
try {
if (matcher.matches()) {
String minS = matcher.group(1);
String maxS = matcher.group(2);
min = (ANY.equals(minS)) ? -Integer.MAX_VALUE : Integer.parseInt(minS);
max = (ANY.equals(maxS)) ? Integer.MAX_VALUE : Integer.parseInt(maxS);
} else {
matcher = CURLY_PATTERN1.matcher(token);
if (matcher.matches()) {
String minS = matcher.group(1);
min = (ANY.equals(minS)) ? -Integer.MAX_VALUE : Integer.parseInt(minS);
max = min;
}
}
intRange = new IntRange(min, max);
} catch (Exception e) {
LOG.error("Cannot parse range: "+token);
}
}
return intRange;
}
/** creates a list of IntRanges from {...} syntax.
*
* uses parseCurlyBracketString()
*
* @param tokens
* @return
*/
public static List createIntRangeList(List tokens) {
List intRangeList = new ArrayList();
for (String token : tokens) {
IntRange intRange = IntRange.parseCurlyBracketString(token);
if (intRange == null) {
throw new RuntimeException("Cannot parse ("+token+") as IntRange in : "+tokens);
}
intRangeList.add(intRange);
}
return intRangeList;
}
/** joins intRanges A and B when A.getMax() == B.getMin().
* Ranges must either "join" within a tolerance or be disjoint.
* Overlapping ranges will throw an RuntimeException.
*
* (1,5), (5,10), (10,15)
* creates
* (1,15)
*
* (1,5), (5,10), (15,20), (20, 25)
* creates
* (1,10) (15, 25)
*
* (1,5), (15,20)
* creates
* (1,5) (15, 20)
*
* (1,5) (2,10) (5, 12) throws an Exception
*
* @param rangeList can be in any order, not altered
* @param tolerance allows for overlap, i.e. (1,5) and (4,10) would "touch" , so would (1,5) and (6,10)
* @return List of non-touching joined ranges, sorted by new R.getMin()
*/
public static List joinRanges(List rangeList, int tolerance) {
if (rangeList.size() <= 1) {
return rangeList;
}
List sortedList = new ArrayList();
for (IntRange range : rangeList) {
sortedList.add(new IntRange(range)); // must copy to preserve original list unaltered
}
Collections.sort(sortedList, ASCENDING_MIN_COMPARATOR);
List newList = new ArrayList();
IntRange currentRange = sortedList.get(0);
newList.add(currentRange);
for (int i = 1; i < sortedList.size(); i++) {
IntRange range = sortedList.get(i);
if (currentRange.canJoin(range, tolerance)) {
currentRange.setMax(range.getMax());
} else {
if (range.getMin() < currentRange.getMax()) {
throw new RuntimeException("ranges overlap: "+currentRange+" / "+range);
}
currentRange = range;
newList.add(currentRange);
}
}
return newList;
}
/** can two ranges be joined at a common point?
* (1,5) and (5,10) can be joined.
* Because ranges are sometimes created from real numbers, a tolerance is allowed.
* This should should normally not be set beyond 1
* (1,5) and (6,10) would join with tolerance=1, as would (1,5) and (4,10)
* (1,5) and (3,10) would not be joinable with tolerance=1
*
* @param range
* @param tolerance
* @return
*/
public boolean canJoin(IntRange range, int tolerance) {
return Math.abs(this.maxval - range.minval) <= tolerance;
}
public void setMax(int max) {
if (max >= this.minval) {
this.maxval = max;
}
}
public void setMin(int min) {
if (min <= this.maxval) {
this.minval = min;
}
}
}
class IntRangeComparator implements Comparator{
@Override
public int compare(IntRange o1, IntRange o2) {
if (o1 == null || o2 == null) return -1;
return o1.getMin() - o2.getMin();
}
}