the-algorithm/product-mixer/component-library/src/main/scala/com/twitter/product_mixer/component_library/selector/DynamicPositionSelector.scala
twitter-team ef4c5eb65e Twitter Recommendation Algorithm
Please note we have force-pushed a new initial commit in order to remove some publicly-available Twitter user information. Note that this process may be required in the future.
2023-03-31 17:36:31 -05:00

125 lines
4.8 KiB
Scala

package com.twitter.product_mixer.component_library.selector
private[selector] object DynamicPositionSelector {
sealed trait IndexType
case object RelativeIndices extends IndexType
case object AbsoluteIndices extends IndexType
/**
* Given an existing `result` seq, inserts candidates from `candidatesToInsertByIndex` into the `result` 1-by-1 with
* the provided index being the index relative to the `result` if given [[RelativeIndices]] or
* absolute index if given [[AbsoluteIndices]] (excluding duplicate insertions at an index, see below).
*
* Indices below 0 are added to the front and indices > the length are added to the end
*
* @note if multiple candidates exist with the same index, they are inserted in the order which they appear and only count
* as a single element with regards to the absolute index values, see the example below
*
* @example when using [[RelativeIndices]] {{{
* mergeByIndexIntoResult(
* Seq(
* 0 -> "a",
* 0 -> "b",
* 0 -> "c",
* 1 -> "e",
* 2 -> "g",
* 2 -> "h"),
* Seq(
* "D",
* "F"
* ),
* RelativeIndices) == Seq(
* "a",
* "b",
* "c",
* "D",
* "e",
* "F",
* "g",
* "h"
* )
* }}}
*
* @example when using [[AbsoluteIndices]] {{{
* mergeByIndexIntoResult(
* Seq(
* 0 -> "a",
* 0 -> "b",
* 1 -> "c",
* 3 -> "e",
* 5 -> "g",
* 6 -> "h"),
* Seq(
* "D",
* "F"
* ),
* AbsoluteIndices) == Seq(
* "a", // index 0, "a" and "b" together only count as 1 element with regards to indexes because they have duplicate insertion points
* "b", // index 0
* "c", // index 1
* "D", // index 2
* "e", // index 3
* "F", // index 4
* "g", // index 5
* "h" // index 6
* )
* }}}
*/
def mergeByIndexIntoResult[T]( // generic on `T` to simplify unit testing
candidatesToInsertByIndex: Seq[(Int, T)],
result: Seq[T],
indexType: IndexType
): Seq[T] = {
val positionAndCandidateList = candidatesToInsertByIndex.sortWith {
case ((indexLeft: Int, _), (indexRight: Int, _)) =>
indexLeft < indexRight // order by desired absolute index ascending
}
// Merge result and positionAndCandidateList into resultUpdated while making sure that the entries
// from the positionAndCandidateList are inserted at the right index.
val resultUpdated = Seq.newBuilder[T]
resultUpdated.sizeHint(result.size + positionAndCandidateList.size)
var currentResultIndex = 0
val inputResultIterator = result.iterator
val positionAndCandidateIterator = positionAndCandidateList.iterator.buffered
var previousInsertPosition: Option[Int] = None
while (inputResultIterator.nonEmpty && positionAndCandidateIterator.nonEmpty) {
positionAndCandidateIterator.head match {
case (nextInsertionPosition, nextCandidateToInsert)
if previousInsertPosition.contains(nextInsertionPosition) =>
// inserting multiple candidates at the same index
resultUpdated += nextCandidateToInsert
// do not increment any indices, but insert the candidate and advance to the next candidate
positionAndCandidateIterator.next()
case (nextInsertionPosition, nextCandidateToInsert)
if currentResultIndex >= nextInsertionPosition =>
// inserting a candidate at a new index
// add candidate to the results
resultUpdated += nextCandidateToInsert
// save the position of the inserted element to handle duplicate index insertions
previousInsertPosition = Some(nextInsertionPosition)
// advance to next candidate
positionAndCandidateIterator.next()
if (indexType == AbsoluteIndices) {
// if the indices are absolute, instead of relative to the original `result` we need to
// count the insertions of candidates into the results towards the `currentResultIndex`
currentResultIndex += 1
}
case _ =>
// no candidate to insert by index so use the candidates from the result and increment the index
resultUpdated += inputResultIterator.next()
currentResultIndex += 1
}
}
// one of the iterators is empty, so append the remaining candidates in order to the end
resultUpdated ++= positionAndCandidateIterator.map { case (_, candidate) => candidate }
resultUpdated ++= inputResultIterator
resultUpdated.result()
}
}