Reputation: 55864
If I have two Paths, how can I find the longest common Path of the two?
import java.nio.file.Path;
import java.nio.file.Paths;
Path common(Path pathA, Path pathB) {
...
}
...
common(Paths.get("/a/b/c/d/e"), Paths.get("/a/b/c/g/h"))
Expected output:
Paths.get("/a/b/c")
Upvotes: 5
Views: 2068
Reputation: 11110
Old question, but none of the answers seemed concise enough. Here's my one:
public static Path common(Path a, Path b) {
while (!a.startsWith(b))
b = b.getParent();
return b;
}
Upvotes: 0
Reputation: 15136
Whatever the question, it's always fun trying to answer using streams:
public static Path commonPath(Path a, Path b) {
Path other = b.normalize();
return Stream.iterate(a.normalize(), Path::getParent)
.takeWhile(Objects::nonNull)
.filter(parent -> Stream.iterate(other, Path::getParent).takeWhile(Objects::nonNull)
.anyMatch(x -> Objects.equals(x, parent)))
.findFirst().orElse(null);
}
This works on Windows / Linux, for example try the test data in @Michał Ziober answer. This just iterates up the parent hierarchy of first until it meets a parent of the other.
With the suggestion by Bass in comments, the common path check is more concise using the other version of Stream.iterate
:
public static Path commonPath(Path a, Path b) {
return Stream.iterate(a.normalize(), Objects::nonNull, Path::getParent)
.filter(parent -> Stream.iterate(b.normalize(), Objects::nonNull, Path::getParent)
.anyMatch(parent::equals))
.findFirst().orElse(null);
}
A small refactoring could pull out Stream.iterate(path.normalize(), Objects::nonNull, Path::getParent)
to a utility method Stream<Path> parentsOf(Path path)
as it is repeated twice above:
public static Path commonPath(Path a, Path b) {
return parentsOf(a)
.filter(parent -> parentsOf(b).anyMatch(parent::equals))
.findFirst().orElse(null);
}
private static Stream<Path> parentsOf(Path a) {
return Stream.iterate(a.normalize(), Objects::nonNull, Path::getParent);
}
Upvotes: 2
Reputation: 5318
While the answer by DuncG is definitely the most concise and probably the most efficient one, I'll add my solution, too.
Consider we have a function to transform a Path
into a Stream
of path entries:
@NonNull Stream<@NonNull Path> pathEntries(final @NonNull Path path) {
return Stream.concat(Stream.ofNullable(path.getRoot()), StreamSupport.stream(path.spliterator(), false));
}
Now, consider we have another helper function that zips two streams together (e.g.: Streams.zip()
from Guava):
<A, B, R> @NonNull Stream<R> zip(
final @NonNull Stream<A> streamA,
final @NonNull Stream<B> streamB,
final BiFunction<? super A, ? super B, ? extends R> function
) {
final Spliterator<A> splitrA = streamA.spliterator();
final Spliterator<B> splitrB = streamB.spliterator();
final int characteristics = splitrA.characteristics()
& splitrB.characteristics()
& (Spliterator.SIZED | Spliterator.ORDERED);
final Iterator<A> itrA = Spliterators.iterator(splitrA);
final Iterator<B> itrB = Spliterators.iterator(splitrB);
return StreamSupport.stream(new AbstractSpliterator<R>(Math.min(splitrA.estimateSize(), splitrB.estimateSize()), characteristics) {
@Override
public boolean tryAdvance(final @NonNull Consumer<? super R> action) {
if (itrA.hasNext() && itrB.hasNext()) {
action.accept(function.apply(itrA.next(), itrB.next()));
return true;
}
return false;
}
}, false)
.onClose(streamA::close)
.onClose(streamB::close);
}
Then, the solution will look like this:
@Nullable Path commonOrNull(final @NonNull Path left, final @NonNull Path right) {
return zip(
pathEntries(left.normalize()),
pathEntries(right.normalize()),
(l, r) -> l.equals(r) ? l : null
)
.takeWhile(Objects::nonNull)
.reduce(Path::resolve)
.orElse(null);
}
This will also work on Windows, returning null
for any two paths that don't share a common file system root, like C:\foo
and D:\foo
.
Upvotes: 1
Reputation: 38655
We can generate all subpaths starting from the longest possible and check which two are equal:
private Path commonPath(Path path0, Path path1) {
if (path0.equals(path1)) {
return path0;
}
path0 = path0.normalize();
path1 = path1.normalize();
int minCount = Math.min(path0.getNameCount(), path1.getNameCount());
for (int i = minCount; i > 0; i--) {
Path sp0 = path0.subpath(0, i);
if (sp0.equals(path1.subpath(0, i))) {
String root = Objects.toString(path0.getRoot(), "");
return Paths.get(root, sp0.toString());
}
}
return path0.getRoot();
}
And usage:
Map<String, String> paths = new LinkedHashMap<>();
paths.put("/a/b/c", "/a/b/d");
paths.put("/a/", "/a/b/d");
paths.put("/f/b/c", "/a/b/d");
paths.put("/a/b/c/d/e", "/a/b/f/../c/g");
paths.put("C:/Winnt/System32", "C:/Winnt/System64");
paths.forEach((k, v) ->
System.out.println(
k + " = " + v + " => " + commonPath(Paths.get(k), Paths.get(v))));
Above code prints:
/a/b/c = /a/b/d => /a/b
/a/ = /a/b/d => /a
/f/b/c = /a/b/d => /
/a/b/c/d/e = /a/b/f/../c/g => /a/b/c
C:/Winnt/System32 = C:/Winnt/System64 => C:/Winnt
Upvotes: 2
Reputation: 2778
Try this simple idea
Path a = Paths.get("a/b/c/d/e");
Path b = Paths.get("a/b/c/g/h");
// Normalize
a = a.normalize();
b = b.normalize();
// Create common root
Path common = null;
if (a.isAbsolute() && b.isAbsolute() && a.getRoot().equals(b.getRoot())) {
common = a.getRoot();
}
else if (!a.isAbsolute() && !b.isAbsolute()) {
common = Paths.get("");
}
// Iterate from root until names differ
if (common != null) {
int n = Math.min(a.getNameCount(), b.getNameCount());
for (int i=0; i<n; i++) {
if (a.getName(i).equals(b.getName(i))) {
common = common.resolve(a.getName(i));
}
else {
break;
}
}
}
// Show
System.out.println(common);
Upvotes: 3
Reputation: 8224
Path path1 = Paths.get("/a/b/c/d/e");
Path path2 = Paths.get("/a/b/c/g/h");
You can relativize the paths to one another:
Path relativePath = path1.relativize(path2).normalize();
// result: ../../g/h
and then go to the parent until the path ends in ..
while(relativePath != null && !relativePath.endsWith("..")) {
relativePath = relativePath.getParent();
}
// result: ../.. (but may also be null)
the result can be applied back to any of the two paths:
Path result = path1.resolve(relativePath).normalize()
// result: /a/b/c
Upvotes: 5