akonsu
akonsu

Reputation: 29538

Extract prefix of a path of given length

Given an absolute path, how to extract the beginning part of this path of some given length? Effectively, the same value that I would get if I invoked getParent() the needed number of times.

I also need this to be filesystem-independent.

I see that there is Path.subpath, but it does not seem to be what I want: Path.of("/","a","b","c").subpath(0,2) gives a/b, but I need /a/b. Yes, I can get the root and then create a new path from the results of subpath() and the root, but then is it going to be system-independent?

Is there a simple way to achieve this?

Upvotes: 0

Views: 122

Answers (2)

DuncG
DuncG

Reputation: 15086

Two simple ways to make it platform independent are the ways you suggest: Path with getParent, or Path getRoot+subpath. These should give identical results:

public static Path subpath1(Path p, int parts) {
    if (parts < 1) throw new IllegalArgumentException("parts must be positive");
    // Go up from path appropriate number of times:
    return Stream.iterate(p, Path::getParent)
                 .skip(Math.max(0, p.getNameCount() - parts))
                 .findFirst().get();
}
public static Path subpath2(Path p, int parts) {
    if (parts < 1) throw new IllegalArgumentException("parts must be positive");
    // Resolve the subpath from the root
    Path sub = p.subpath(0, parts);
    Path root = p.getRoot();
    return root != null ? root.resolve(sub) : sub;
}

However note that system-independence rather depends on your input data because Path.of("/","a","b","c") on Windows means UNC pathname \\a\b\c and NOT a path \a\b\c which starts from whatever your current drive happens to be. So if you use Path.of("/","a","b","c") in all platforms you'll see different results if run on Windows vs Linux.

To be consistent, specify Windows and Linux paths using "/a/b/c" form not "/","a","b","c". Here are some tests which show these differences:

private static void check(Path expected, Path input, int parts) {
    assertEquals(expected, subpath1(input, parts));
    assertEquals(expected, subpath2(input, parts));
}
@EnabledOnOs(OS.WINDOWS)
@Test void testSubpathsWindows() {
    check(Path.of("C:/a/b"), Path.of("C:\\", "a", "b", "c", "d"), 2);
    check(Path.of("C:\\a"),  Path.of("C:\\", "a", "b", "c", "d"), 1);

    // UNC paths - note that Path.of("/", "a", "b", ... ) is UNC \\a\b
    check(Path.of("\\\\a\\b\\c"), Path.of("/", "a", "b", "c", "d"), 1);
    check(Path.of("\\\\a\\b\\c"), Path.of("\\\\a\\b\\c\\d"), 1);
}

@EnabledOnOs(OS.LINUX)
@Test void testSubpathsLinux() {
    check(Path.of("/", "a", "b"),  Path.of("/", "a", "b", "c", "d"), 2);
    check(Path.of("/a/b/c"),       Path.of("/", "a", "b", "c", "d"), 3);
    check(Path.of("a/b/c"),        Path.of("a", "b", "c", "d"), 3);
}
@Test void testSubpathsAllOS() {
    check(Path.of("/a/b"),  Path.of("/a/b/c/d"), 2);
    check(Path.of("/a/b/c"),       Path.of("/a/b/c/d/e"), 3);
    check(Path.of("a/b/c"),        Path.of("a/b/c/d"), 3);
}

Upvotes: 1

DevilsHnd - 退した
DevilsHnd - 退した

Reputation: 9192

I'm not exactly sure if this is what you need but is does parse the supplied path string and return the number of segments desired. Read the comments in code:

/**
 * Returns the supplied path string at the desired path depth, for example:<pre>
 * 
 * If a path string consisted of: 
 * 
 *   "C:/a/b/c/d/e/f/g/h/i/j/k"  OR  "C:\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k";
 * 
 * And we want the path from a depth of 6 then what is returned is:
 * 
 *     1  2 3 4 5 6
 *    "C:\a\b\c\d\e" 
 * 
 * If a path string consisted of: 
 * 
 *   "/a/b/c/d/e/f/g/h/i/j/k"  OR  "\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k";
 * 
 * And we want the path from a depth of 6 then what is returned is:
 * 
 *      1 2 3 4 5 6
 *    "\a\b\c\d\e\f"  
 * 
 * Note that the File System Separator is used on the returned String. This 
 * can be changed in code by replaceing all instances of `File.Separator` 
 * with whatever string character you want.</pre>
 * 
 * @param absolutePath (String) The path string to acquire the path depth 
 * from.<br>
 * 
 * @param desiredDepth (int) The desired path depth to retrieve. If a path 
 * depth is provided that exceeds the number of directories within the 
 * supplied path string then the depth value will be modified to the MAX 
 * depth of that supplied path string. If a depth of 0 is provided, then
 * a Null String ("") is returned.<br>
 * 
 * @return (String) The Path string at the specified depth.
 */
public String getPathFromDepth(String absolutePath, int desiredDepth) {
    // Convert separators in supplied path string to forward slash (/)
    if (absolutePath.contains("\\")) {
        absolutePath = absolutePath.replace("\\", "/");
    }
    
    // Determine if the path string starts with a separator.
    boolean separatorStart = false;
    if (absolutePath.startsWith("/")) {
        separatorStart = true;                    // Flag the fact
        absolutePath = absolutePath.substring(1); // Remove the starting seperator
    }
    
    // Split the supplied (and modified) path string into an array:
    String[] pathParts = absolutePath.split("/");
    
    // See if the supplied depth goes beyond limits...
    if (desiredDepth > pathParts.length) {
        // It does, so make it to Max Limit.
        desiredDepth = pathParts.length;
    }
    
    // Prepare to build the new Path.
    StringBuilder sb = new StringBuilder(""); 
    // Iterate through the created array one element at a time:
    for (int i = 0; i < desiredDepth; i++) {
        /* If the StringBuilder object contains something 
           then append a System File Separator character. */
        if (!sb.toString().isEmpty()) {
            sb.append(File.separator);
        }
        /* If the supplied path started with a Seperator then 
           make sure the returned path does too. Append a File
           Sperator character to the build.            */
        if (separatorStart) {
            sb.append(File.separator);
            separatorStart = false; // Remove the flag so this doesn't get applied again.
        }
        // Append the current path segment from the String[] Array:
        sb.append(pathParts[i]);
    }
    
    // Return the Build Path String:
    return sb.toString();
}

Upvotes: 0

Related Questions