Rework smart shuffle
This commit is contained in:
parent
6e199de7ab
commit
a265d17f54
|
@ -8,6 +8,7 @@ import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -116,38 +117,23 @@ public class FeedItemPermutors {
|
||||||
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
|
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
|
||||||
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
|
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
|
||||||
*
|
*
|
||||||
* The Smart Shuffle algorithm involves spreading episodes from each feed out over the whole
|
* Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
|
||||||
* queue. To do this, we calculate the number of episodes in each feed, then a common multiple
|
* This method first starts with a queue of the final size, where each slot is empty (null).
|
||||||
* (not the smallest); each episode is then spread out, and we sort the resulting list of
|
* It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
|
||||||
* episodes by "spread out factor" and feed name.
|
* The podcast with the second-most number of episodes (`D`) is then
|
||||||
|
* placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
|
||||||
|
* This continues, until we end up with: `EEBEDEECEDEEAEE`.
|
||||||
*
|
*
|
||||||
* For example, given a queue containing three episodes each from three different feeds
|
* Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
|
||||||
* (A, B, and C), a simple pubdate sort might result in a queue that looks like the following:
|
|
||||||
*
|
|
||||||
* B1, B2, B3, A1, A2, C1, C2, C3, A3
|
|
||||||
*
|
|
||||||
* (note that feed B episodes were all published before the first feed A episode, so a simple
|
|
||||||
* pubdate sort will often result in significant clustering of episodes from a single feed)
|
|
||||||
*
|
|
||||||
* Using Smart Shuffle, the resulting queue would look like the following:
|
|
||||||
*
|
|
||||||
* A1, B1, C1, A2, B2, C2, A3, B3, C3
|
|
||||||
*
|
|
||||||
* (note that episodes above <i>aren't strictly ordered in terms of pubdate</i>, but episodes
|
|
||||||
* of each feed <b>do</b> appear in pubdate order)
|
|
||||||
*
|
*
|
||||||
* @param queue A (modifiable) list of FeedItem elements to be reordered.
|
* @param queue A (modifiable) list of FeedItem elements to be reordered.
|
||||||
* @param ascending {@code true} to use ascending pubdate in the reordering;
|
* @param ascending {@code true} to use ascending pubdate in the reordering;
|
||||||
* {@code false} for descending.
|
* {@code false} for descending.
|
||||||
*/
|
*/
|
||||||
private static void smartShuffle(List<FeedItem> queue, boolean ascending) {
|
private static void smartShuffle(List<FeedItem> queue, boolean ascending) {
|
||||||
|
|
||||||
// Divide FeedItems into lists by feed
|
// Divide FeedItems into lists by feed
|
||||||
|
|
||||||
Map<Long, List<FeedItem>> map = new HashMap<>();
|
Map<Long, List<FeedItem>> map = new HashMap<>();
|
||||||
|
for (FeedItem item : queue) {
|
||||||
while (!queue.isEmpty()) {
|
|
||||||
FeedItem item = queue.remove(0);
|
|
||||||
Long id = item.getFeedId();
|
Long id = item.getFeedId();
|
||||||
if (!map.containsKey(id)) {
|
if (!map.containsKey(id)) {
|
||||||
map.put(id, new ArrayList<>());
|
map.put(id, new ArrayList<>());
|
||||||
|
@ -156,55 +142,43 @@ public class FeedItemPermutors {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort each individual list by PubDate (ascending/descending)
|
// Sort each individual list by PubDate (ascending/descending)
|
||||||
|
|
||||||
Comparator<FeedItem> itemComparator = ascending
|
Comparator<FeedItem> itemComparator = ascending
|
||||||
? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate())
|
? (f1, f2) -> f1.getPubDate().compareTo(f2.getPubDate())
|
||||||
: (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate());
|
: (f1, f2) -> f2.getPubDate().compareTo(f1.getPubDate());
|
||||||
|
List<List<FeedItem>> feeds = new ArrayList<>();
|
||||||
// Calculate the spread
|
|
||||||
|
|
||||||
long spread = 0;
|
|
||||||
for (Map.Entry<Long, List<FeedItem>> mapEntry : map.entrySet()) {
|
for (Map.Entry<Long, List<FeedItem>> mapEntry : map.entrySet()) {
|
||||||
List<FeedItem> feedItems = mapEntry.getValue();
|
Collections.sort(mapEntry.getValue(), itemComparator);
|
||||||
Collections.sort(feedItems, itemComparator);
|
feeds.add(mapEntry.getValue());
|
||||||
if (spread == 0) {
|
|
||||||
spread = feedItems.size();
|
|
||||||
} else if (spread % feedItems.size() != 0){
|
|
||||||
spread *= feedItems.size();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a list of the individual FeedItems lists, and sort it by feed title (ascending).
|
ArrayList<Integer> emptySlots = new ArrayList<>();
|
||||||
// Doing this ensures that the feed order we use is predictable/deterministic.
|
for (int i = 0; i < queue.size(); i++) {
|
||||||
|
queue.set(i, null);
|
||||||
|
emptySlots.add(i);
|
||||||
|
}
|
||||||
|
|
||||||
List<List<FeedItem>> feeds = new ArrayList<>(map.values());
|
// Starting with the largest feed, place items spread out through the empty slots in the queue
|
||||||
Collections.sort(feeds,
|
Collections.sort(feeds, (f1, f2) -> Integer.compare(f2.size(), f1.size()));
|
||||||
(f1, f2) -> f1.get(0).getFeed().getTitle().compareTo(f2.get(0).getFeed().getTitle()));
|
|
||||||
|
|
||||||
// Spread each episode out
|
|
||||||
Map<Long, List<FeedItem>> spreadItems = new HashMap<>();
|
|
||||||
for (List<FeedItem> feedItems : feeds) {
|
for (List<FeedItem> feedItems : feeds) {
|
||||||
long thisSpread = spread / feedItems.size();
|
double spread = (double) emptySlots.size() / (feedItems.size() + 1);
|
||||||
if (thisSpread == 0) {
|
Iterator<Integer> emptySlotIterator = emptySlots.iterator();
|
||||||
thisSpread = 1;
|
int skipped = 0;
|
||||||
}
|
int placed = 0;
|
||||||
// Starting from 0 ensures we front-load, so the queue starts with one episode from
|
while (emptySlotIterator.hasNext()) {
|
||||||
// each feed in the queue
|
int nextEmptySlot = emptySlotIterator.next();
|
||||||
long itemSpread = 0;
|
skipped++;
|
||||||
for (FeedItem feedItem : feedItems) {
|
if (skipped >= spread * (placed + 1)) {
|
||||||
if (!spreadItems.containsKey(itemSpread)) {
|
if (queue.get(nextEmptySlot) != null) {
|
||||||
spreadItems.put(itemSpread, new ArrayList<>());
|
throw new RuntimeException("Slot to be placed in not empty");
|
||||||
|
}
|
||||||
|
queue.set(nextEmptySlot, feedItems.get(placed));
|
||||||
|
emptySlotIterator.remove();
|
||||||
|
placed++;
|
||||||
|
if (placed == feedItems.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
spreadItems.get(itemSpread).add(feedItem);
|
|
||||||
itemSpread += thisSpread;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go through the spread items and add them to the queue
|
|
||||||
List<Long> spreads = new ArrayList<>(spreadItems.keySet());
|
|
||||||
Collections.sort(spreads);
|
|
||||||
for (long itemSpread : spreads) {
|
|
||||||
queue.addAll(spreadItems.get(itemSpread));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue