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

net.solarnetwork.util.IntRangeSet Maven / Gradle / Ivy

There is a newer version: 3.27.0
Show newest version
/* ==================================================================
 * IntRangeSet.java - 15/01/2020 10:47:50 am
 *
 * Copyright 2020 SolarNetwork.net Dev Team
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 * 02111-1307 USA
 * ==================================================================
 */

package net.solarnetwork.util;

import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NavigableSet;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.SortedSet;
import java.util.function.IntConsumer;

/**
 * A {@code Set} implementation based on ordered and disjoint integer ranges.
 *
 * 

* This set is optimized for integer sets where ranges of consecutive integers * are common. Instead of storing individual integer values, it stores an * ordered list of {@link IntRange} objects whose ranges do not overlap. The * {@link #ranges()} method can be used to get the list of ranges. *

* * @author matt * @version 1.2 * @since 1.58 */ public class IntRangeSet extends AbstractSet implements NavigableSet, IntRangeContainer, IntOrderedIterable, Cloneable { private final boolean immutable; private final List ranges; /** * Default constructor. */ public IntRangeSet() { this(16); } /** * Construct with an initial capacity. * * @param initialCapacity * the initial capacity, which refers to the number of discreet * ranges, not elements */ public IntRangeSet(int initialCapacity) { super(); this.ranges = new ArrayList<>(initialCapacity); this.immutable = false; } /** * Construct from an existing set of values. * * @param c * the integer collection to copy into this set */ public IntRangeSet(Collection c) { this(); addAll(c); } /** * Construct from an existing set of ranges. * * @param ranges * the ranges to copy into this set */ public IntRangeSet(IntRange... ranges) { this(ranges != null ? ranges.length : 16); if ( ranges != null ) { for ( IntRange r : ranges ) { addRange(r.getMin(), r.getMax()); } } } private IntRangeSet(List ranges, boolean immutable) { super(); this.ranges = new ArrayList<>(ranges.size()); this.ranges.addAll(ranges); this.immutable = immutable; } /** * Get an immutable copy of this set. * * @return an immutable copy of this set */ public IntRangeSet immutableCopy() { if ( immutable ) { return this; } return new IntRangeSet(this.ranges, true); } @Override public Object clone() { return new IntRangeSet(ranges, immutable); } @Override public String toString() { return ranges.toString(); } @Override public boolean contains(Object o) { if ( !(o instanceof Integer) ) { return false; } return contains(((Integer) o).intValue()); } @Override public boolean contains(int value) { for ( IntRange r : ranges ) { if ( r.contains(value) ) { return true; } } return false; } @Override public void forEachOrdered(IntConsumer action) { Objects.requireNonNull(action); for ( IntRange r : ranges ) { for ( int i = r.getMin(); i <= r.getMax(); i++ ) { action.accept(i); if ( i == Integer.MAX_VALUE ) { // prevent overflow return; } } } } @Override public void forEachOrdered(int min, int max, IntConsumer action) { Objects.requireNonNull(action); for ( IntRange r : ranges ) { if ( min > r.getMax() ) { continue; } else if ( max <= r.getMin() ) { break; } int i = r.getMin(); if ( i < min ) { i = min; } for ( final int stop = max <= r.getMax() ? max - 1 : r.getMax(); i <= stop; i++ ) { action.accept(i); if ( i == Integer.MAX_VALUE ) { // prevent overflow return; } } } } /** * Add an integer to this set. * * {@inheritDoc} * * @see #add(int) */ @Override public boolean add(Integer e) { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } if ( e == null ) { throw new IllegalArgumentException("Integer cannot be null"); } return add(e.intValue()); } /** * Add a single integer to this set. * *

* This method requires a linear search of all existing discreet ranges to * maintain ordering and possibly merge the value into an existing range or * cause two ranges to merge together. *

* * @param v * the integer to add * @return {@literal true} if this set changed as a result */ public boolean add(final int v) { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } IntRange p = null; boolean changed = false; if ( ranges.isEmpty() ) { // first range to add ranges.add(new IntRange(v, v)); changed = true; } else { for ( ListIterator itr = ranges.listIterator(); itr.hasNext(); ) { IntRange r = itr.next(); if ( r.contains(v) ) { // already in this set, nothing to do return false; } else if ( v < r.getMin() ) { if ( v + 1 == r.getMin() ) { // new value adjacent to curr minimum; expand curr minimum - 1 if ( p != null && v - 1 == p.getMax() ) { // inserting such that two existing ranges are merged itr.remove(); ranges.set(itr.previousIndex(), new IntRange(p.getMin(), r.getMax())); } else { // just expand curr range left itr.set(new IntRange(v, r.getMax())); } } else if ( p != null && v - 1 == p.getMax() ) { // just expand prev range right ranges.set(itr.previousIndex() - 1, new IntRange(p.getMin(), v)); } else { // insert singleton range before curr ranges.add(itr.previousIndex(), new IntRange(v, v)); } changed = true; break; } p = r; } if ( !changed ) { // append to end if ( p != null && v - 1 == p.getMax() ) { // just expand the last range ranges.set(ranges.size() - 1, new IntRange(p.getMin(), v)); } else { ranges.add(new IntRange(v, v)); } changed = true; } } return changed; } @Override public boolean addAll(Collection col) { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } if ( col == null || col.isEmpty() ) { return false; } final int[] sorted = col.stream().mapToInt(Integer::intValue).toArray(); Arrays.sort(sorted); final int len = sorted.length; if ( len == 1 ) { return add(sorted[0]); } int a = sorted[0]; int b = a; boolean changed = false; for ( int i = 1; i < len; i++ ) { int c = sorted[i]; int d = c - b; if ( d > 1 ) { changed |= addRange(a, b); a = c; } b = c; } changed |= addRange(a, b); return changed; } /** * Add a range of integers, inclusive. * * @param min * the starting value to add * @param max * the last value to add * @return {@literal true} if any changes resulted from adding the given * range * @see #addRange(IntRange) */ public boolean addRange(final int min, final int max) { return addRange(new IntRange(min, max)); } /** * Add a range of integers, inclusive. * *

* This method requires a linear search of all existing discreet ranges to * maintain ordering and possibly merge the given range into an existing * range or cause existing ranges to merge together. *

* * @param range * the range to add * @return {@literal true} if any changes resulted from adding the given * range */ public boolean addRange(IntRange range) { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } boolean changed = false; if ( ranges.isEmpty() ) { // first range to add ranges.add(range); changed = true; } else { for ( ListIterator itr = ranges.listIterator(); itr.hasNext(); ) { IntRange r = itr.next(); if ( r.containsAll(range) ) { // already in this set, nothing to do return false; } else if ( r.canMergeWith(range) ) { IntRange merged = r.mergeWith(range); // try to expand right as far as possible while ( itr.hasNext() ) { IntRange n = itr.next(); if ( merged.canMergeWith(n) ) { merged = merged.mergeWith(n); itr.remove(); } else { // back up to last merged itr.previous(); break; } } ranges.set(itr.previousIndex(), merged); changed = true; break; } else if ( range.getMin() < r.getMin() ) { // no overlap, just insert range before curr ranges.add(itr.previousIndex(), range); changed = true; break; } } if ( !changed ) { // append to end ranges.add(range); changed = true; } } return changed; } @Override public void clear() { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } ranges.clear(); } @Override public Iterator iterator() { return new IntegerIterator(); } /** * Get the ranges of this set. * *

* This returns a "live" reference to the ranges in this set; mutations to * this set will impact the values in the returned collection. *

* * @return the disjoint ranges in this set, ordered from least to greatest, * never {@literal null} */ @Override public Iterable ranges() { return ranges; } @Override public boolean isEmpty() { return ranges.isEmpty(); } @Override public int size() { return ranges.stream().mapToInt(IntRange::length).sum(); } @Override public Comparator comparator() { return null; } @Override public Integer first() { IntRange r = (ranges.isEmpty() ? null : ranges.get(0)); return (r != null ? r.getMin() : null); } @Override public Integer last() { IntRange r = (ranges.isEmpty() ? null : ranges.get(ranges.size() - 1)); return (r != null ? r.getMax() : null); } @Override public Integer min() { // TODO Auto-generated method stub return first(); } @Override public Integer max() { return last(); } @Override public Integer lower(Integer e) { final int v = e; for ( ListIterator itr = ranges.listIterator(); itr.hasNext(); ) { IntRange r = itr.next(); if ( r.contains(v) || r.getMin() >= v ) { if ( v > r.getMin() ) { // current range starts _before_ e, so can return e - 1 return v - 1; } else if ( itr.previousIndex() > 0 ) { // current range starts _on_ or _after_ e, so return previous range max return ranges.get(itr.previousIndex() - 1).getMax(); } else { // there is no lower value available break; } } else if ( r.getMax() < v && !itr.hasNext() ) { // last element's max is lower than e, return that return r.getMax(); } } return null; } @Override public Integer floor(Integer e) { final int v = e; for ( ListIterator itr = ranges.listIterator(); itr.hasNext(); ) { IntRange r = itr.next(); if ( r.contains(v) ) { // current range contains e, so return e return v; } else if ( r.getMin() > v ) { if ( itr.previousIndex() > 0 ) { // current range starts _after_ e, so return previous range max return ranges.get(itr.previousIndex() - 1).getMax(); } else { // no lower element break; } } else if ( r.getMax() < v && !itr.hasNext() ) { // last element's max is lower than e, return that return r.getMax(); } } return null; } @Override public Integer ceiling(Integer e) { final int v = e; final int len = ranges.size(); for ( ListIterator itr = ranges.listIterator(len); itr.hasPrevious(); ) { IntRange r = itr.previous(); if ( r.contains(v) ) { // current range contains e, so return e return v; } else if ( r.getMax() < v ) { if ( itr.nextIndex() + 1 < len ) { // current range ends _before_ e, so return next range min return ranges.get(itr.nextIndex() + 1).getMin(); } else { // no higher element break; } } else if ( r.getMin() > v && !itr.hasPrevious() ) { // first element's min is higher than e, return that return r.getMin(); } } return null; } @Override public Integer higher(Integer e) { final int v = e; final int len = ranges.size(); for ( ListIterator itr = ranges.listIterator(len); itr.hasPrevious(); ) { IntRange r = itr.previous(); if ( r.contains(v) || r.getMax() <= v ) { if ( v < r.getMax() ) { // current range ends _before_ e, so can return e + 1 return v + 1; } else if ( itr.nextIndex() + 1 < len ) { // current range ends _on_ or _before_ e, so return next range min return ranges.get(itr.nextIndex() + 1).getMin(); } else { // there is no higher value available break; } } else if ( r.getMin() > v && !itr.hasPrevious() ) { // first element's min is higher than e, return that return r.getMin(); } } return null; } @Override public boolean remove(Object o) { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } if ( !(o instanceof Integer) ) { return false; } final int v = (Integer) o; for ( ListIterator itr = ranges.listIterator(); itr.hasNext(); ) { IntRange r = itr.next(); if ( r.contains(v) ) { if ( v == r.getMin() ) { if ( v < r.getMax() ) { // contract range from left itr.set(new IntRange(v + 1, r.getMax())); } else { // remove singleton range itr.remove(); } } else if ( v == r.getMax() ) { // contract range from right itr.set(new IntRange(r.getMin(), v - 1)); } else { // create hole by splitting range itr.set(new IntRange(r.getMin(), v - 1)); ranges.add(itr.nextIndex(), new IntRange(v + 1, r.getMax())); } return true; } } return false; } @Override public boolean removeAll(Collection c) { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } if ( c == null ) { return false; } boolean modified = false; for ( Iterator i = c.iterator(); i.hasNext(); ) { modified |= remove(i.next()); } return modified; } @Override public Integer pollFirst() { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } if ( ranges.isEmpty() ) { return null; } IntRange old = ranges.get(0); if ( old.isSingleton() ) { // remove singleton ranges.remove(0); } else { // contract from left ranges.set(0, new IntRange(old.getMin() + 1, old.getMax())); } return old.getMin(); } @Override public Integer pollLast() { if ( immutable ) { throw new UnsupportedOperationException("Set it immutable."); } if ( ranges.isEmpty() ) { return null; } final int lastIndex = ranges.size() - 1; IntRange old = ranges.get(lastIndex); if ( old.isSingleton() ) { // remove singleton ranges.remove(lastIndex); } else { // contract from right ranges.set(lastIndex, new IntRange(old.getMin(), old.getMax() - 1)); } return old.getMax(); } @Override public NavigableSet descendingSet() { return new ReverseSet(this); } @Override public Iterator descendingIterator() { return new IntegerReverseIterator(); } @Override public SortedSet subSet(Integer fromElement, Integer toElement) { return subSet(fromElement, true, toElement, false); } @Override public NavigableSet subSet(Integer fromElement, boolean fromInclusive, Integer toElement, boolean toInclusive) { throw new UnsupportedOperationException(); } @Override public NavigableSet headSet(Integer toElement, boolean inclusive) { throw new UnsupportedOperationException(); } @Override public NavigableSet tailSet(Integer fromElement, boolean inclusive) { throw new UnsupportedOperationException(); } @Override public SortedSet headSet(Integer toElement) { return headSet(toElement, false); } @Override public SortedSet tailSet(Integer fromElement) { return tailSet(fromElement, true); } private class IntegerIterator implements Iterator { private final Iterator rangeItr; private IntRange curr; private int next; private IntegerIterator() { super(); rangeItr = ranges.iterator(); if ( rangeItr.hasNext() ) { curr = rangeItr.next(); next = curr.getMin(); } else { curr = null; } } private IntegerIterator(int min, int max) { super(); rangeItr = ranges.iterator(); next = min; while ( rangeItr.hasNext() ) { curr = rangeItr.next(); if ( curr.contains(min) ) { break; } } if ( !curr.contains(min) ) { curr = null; } } @Override public boolean hasNext() { return (curr != null && (next <= curr.getMax() || rangeItr.hasNext())); } @Override public Integer next() { if ( curr == null ) { throw new NoSuchElementException(); } int n = next; next++; if ( !curr.contains(next) ) { if ( rangeItr.hasNext() ) { curr = rangeItr.next(); next = curr.getMin(); } else { curr = null; } } return n; } } private class IntegerReverseIterator implements Iterator { private final ListIterator rangeItr; private IntRange curr; private int next; private IntegerReverseIterator() { super(); rangeItr = ranges.listIterator(ranges.size()); if ( rangeItr.hasPrevious() ) { curr = rangeItr.previous(); next = curr.getMax(); } else { curr = null; } } private IntegerReverseIterator(int min, int max) { super(); rangeItr = ranges.listIterator(ranges.size()); next = min; while ( rangeItr.hasPrevious() ) { curr = rangeItr.previous(); if ( curr.contains(max) ) { break; } } if ( !curr.contains(max) ) { curr = null; } } @Override public boolean hasNext() { return (curr != null && (next >= curr.getMin() || rangeItr.hasPrevious())); } @Override public Integer next() { if ( curr == null ) { throw new NoSuchElementException(); } int n = next; next--; if ( !curr.contains(next) ) { if ( rangeItr.hasPrevious() ) { curr = rangeItr.previous(); next = curr.getMax(); } else { curr = null; } } return n; } } private static class ReverseSet extends AbstractSet implements NavigableSet { private final IntRangeSet delegate; private ReverseSet(IntRangeSet delegate) { super(); this.delegate = delegate; } @Override public Comparator comparator() { return null; } @Override public boolean add(Integer e) { return delegate.add(e); } @Override public boolean addAll(Collection col) { return delegate.addAll(col); } @Override public void clear() { delegate.clear(); } @Override public Integer first() { return delegate.last(); } @Override public Integer last() { return delegate.first(); } @Override public Integer lower(Integer e) { return delegate.higher(e); } @Override public Integer floor(Integer e) { return delegate.ceiling(e); } @Override public Integer ceiling(Integer e) { return delegate.floor(e); } @Override public Integer higher(Integer e) { return delegate.lower(e); } @Override public Integer pollFirst() { return delegate.pollLast(); } @Override public Integer pollLast() { return delegate.pollFirst(); } @Override public NavigableSet descendingSet() { return delegate; } @Override public Iterator descendingIterator() { return delegate.iterator(); } @Override public SortedSet subSet(Integer fromElement, Integer toElement) { return subSet(fromElement, true, toElement, false); } @Override public NavigableSet subSet(Integer fromElement, boolean fromInclusive, Integer toElement, boolean toInclusive) { throw new UnsupportedOperationException(); } @Override public NavigableSet headSet(Integer toElement, boolean inclusive) { return delegate.tailSet(toElement, inclusive); } @Override public NavigableSet tailSet(Integer fromElement, boolean inclusive) { return delegate.headSet(fromElement, inclusive); } @Override public SortedSet headSet(Integer toElement) { return delegate.tailSet(toElement); } @Override public SortedSet tailSet(Integer fromElement) { return delegate.headSet(fromElement); } @Override public Iterator iterator() { return delegate.descendingIterator(); } @Override public boolean isEmpty() { return delegate.isEmpty(); } @Override public int size() { return delegate.size(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy