Skip to content

Commit d8436cc

Browse files
committed
Added initial user-defined integrals project.
1 parent 8314479 commit d8436cc

File tree

10 files changed

+290
-0
lines changed

10 files changed

+290
-0
lines changed

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ include("subprojects:sorting-in-kotlin")
5656
include("subprojects:tdd-in-kotlin")
5757
include("subprojects:testing-with-mokkery")
5858
include("subprojects:typed-errors-in-kotlin")
59+
include("subprojects:user-defined-integrals-in-kotlin")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
plugins {
2+
id("org.sdkotlin.buildlogic.kotlin-project")
3+
id("org.sdkotlin.buildlogic.mockk-project")
4+
id("org.sdkotlin.buildlogic.test.unit-test-suite")
5+
}
6+
7+
dependencies {
8+
9+
testImplementation(platform("org.sdkotlin.platforms:test-platform"))
10+
11+
testImplementation(libs.equalsverifier)
12+
13+
testRuntimeOnly(libs.mockito) {
14+
because("Used by EqualsVerifier for prefab value generation.")
15+
}
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.sdkotlin.integral
2+
3+
interface Integral<I> : Comparable<I> where I : Comparable<I> {
4+
5+
val minValue: I
6+
val maxValue: I
7+
val zero: I
8+
val one: I
9+
10+
operator fun plus(other: I): I
11+
operator fun minus(other: I): I
12+
operator fun unaryMinus(): I
13+
operator fun rem(other: I): I
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package org.sdkotlin.integral
2+
3+
class IntegralProgression<I : Integral<I>> internal constructor(
4+
start: I,
5+
endInclusive: I,
6+
override val step: I,
7+
) : Progression<I> {
8+
9+
init {
10+
require(step != step.zero) { "Step must be non-zero." }
11+
require(step != step.minValue) {
12+
"Step must be greater than the type's minimum value to avoid overflow on negation."
13+
}
14+
}
15+
16+
override val first: I = start
17+
18+
override val last: I = getProgressionLastElement(start, endInclusive, step)
19+
20+
override fun iterator(): Iterator<I> =
21+
IntegralProgressionIterator(first, last, step)
22+
23+
override fun isEmpty(): Boolean =
24+
if (step > step.zero) first > last else first < last
25+
26+
override fun equals(other: Any?): Boolean =
27+
other is IntegralProgression<*> && (isEmpty() && other.isEmpty() ||
28+
first == other.first && last == other.last && step == other.step)
29+
30+
override fun hashCode(): Int =
31+
if (isEmpty()) {
32+
-1
33+
} else {
34+
(31 * (31 * first.hashCode() + last.hashCode()) + step.hashCode())
35+
}
36+
37+
override fun toString(): String =
38+
if (step > step.zero) {
39+
"$first..$last step $step"
40+
} else {
41+
"$first downTo $last step ${-step}"
42+
}
43+
44+
companion object {
45+
46+
fun <I : Integral<I>> fromClosedRange(
47+
rangeStart: I,
48+
rangeEnd: I,
49+
step: I,
50+
): IntegralProgression<I> =
51+
IntegralProgression(rangeStart, rangeEnd, step)
52+
53+
private fun <I : Integral<I>> getProgressionLastElement(
54+
start: I,
55+
end: I,
56+
step: I,
57+
): I =
58+
when {
59+
step > step.zero -> if (start >= end) {
60+
end
61+
} else {
62+
end - differenceModulo(end, start, step)
63+
}
64+
step < step.zero -> if (start <= end) {
65+
end
66+
} else {
67+
end + differenceModulo(start, end, -step)
68+
}
69+
else -> throw IllegalArgumentException("Step is zero.")
70+
}
71+
72+
private fun <I : Integral<I>> differenceModulo(a: I, b: I, c: I): I {
73+
val ac = a % c
74+
val bc = b % c
75+
return if (ac >= bc) ac - bc else ac - bc + c
76+
}
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.sdkotlin.integral
2+
3+
internal class IntegralProgressionIterator<I : Integral<I>>(
4+
first: I,
5+
private val last: I,
6+
private val step: I,
7+
) : Iterator<I> {
8+
9+
private var hasNext: Boolean =
10+
if (step > step.zero) {
11+
first <= last
12+
} else {
13+
first >= last
14+
}
15+
16+
private var next = if (hasNext) first else last
17+
18+
override fun hasNext(): Boolean = hasNext
19+
20+
override fun next(): I {
21+
val value = next
22+
if (value == last) {
23+
if (!hasNext) throw NoSuchElementException()
24+
hasNext = false
25+
} else {
26+
next += step
27+
}
28+
return value
29+
}
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.sdkotlin.integral
2+
3+
class IntegralRange<I : Integral<I>>(
4+
start: I,
5+
endInclusive: I,
6+
) : ClosedRange<I>, OpenEndRange<I>,
7+
Progression<I> by IntegralProgression(
8+
start,
9+
endInclusive,
10+
step = start.one
11+
) {
12+
13+
override val start: I = first
14+
15+
override val endInclusive: I = last
16+
17+
override val endExclusive: I by lazy {
18+
check(endInclusive == start.maxValue) {
19+
"Cannot return the exclusive upper bound of a range that includes MAX_VALUE."
20+
}
21+
endInclusive + endInclusive.one
22+
}
23+
24+
override fun contains(value: I): Boolean =
25+
start <= value && value <= endInclusive
26+
27+
override fun isEmpty(): Boolean = start > endInclusive
28+
29+
override fun equals(other: Any?): Boolean =
30+
other is IntegralRange<*> && (isEmpty() && other.isEmpty() ||
31+
start == other.start && endInclusive == other.endInclusive)
32+
33+
override fun hashCode(): Int =
34+
if (isEmpty()) -1 else (31 * start.hashCode() + endInclusive.hashCode())
35+
36+
override fun toString(): String = "$start..$endInclusive"
37+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.sdkotlin.integral
2+
3+
interface Progression<out I> : Iterable<I> {
4+
val first: I
5+
val last: I
6+
val step: I
7+
8+
fun isEmpty(): Boolean
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.sdkotlin.integral
2+
3+
import io.mockk.mockk
4+
import nl.jqno.equalsverifier.EqualsVerifier
5+
import org.junit.jupiter.api.Test
6+
7+
class IntegralProgressionTest {
8+
9+
@Test
10+
fun `test equals, hashCode, and toString`() {
11+
EqualsVerifier.forClass(IntegralProgression::class.java)
12+
// Required per https://github.com/jqno/equalsverifier/issues/1082.
13+
.withPrefabValues(
14+
Integral::class.java,
15+
mockk(relaxed = true),
16+
mockk(relaxed = true),
17+
)
18+
.verify()
19+
}
20+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.sdkotlin.integral
2+
3+
import io.mockk.mockk
4+
import nl.jqno.equalsverifier.EqualsVerifier
5+
import org.junit.jupiter.api.Test
6+
7+
class IntegralRangeTest {
8+
9+
@Test
10+
fun `test equals, hashCode, and toString`() {
11+
12+
EqualsVerifier.forClass(IntegralRange::class.java)
13+
.withIgnoredFields("endExclusive\$delegate")
14+
// Required per https://github.com/jqno/equalsverifier/issues/1082.
15+
.withPrefabValues(
16+
Integral::class.java,
17+
mockk(relaxed = true),
18+
mockk(relaxed = true),
19+
)
20+
.verify()
21+
}
22+
23+
@Test
24+
fun `test equals, hashCode, and toString with examples`() {
25+
26+
val zero = SignedIntegral(0)
27+
val one = SignedIntegral(1)
28+
val two = SignedIntegral(2)
29+
val red: ClosedRange<SignedIntegral> = zero..one
30+
val blue: ClosedRange<SignedIntegral> = one..two
31+
32+
EqualsVerifier.forExamples(red, blue)
33+
.withIgnoredFields("endExclusive\$delegate")
34+
.verify()
35+
}
36+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.sdkotlin.integral
2+
3+
@JvmInline
4+
value class SignedIntegral(private val i: Int) : Integral<SignedIntegral> {
5+
6+
companion object {
7+
8+
val MIN_VALUE: SignedIntegral = SignedIntegral(Int.MIN_VALUE)
9+
10+
val MAX_VALUE: SignedIntegral = SignedIntegral(Int.MAX_VALUE)
11+
12+
val ZERO: SignedIntegral = SignedIntegral(0)
13+
14+
val ONE: SignedIntegral = SignedIntegral(1)
15+
}
16+
17+
override val minValue: SignedIntegral
18+
get() = MIN_VALUE
19+
20+
override val maxValue: SignedIntegral
21+
get() = MAX_VALUE
22+
23+
override val zero: SignedIntegral
24+
get() = ZERO
25+
26+
override val one: SignedIntegral
27+
get() = ONE
28+
29+
override fun plus(other: SignedIntegral): SignedIntegral =
30+
SignedIntegral(i + other.i)
31+
32+
override fun minus(other: SignedIntegral): SignedIntegral =
33+
SignedIntegral(i - other.i)
34+
35+
override fun unaryMinus(): SignedIntegral =
36+
SignedIntegral(-i)
37+
38+
override fun rem(other: SignedIntegral): SignedIntegral =
39+
SignedIntegral(i % other.i)
40+
41+
override fun compareTo(other: SignedIntegral): Int =
42+
i.compareTo(other.i)
43+
44+
operator fun rangeTo(other: SignedIntegral): ClosedRange<SignedIntegral> =
45+
IntegralRange(this, other)
46+
47+
operator fun rangeUntil(other: SignedIntegral): OpenEndRange<SignedIntegral> =
48+
IntegralRange(this, other - ONE)
49+
}

0 commit comments

Comments
 (0)