All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.lib.commons.region.RegionSet Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) CQSE GmbH
 *
 * 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.conqat.lib.commons.region;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.checkerframework.checker.nullness.qual.NonNull;
import org.conqat.lib.commons.collections.CollectionUtils;
import org.conqat.lib.commons.string.StringUtils;

/**
 * Set of {@link Region} objects. Allows tests for containment on the entire set of regions.
 */
public class RegionSet implements Set {

	/** Name that is used if {@link RegionSet} is created without name */
	private static final String ANONYMOUS = "Anonymous";

	/** Name of this RegionSet */
	private final String name;

	/**
	 * The size the map had when our internal structure was updated (or -1 to indicate a dirty state).
	 * We have to store the size rather than just a flag, as we do not catch remove calls in the
	 * iterator returned. But as you only can remove via an iterator we cannot be fooled by a sequence
	 * of add/remove calls, because we trap add in this class.
	 */
	private transient int cleanSize = -1;

	/** The inner set we are delegating to. */
	private final Set inner = new TreeSet<>(MemberComparator.INSTANCE);

	/** List of start points of the merged regions. */
	private final transient List mergedStart = new ArrayList<>();

	/** List of end points of the merged regions. */
	private final transient List mergedEnd = new ArrayList<>();

	/**
	 * Creates a named {@link RegionSet}.
	 *
	 * @param name
	 *            Name of this region set.
	 */
	public RegionSet(String name) {
		this.name = name;
	}

	/** Creates an unnamed region set. */
	public RegionSet() {
		name = ANONYMOUS;
	}

	/** Returns the name. */
	public String getName() {
		return name;
	}

	/**
	 * Returns true if the position is contained in one of the {@link Region}s in the {@link RegionSet}
	 */
	public boolean contains(int position) {
		int k = getMergedIndex(position);
		return k >= 0 && position <= mergedEnd.get(k);
	}

	/**
	 * Tests whether all of the positions of the region are contained in the {@link RegionSet}
	 */
	public boolean contains(Region region) {
		int k = getMergedIndex(region.getStart());
		return k >= 0 && region.getEnd() <= mergedEnd.get(k);
	}

	/**
	 * Returns the index of the merged region whose start position is before or at the given position
	 * (or -1 if no such region exists).
	 */
	private int getMergedIndex(int position) {
		ensureMergedUpToDate();
		int k = Collections.binarySearch(mergedStart, position);

		// exact match, so return
		if (k >= 0) {
			return k;
		}

		// get insertion point (see the JavaDoc of binarySearch for an
		// explanation of the conversion)
		int insertionPoint = -(k + 1);

		// if it would be inserted at the beginning, there is no such index
		if (insertionPoint == 0) {
			return -1;
		}

		// otherwise, the index must be the one before the insertion point
		return insertionPoint - 1;
	}

	/**
	 * Tests whether any of the positions in the region are contained in the {@link RegionSet}.
	 */
	public boolean containsAny(SimpleRegion region) {
		// if either start or end are in, it contains "any".
		if (contains(region.getStart()) || contains(region.getEnd())) {
			return true;
		}

		// now we know that start and end are not contained in an interval, so
		// to have any point contained, there must be an interval which is
		// completely contained in the given region. But this means that the
		// binary search has to give different results for start and end.
		int startIndex = Collections.binarySearch(mergedStart, region.getStart());
		int endIndex = Collections.binarySearch(mergedStart, region.getEnd());
		return startIndex != endIndex;
	}

	/** Makes sure the merged regions lists are up to date. */
	private void ensureMergedUpToDate() {
		if (inner.size() == cleanSize) {
			return;
		}

		mergedStart.clear();
		mergedEnd.clear();

		int start = -1;
		int end = -2;
		for (Region region : inner) {
			if (region.getStart() <= end + 1) {
				end = Math.max(end, region.getEnd());
			} else {
				if (start >= 0) {
					mergedStart.add(start);
					mergedEnd.add(end);
				}
				start = region.getStart();
				end = region.getEnd();
			}
		}

		if (start >= 0) {
			mergedStart.add(start);
			mergedEnd.add(end);
		}

		cleanSize = inner.size();
	}

	/**
	 * Gets the number of positions contained in the RegionSet. This corresponds to the
	 * (non-overlapping) sum of the length of the regions.
	 */
	public int getPositionCount() {
		ensureMergedUpToDate();
		int count = 0;

		int size = mergedStart.size();
		for (int i = 0; i < size; ++i) {
			count += mergedEnd.get(i) - mergedStart.get(i) + 1;
		}
		return count;
	}

	/**
	 * Returns last position in {@link RegionSet}.
	 *
	 * @throws IllegalStateException
	 *             if {@link RegionSet} is empty
	 */
	public int getLastPosition() {
		if (isEmpty()) {
			throw new IllegalStateException("RegionSet is empty");
		}

		ensureMergedUpToDate();
		// We never add null items to mergedEnd. Therefore, unboxing can't result in an
		// NPE.
		return CollectionUtils.getLast(mergedEnd);
	}

	/**
	 * Returns first position in {@link RegionSet}.
	 *
	 * @throws IllegalStateException
	 *             if {@link RegionSet} is empty
	 */
	public int getFirstPosition() {
		if (isEmpty()) {
			throw new IllegalStateException("RegionSet is empty");
		}

		ensureMergedUpToDate();
		return mergedStart.get(0);
	}

	/**
	 * Creates a new {@link RegionSet} whose regions are a compactified version of this region set, i.e.
	 * the returned set is the minimal set that creates the same answers for all possible
	 * {@link #contains(int)} queries.
	 */
	public RegionSet createCompact() {
		ensureMergedUpToDate();

		RegionSet compacted = new RegionSet(name);
		for (int i = 0; i < mergedStart.size(); ++i) {
			Region region = new Region(mergedStart.get(i), mergedEnd.get(i), name);
			compacted.add(region);
		}
		return compacted;
	}

	/**
	 * Creates a new {@link RegionSet} whose regions are a complement to this {@link RegionSet}.
	 * 

* Inversion is relative to the interval [0, last position] */ public RegionSet createInverted(String name, int lastPosition) { ensureMergedUpToDate(); RegionSet inverted = new RegionSet(name); int lastPos = 0; int size = mergedStart.size(); for (int i = 0; i < size; ++i) { if (mergedStart.get(i) > lastPos) { inverted.add(new Region(lastPos, mergedStart.get(i) - 1, name)); } lastPos = mergedEnd.get(i) + 1; } if (lastPos <= lastPosition) { inverted.add(new Region(lastPos, lastPosition, name)); } return inverted; } /** * Returns true if both {@link RegionSet}s contain the same positions and gaps. */ public boolean positionsEqual(RegionSet other) { if (other == null) { return false; } ensureMergedUpToDate(); other.ensureMergedUpToDate(); return mergedStart.equals(other.mergedStart) && mergedEnd.equals(other.mergedEnd); } /** * Comparator used for sorting the members of this set. Sorts ascending by start, then by end, then * by name. */ private static class MemberComparator implements Comparator { /** Unique instance of this comparator. */ private static final MemberComparator INSTANCE = new MemberComparator(); /** {@inheritDoc} */ @Override public int compare(Region region1, Region region2) { int startDiff = region1.getStart() - region2.getStart(); if (startDiff != 0) { return startDiff; } int lengthDiff = region1.getLength() - region2.getLength(); if (lengthDiff != 0) { return lengthDiff; } return region1.getOrigin().compareTo(region2.getOrigin()); } } /** {@inheritDoc} */ @Override public String toString() { return "{" + StringUtils.concat(inner, ",") + "}"; } /** {@inheritDoc} */ @Override public boolean add(Region o) { cleanSize = -1; return inner.add(o); } /** {@inheritDoc} */ @Override public boolean addAll(@NonNull Collection c) { cleanSize = -1; return inner.addAll(c); } /** {@inheritDoc} */ @Override public void clear() { cleanSize = -1; inner.clear(); } /** {@inheritDoc} */ @Override public boolean contains(Object o) { return inner.contains(o); } /** {@inheritDoc} */ @Override public boolean containsAll(@NonNull Collection c) { return inner.containsAll(c); } /** {@inheritDoc} */ @Override public boolean equals(Object o) { return inner.equals(o); } /** {@inheritDoc} */ @Override public int hashCode() { return inner.hashCode(); } /** {@inheritDoc} */ @Override public boolean isEmpty() { return inner.isEmpty(); } /** {@inheritDoc} */ @Override public @NonNull Iterator iterator() { return inner.iterator(); } /** {@inheritDoc} */ @Override public boolean remove(Object o) { cleanSize = -1; return inner.remove(o); } /** {@inheritDoc} */ @Override public boolean removeAll(@NonNull Collection c) { cleanSize = -1; return inner.removeAll(c); } /** {@inheritDoc} */ @Override public boolean retainAll(@NonNull Collection c) { cleanSize = -1; return inner.retainAll(c); } /** {@inheritDoc} */ @Override public int size() { return inner.size(); } /** {@inheritDoc} */ @Override public Object @NonNull [] toArray() { return inner.toArray(); } /** {@inheritDoc} */ @Override public T @NonNull [] toArray(T @NonNull [] a) { return inner.toArray(a); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy