Is there a cleaner way of doing this? Degrees need...
# getting-started
s
Is there a cleaner way of doing this? Degrees needs to be immutable, greater than 0, and less than 360.
Copy code
class CompassHeading(degrees: Double) {

    private var _degrees by Delegates.notNull<Double>()
    var degrees: Double
        get() = _degrees
        set(value) {
            var d = value % 360.0
            if (d < 0.0) d += 360
            _degrees = d
        }

    init {
        this.degrees = degrees
    }
}
w
h
Untitled.kt
r
Copy code
class CompassHeading(degrees: Double) {
    var degrees: Double = degrees.mod(360.0)
        set(value) {
            field = value.mod(360.0)
        }
}
k
Copy code
@JvmInline
value class CompassHeading private constructor(val degrees: Double) {
    companion object {
        operator fun invoke(degrees: Double) = CompassHeading(degrees.mod(360.0))
    }
}
or a little more complete:
Copy code
@JvmInline
value class CompassHeading private constructor(val degrees: Double) : Comparable<CompassHeading> {
    val radians: Double
        get() = degrees / 180 * PI

    companion object {
        fun ofDegrees(degrees: Double) = CompassHeading(degrees.mod(360.0))
        fun ofDegrees(degrees: Int) = CompassHeading(degrees.mod(360).toDouble())
        fun ofRadians(radians: Double) = ofDegrees(radians / PI * 180)
    }

    override fun compareTo(other: CompassHeading) = degrees.compareTo(other.degrees)
}
r
Why all the complex handling for
%
? That's exactly what
mod
is for.
🙏 1
k
You're right, I thought
%
was a shortcut for
mod
. In fact, it's a shortcut for
rem
. Thanks for pointing this out. I will edit my answer.
👍 1
s
Thanks Everyone! This is what I am now using. I can't make it a value class because I need to inherit from my AppUnit<> class for some other app requirements.
Copy code
public class CompassHeading private constructor(
    override val internalValue: Double
) : AppUnit<CompassHeading>() {

    public val degrees: Double get() = internalValue
    public val radians: Double get() = internalValue / 180 * PI

    public val cardinal: CardinalDirection
        get() = run {
            require(degrees in 0.0..360.0) { "degrees must be between 0 and 360" }
            return@run when {
                degrees >= 0.0 && degrees < 11.25 -> CardinalDirection.N
                degrees >= 11.25 && degrees < 33.75 -> CardinalDirection.NNE
                degrees >= 33.75 && degrees < 56.25 -> <http://CardinalDirection.NE|CardinalDirection.NE>
                degrees >= 56.25 && degrees < 78.75 -> CardinalDirection.ENE
                degrees >= 78.75 && degrees < 101.25 -> CardinalDirection.E
                degrees >= 101.25 && degrees < 123.75 -> CardinalDirection.ESE
                degrees >= 123.75 && degrees < 146.25 -> <http://CardinalDirection.SE|CardinalDirection.SE>
                degrees >= 146.25 && degrees < 168.75 -> CardinalDirection.SSE
                degrees >= 168.75 && degrees < 191.25 -> CardinalDirection.S
                degrees >= 191.25 && degrees < 213.75 -> CardinalDirection.SSW
                degrees >= 213.75 && degrees < 236.25 -> CardinalDirection.SW
                degrees >= 236.25 && degrees < 258.75 -> CardinalDirection.WSW
                degrees >= 258.75 && degrees < 281.25 -> CardinalDirection.W
                degrees >= 281.25 && degrees < 303.75 -> CardinalDirection.WNW
                degrees >= 303.75 && degrees < 326.25 -> CardinalDirection.NW
                degrees >= 326.25 && degrees < 348.75 -> CardinalDirection.NNW
                degrees >= 348.75 && degrees < 360.0 -> CardinalDirection.N
                else -> throw IllegalStateException("Error converting $degrees to ordinal direction")
            }
        }

    override fun toString(): String {
        return "($cardinal, $degrees)"
    }

    public companion object {
        public operator fun invoke(degrees: Double): CompassHeading = degrees(degrees)
        public fun degrees(degrees: Double): CompassHeading = CompassHeading(degrees.mod(360.toDouble()))
        public fun radians(radians: Double): CompassHeading = degrees(radians / PI * 180)
    }
}

public val Number.compassHeading: CompassHeading get() = CompassHeading(this.toDouble())
public val String.compassHeading: CompassHeading get() = CompassHeading(this.toDouble())
k
You can probably reduce the many lines of
cardinal
to just a few (untested):
Copy code
val numPoints = CardinalDirection.values().length
val pointWidth = 360.0 / numPoints
val directionIndex = (degrees / pointWidth + 0.5).toInt() % numPoints
CardinalDirection.values()[directionIndex]
s
Thanks again. All my unit tests are still passing and much more readable!
Copy code
public class CompassHeading private constructor(
    override val internalValue: Double
) : AppUnit<CompassHeading>() {

    public val degrees: Double get() = internalValue
    public val radians: Double get() = internalValue / 180 * PI

    public val cardinal: CardinalDirection get() = CardinalDirection.fromDegrees(degrees)

    override fun toString(): String {
        return "($cardinal, $degrees)"
    }

    public companion object {
        public fun degrees(degrees: Double): CompassHeading = CompassHeading(degrees.mod(360.toDouble()))
        public fun radians(radians: Double): CompassHeading = degrees(radians / PI * 180)
    }
}

public val Number.degrees: CompassHeading get() = CompassHeading.degrees(this.toDouble())
public val Number.radians: CompassHeading get() = CompassHeading.degrees(this.toDouble())

public val String.degrees: CompassHeading get() = CompassHeading.radians(this.toDouble())
public val String.radians: CompassHeading get() = CompassHeading.radians(this.toDouble())
Copy code
public enum class CardinalDirection {
    N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW;

    public companion object {
        public fun fromDegrees(degrees: Number): CardinalDirection {
            val numPoints = values().size
            val pointWidth = 360.0 / numPoints
            val directionIndex = (degrees.toDouble().mod(360.0) / pointWidth + 0.5).toInt().mod(numPoints)
            return values()[directionIndex]
        }
    }
}