Tuple.java

/*
 * Copyright 2020 Collect Utils Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 net.morimekta.collect.util;

import java.util.Arrays;
import java.util.RandomAccess;

import static java.lang.String.format;
import static java.util.Arrays.copyOfRange;
import static java.util.Objects.requireNonNull;

/**
 * A tuple is a simple unmodifiable structure with an ordered list of value
 * of any type. The Tuple does not implement the collection or list types
 * as it does not behave as a normal collection in regard to item types.
 * <p>
 * The class is optimized for short (e.g. &lt;16 ) length tuples, so does <b>NOT</b>
 * try to optimize memory allocation and copying using views and similar.
 * <p>
 * If any of the classes contained in the tuple is not immutable in of itself,
 * this will also not be immutable.
 * <p>
 * Notable differences between tuple and collection, and compared to the
 * {@link net.morimekta.collect.UnmodifiableCollection} implementations:
 * <ul>
 *     <li>
 *         Allows {@code null} values.
 *     </li>
 *     <li>
 *         With handy 'replace one or more' elements methods.
 *     </li>
 *     <li>
 *         With handy indexed sub-selection.
 *     </li>
 *     <li>
 *         Not iterable, all iterative access should use random
 *         access methods with {@link #get(int)} or {@link #get(Class, int)}
 *     </li>
 * </ul>
 */
public class Tuple implements RandomAccess {
    /**
     * The empty tuple.
     */
    public static final Tuple EMPTY = new Tuple();

    /**
     * Create a new tuple with the provided values.
     *
     * @param values The values to make a tuple from.
     * @return The created tuple.
     */
    public static Tuple newTuple(Object... values) {
        if (values.length == 0) {
            return EMPTY;
        }
        return new Tuple(Arrays.copyOf(values, values.length));
    }

    /**
     * @return The length of the tuple.
     */
    public int length() {
        return values.length;
    }

    /**
     * Get the object at index i with the type T. This essentially
     * does the same as {@link #get(Class, int)}, but does an implicit
     * type casting based on the called return type.
     *
     * @param i The value index.
     * @return The value at index i.
     */
    public Object get(int i) {
        return values[ix(i)];
    }

    /**
     * Get the object at index i with the type T. This essentially
     * does the same as {@link #get(int)}, but does an explicit
     * type cast to ensure the object is compatible <i>before</i>
     * returning to caller.
     *
     * @param type The value type class.
     * @param i    The value index.
     * @param <T>  The value type.
     * @return The value at index i.
     */
    public <T> T get(Class<T> type, int i) {
        requireNonNull(type, "type == null");
        try {
            return type.cast(values[ix(i)]);
        } catch (ClassCastException e) {
            throw new IllegalStateException(e.getMessage() + " at index " + i, e);
        }
    }

    // ----------------

    /**
     * Replace a value in the tuple.
     *
     * @param i     The index of the first value.
     * @param value The replacement value.
     * @return The tuple with the replaced value.
     */
    public Tuple with(int i, Object value) {
        Object[] nv = Arrays.copyOf(values, values.length);
        nv[ix(i)] = value;
        return new Tuple(nv);
    }

    /**
     * Replace 2 values in the tuple.
     *
     * @param i1     The index of the first value.
     * @param value1 The first replacement value.
     * @param i2     The index of the second value.
     * @param value2 The second replacement value.
     * @return The tuple with the replaced values.
     */
    public Tuple with(int i1, Object value1,
                      int i2, Object value2) {
        if (i1 == i2) {
            throw new IllegalArgumentException("Replacing same entry twice: " + i1);
        }
        Object[] nv = Arrays.copyOf(values, values.length);
        nv[ix(i1)] = value1;
        nv[ix(i2)] = value2;
        return new Tuple(nv);
    }

    /**
     * Replace 3 values in the tuple.
     *
     * @param i1     The index of the first value.
     * @param value1 The first replacement value.
     * @param i2     The index of the second value.
     * @param value2 The second replacement value.
     * @param i3     The index of the third value.
     * @param value3 The third replacement value.
     * @return The tuple with the replaced values.
     */
    public Tuple with(int i1, Object value1,
                      int i2, Object value2,
                      int i3, Object value3) {
        if (i1 == i2 || i2 == i3 || i3 == i1) {
            throw new IllegalArgumentException(format(
                    "Replacing same entry more than once: [%d, %d, %d]", i1, i2, i3));
        }
        Object[] nv = Arrays.copyOf(values, values.length);
        nv[ix(i1)] = value1;
        nv[ix(i2)] = value2;
        nv[ix(i3)] = value3;
        return new Tuple(nv);
    }

    /**
     * Append a tuple to this tuple's values and return the combined
     * tuple.
     *
     * @param appended Tuple with values to get appended to this tuple's values.
     * @return The combined tuple.
     */
    public Tuple appendedFrom(Tuple appended) {
        if (appended.length() == 0) {
            return this;
        }
        if (values.length == 0) {
            return appended;
        }
        return appended(appended.values);
    }

    /**
     * Append a set of values to the tuple's values and return the combined
     * tuple.
     *
     * @param appended Values to get appended to this tuple's values.
     * @return The combined tuple.
     */
    public Tuple appended(Object... appended) {
        if (appended.length == 0) {
            throw new IllegalArgumentException("Not appending any values");
        }
        if (values.length == 0) {
            return new Tuple(appended);
        }
        Object[] nv = Arrays.copyOf(values, values.length + appended.length);
        System.arraycopy(appended, 0, nv, values.length, appended.length);
        return new Tuple(nv);
    }

    /**
     * Truncate the tuple to length.
     *
     * @param len The new max length.
     * @return The truncated tuple.
     */
    public Tuple truncate(int len) {
        if (len == 0) {
            return EMPTY;
        }
        if (len == values.length) {
            return this;
        }
        if (len > values.length) {
            throw new IllegalArgumentException("Truncate to " + len + " of tuple length " + values.length);
        }
        return new Tuple(copyOfRange(values, 0, len));
    }

    /**
     * Extract slice out of the tuple.
     *
     * @param off The index offset to the first object to be in the response.
     * @param len Number of items in the target slice.
     * @return The tuple slice.
     */
    public Tuple slice(int off, int len) {
        if (off < 0 || off >= values.length ||
            len < 0 || off + len >= values.length) {
            throw new IllegalArgumentException(format(
                    "Invalid slice [off=%d, len=%d] of len=%d", off, len, values.length));
        }
        if (len == 0) {
            return EMPTY;
        }
        return new Tuple(copyOfRange(values, off, off + len));
    }

    /**
     * Extract a subset of the values from the tuple into it's own tuple.
     * If the same index is repeated, that value will appear more than once
     * in the response.
     *
     * @param indexes The indexed of the values to extract.
     * @return The tuple with the values in extracted index order.
     */
    public Tuple extract(int... indexes) {
        if (indexes.length == 0) return EMPTY;
        Object[] nv = new Object[indexes.length];
        for (int i = 0; i < indexes.length; ++i) {
            nv[i] = values[ix(indexes[i])];
        }
        return new Tuple(nv);
    }

    // ---- Object ----

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder(getClass().getSimpleName() + "{");
        boolean first = true;
        for (Object o : values) {
            if (first) first = false;
            else builder.append(", ");
            builder.append(o);
        }
        builder.append('}');
        return builder.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Tuple)) return false;
        Tuple tuple = (Tuple) o;
        return Arrays.equals(values, tuple.values);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(values);
    }

    // ---- Private ----

    private final Object[] values;

    private Tuple(Object... values) {
        this.values = values;
    }

    private int ix(int i) {
        if (i < 0 || i >= values.length) {
            throw new IndexOutOfBoundsException(i + " outside bounds of [0.." + values.length + ")");
        }
        return i;
    }
}