IndexingAdapter.java

package org.loudouncodes.combinatorics;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;

/**
 * Bridges from <em>indices</em> to <em>elements</em>.
 *
 * <p>Wrap any {@code Iterable<int[]>} together with a backing {@code List<E>} and iterate {@code
 * List<E>} tuples instead of raw index arrays. Each returned list is a fresh, unmodifiable snapshot
 * of the mapped elements (safe for students to keep or mutate references inside without affecting
 * the iterator's internal state).
 *
 * <h2>Example (pairs of toppings)</h2>
 *
 * <pre>{@code
 * List<String> toppings = List.of("Pepperoni","Mushrooms","Onions","Olives");
 * for (List<String> pizza :
 *       new IndexingAdapter<>(Combinations.of(toppings.size()).choose(2), toppings)) {
 *   System.out.println(pizza);
 * }
 * // [Pepperoni, Mushrooms]
 * // [Pepperoni, Onions]
 * // [Pepperoni, Olives]
 * // [Mushrooms, Onions]
 * // [Mushrooms, Olives]
 * // [Onions, Olives]
 * }</pre>
 *
 * <p>You can wrap <em>any</em> of the generators in this package: combinations, permutations,
 * derangements, Cartesian product, etc.
 *
 * @param <E> element type of the backing list
 * @since 0.1.0
 */
public final class IndexingAdapter<E> implements Iterable<List<E>> {

  private final Iterable<int[]> tuples;
  private final List<E> items;

  /**
   * Creates an adapter that maps each emitted index tuple to a list of elements from {@code items}.
   *
   * @param tuples iterable of index arrays (each index must be in {@code [0, items.size()-1]})
   * @param items backing list of elements (not copied; element references are reused)
   * @throws NullPointerException if either argument is {@code null}
   */
  public IndexingAdapter(Iterable<int[]> tuples, List<E> items) {
    this.tuples = Objects.requireNonNull(tuples, "tuples");
    this.items = Objects.requireNonNull(items, "items");
  }

  /**
   * Convenience factory identical to {@link #IndexingAdapter(Iterable, List)}.
   *
   * @param tuples iterable of index arrays
   * @param items backing list of elements
   * @param <E> element type
   * @return a new adapter
   */
  public static <E> IndexingAdapter<E> of(Iterable<int[]> tuples, List<E> items) {
    return new IndexingAdapter<>(tuples, items);
  }

  @Override
  public Iterator<List<E>> iterator() {
    return new Iterator<List<E>>() {
      private final Iterator<int[]> it = tuples.iterator();

      @Override
      public boolean hasNext() {
        return it.hasNext();
      }

      @Override
      public List<E> next() {
        if (!hasNext()) throw new NoSuchElementException();
        int[] idx = it.next(); // defensive copy not required; we read-and-map immediately
        // Map indices -> elements with bounds checks
        List<E> out = new ArrayList<>(idx.length);
        int size = items.size();
        for (int i : idx) {
          if (i < 0 || i >= size) {
            throw new IndexOutOfBoundsException(
                "Index " + i + " out of bounds for items size " + size);
          }
          out.add(items.get(i));
        }
        return Collections.unmodifiableList(out);
      }
    };
  }
}