001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.region;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Set;
026import java.util.TreeSet;
027
028import org.conqat.lib.commons.collections.CollectionUtils;
029import org.conqat.lib.commons.string.StringUtils;
030
031/**
032 * Set of {@link Region} objects. Allows tests for containment on the entire set
033 * of regions.
034 */
035public class RegionSet implements Set<Region> {
036
037        /** Name that is used if {@link RegionSet} is created without name */
038        public static final String ANONYMOUS = "Anonymous";
039
040        /** Name of this RegionSet */
041        private final String name;
042
043        /**
044         * The size the map had when our internal structure was updated (or -1 to
045         * indicate a dirty state). We have to store the size rather than just a
046         * flag, as we do not catch remove calls in the iterator returned. But as
047         * you only can remove via an iterator we cannot be fooled by a sequence of
048         * add/remove calls, because we trap add in this class.
049         */
050        private transient int cleanSize = -1;
051
052        /** The inner set we are delegating to. */
053        private final Set<Region> inner = new TreeSet<Region>(MemberComparator.INSTANCE);
054
055        /** List of start points of the merged regions. */
056        private transient final List<Integer> mergedStart = new ArrayList<Integer>();
057
058        /** List of end points of the merged regions. */
059        private transient final List<Integer> mergedEnd = new ArrayList<Integer>();
060
061        /**
062         * Creates a named {@link RegionSet}.
063         * 
064         * @param name
065         *            Name of this region set.
066         */
067        public RegionSet(String name) {
068                this.name = name;
069        }
070
071        /** Creates an unnamed region set. */
072        public RegionSet() {
073                name = ANONYMOUS;
074        }
075
076        /** Returns the name. */
077        public String getName() {
078                return name;
079        }
080
081        /**
082         * Returns true if the position is contained in one of the {@link Region}s
083         * in the {@link RegionSet}
084         */
085        public boolean contains(int position) {
086                int k = getMergedIndex(position);
087                return k >= 0 && position <= mergedEnd.get(k);
088        }
089
090        /**
091         * Tests whether all of the positions of the region are contained in the
092         * {@link RegionSet}
093         */
094        public boolean contains(Region region) {
095                int k = getMergedIndex(region.getStart());
096                return k >= 0 && region.getEnd() <= mergedEnd.get(k);
097        }
098
099        /**
100         * Returns the index of the merged region whose start position is before or
101         * at the given position (or -1 if no such region exists).
102         */
103        private int getMergedIndex(int position) {
104                ensureMergedUpToDate();
105                int k = Collections.binarySearch(mergedStart, position);
106
107                // exact match, so return
108                if (k >= 0) {
109                        return k;
110                }
111
112                // get insertion point (see the JavaDoc of binarySearch for an
113                // explanation of the conversion)
114                int insertionPoint = -(k + 1);
115
116                // if it would be inserted at the beginning, there is no such index
117                if (insertionPoint == 0) {
118                        return -1;
119                }
120
121                // otherwise, the index must be the one before the insertion point
122                return insertionPoint - 1;
123        }
124
125        /**
126         * Tests whether any of the positions in the region are contained in the
127         * {@link RegionSet}.
128         */
129        public boolean containsAny(Region region) {
130                // if either start or end are in, it contains "any".
131                if (contains(region.getStart()) || contains(region.getEnd())) {
132                        return true;
133                }
134
135                // now we know that start and end are not contained in an interval, so
136                // to have any point contained, there must be an interval which is
137                // completely contained in the given region. But this means that the
138                // binary search has to give different results for start and end.
139                int startIndex = Collections.binarySearch(mergedStart, region.getStart());
140                int endIndex = Collections.binarySearch(mergedStart, region.getEnd());
141                return startIndex != endIndex;
142        }
143
144        /** Makes sure the merged regions lists are up to date. */
145        private void ensureMergedUpToDate() {
146                if (inner.size() == cleanSize) {
147                        return;
148                }
149
150                mergedStart.clear();
151                mergedEnd.clear();
152
153                int start = -1;
154                int end = -2;
155                for (Region region : inner) {
156                        if (region.getStart() <= end + 1) {
157                                end = Math.max(end, region.getEnd());
158                        } else {
159                                if (start >= 0) {
160                                        mergedStart.add(start);
161                                        mergedEnd.add(end);
162                                }
163                                start = region.getStart();
164                                end = region.getEnd();
165                        }
166                }
167
168                if (start >= 0) {
169                        mergedStart.add(start);
170                        mergedEnd.add(end);
171                }
172
173                cleanSize = inner.size();
174        }
175
176        /**
177         * Gets the number of positions contained in the RegionSet. This corresponds
178         * to the (non-overlapping) sum of the length of the regions.
179         */
180        public int getPositionCount() {
181                ensureMergedUpToDate();
182                int count = 0;
183
184                int size = mergedStart.size();
185                for (int i = 0; i < size; ++i) {
186                        count += mergedEnd.get(i) - mergedStart.get(i) + 1;
187                }
188                return count;
189        }
190
191        /**
192         * Returns last position in {@link RegionSet}.
193         * 
194         * @throws IllegalStateException
195         *             if {@link RegionSet} is empty
196         */
197        public int getLastPosition() {
198                if (isEmpty()) {
199                        throw new IllegalStateException("RegionSet is empty");
200                }
201
202                ensureMergedUpToDate();
203                return CollectionUtils.getLast(mergedEnd);
204        }
205
206        /**
207         * Returns first position in {@link RegionSet}.
208         * 
209         * @throws IllegalStateException
210         *             if {@link RegionSet} is empty
211         */
212        public int getFirstPosition() {
213                if (isEmpty()) {
214                        throw new IllegalStateException("RegionSet is empty");
215                }
216
217                ensureMergedUpToDate();
218                return mergedStart.get(0);
219        }
220
221        /**
222         * Creates a new {@link RegionSet} whose regions are a compactified version
223         * of this region set, i.e. the returned set is the minimal set that creates
224         * the same answers for all possible {@link #contains(int)} queries.
225         */
226        public RegionSet createCompact() {
227                ensureMergedUpToDate();
228
229                RegionSet compacted = new RegionSet(name);
230                for (int i = 0; i < mergedStart.size(); ++i) {
231                        Region region = new Region(mergedStart.get(i), mergedEnd.get(i), name);
232                        compacted.add(region);
233                }
234                return compacted;
235        }
236
237        /**
238         * Creates a new {@link RegionSet} whose regions are a complement to this
239         * {@link RegionSet}.
240         * 
241         * Inversion is relative to the interval [0, last position]
242         */
243        public RegionSet createInverted(String name, int lastPosition) {
244                ensureMergedUpToDate();
245
246                RegionSet inverted = new RegionSet(name);
247                int lastPos = 0;
248                int size = mergedStart.size();
249                for (int i = 0; i < size; ++i) {
250                        if (mergedStart.get(i) > lastPos) {
251                                inverted.add(new Region(lastPos, mergedStart.get(i) - 1, name));
252                        }
253                        lastPos = mergedEnd.get(i) + 1;
254                }
255
256                if (lastPos <= lastPosition) {
257                        inverted.add(new Region(lastPos, lastPosition, name));
258                }
259
260                return inverted;
261        }
262
263        /**
264         * Returns true if both {@link RegionSet}s contain the same positions and
265         * gaps.
266         */
267        public boolean positionsEqual(RegionSet other) {
268                if (other == null) {
269                        return false;
270                }
271
272                ensureMergedUpToDate();
273                other.ensureMergedUpToDate();
274
275                return mergedStart.equals(other.mergedStart) && mergedEnd.equals(other.mergedEnd);
276        }
277
278        /**
279         * Comparator used for sorting the members of this set. Sorts ascending by
280         * start, then by end, then by name.
281         */
282        private static class MemberComparator implements Comparator<Region> {
283
284                /** Unique instance of this comparator. */
285                private final static MemberComparator INSTANCE = new MemberComparator();
286
287                /** {@inheritDoc} */
288                @Override
289                public int compare(Region region1, Region region2) {
290                        int startDiff = region1.getStart() - region2.getStart();
291                        if (startDiff != 0) {
292                                return startDiff;
293                        }
294
295                        int lengthDiff = region1.getLength() - region2.getLength();
296                        if (lengthDiff != 0) {
297                                return lengthDiff;
298                        }
299
300                        return region1.getOrigin().compareTo(region2.getOrigin());
301                }
302        }
303
304        /** {@inheritDoc} */
305        @Override
306        public String toString() {
307                return "{" + StringUtils.concat(inner, ",") + "}";
308        }
309
310        /** {@inheritDoc} */
311        @Override
312        public boolean add(Region o) {
313                cleanSize = -1;
314                return inner.add(o);
315        }
316
317        /** {@inheritDoc} */
318        @Override
319        public boolean addAll(Collection<? extends Region> c) {
320                cleanSize = -1;
321                return inner.addAll(c);
322        }
323
324        /** {@inheritDoc} */
325        @Override
326        public void clear() {
327                cleanSize = -1;
328                inner.clear();
329        }
330
331        /** {@inheritDoc} */
332        @Override
333        public boolean contains(Object o) {
334                return inner.contains(o);
335        }
336
337        /** {@inheritDoc} */
338        @Override
339        public boolean containsAll(Collection<?> c) {
340                return inner.containsAll(c);
341        }
342
343        /** {@inheritDoc} */
344        @Override
345        public boolean equals(Object o) {
346                return inner.equals(o);
347        }
348
349        /** {@inheritDoc} */
350        @Override
351        public int hashCode() {
352                return inner.hashCode();
353        }
354
355        /** {@inheritDoc} */
356        @Override
357        public boolean isEmpty() {
358                return inner.isEmpty();
359        }
360
361        /** {@inheritDoc} */
362        @Override
363        public Iterator<Region> iterator() {
364                return inner.iterator();
365        }
366
367        /** {@inheritDoc} */
368        @Override
369        public boolean remove(Object o) {
370                cleanSize = -1;
371                return inner.remove(o);
372        }
373
374        /** {@inheritDoc} */
375        @Override
376        public boolean removeAll(Collection<?> c) {
377                cleanSize = -1;
378                return inner.removeAll(c);
379        }
380
381        /** {@inheritDoc} */
382        @Override
383        public boolean retainAll(Collection<?> c) {
384                cleanSize = -1;
385                return inner.retainAll(c);
386        }
387
388        /** {@inheritDoc} */
389        @Override
390        public int size() {
391                return inner.size();
392        }
393
394        /** {@inheritDoc} */
395        @Override
396        public Object[] toArray() {
397                return inner.toArray();
398        }
399
400        /** {@inheritDoc} */
401        @Override
402        public <T> T[] toArray(T[] a) {
403                return inner.toArray(a);
404        }
405}