Add shuffle order interface and default implementation.

These classes maintain a shuffled order of indices allowing to query the
next, previous, first, and last indices. And also support inserting and
removing elements without changing the shuffled order of the rest.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=164116287
This commit is contained in:
tonihei 2017-08-03 06:32:22 -07:00 committed by Oliver Woodman
parent cb00b0209f
commit 3c6ad40481
2 changed files with 390 additions and 0 deletions

View File

@ -0,0 +1,134 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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 com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder;
import junit.framework.TestCase;
/**
* Unit test for {@link ShuffleOrder}.
*/
public final class ShuffleOrderTest extends TestCase {
public static final long RANDOM_SEED = 1234567890L;
public void testDefaultShuffleOrder() {
assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0);
assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1);
assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5);
for (int initialLength = 0; initialLength < 4; initialLength++) {
for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {
testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 0);
testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 1);
testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5);
}
}
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4);
testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0);
}
public void testUnshuffledShuffleOrder() {
assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0);
assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1);
assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5);
for (int initialLength = 0; initialLength < 4; initialLength++) {
for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {
testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 0);
testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 1);
testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5);
}
}
testCloneAndRemove(new UnshuffledShuffleOrder(5), 0);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 2);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 4);
testCloneAndRemove(new UnshuffledShuffleOrder(1), 0);
}
public void testUnshuffledShuffleOrderIsUnshuffled() {
ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5);
assertEquals(0, shuffleOrder.getFirstIndex());
assertEquals(4, shuffleOrder.getLastIndex());
for (int i = 0; i < 4; i++) {
assertEquals(i + 1, shuffleOrder.getNextIndex(i));
}
}
private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) {
assertEquals(length, shuffleOrder.getLength());
if (length == 0) {
assertEquals(C.INDEX_UNSET, shuffleOrder.getFirstIndex());
assertEquals(C.INDEX_UNSET, shuffleOrder.getLastIndex());
} else {
int[] indices = new int[length];
indices[0] = shuffleOrder.getFirstIndex();
assertEquals(C.INDEX_UNSET, shuffleOrder.getPreviousIndex(indices[0]));
for (int i = 1; i < length; i++) {
indices[i] = shuffleOrder.getNextIndex(indices[i - 1]);
assertEquals(indices[i - 1], shuffleOrder.getPreviousIndex(indices[i]));
for (int j = 0; j < i; j++) {
assertTrue(indices[i] != indices[j]);
}
}
assertEquals(indices[length - 1], shuffleOrder.getLastIndex());
assertEquals(C.INDEX_UNSET, shuffleOrder.getNextIndex(indices[length - 1]));
for (int i = 0; i < length; i++) {
assertTrue(indices[i] >= 0 && indices[i] < length);
}
}
}
private static void testCloneAndInsert(ShuffleOrder shuffleOrder, int position, int count) {
ShuffleOrder newOrder = shuffleOrder.cloneAndInsert(position, count);
assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() + count);
// Assert all elements still have the relative same order
for (int i = 0; i < shuffleOrder.getLength(); i++) {
int expectedNextIndex = shuffleOrder.getNextIndex(i);
if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {
expectedNextIndex += count;
}
int newNextIndex = newOrder.getNextIndex(i < position ? i : i + count);
while (newNextIndex >= position && newNextIndex < position + count) {
newNextIndex = newOrder.getNextIndex(newNextIndex);
}
assertEquals(expectedNextIndex, newNextIndex);
}
}
private static void testCloneAndRemove(ShuffleOrder shuffleOrder, int position) {
ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(position);
assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - 1);
// Assert all elements still have the relative same order
for (int i = 0; i < shuffleOrder.getLength(); i++) {
if (i == position) {
continue;
}
int expectedNextIndex = shuffleOrder.getNextIndex(i);
if (expectedNextIndex == position) {
expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex);
}
if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {
expectedNextIndex--;
}
int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1);
assertEquals(expectedNextIndex, newNextIndex);
}
}
}

View File

@ -0,0 +1,256 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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 com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import java.util.Arrays;
import java.util.Random;
/**
* Shuffled order of indices.
*/
public interface ShuffleOrder {
/**
* The default {@link ShuffleOrder} implementation for random shuffle order.
*/
class DefaultShuffleOrder implements ShuffleOrder {
private final Random random;
private final int[] shuffled;
private final int[] indexInShuffled;
/**
* Creates an instance with a specified length.
*
* @param length The length of the shuffle order.
*/
public DefaultShuffleOrder(int length) {
this(length, new Random());
}
/**
* Creates an instance with a specified length and the specified random seed. Shuffle orders of
* the same length initialized with the same random seed are guaranteed to be equal.
*
* @param length The length of the shuffle order.
* @param randomSeed A random seed.
*/
public DefaultShuffleOrder(int length, long randomSeed) {
this(length, new Random(randomSeed));
}
private DefaultShuffleOrder(int length, Random random) {
this(createShuffledList(length, random), random);
}
private DefaultShuffleOrder(int[] shuffled, Random random) {
this.shuffled = shuffled;
this.random = random;
this.indexInShuffled = new int[shuffled.length];
for (int i = 0; i < shuffled.length; i++) {
indexInShuffled[shuffled[i]] = i;
}
}
@Override
public int getLength() {
return shuffled.length;
}
@Override
public int getNextIndex(int index) {
int shuffledIndex = indexInShuffled[index];
return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET;
}
@Override
public int getPreviousIndex(int index) {
int shuffledIndex = indexInShuffled[index];
return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET;
}
@Override
public int getLastIndex() {
return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET;
}
@Override
public int getFirstIndex() {
return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET;
}
@Override
public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
int[] insertionPoints = new int[insertionCount];
int[] insertionValues = new int[insertionCount];
for (int i = 0; i < insertionCount; i++) {
insertionPoints[i] = random.nextInt(shuffled.length + 1);
int swapIndex = random.nextInt(i + 1);
insertionValues[i] = insertionValues[swapIndex];
insertionValues[swapIndex] = i + insertionIndex;
}
Arrays.sort(insertionPoints);
int[] newShuffled = new int[shuffled.length + insertionCount];
int indexInOldShuffled = 0;
int indexInInsertionList = 0;
for (int i = 0; i < shuffled.length + insertionCount; i++) {
if (indexInInsertionList < insertionCount
&& indexInOldShuffled == insertionPoints[indexInInsertionList]) {
newShuffled[i] = insertionValues[indexInInsertionList++];
} else {
newShuffled[i] = shuffled[indexInOldShuffled++];
if (newShuffled[i] >= insertionIndex) {
newShuffled[i] += insertionCount;
}
}
}
return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
}
@Override
public ShuffleOrder cloneAndRemove(int removalIndex) {
int[] newShuffled = new int[shuffled.length - 1];
boolean foundRemovedElement = false;
for (int i = 0; i < shuffled.length; i++) {
if (shuffled[i] == removalIndex) {
foundRemovedElement = true;
} else {
newShuffled[foundRemovedElement ? i - 1 : i] = shuffled[i] > removalIndex
? shuffled[i] - 1 : shuffled[i];
}
}
return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
}
private static int[] createShuffledList(int length, Random random) {
int[] shuffled = new int[length];
for (int i = 0; i < length; i++) {
int swapIndex = random.nextInt(i + 1);
shuffled[i] = shuffled[swapIndex];
shuffled[swapIndex] = i;
}
return shuffled;
}
}
/**
* A {@link ShuffleOrder} implementation which does not shuffle.
*/
final class UnshuffledShuffleOrder implements ShuffleOrder {
private final int length;
/**
* Creates an instance with a specified length.
*
* @param length The length of the shuffle order.
*/
public UnshuffledShuffleOrder(int length) {
this.length = length;
}
@Override
public int getLength() {
return length;
}
@Override
public int getNextIndex(int index) {
return ++index < length ? index : C.INDEX_UNSET;
}
@Override
public int getPreviousIndex(int index) {
return --index >= 0 ? index : C.INDEX_UNSET;
}
@Override
public int getLastIndex() {
return length > 0 ? length - 1 : C.INDEX_UNSET;
}
@Override
public int getFirstIndex() {
return length > 0 ? 0 : C.INDEX_UNSET;
}
@Override
public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
return new UnshuffledShuffleOrder(length + insertionCount);
}
@Override
public ShuffleOrder cloneAndRemove(int removalIndex) {
return new UnshuffledShuffleOrder(length - 1);
}
}
/**
* Returns length of shuffle order.
*/
int getLength();
/**
* Returns the next index in the shuffle order.
*
* @param index An index.
* @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last
* element.
*/
int getNextIndex(int index);
/**
* Returns the previous index in the shuffle order.
*
* @param index An index.
* @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first
* element.
*/
int getPreviousIndex(int index);
/**
* Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is
* empty.
*/
int getLastIndex();
/**
* Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is
* empty.
*/
int getFirstIndex();
/**
* Return a copy of the shuffle order with newly inserted elements.
*
* @param insertionIndex The index in the unshuffled order at which elements are inserted.
* @param insertionCount The number of elements inserted at {@code insertionIndex}.
* @return A copy of this {@link ShuffleOrder} with newly inserted elements.
*/
ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount);
/**
* Return a copy of the shuffle order with one element removed.
*
* @param removalIndex The index of the element in the unshuffled order which is to be removed.
* @return A copy of this {@link ShuffleOrder} without the removed element.
*/
ShuffleOrder cloneAndRemove(int removalIndex);
}