I am using Okio in Kotlin/Native and I would like to check if one path is inside another path.
Although there is a equal/greater than/less than operator, it looks like it only compares the length.
Example:
"/a/b/c/d".toPath().startsWith("/a/b/c".toPath()) // should return true
"/a/b/d/d".toPath().startsWith("/a/b/c".toPath()) // should return false
But startsWith does not exist.
Kotlin/JVM supports this via Java: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/starts-with.html
I have created this extension function which implements startsWith
as described in the question:
fun Path.startsWith(other: Path) = normalized().run {
other.normalized().let { normalizedOther ->
normalizedOther.segments.size <= segments.size &&
segments
.slice(0 until normalizedOther.segments.size)
.filterIndexed { index, s -> normalizedOther.segments[index] != s }
.isEmpty()
}
}
It first checks if the other
path has more segments (or components) than this
path which would already mean they don't match since /a/b/c
can never start with /a/b/c/d
(or even /1/2/3/4
).
If the segment count of other
is the same or less, it proceeds with slicing this
into as many segments as other
has so that any sub-entries are ignored.
Then, it filters the sliced segments of this
that don't match by using the same index for accessing the segments of other
.
Now we have a list of segments that don't match on the same index. By checking if the list isEmpty()
, we now have the conclusion of whether this
startsWith other
(you can turn this into an infix if you want.).
Passing test:
import okio.Path.Companion.toPath
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class PathsTests {
@Test
fun testStartsWith() {
listOf(
"/a/b/c/d" to "/a/b/c",
"/a/b/c/d" to "/a/b/c/",
"/A/B/C/D" to "/A/B/C",
"/a/b/c/d" to "/a/b//c/",
"/a/b/c/d" to "/a/b/../b/c",
"/a/b/c/d" to "/a/../a/./b/../b///c",
"\\a\\b\\c\\d" to "/a/../a/./b/../b///c",
"/home/user/.config/test" to "/home/user",
"/var/www/html/app" to "/var/www/html",
"/home/user" to "/",
"/" to "/",
"////////////////////////" to "/",
"/" to "////////////////////////",
"/home/user" to "/home/user",
"/home/user/./" to "/home/user",
"/home/user" to "/home/user/./",
"/./var" to "/var",
"." to ".",
"./" to ".",
".." to "..",
"../.." to "../..",
"./.." to "../.",
"../." to "./..",
"./../." to ".././.",
"/." to "/.",
"./" to ".",
"/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" to "/a/b/c",
"/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a" to "/a/a/a"
).forEach { (pathString, otherPathString) ->
assertTrue(
pathString.toPath().startsWith(otherPathString.toPath()),
"$pathString should start with $otherPathString"
)
}
listOf(
"/a/b/c" to "/a/b/c/d/",
"/a/b/c/" to "/a/b/c/d",
"/a/b/d/d" to "/a/b/c",
"/a/b/d/d" to "/a/b/ce",
"/a/b/ce" to "/a/b/c",
"/a/b/c" to "/a/b/ce",
"/abcd" to "/a/b/c/d",
"/a/../b/c" to "/a/b/c",
"/a/b/" to "/a/b//c",
"/a/b/c/d" to "/a/b/../c",
"/a/b/c/d" to "/a/./a/../b/./b///c",
"/a/b/c" to "/c/b/a",
"/a/a/a/a" to "/a/a/a/a/a",
"\\a\\b\\d\\d" to "\\a\\b\\c",
"\\a\\b\\d\\d" to "/a/b/c",
"/home/user/.config/test" to "/home/user2",
"/var/www/html/app" to "/var/local/www/html/app",
"/home/user" to ".",
"/" to "./",
"/home/user" to "/home/user2",
"/home/user/./" to "/home/user2",
"/home/user2" to "/home/user/./",
"../var" to "/var",
"." to "..",
"./" to "..",
".." to ".",
"/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" to "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/z",
"/a/a/a" to "/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a",
"/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a" to "/A",
).forEach { (pathString, otherPathString) ->
assertFalse(
pathString.toPath().startsWith(otherPathString.toPath()),
"$pathString should not start with $otherPathString"
)
}
}
}