Skip to content

Instantly share code, notes, and snippets.

@claudemartin
Created December 8, 2023 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save claudemartin/87e646fb54ff5cfc49984002d013000a to your computer and use it in GitHub Desktop.
Save claudemartin/87e646fb54ff5cfc49984002d013000a to your computer and use it in GitHub Desktop.
Solution: replaceAll(T oldValue, T newValue, T... data)
package ch.claude_martin;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Supplier;
class SomeClass {
/**
* In-place algorithm to replace values of a given array with the provided
* replacement value. Each element equal to oldValue is replaced by newValue.
*
* @param <T> Generic type of the array's elements
* @param oldValue the value to be replaced
* @param newValue the replacement value
* @param data the array to be processed
* @return the same array, which has the elements replaced.
*/
// SafeVarargs is one of those weird Java oddities, that you just have to use
// even though many other methods that are not safe at all don't require it.
@SafeVarargs
static <T> T[] replaceAll(T oldValue, T newValue, T... data) {
// The method call would make no sense if the array was null, we don't allow it.
// You might want to do nothing and return null instead.
Objects.requireNonNull(data, "data");
// An empty array wouldn't have anything that needs to be replaced.
// If both values are the same it would make no sense to actually do anything.
if (data.length > 0 && oldValue != newValue) {
// This could be called as replaceNulls(null, Double.NaN, new Integer[] { 5 }),
// and the compiler doesn't help us at all to make it type safe, so we must
// check this at runtime. However, all arrays allow to store null.
if (newValue != null) {
final Class<?> componentType = data.getClass().getComponentType(); // Type of a valid element
final Class<?> valueType = newValue.getClass(); // Type of the new value (not null)
if (!componentType.isAssignableFrom(valueType)) // Can we store new value to the array?
throw new IllegalArgumentException( // if not this won't work.
MessageFormat.format("Given {0}[] would not accept {1}", componentType, valueType));
}
// Now we simply iterate the array and replace all that are equal to oldValue
for (int i = 0; i < data.length; i++) {
// Object.equals handles null very well and does exactly what we need.
// Objects.deepEquals also works with arrays while Objects.equals would not.
if (Objects.deepEquals(data[i], oldValue)) {
data[i] = newValue;
}
}
}
return data;
}
// Java can't really do this so it allows (auto-boxed) primitives, and so
// we need an overload for each primitive type. The implementation is trivial.
// This is the one for primitive integers. The others are: short, long, float,
// double, byte, and char. It wouldn't make any sense for boolean. To prevent
// problems, this isn't actually an overload. It has a different name because
// the other method would accept int[] as <T>.
static int[] replaceAllInts(int oldValue, int newValue, int... data) {
Objects.requireNonNull(data, "data");
for (int i = 0; i < data.length; i++) {
if (data[i] == oldValue) {
data[i] = newValue;
}
}
return data;
}
public static void main(String[] args) {
test(replaceAll(7, 8));
test(replaceAll(null, 0, integers()), 5, 0, 0);
test(replaceAll(5, 0, integers()), 0, 0, null);
test(() -> replaceAll(5, 0, _null()), NullPointerException.class);
test(() -> replaceAll(5, Double.NaN, integers()), IllegalArgumentException.class);
test(replaceAll(Double.NaN, 0, integers()), 5, 0, null);
test(replaceAll(5, Double.NaN, 5, 2), Double.NaN, 2);
// This even works well with arrays of arrays (i.e. Integer[][]):
test(replaceAll(null, empty(), ints(), integers(), null), ints(), integers(), empty());
test(replaceAll(empty(), null, ints(), integers(), empty()), ints(), integers(), null);
// These don't do what you might expect:
replaceAll(5, null, ints());
replaceAll(5, Double.NaN, ints());
// Both return [[5, 0, -1]] because the compiler thinks ints() is the first
// element of the varargs. So we must use a different names instead of
// overloading the method.
// It works when we use the correct method:
testInts(replaceAllInts(-1, 0, ints()), 5, 0, 0);
testInts(replaceAllInts(5, 0, ints()), 0, 0, -1);
System.out.println("Success.");
}
static <T> T[] _null() {
return null;
}
static Object[] empty() {
return new Object[0];
}
static Integer[] integers() {
return new Integer[] { 5, 0, null };
}
static int[] ints() {
return new int[] { 5, 0, -1 };
}
@SafeVarargs
static <T> void test(T[] actual, T... expected) {
if (actual.length != expected.length)
throw new AssertionError("Lengths are not the same");
for (int i = 0; i < actual.length; i++) {
T a = actual[i];
T e = expected[i];
if (!Objects.deepEquals(a, e))
throw new AssertionError(MessageFormat
.format("Values at index {0} are not the same. Expected: {1} Actual: {2}", i, e, a));
}
}
static <T> void testInts(int[] actual, int... expected) {
// The same test can be used but everything needs to be boxed.
test(Arrays.stream(actual).boxed().toArray(Integer[]::new),
Arrays.stream(expected).boxed().toArray(Integer[]::new));
}
static <T> void test(Supplier<T[]> actual, Class<? extends RuntimeException> expected) {
try {
actual.get();
throw new AssertionError("No exception.");
} catch (Exception e) {
if (!expected.isAssignableFrom(e.getClass()))
throw new AssertionError("Wrong exception.", e);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment