DEV Community

Cover image for From PHP to Kotlin - Day 5 - Extending the use of Enum
Fernando Aparicio
Fernando Aparicio

Posted on

From PHP to Kotlin - Day 5 - Extending the use of Enum

In the previous post I told you how easy it's to add information to Enum. Apart from context, we can add values to it to give it functionality.

Finally, adding functionalities to mower orientation, we need to add directionality in the displacement axes to know if moving should add values (N, E), or substract values (S, W).

The tests and the interaction with the MowerPosition object led us to this point:

Testing and interaction with the MowerPosition object brought us to this point:

package mower.mower.domain.value_object

import mower.mower.domain.exception.InvalidOrientationException
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.params.provider.ValueSource
import java.util.stream.Stream
import kotlin.test.Test

private const val ORIENTATION: String = "N"
private const val INVALID_ORIENTATION: String = "H"

internal class MowerOrientationTest
{
    @Test
    fun `Should be build`() {
        val orientation = MowerOrientation.build(ORIENTATION)

        assertInstanceOf(MowerOrientation::class.java, orientation)
        assertThat(orientation.value).isEqualTo(ORIENTATION)
    }

    @ParameterizedTest(name = "{index} => orientation = ''{0}''")
    @ValueSource(strings = [ "N", "S", "E", "W" ])
    fun `Should be build with valid orientations`(value: String) {
        val orientation = MowerOrientation.build(value)

        assertThat(orientation.value).isEqualTo(value)
    }

    @Test
    fun `Should throw exception for invalid Orientation`() {
        assertThrows(InvalidOrientationException::class.java) {
            MowerOrientation.build(INVALID_ORIENTATION)
        }
    }

    @ParameterizedTest(name = "{index} => orientation = ''{0}'', movement = ''{1}'', result = ''{2}''")
    @MethodSource("orientationAndMovementProvider")
    fun `Should apply orientation movements`(orientationData: String, movementData: String, expectedOrientationData: String) {
        var orientation = MowerOrientation.build(orientationData)
        val movement = MowerMovement.build(movementData)
        val expectedOrientation = MowerOrientation.build(expectedOrientationData)

        orientation = orientation.changeOrientation(movement)

        assertThat(orientation.value).isEqualTo(expectedOrientation.value)
    }

    @ParameterizedTest
    @MethodSource("orientationAndYAxisAffectationProvider")
    fun `Should eval if affects Y axis`(orientationData: String, affectsYAxis: Boolean) {
        val orientation = MowerOrientation.build(orientationData)

        assertThat(orientation.affectsYAxis()).isEqualTo(affectsYAxis)
    }

    @ParameterizedTest
    @MethodSource("orientationAndXAxisAffectationProvider")
    fun `Should eval if affects X axis`(orientationData: String, affectsXAxis: Boolean) {
        val orientation = MowerOrientation.build(orientationData)

        assertThat(orientation.affectsXAxis()).isEqualTo(affectsXAxis)
    }

    @ParameterizedTest
    @MethodSource("orientationAndStepDirectionProvider")
    fun `Should eval direction step` (orientationData: String, expectedStepDirection: Int) {
        val orientation = MowerOrientation.build(orientationData)

        assertThat(orientation.stepMovement()).isEqualTo(expectedStepDirection)
    }

    companion object {
        @JvmStatic
        fun orientationAndMovementProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("N", "F", "N"),
                Arguments.arguments("N", "R", "E"),
                Arguments.arguments("E", "R", "S"),
                Arguments.arguments("S", "R", "W"),
                Arguments.arguments("W", "R", "N"),
                Arguments.arguments("N", "L", "W"),
                Arguments.arguments("W", "L", "S"),
                Arguments.arguments("S", "L", "E"),
                Arguments.arguments("E", "L", "N")
            )
        }

        @JvmStatic
        fun orientationAndYAxisAffectationProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("N", true),
                Arguments.arguments("S", true),
                Arguments.arguments("E", false),
                Arguments.arguments("W", false)
            )
        }

        @JvmStatic
        fun orientationAndXAxisAffectationProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("N", false),
                Arguments.arguments("S", false),
                Arguments.arguments("E", true),
                Arguments.arguments("W", true)
            )
        }

        @JvmStatic
        fun orientationAndStepDirectionProvider(): Stream<Arguments> {
            return Stream.of(
                Arguments.arguments("N", 1),
                Arguments.arguments("E", 1),
                Arguments.arguments("S", -1),
                Arguments.arguments("W", -1),
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The class that implements it was left like this

@JvmInline
value class MowerOrientation private constructor(val value: String) {
    private enum class Compass (val stepDirection: Int){
        N (POSITIVE_DIRECTION),
        E (POSITIVE_DIRECTION),
        S (NEGATIVE_DIRECTION),
        W (NEGATIVE_DIRECTION)
    }

    init {
        try {
            Compass.valueOf(value)
        } catch (exception: IllegalArgumentException) {
            throw InvalidOrientationException.withValues(value, Compass.values().contentToString())
        }
    }

    companion object {
        private const val COMPASS_STEP: Int = 1
        private const val POSITIVE_DIRECTION: Int = 1
        private const val NEGATIVE_DIRECTION: Int = -1

        @JvmStatic
        fun build(value: String): MowerOrientation
        {
            return MowerOrientation(value)
        }
    }

    fun changeOrientation(mowerMovement: MowerMovement): MowerOrientation {
        val currentCompass = Compass.valueOf(value)

        if (mowerMovement.isClockWise()) {
            val futureCompass = Compass.values().getOrElse(currentCompass.ordinal + COMPASS_STEP) { Compass.N }
            return MowerOrientation(futureCompass.name)
        }

        if (mowerMovement.isCounterClockWise()) {
            val futureCompass = Compass.values().getOrElse(currentCompass.ordinal - COMPASS_STEP) { Compass.W }
            return MowerOrientation(futureCompass.name)
        }

        return this
    }

    fun affectsYAxis(): Boolean {
        return Compass.N.name == value || Compass.S.name == value
    }

    fun affectsXAxis(): Boolean {
        return Compass.E.name == value || Compass.W.name == value
    }

    fun stepMovement(): Int {
        return Compass.valueOf(value).stepDirection
    }
}
Enter fullscreen mode Exit fullscreen mode

Possible refactor

We could add features to Enum to simplify affectsYAxis and affectsXAxis functions in the same way that it's articulated with the stepMovement function. Leaving the class with these changes.

...

@JvmInline
value class MowerOrientation private constructor(val value: String) {
    private enum class Compass (val stepDirection: Int, val affectsXAxis: Boolean, val affectsYAxis: Boolean){
        N(POSITIVE_DIRECTION, NOT_X_AXIS, Y_AXIS),
        E(POSITIVE_DIRECTION, X_AXIS, NOT_Y_AXIS),
        S(NEGATIVE_DIRECTION, NOT_X_AXIS, Y_AXIS),
        W(NEGATIVE_DIRECTION, X_AXIS, NOT_Y_AXIS)
    }
...
    companion object {
...
        private const val NOT_X_AXIS: Boolean = false
        private const val NOT_Y_AXIS: Boolean = false
        private const val X_AXIS: Boolean = true
        private const val Y_AXIS: Boolean = true
...
    }
...
    fun affectsYAxis(): Boolean {
        return Compass.valueOf(value).affectsYAxis
    }

    fun affectsXAxis(): Boolean {
        return Compass.valueOf(value).affectsXAxis
    }

    fun stepMovement(): Int {
        return Compass.valueOf(value).stepDirection
    }
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)