Created
December 8, 2023 14:13
-
-
Save claudemartin/87e646fb54ff5cfc49984002d013000a to your computer and use it in GitHub Desktop.
Solution: replaceAll(T oldValue, T newValue, T... data)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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