Skip to content

Commit c4d67c9

Browse files
mtrewarthaclaude
andauthored
feat(measurement): enforce finite Distance magnitudes (#599)
* feat(measurement): enforce finite Distance magnitudes Throw IllegalArgumentException when constructing a Distance with a NaN or infinite magnitude. Update tests to cover the new validation and fix all property-based tests that previously used Arb.double() (which can produce non-finite values). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(measurement): remove obsolete Distance.isFinite property --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fd2da0d commit c4d67c9

4 files changed

Lines changed: 47 additions & 69 deletions

File tree

core/measurement/src/main/kotlin/io/trewartha/positional/core/measurement/Distance.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ package io.trewartha.positional.core.measurement
88
*/
99
public data class Distance(val magnitude: Double, val unit: Unit) {
1010

11-
/**
12-
* `true` if the distance is finite, `false` if it is negative or positive infinity
13-
*/
14-
val isFinite: Boolean get() = magnitude.isFinite()
11+
init {
12+
require(magnitude.isFinite()) { "Distance magnitude must be finite, but was $magnitude" }
13+
}
1514

1615
/**
1716
* `true` if the distance is negative, `false` if it is positive or zero

core/measurement/src/main/kotlin/io/trewartha/positional/core/measurement/MgrsCoordinates.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public data class MgrsCoordinates(
2323
) : Coordinates {
2424

2525
private val worldWindMgrsCoordinates = try {
26-
require(easting.isFinite && !easting.isNegative) { "Invalid easting: $easting" }
27-
require(northing.isFinite && !northing.isNegative) { "Invalid northing: $northing" }
26+
require(!easting.isNegative) { "Easting must not be negative but was $easting" }
27+
require(!northing.isNegative) { "Northing must not be negative but was $northing" }
2828
val easting = easting.inRoundedAndPaddedMeters()
2929
val northing = northing.inRoundedAndPaddedMeters()
3030
MGRSCoord.fromString("$gridZoneDesignator $gridSquareID $easting $northing")

core/measurement/src/main/kotlin/io/trewartha/positional/core/measurement/UtmCoordinates.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public data class UtmCoordinates(
2525
Hemisphere.NORTH -> earth.worldwind.geom.coords.Hemisphere.N
2626
Hemisphere.SOUTH -> earth.worldwind.geom.coords.Hemisphere.S
2727
}
28-
require(easting.isFinite && !easting.isNegative) { "Invalid easting: $easting" }
29-
require(northing.isFinite && !northing.isNegative) { "Invalid northing: $northing" }
28+
require(!easting.isNegative) { "Easting must not be negative but was $easting" }
29+
require(!northing.isNegative) { "Northing must not be negative but was $northing" }
3030
val easting = easting.inMeters().magnitude
3131
val northing = northing.inMeters().magnitude
3232
UTMCoord.fromUTM(zone, hemisphere, easting, northing)

core/measurement/src/test/kotlin/io/trewartha/positional/core/measurement/DistanceTest.kt

Lines changed: 40 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,71 @@
11
package io.trewartha.positional.core.measurement
22

3+
import io.kotest.assertions.throwables.shouldThrow
34
import io.kotest.core.spec.style.DescribeSpec
45
import io.kotest.matchers.booleans.shouldBeFalse
56
import io.kotest.matchers.booleans.shouldBeTrue
67
import io.kotest.matchers.doubles.plusOrMinus
78
import io.kotest.matchers.shouldBe
89
import io.kotest.property.Arb
9-
import io.kotest.property.arbitrary.double
1010
import io.kotest.property.arbitrary.enum
1111
import io.kotest.property.arbitrary.filter
12-
import io.kotest.property.arbitrary.negativeDouble
1312
import io.kotest.property.arbitrary.numericDouble
1413
import io.kotest.property.arbitrary.of
15-
import io.kotest.property.arbitrary.positiveDouble
1614
import io.kotest.property.checkAll
1715
import kotlin.math.abs
1816

1917
class DistanceTest : DescribeSpec({
2018

19+
describe("creating a Distance") {
20+
context("with a finite magnitude") {
21+
it("succeeds") {
22+
checkAll(Arb.numericDouble(), Arb.enum<Distance.Unit>()) { magnitude, unit ->
23+
Distance(magnitude, unit)
24+
}
25+
}
26+
}
27+
28+
context("with a NaN magnitude") {
29+
it("throws IllegalArgumentException") {
30+
checkAll(Arb.enum<Distance.Unit>()) { unit ->
31+
shouldThrow<IllegalArgumentException> { Distance(Double.NaN, unit) }
32+
}
33+
}
34+
}
35+
36+
context("with an infinite magnitude") {
37+
it("throws IllegalArgumentException") {
38+
checkAll(
39+
Arb.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY),
40+
Arb.enum<Distance.Unit>()
41+
) { magnitude, unit ->
42+
shouldThrow<IllegalArgumentException> { Distance(magnitude, unit) }
43+
}
44+
}
45+
}
46+
}
47+
2148
describe("creating a distance in feet") {
2249
it("creates a distance with the correct magnitude and unit") {
23-
checkAll(Arb.double()) { number ->
50+
checkAll(Arb.numericDouble()) { number ->
2451
number.feet.shouldBe(Distance(number, Distance.Unit.FEET))
2552
}
2653
}
2754
}
2855

2956
describe("creating a distance in meters") {
3057
it("creates a distance with the correct magnitude and unit") {
31-
checkAll(Arb.double()) { number ->
58+
checkAll(Arb.numericDouble()) { number ->
3259
number.meters.shouldBe(Distance(number, Distance.Unit.METERS))
3360
}
3461
}
3562
}
3663

37-
describe("checking whether the distance is negative") {
64+
describe("whether the distance is negative") {
3865
context("when the magnitude is negative") {
3966
it("returns true") {
4067
checkAll(
41-
Arb.negativeDouble().filter { !it.isNaN() },
68+
Arb.numericDouble().filter { it < 0 },
4269
Arb.enum<Distance.Unit>()
4370
) { magnitude, unit ->
4471
Distance(magnitude, unit).isNegative.shouldBeTrue()
@@ -57,28 +84,20 @@ class DistanceTest : DescribeSpec({
5784
context("when the magnitude is positive") {
5885
it("returns false") {
5986
checkAll(
60-
Arb.positiveDouble().filter { !it.isNaN() },
87+
Arb.numericDouble().filter { it > 0 },
6188
Arb.enum<Distance.Unit>()
6289
) { magnitude, unit ->
6390
Distance(magnitude, unit).isNegative.shouldBeFalse()
6491
}
6592
}
6693
}
67-
68-
context("when the magnitude is NaN") {
69-
it("returns false") {
70-
checkAll(Arb.enum<Distance.Unit>()) { unit ->
71-
Distance(Double.NaN, unit).isNegative.shouldBeFalse()
72-
}
73-
}
74-
}
7594
}
7695

77-
describe("checking whether the distance is positive") {
96+
describe("whether the distance is positive") {
7897
context("when the magnitude is negative") {
7998
it("returns false") {
8099
checkAll(
81-
Arb.negativeDouble().filter { !it.isNaN() },
100+
Arb.numericDouble().filter { it < 0 },
82101
Arb.enum<Distance.Unit>()
83102
) { magnitude, unit ->
84103
Distance(magnitude, unit).isPositive.shouldBeFalse()
@@ -97,59 +116,19 @@ class DistanceTest : DescribeSpec({
97116
context("when the magnitude is positive") {
98117
it("returns true") {
99118
checkAll(
100-
Arb.positiveDouble().filter { !it.isNaN() },
119+
Arb.numericDouble().filter { it > 0 },
101120
Arb.enum<Distance.Unit>()
102121
) { magnitude, unit ->
103122
Distance(magnitude, unit).isPositive.shouldBeTrue()
104123
}
105124
}
106125
}
107-
108-
context("when the magnitude is NaN") {
109-
it("returns false") {
110-
checkAll(Arb.enum<Distance.Unit>()) { unit ->
111-
Distance(Double.NaN, unit).isPositive.shouldBeFalse()
112-
}
113-
}
114-
}
115-
}
116-
117-
describe("checking whether the distance is finite") {
118-
context("when the magnitude is NaN") {
119-
it("returns false") {
120-
checkAll(Arb.enum<Distance.Unit>()) { unit ->
121-
Distance(Double.NaN, unit).isFinite.shouldBeFalse()
122-
}
123-
}
124-
}
125-
126-
context("when the magnitude is infinite") {
127-
it("returns false") {
128-
checkAll(
129-
Arb.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY),
130-
Arb.enum<Distance.Unit>()
131-
) { magnitude, unit ->
132-
Distance(magnitude, unit).isFinite.shouldBeFalse()
133-
}
134-
}
135-
}
136-
137-
context("when the magnitude is finite") {
138-
it("returns true") {
139-
checkAll(
140-
Arb.numericDouble(),
141-
Arb.enum<Distance.Unit>()
142-
) { magnitude, unit ->
143-
Distance(magnitude, unit).isFinite.shouldBeTrue()
144-
}
145-
}
146-
}
147126
}
148127

149128
describe("converting to feet") {
150129
context("when the distance is already in feet") {
151130
it("returns the original distance") {
152-
checkAll(Arb.double()) { magnitude ->
131+
checkAll(Arb.numericDouble()) { magnitude ->
153132
magnitude.feet.inFeet().shouldBe(magnitude.feet)
154133
}
155134
}
@@ -191,7 +170,7 @@ class DistanceTest : DescribeSpec({
191170

192171
context("when the distance is already in meters") {
193172
it("returns the original distance") {
194-
checkAll(Arb.double()) { magnitude ->
173+
checkAll(Arb.numericDouble()) { magnitude ->
195174
magnitude.meters.inMeters().shouldBe(magnitude.meters)
196175
}
197176
}

0 commit comments

Comments
 (0)