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. <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;
}
}