Is there a more concise way to assert that an array's elements each match a predicate?

Viewed 268

We all know that you shouldn't assert exact equality for floating-point numbers. assertk comes with a convenient way to do it better:

assertThat(value).isCloseTo(3.0, 0.000001)

I'd like to extend that to arrays:

assertThat(array).isCloseTo(doubleArrayOf(2.0, 3.0), 0.000001)

But the best I can do is:

fun Assert<DoubleArray>.isCloseTo(expected: DoubleArray, delta: Double) {
    hasSameSizeAs(expected)
    this.matchesPredicate { actual : DoubleArray ->
        expected.indices.forEach { index ->
            if (!actual[index].isCloseTo(expected[index], epsilon)) {
                return@matchesPredicate false
            }
        }
        true
    }
}

fun Double.isCloseTo(expected: Double, epsilon: Double): Boolean {
    abs(this - expected) <= epsilon
}

Is there any way to reduce this matchesPredicate to something like (pseudocode):

    hasElementsMatchingPredicate { index, actual ->
        actual.isCloseTo(expected, epsilon)
    }
1 Answers

To answer the question in the title:

assert that an array's elements each match a predicate

You could write a new AssertK assertion for DoubleArray:

fun Assert<DoubleArray>.allMatchPredicate(f: (Double) -> Boolean) = given { actual ->
    if (actual.all(f)) return
    val rejected = actual.filterNot(f)
    expected("all items to satisfy the predicate, but these items didn't: ${show(rejected)}")
}

Then use it like this:

@Test
fun test() {
    val positive = doubleArrayOf(1.0, 2.0, 3.0)
    val mixed = doubleArrayOf(-1.0, 1.0, -4.0)

    assertThat(positive).allMatchPredicate { it > 0.0 }
    assertThat(mixed).allMatchPredicate { it > 0.0 }
}

Result:

org.opentest4j.AssertionFailedError: expected all items to satisfy the predicate, but these items didn't: <[-1.0, -4.0]>

For comparing two DoubleArrays, as the question body asks for, you could do this:

fun Assert<DoubleArray>.allCloseTo(expected: DoubleArray, delta: Double) = given { actual ->
    if (actual.size != expected.size)
        expected("array to have size ${expected.size} but was ${actual.size}")

    val rejected = actual.asSequence()
        .withIndex().filterNot {
            // adapted from Assert<Double>.isCloseTo
            it.value >= expected[it.index].minus(delta) &&
                it.value <= expected[it.index].plus(delta)
        }
        .map { it.value }.toList()

    if (rejected.isNotEmpty())
        expected("all items to satisfy the predicate, but these items didn't: ${show(rejected)}")
}

Usage:

@Test
fun test() {
    val same = doubleArrayOf(1.001, 2.001, 3.001)
    val diff = doubleArrayOf(1.1, 2.001, 3.1)

    val expected = doubleArrayOf(1.0, 2.0, 3.0)

    assertThat(same).allCloseTo(expected, 0.01)
    assertThat(diff).allCloseTo(expected, 0.01)
}

Result:

org.opentest4j.AssertionFailedError: expected all items to satisfy the predicate, but these items didn't: <[1.1, 3.1]>
Related