Skip to content

Commit

Permalink
✨ Introducing ExcludingRange utility
Browse files Browse the repository at this point in the history
♻️ VFXListManager: replace IntegerRange.expandRangeToSet(...) with ExcludingRange

Signed-off-by: palexdev <alessandro.parisi406@gmail.com>
  • Loading branch information
palexdev committed Mar 11, 2024
1 parent 6cb56f1 commit b1e94ed
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.github.palexdev.mfxcore.base.beans.range.IntegerRange;
import io.github.palexdev.mfxcore.behavior.BehaviorBase;
import io.github.palexdev.virtualizedfx.cells.Cell;
import io.github.palexdev.virtualizedfx.utils.ExcludingRange;
import io.github.palexdev.virtualizedfx.utils.StateMap;
import io.github.palexdev.virtualizedfx.utils.Utils;
import io.github.palexdev.virtualizedfx.utils.VFXCellsCache;
Expand Down Expand Up @@ -245,10 +246,9 @@ protected void onCellFactoryChanged() {
* For this reason, cells from the old state are not removed by index, but by <b>item</b>,
* {@link VFXListState#removeCell(Object)}. First, we retrieve the item from the list that is now at index i
* (this index comes from the loop on the range), then we try to remove the cell for this item from the old state.
* If the cell is found, we update it by index and add it to the new state. Note that the index is also removed from
* the expanded range.
* If the cell is found, we update it by index and add it to the new state. Note that the index is also excluded from the range.
* <p>
* Now that 'common' cells have been properly updated, the remaining items are processed by the {@link #remainingAlgorithm(Set, VFXListState)}.
* Now that 'common' cells have been properly updated, the remaining items are processed by the {@link #remainingAlgorithm(ExcludingRange, VFXListState)}.
* <p></p>
* Last notes:
* <p> 1) This is one of those methods that to produce a valid new state needs to validate the list's positions,
Expand Down Expand Up @@ -276,22 +276,22 @@ protected void onItemsChanged() {

// Compute range and new state
IntegerRange range = helper.range();
Set<Integer> expanded = IntegerRange.expandRangeToSet(range);
ExcludingRange eRange = ExcludingRange.of(range);
VFXListState<T, C> newState = new VFXListState<>(list, range);

// First update by index
for (Integer index : range) {
T item = helper.indexToItem(index);
C c = current.removeCell(item);
if (c != null) {
expanded.remove(index);
eRange.exclude(index);
c.updateIndex(index);
newState.addCell(index, item, c);
}
}

// Process remaining with the "remaining' algorithm"
remainingAlgorithm(expanded, newState);
remainingAlgorithm(eRange, newState);

if (disposeCurrent()) newState.setCellsChanged(true);
list.update(newState);
Expand Down Expand Up @@ -406,73 +406,73 @@ protected void onSpacingChanged() {
* it's enough to move the cells from the current state to the new state. For indexes which are not found
* in the current state, a new cell is either taken from the old state, taken from cache or created by the cell factory.
* <p>
* (The last operations are delegated to the {@link #remainingAlgorithm(Set, VFXListState)}).
* (The last operations are delegated to the {@link #remainingAlgorithm(ExcludingRange, VFXListState)}).
*
* @see VFXListHelper#indexToCell(int)
* @see VFXList#cellFactoryProperty()
*/
protected void moveReuseCreateAlgorithm(IntegerRange range, VFXListState<T, C> newState) {
VFXList<T, C> list = getNode();
VFXListState<T, C> current = list.getState();
Set<Integer> remaining = new LinkedHashSet<>();
ExcludingRange eRange = new ExcludingRange(range);
for (Integer index : range) {
C c = current.removeCell(index);
if (c == null) {
remaining.add(index);
continue;
}
if (c == null) continue;
eRange.exclude(index);
newState.addCell(index, c);
}
remainingAlgorithm(remaining, newState);
remainingAlgorithm(eRange, newState);
}

/**
* Avoids code duplication. Typically used in situations where the previous range and the new one are likely to be
* very close, but most importantly, that do not involve any change in the items' list.
* In such cases, the computation for the new state is divided in two parts:
* <p> 0) Prerequisites: the new range [min, max], the expanded range (a collection of indexes that goes from min to max),
* <p> 0) Prerequisites: the new range [min, max], the excluding range (a helper class to keep track of common cells),
* the current state, and the intersection between the current state's range and the new range
* <p> 1) The intersection allows us to distinguish between cells that can be moved as they are, without any update,
* from the current state to the new one. For this, it's enough to check that the intersection range is valid, and then
* a for loop. Common indexes are also removed from the expanded range!
* <p> 2) The remaining indexes in the expanded range are items that are new. Which means that if there are still cells
* a for loop. Common indexes are also excluded from the range!
* <p> 2) The remaining indexes are items that are new. Which means that if there are still cells
* in the current state, they need to be updated (both index and item). Otherwise, new ones are created by the cell factory.
* <p></p>
* <p> - See {@link Utils#intersection}: used to find the intersection between two ranges
* <p> - See {@link #rangeCheck(IntegerRange, boolean, boolean)}: used to validate the intersection range, both parameters
* are false!
* <p> - See {@link #remainingAlgorithm(Set, VFXListState)}: the second part of the algorithm is delegated to this
* <p> - See {@link #remainingAlgorithm(ExcludingRange, VFXListState)}: the second part of the algorithm is delegated to this
* method
*
* @see ExcludingRange
*/
protected VFXListState<T, C> intersectionAlgorithm() {
VFXList<T, C> list = getNode();
VFXListHelper<T, C> helper = list.getHelper();

// New range, also expanded
// New range
IntegerRange range = helper.range();
Set<Integer> expandedRange = IntegerRange.expandRangeToSet(range);
ExcludingRange eRange = ExcludingRange.of(range);

// Current and new states, intersection between current and new range
VFXListState<T, C> current = list.getState();
VFXListState<T, C> newState = new VFXListState<>(list, range);
IntegerRange intersection = Utils.intersection(current.getRange(), range);

// If range valid, move common cells from current to new state. Also, remove common indexes from expanded range
// If range valid, move common cells from current to new state. Also, exclude common indexes
if (rangeCheck(intersection, false, false))
for (Integer common : intersection) {
newState.addCell(common, current.removeCell(common));
expandedRange.remove(common);
eRange.exclude(common);
}

// Process remaining with the "remaining' algorithm"
remainingAlgorithm(expandedRange, newState);
remainingAlgorithm(eRange, newState);
return newState;
}

/**
* Avoids code duplication. Typically used to process indexes not found in the current state.
* <p>
* For any index in the given collection, a cell is needed. Also, it needs to be updated by index and item both.
* For any index in the given {@link ExcludingRange}, a cell is needed. Also, it needs to be updated by index and item both.
* This cell can come from three sources:
* <p> 1) from the current state if it's not empty yet. Since the cells are stored in a {@link SequencedMap}, one
* is removed by calling {@link StateMap#poll()}.
Expand All @@ -483,15 +483,15 @@ protected VFXListState<T, C> intersectionAlgorithm() {
* be taken from the cache, automatically updates its item then returns it. Otherwise, invokes the
* {@link VFXList#cellFactoryProperty()} to create a new one
*/
protected void remainingAlgorithm(Set<Integer> remaining, VFXListState<T, C> newState) {
protected void remainingAlgorithm(ExcludingRange eRange, VFXListState<T, C> newState) {
VFXList<T, C> list = getNode();
VFXListHelper<T, C> helper = list.getHelper();
VFXListState<T, C> current = list.getState();

// Indexes in the given set were not found in the current state.
// Which means item updates. Cells are retrieved either from the current state (if not empty), from the cache,
// or created from the factory
for (Integer index : remaining) {
for (Integer index : eRange) {
T item = helper.indexToItem(index);
C c;
if (!current.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package io.github.palexdev.virtualizedfx.utils;

import io.github.palexdev.mfxcore.base.beans.range.IntegerRange;

import java.util.*;

/**
* A utility class which, given a starting {@link IntegerRange}, allows to exclude some or all of the values for the given
* range.
* <p>
* This basically offers a much faster alternative to {@link IntegerRange#expandRangeToSet(IntegerRange)}. Let's see an example:
* <pre>
* {@code
* // Let's say you have a range of [3, 7] and you want to perform some checks on each value in the range
* // You want to perform a certain operation on every value for which the check fails
* // Before this class you could do something like this...
* IntegerRange range = IntegerRange.of(3, 7);
* Set<Integer> expanded = IntegerRange.expandRangeToSet(range);
* for (Integer val : range) {
* if (check(val)) {
* expanded.remove(val);
* }
* }
* // At the end of the for you have a Set of values for which the check failed, at this point...
* for (Integer failingVal : expanded) {
* process(failingVal)
* }
*
* //____________________________________________________________________________________________________//
*
* // Now, the issue here is that the `expandRangeToSet(...)` operation can be costly, and we don't really need it
* // By using an ExcludingRange the code becomes like this...
* IntegerRange range = IntegerRange.of(3, 7);
* ExcludingRange eRange = ExcludingRange.of(range);
* for (Integer val : range) {
* if (check(val)) {
* eRange.exclude(val);
* }
* }
*
* for (Integer failingVal : eRange) {
* process(failingVal);
* }
* // There's no need to expand the whole range to a Set anymore and ExcludingRange also implements Iterable thus offering
* // a pretty efficient Iterator which allows to use enhanced for loops too.
* }
* </pre>
*/
public class ExcludingRange implements Iterable<Integer> {
//================================================================================
// Properties
//================================================================================
private final IntegerRange range;
private final Set<Integer> excluded;

//================================================================================
// Constructors
//================================================================================
public ExcludingRange(IntegerRange range) {
this.range = range;
this.excluded = new HashSet<>(range.diff() + 1);
}

public ExcludingRange(int min, int max) {
this(IntegerRange.of(min, max));
}

public static ExcludingRange of(IntegerRange range) {
return new ExcludingRange(range);
}

public static ExcludingRange of(int min, int max) {
return new ExcludingRange(min, max);
}

//================================================================================
// Overridden Methods
//================================================================================
@Override
public Iterator<Integer> iterator() {
return new ExcludingIterator();
}

//================================================================================
// Methods
//================================================================================

/**
* Adds the given value to the set of excluded values.
* <p>
* If it is out of range, nothing is done.
*
* @see IntegerRange#inRangeOf(int, IntegerRange)
*/
public ExcludingRange exclude(int val) {
if (IntegerRange.inRangeOf(val, range)) {
excluded.add(val);
}
return this;
}

/**
* Iterates over the given values and delegates to {@link #exclude(int)}
*/
public ExcludingRange excludeAll(int... vals) {
for (int val : vals) exclude(val);
return this;
}

/**
* Iterates over the given range and delegates to {@link #exclude(int)}
*/
public ExcludingRange excludeAll(IntegerRange range) {
range.forEach(this::exclude);
return this;
}

/**
* @return whether the given value was excluded or outside the starting range
*/
public boolean isExcluded(int val) {
return excluded.contains(val) || !IntegerRange.inRangeOf(val, range);
}

/**
* @return the set of excluded values, unmodifiable
*/
public Set<Integer> getExcluded() {
return Collections.unmodifiableSet(excluded);
}

//================================================================================
// Internal Classes
//================================================================================

/**
* A very simple iterator loop on the elements of an {@link ExcludingRange}.
* <p>
* Of course, it takes into account the values that were excluded from the range, {@link ExcludingRange#isExcluded(int)}.
*/
private class ExcludingIterator implements Iterator<Integer> {
private int current = range.getMin();

@Override
public boolean hasNext() {
while (current <= range.getMax() && isExcluded(current)) current++;
return current <= range.getMax();
}

@Override
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
return current++;
}
}
}

0 comments on commit b1e94ed

Please sign in to comment.