Як побудувати відносний шлях на Яві з двох абсолютних шляхів (або URL-адрес)?


275

Дано два абсолютні шляхи, наприклад

/var/data/stuff/xyz.dat
/var/data

Як можна створити відносний шлях, який використовує другий шлях як основу? У наведеному вище прикладі результат повинен бути:./stuff/xyz.dat


3
Для Java 7 та новіших версій дивіться відповідь @ VitaliiFedorenko.
Енді Томас

1
tl; dr відповідь: Paths.get (startPath) .relativize (Paths.get (endPath)). toString () (який, до речі, здається, працює нормально, наприклад, "../" для мене в Java 8 , отже ...)
Андрій

Відповіді:


297

Це трохи кругле, але чому б не використовувати URI? Він має метод релятивізації, який робить усі необхідні для вас перевірки.

String path = "/var/data/stuff/xyz.dat";
String base = "/var/data";
String relative = new File(base).toURI().relativize(new File(path).toURI()).getPath();
// relative == "stuff/xyz.dat"

Зверніть увагу, що шлях до файлів існує java.nio.file.Path#relativizeз Java 1.7, на що вказував @Jirka Meluzin в іншій відповіді .


17
Дивіться відповідь Пітера Мюллера. relativize () видається досить зламаним для всіх, крім найпростіших випадків.
Дейв Рей

11
Так, він працює лише в тому випадку, якщо базовий шлях є батьківським для першого шляху. Якщо вам потрібен ієрархічний відсталий на кшталт "../../relativepath", він не працюватиме. Я знайшов рішення: mrpmorris.blogspot.com/2007/05/…
Aurelien Ribon

4
Як писав @VitaliiFedorenko: використовуйте java.nio.file.Path#relativize(Path), він просто працює з батьківськими подвійними крапками та всіма.
Кампа

Подумайте про використання toPath()замість toURI(). Це чудово вміє створювати такі речі "..\..". Але пам’ятайте про java.lang.IllegalArgumentException: 'other' has different rootвиняток, коли просите відносний шлях від "C:\temp"до "D:\temp".
Ігор

Це не працює, як очікувалося, він повертає дані / речі / xyz.dat у моєму тестовому випадку.
невдалий

238

З Java 7 ви можете використовувати метод релятивізації :

import java.nio.file.Path;
import java.nio.file.Paths;

public class Test {

     public static void main(String[] args) {
        Path pathAbsolute = Paths.get("/var/data/stuff/xyz.dat");
        Path pathBase = Paths.get("/var/data");
        Path pathRelative = pathBase.relativize(pathAbsolute);
        System.out.println(pathRelative);
    }

}

Вихід:

stuff/xyz.dat

3
Приємно, коротко, без зайвих ліб +1. Рішення Адама Крюма (хіт 1) не проходить мої тести, і наступна відповідь (hit2) "Єдине" робоче рішення "додає нову банку І є більше кодом, ніж моя реалізація, я знаходжу це тут згодом ... краще, ніж ніколи .- )
хокр

1
Але стежте за цією проблемою .
ben3000

1
Перевірив, що це обробляє додавання, ..де потрібно (це робить).
Оуен

На жаль, Android не включає java.nio.file:(
Натан Осман

1
Я виявив, що ви отримуєте дивні результати, якщо "pathBase" не "нормалізується" до "відновлення". Хоча це добре в цьому прикладі, я б робив pathBase.normalize().relativize(pathAbsolute);як загальне правило.
pstanton

77

На момент написання (червень 2010 р.) Це єдине рішення, яке пройшло мої тестові справи. Я не можу гарантувати, що це рішення не містить помилок, але воно проходить включені тестові приклади. Метод та тести, які я написав, залежать від FilenameUtilsкласу з IO Apache .

Рішення було протестовано на Java 1.4. Якщо ви використовуєте Java 1.5 (або вище), вам слід розглянути можливість заміни StringBufferна StringBuilder(якщо ви все ще використовуєте Java 1.4, замість цього слід розглянути зміну роботодавця).

import java.io.File;
import java.util.regex.Pattern;

import org.apache.commons.io.FilenameUtils;

public class ResourceUtils {

    /**
     * Get the relative path from one file to another, specifying the directory separator. 
     * If one of the provided resources does not exist, it is assumed to be a file unless it ends with '/' or
     * '\'.
     * 
     * @param targetPath targetPath is calculated to this file
     * @param basePath basePath is calculated from this file
     * @param pathSeparator directory separator. The platform default is not assumed so that we can test Unix behaviour when running on Windows (for example)
     * @return
     */
    public static String getRelativePath(String targetPath, String basePath, String pathSeparator) {

        // Normalize the paths
        String normalizedTargetPath = FilenameUtils.normalizeNoEndSeparator(targetPath);
        String normalizedBasePath = FilenameUtils.normalizeNoEndSeparator(basePath);

        // Undo the changes to the separators made by normalization
        if (pathSeparator.equals("/")) {
            normalizedTargetPath = FilenameUtils.separatorsToUnix(normalizedTargetPath);
            normalizedBasePath = FilenameUtils.separatorsToUnix(normalizedBasePath);

        } else if (pathSeparator.equals("\\")) {
            normalizedTargetPath = FilenameUtils.separatorsToWindows(normalizedTargetPath);
            normalizedBasePath = FilenameUtils.separatorsToWindows(normalizedBasePath);

        } else {
            throw new IllegalArgumentException("Unrecognised dir separator '" + pathSeparator + "'");
        }

        String[] base = normalizedBasePath.split(Pattern.quote(pathSeparator));
        String[] target = normalizedTargetPath.split(Pattern.quote(pathSeparator));

        // First get all the common elements. Store them as a string,
        // and also count how many of them there are.
        StringBuffer common = new StringBuffer();

        int commonIndex = 0;
        while (commonIndex < target.length && commonIndex < base.length
                && target[commonIndex].equals(base[commonIndex])) {
            common.append(target[commonIndex] + pathSeparator);
            commonIndex++;
        }

        if (commonIndex == 0) {
            // No single common path element. This most
            // likely indicates differing drive letters, like C: and D:.
            // These paths cannot be relativized.
            throw new PathResolutionException("No common path element found for '" + normalizedTargetPath + "' and '" + normalizedBasePath
                    + "'");
        }   

        // The number of directories we have to backtrack depends on whether the base is a file or a dir
        // For example, the relative path from
        //
        // /foo/bar/baz/gg/ff to /foo/bar/baz
        // 
        // ".." if ff is a file
        // "../.." if ff is a directory
        //
        // The following is a heuristic to figure out if the base refers to a file or dir. It's not perfect, because
        // the resource referred to by this path may not actually exist, but it's the best I can do
        boolean baseIsFile = true;

        File baseResource = new File(normalizedBasePath);

        if (baseResource.exists()) {
            baseIsFile = baseResource.isFile();

        } else if (basePath.endsWith(pathSeparator)) {
            baseIsFile = false;
        }

        StringBuffer relative = new StringBuffer();

        if (base.length != commonIndex) {
            int numDirsUp = baseIsFile ? base.length - commonIndex - 1 : base.length - commonIndex;

            for (int i = 0; i < numDirsUp; i++) {
                relative.append(".." + pathSeparator);
            }
        }
        relative.append(normalizedTargetPath.substring(common.length()));
        return relative.toString();
    }


    static class PathResolutionException extends RuntimeException {
        PathResolutionException(String msg) {
            super(msg);
        }
    }    
}

Тестові випадки, які це проходить, є

public void testGetRelativePathsUnix() {
    assertEquals("stuff/xyz.dat", ResourceUtils.getRelativePath("/var/data/stuff/xyz.dat", "/var/data/", "/"));
    assertEquals("../../b/c", ResourceUtils.getRelativePath("/a/b/c", "/a/x/y/", "/"));
    assertEquals("../../b/c", ResourceUtils.getRelativePath("/m/n/o/a/b/c", "/m/n/o/a/x/y/", "/"));
}

public void testGetRelativePathFileToFile() {
    String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf";
    String base = "C:\\Windows\\Speech\\Common\\sapisvr.exe";

    String relPath = ResourceUtils.getRelativePath(target, base, "\\");
    assertEquals("..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath);
}

public void testGetRelativePathDirectoryToFile() {
    String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf";
    String base = "C:\\Windows\\Speech\\Common\\";

    String relPath = ResourceUtils.getRelativePath(target, base, "\\");
    assertEquals("..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath);
}

public void testGetRelativePathFileToDirectory() {
    String target = "C:\\Windows\\Boot\\Fonts";
    String base = "C:\\Windows\\Speech\\Common\\foo.txt";

    String relPath = ResourceUtils.getRelativePath(target, base, "\\");
    assertEquals("..\\..\\Boot\\Fonts", relPath);
}

public void testGetRelativePathDirectoryToDirectory() {
    String target = "C:\\Windows\\Boot\\";
    String base = "C:\\Windows\\Speech\\Common\\";
    String expected = "..\\..\\Boot";

    String relPath = ResourceUtils.getRelativePath(target, base, "\\");
    assertEquals(expected, relPath);
}

public void testGetRelativePathDifferentDriveLetters() {
    String target = "D:\\sources\\recovery\\RecEnv.exe";
    String base = "C:\\Java\\workspace\\AcceptanceTests\\Standard test data\\geo\\";

    try {
        ResourceUtils.getRelativePath(target, base, "\\");
        fail();

    } catch (PathResolutionException ex) {
        // expected exception
    }
}

5
Приємно! Одне, однак, воно руйнується, якщо база та ціль однакові - рядок, який розповсюджується, закінчується роздільником, якого нормалізований цільовий шлях не має, тому виклик підрядка вимагає занадто багато цифр. Подумайте, що я виправив це, додавши наступне перед останніми двома рядками функції: if (common.length ()> = normalizedTargetPath.length ()) {return "." }
Ерханніс

4
Сказати, що це єдине робоче рішення - вводить в оману. Інші відповіді працюють краще (ця відповідь випадає, коли база і ціль однакові), простіші і не покладаються на загальнодоступні.
NateS

26

Під час використання java.net.URI.relativize ви повинні знати про помилку на Java: JDK-6226081 (URI повинен мати можливість відновити шляхи з частковими коренями)

На даний момент relativize()метод URIбуде релятивізувати URI лише тоді, коли один є префіксом іншого.

Що по суті означає java.net.URI.relativize, що не створить ".." для вас.


6
Неприємний. Існує обхідний шлях для цього, по- видимому: stackoverflow.com/questions/204784 / ...
skaffman

Paths.get (startPath) .relativize (Paths.get (endPath)). ToString‌ (), здається, працює на відмінно, наприклад, для мене "Java" на Java 8.
Ендрю

@skaffman Ви впевнені? Ця відповідь посилається на помилку JDK-6226081, але URIUtils.resolve()згадує JDK-4708535. І з вихідного коду я не бачу нічого, пов’язаного з зворотним відстеженням (тобто ..сегментами). Ви переплутали два помилки?
Гаррет Вілсон

JDK-6920138 позначений як дублікат JDK-4708535.
Крістіан К.

17

Помилка, на яку йдеться в іншій відповіді , адресується URIUtils в Apache HttpComponents

public static URI resolve(URI baseURI,
                          String reference)

Вирішує посилання URI проти базового URI. Обхід помилки на java.net.URI ()


Чи не метод вирішення генерує абсолютний URI з базового та відносного шляху? Як би допоміг цей метод?
Чейз

17

У Java 7 і пізніших версіях ви можете просто використовувати (і на відміну від URI, це не помилка):

Path#relativize(Path)

10

Якщо вам відомо, що другий рядок є частиною першого:

String s1 = "/var/data/stuff/xyz.dat";
String s2 = "/var/data";
String s3 = s1.substring(s2.length());

або якщо ви дійсно хочете періоду на початку, як у вашому прикладі:

String s3 = ".".concat(s1.substring(s2.length()));

3
Рядок s3 = "." + s1.substring (s2.length ()); трохи читабельніший IMO
Dónal

10

Рекурсія виробляє менший розчин. Це викидає виняток, якщо результат неможливий (наприклад, різний диск Windows) або непрактичний (корінь - лише загальна директорія.)

/**
 * Computes the path for a file relative to a given base, or fails if the only shared 
 * directory is the root and the absolute form is better.
 * 
 * @param base File that is the base for the result
 * @param name File to be "relativized"
 * @return the relative name
 * @throws IOException if files have no common sub-directories, i.e. at best share the
 *                     root prefix "/" or "C:\"
 */

public static String getRelativePath(File base, File name) throws IOException  {
    File parent = base.getParentFile();

    if (parent == null) {
        throw new IOException("No common directory");
    }

    String bpath = base.getCanonicalPath();
    String fpath = name.getCanonicalPath();

    if (fpath.startsWith(bpath)) {
        return fpath.substring(bpath.length() + 1);
    } else {
        return (".." + File.separator + getRelativePath(parent, name));
    }
}

getCanonicalPath може мати велику вагу, тому це рішення не можна рекомендувати, коли вам потрібно обробити сто тисяч записів. Наприклад, у мене є деякі файли з переліком файлів, що містять до мільйона записів, і тепер я хочу перемістити їх, щоб використовувати відносний шлях для переносимості.
user2305886

8

Ось рішення іншої бібліотеки безкоштовно:

Path sourceFile = Paths.get("some/common/path/example/a/b/c/f1.txt");
Path targetFile = Paths.get("some/common/path/example/d/e/f2.txt"); 
Path relativePath = sourceFile.relativize(targetFile);
System.out.println(relativePath);

Виходи

..\..\..\..\d\e\f2.txt

[EDIT] насправді він виводить більше .. \, оскільки джерелом є файл, а не каталог. Правильне рішення для моєї справи:

Path sourceFile = Paths.get(new File("some/common/path/example/a/b/c/f1.txt").parent());
Path targetFile = Paths.get("some/common/path/example/d/e/f2.txt"); 
Path relativePath = sourceFile.relativize(targetFile);
System.out.println(relativePath);

6

Моя версія грунтується на версіях Метта та Стіва :

/**
 * Returns the path of one File relative to another.
 *
 * @param target the target directory
 * @param base the base directory
 * @return target's path relative to the base directory
 * @throws IOException if an error occurs while resolving the files' canonical names
 */
 public static File getRelativeFile(File target, File base) throws IOException
 {
   String[] baseComponents = base.getCanonicalPath().split(Pattern.quote(File.separator));
   String[] targetComponents = target.getCanonicalPath().split(Pattern.quote(File.separator));

   // skip common components
   int index = 0;
   for (; index < targetComponents.length && index < baseComponents.length; ++index)
   {
     if (!targetComponents[index].equals(baseComponents[index]))
       break;
   }

   StringBuilder result = new StringBuilder();
   if (index != baseComponents.length)
   {
     // backtrack to base directory
     for (int i = index; i < baseComponents.length; ++i)
       result.append(".." + File.separator);
   }
   for (; index < targetComponents.length; ++index)
     result.append(targetComponents[index] + File.separator);
   if (!target.getPath().endsWith("/") && !target.getPath().endsWith("\\"))
   {
     // remove final path separator
     result.delete(result.length() - File.separator.length(), result.length());
   }
   return new File(result.toString());
 }

2
+1 працює для мене. Лише незначна корекція: замість "/".length()вас слід використовувати separator.length
leonbloy

5

Рішення Метта Б отримує кількість каталогів, щоб неправильно відслідковувати - це має бути довжина базового шляху мінус кількість загальних елементів шляху, мінус один (для останнього елемента шляху, який є або ім'ям файлу, або трейлінг, ""згенерований split) . Це трапляється працювати з /a/b/c/і /a/x/y/, але замініть аргументи на /m/n/o/a/b/c/і, /m/n/o/a/x/y/і ви побачите проблему.

Крім того, він потрібен else breakвсередині першого для циклу, інакше він буде неправильно обробляти контури, які, мабуть, мають відповідні назви каталогів, наприклад, /a/b/c/d/та /x/y/c/z- the cзнаходиться в одному слоті в обох масивах, але не є фактичним збігом.

Усі ці рішення не мають можливості обробляти шляхи, які не можна відновити один до одного, оскільки вони мають несумісні корені, такі як C:\foo\barі D:\baz\quux. Напевно, лише проблема в Windows, але варто зазначити.

Я витратив на це набагато довше, ніж задумав, але це нормально. Насправді мені це було потрібно для роботи, тож дякую всім, хто звучав, і я впевнений, що буде виправлено і цю версію!

public static String getRelativePath(String targetPath, String basePath, 
        String pathSeparator) {

    //  We need the -1 argument to split to make sure we get a trailing 
    //  "" token if the base ends in the path separator and is therefore
    //  a directory. We require directory paths to end in the path
    //  separator -- otherwise they are indistinguishable from files.
    String[] base = basePath.split(Pattern.quote(pathSeparator), -1);
    String[] target = targetPath.split(Pattern.quote(pathSeparator), 0);

    //  First get all the common elements. Store them as a string,
    //  and also count how many of them there are. 
    String common = "";
    int commonIndex = 0;
    for (int i = 0; i < target.length && i < base.length; i++) {
        if (target[i].equals(base[i])) {
            common += target[i] + pathSeparator;
            commonIndex++;
        }
        else break;
    }

    if (commonIndex == 0)
    {
        //  Whoops -- not even a single common path element. This most
        //  likely indicates differing drive letters, like C: and D:. 
        //  These paths cannot be relativized. Return the target path.
        return targetPath;
        //  This should never happen when all absolute paths
        //  begin with / as in *nix. 
    }

    String relative = "";
    if (base.length == commonIndex) {
        //  Comment this out if you prefer that a relative path not start with ./
        //relative = "." + pathSeparator;
    }
    else {
        int numDirsUp = base.length - commonIndex - 1;
        //  The number of directories we have to backtrack is the length of 
        //  the base path MINUS the number of common path elements, minus
        //  one because the last element in the path isn't a directory.
        for (int i = 1; i <= (numDirsUp); i++) {
            relative += ".." + pathSeparator;
        }
    }
    relative += targetPath.substring(common.length());

    return relative;
}

Ось тести на кілька випадків:

public void testGetRelativePathsUnixy() 
{        
    assertEquals("stuff/xyz.dat", FileUtils.getRelativePath(
            "/var/data/stuff/xyz.dat", "/var/data/", "/"));
    assertEquals("../../b/c", FileUtils.getRelativePath(
            "/a/b/c", "/a/x/y/", "/"));
    assertEquals("../../b/c", FileUtils.getRelativePath(
            "/m/n/o/a/b/c", "/m/n/o/a/x/y/", "/"));
}

public void testGetRelativePathFileToFile() 
{
    String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf";
    String base = "C:\\Windows\\Speech\\Common\\sapisvr.exe";

    String relPath = FileUtils.getRelativePath(target, base, "\\");
    assertEquals("..\\..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath);
}

public void testGetRelativePathDirectoryToFile() 
{
    String target = "C:\\Windows\\Boot\\Fonts\\chs_boot.ttf";
    String base = "C:\\Windows\\Speech\\Common";

    String relPath = FileUtils.getRelativePath(target, base, "\\");
    assertEquals("..\\..\\Boot\\Fonts\\chs_boot.ttf", relPath);
}

public void testGetRelativePathDifferentDriveLetters() 
{
    String target = "D:\\sources\\recovery\\RecEnv.exe";
    String base   = "C:\\Java\\workspace\\AcceptanceTests\\Standard test data\\geo\\";

    //  Should just return the target path because of the incompatible roots.
    String relPath = FileUtils.getRelativePath(target, base, "\\");
    assertEquals(target, relPath);
}

4

Насправді моя інша відповідь не спрацювала, якщо цільовий шлях не був дитиною базового шляху.

Це має спрацювати.

public class RelativePathFinder {

    public static String getRelativePath(String targetPath, String basePath, 
       String pathSeparator) {

        // find common path
        String[] target = targetPath.split(pathSeparator);
        String[] base = basePath.split(pathSeparator);

        String common = "";
        int commonIndex = 0;
        for (int i = 0; i < target.length && i < base.length; i++) {

            if (target[i].equals(base[i])) {
                common += target[i] + pathSeparator;
                commonIndex++;
            }
        }


        String relative = "";
        // is the target a child directory of the base directory?
        // i.e., target = /a/b/c/d, base = /a/b/
        if (commonIndex == base.length) {
            relative = "." + pathSeparator + targetPath.substring(common.length());
        }
        else {
            // determine how many directories we have to backtrack
            for (int i = 1; i <= commonIndex; i++) {
                relative += ".." + pathSeparator;
            }
            relative += targetPath.substring(common.length());
        }

        return relative;
    }

    public static String getRelativePath(String targetPath, String basePath) {
        return getRelativePath(targetPath, basePath, File.pathSeparator);
    }
}

public class RelativePathFinderTest extends TestCase {

    public void testGetRelativePath() {
        assertEquals("./stuff/xyz.dat", RelativePathFinder.getRelativePath(
                "/var/data/stuff/xyz.dat", "/var/data/", "/"));
        assertEquals("../../b/c", RelativePathFinder.getRelativePath("/a/b/c",
                "/a/x/y/", "/"));
    }

}

2
Замість File.pathSeparator повинен бути File.separator. pathSeparator слід використовувати лише для сплітку (регулярного виразів), а для "////" регулярного виразу (переможна виразка), шлях результату буде неправильним.
Олексій Івасюв

3

Класно !! Мені потрібно трохи такого коду, але для порівняння шляхів до каталогу на машинах Linux. Я виявив, що це не працює в ситуаціях, коли цільовим був батьківський каталог.

Ось зручна для каталогу версія методу:

 public static String getRelativePath(String targetPath, String basePath, 
     String pathSeparator) {

 boolean isDir = false;
 {
   File f = new File(targetPath);
   isDir = f.isDirectory();
 }
 //  We need the -1 argument to split to make sure we get a trailing 
 //  "" token if the base ends in the path separator and is therefore
 //  a directory. We require directory paths to end in the path
 //  separator -- otherwise they are indistinguishable from files.
 String[] base = basePath.split(Pattern.quote(pathSeparator), -1);
 String[] target = targetPath.split(Pattern.quote(pathSeparator), 0);

 //  First get all the common elements. Store them as a string,
 //  and also count how many of them there are. 
 String common = "";
 int commonIndex = 0;
 for (int i = 0; i < target.length && i < base.length; i++) {
     if (target[i].equals(base[i])) {
         common += target[i] + pathSeparator;
         commonIndex++;
     }
     else break;
 }

 if (commonIndex == 0)
 {
     //  Whoops -- not even a single common path element. This most
     //  likely indicates differing drive letters, like C: and D:. 
     //  These paths cannot be relativized. Return the target path.
     return targetPath;
     //  This should never happen when all absolute paths
     //  begin with / as in *nix. 
 }

 String relative = "";
 if (base.length == commonIndex) {
     //  Comment this out if you prefer that a relative path not start with ./
     relative = "." + pathSeparator;
 }
 else {
     int numDirsUp = base.length - commonIndex - (isDir?0:1); /* only subtract 1 if it  is a file. */
     //  The number of directories we have to backtrack is the length of 
     //  the base path MINUS the number of common path elements, minus
     //  one because the last element in the path isn't a directory.
     for (int i = 1; i <= (numDirsUp); i++) {
         relative += ".." + pathSeparator;
     }
 }
 //if we are comparing directories then we 
 if (targetPath.length() > common.length()) {
  //it's OK, it isn't a directory
  relative += targetPath.substring(common.length());
 }

 return relative;
}

2

Я припускаю, що ви маєте fromPath (абсолютний шлях для папки), і toPath (абсолютний шлях для папки / файлу), і ви шукаєте шлях, який представляє файл / папку в toPath як відносний шлях від fromPath (ваш поточний робочий каталог - fromPath ), то щось подібне має працювати:

public static String getRelativePath(String fromPath, String toPath) {

  // This weirdness is because a separator of '/' messes with String.split()
  String regexCharacter = File.separator;
  if (File.separatorChar == '\\') {
    regexCharacter = "\\\\";
  }

  String[] fromSplit = fromPath.split(regexCharacter);
  String[] toSplit = toPath.split(regexCharacter);

  // Find the common path
  int common = 0;
  while (fromSplit[common].equals(toSplit[common])) {
    common++;
  }

  StringBuffer result = new StringBuffer(".");

  // Work your way up the FROM path to common ground
  for (int i = common; i < fromSplit.length; i++) {
    result.append(File.separatorChar).append("..");
  }

  // Work your way down the TO path
  for (int i = common; i < toSplit.length; i++) {
    result.append(File.separatorChar).append(toSplit[i]);
  }

  return result.toString();
}

1

Тут вже знайдено багато відповідей, але я виявив, що вони не розглядають усі випадки, такі як база та ціль однакові. Ця функція бере базовий каталог і цільовий шлях і повертає відносний шлях. Якщо відносного шляху не існує, цільовий шлях повертається. File.separator непотрібний.

public static String getRelativePath (String baseDir, String targetPath) {
    String[] base = baseDir.replace('\\', '/').split("\\/");
    targetPath = targetPath.replace('\\', '/');
    String[] target = targetPath.split("\\/");

    // Count common elements and their length.
    int commonCount = 0, commonLength = 0, maxCount = Math.min(target.length, base.length);
    while (commonCount < maxCount) {
        String targetElement = target[commonCount];
        if (!targetElement.equals(base[commonCount])) break;
        commonCount++;
        commonLength += targetElement.length() + 1; // Directory name length plus slash.
    }
    if (commonCount == 0) return targetPath; // No common path element.

    int targetLength = targetPath.length();
    int dirsUp = base.length - commonCount;
    StringBuffer relative = new StringBuffer(dirsUp * 3 + targetLength - commonLength + 1);
    for (int i = 0; i < dirsUp; i++)
        relative.append("../");
    if (commonLength < targetLength) relative.append(targetPath.substring(commonLength));
    return relative.toString();
}

0

Тут метод, який вирішує відносний шлях від базового шляху, незалежно від того, вони перебувають в одному або іншому корені:

public static String GetRelativePath(String path, String base){

    final String SEP = "/";

    // if base is not a directory -> return empty
    if (!base.endsWith(SEP)){
        return "";
    }

    // check if path is a file -> remove last "/" at the end of the method
    boolean isfile = !path.endsWith(SEP);

    // get URIs and split them by using the separator
    String a = "";
    String b = "";
    try {
        a = new File(base).getCanonicalFile().toURI().getPath();
        b = new File(path).getCanonicalFile().toURI().getPath();
    } catch (IOException e) {
        e.printStackTrace();
    }
    String[] basePaths = a.split(SEP);
    String[] otherPaths = b.split(SEP);

    // check common part
    int n = 0;
    for(; n < basePaths.length && n < otherPaths.length; n ++)
    {
        if( basePaths[n].equals(otherPaths[n]) == false )
            break;
    }

    // compose the new path
    StringBuffer tmp = new StringBuffer("");
    for(int m = n; m < basePaths.length; m ++)
        tmp.append(".."+SEP);
    for(int m = n; m < otherPaths.length; m ++)
    {
        tmp.append(otherPaths[m]);
        tmp.append(SEP);
    }

    // get path string
    String result = tmp.toString();

    // remove last "/" if path is a file
    if (isfile && result.endsWith(SEP)){
        result = result.substring(0,result.length()-1);
    }

    return result;
}

0

Проходить тести Донала, єдина зміна - якщо немає спільного кореня, він повертає цільовий шлях (він може бути вже відносним)

import static java.util.Arrays.asList;
import static java.util.Collections.nCopies;
import static org.apache.commons.io.FilenameUtils.normalizeNoEndSeparator;
import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
import static org.apache.commons.lang3.StringUtils.getCommonPrefix;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.apache.commons.lang3.StringUtils.join;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class ResourceUtils {

    public static String getRelativePath(String targetPath, String basePath, String pathSeparator) {
        File baseFile = new File(basePath);
        if (baseFile.isFile() || !baseFile.exists() && !basePath.endsWith("/") && !basePath.endsWith("\\"))
            basePath = baseFile.getParent();

        String target = separatorsToUnix(normalizeNoEndSeparator(targetPath));
        String base = separatorsToUnix(normalizeNoEndSeparator(basePath));

        String commonPrefix = getCommonPrefix(target, base);
        if (isBlank(commonPrefix))
            return targetPath.replaceAll("/", pathSeparator);

        target = target.replaceFirst(commonPrefix, "");
        base = base.replaceFirst(commonPrefix, "");

        List<String> result = new ArrayList<>();
        if (isNotEmpty(base))
            result.addAll(nCopies(base.split("/").length, ".."));
        result.addAll(asList(target.replaceFirst("^/", "").split("/")));

        return join(result, pathSeparator);
    }
}


0

Якщо Шляхи недоступні для JRE 1.5 або підключення Maven

package org.afc.util;

import java.io.File;
import java.util.LinkedList;
import java.util.List;

public class FileUtil {

    public static String getRelativePath(String basePath, String filePath)  {
        return getRelativePath(new File(basePath), new File(filePath));
    }

    public static String getRelativePath(File base, File file)  {

        List<String> bases = new LinkedList<String>();
        bases.add(0, base.getName());
        for (File parent = base.getParentFile(); parent != null; parent = parent.getParentFile()) {
            bases.add(0, parent.getName());
        }

        List<String> files = new LinkedList<String>();
        files.add(0, file.getName());
        for (File parent = file.getParentFile(); parent != null; parent = parent.getParentFile()) {
            files.add(0, parent.getName());
        }

        int overlapIndex = 0;
        while (overlapIndex < bases.size() && overlapIndex < files.size() && bases.get(overlapIndex).equals(files.get(overlapIndex))) {
            overlapIndex++;
        }

        StringBuilder relativePath = new StringBuilder();
        for (int i = overlapIndex; i < bases.size(); i++) {
            relativePath.append("..").append(File.separatorChar);
        }

        for (int i = overlapIndex; i < files.size(); i++) {
            relativePath.append(files.get(i)).append(File.separatorChar);
        }

        relativePath.deleteCharAt(relativePath.length() - 1);
        return relativePath.toString();
    }

}


-1
private String relative(String left, String right){
    String[] lefts = left.split("/");
    String[] rights = right.split("/");
    int min = Math.min(lefts.length, rights.length);
    int commonIdx = -1;
    for(int i = 0; i < min; i++){
        if(commonIdx < 0 && !lefts[i].equals(rights[i])){
            commonIdx = i - 1;
            break;
        }
    }
    if(commonIdx < 0){
        return null;
    }
    StringBuilder sb = new StringBuilder(Math.max(left.length(), right.length()));
    sb.append(left).append("/");
    for(int i = commonIdx + 1; i < lefts.length;i++){
        sb.append("../");
    }
    for(int i = commonIdx + 1; i < rights.length;i++){
        sb.append(rights[i]).append("/");
    }

    return sb.deleteCharAt(sb.length() -1).toString();
}

-2

Псуедо-код:

  1. Розділіть рядки по сепаратору шляху ("/")
  2. Знайдіть найбільший загальний шлях шляхом повторення результату розділеного рядка (щоб у ваших двох прикладах було "/ var / data" або "/ a")
  3. return "." + whicheverPathIsLonger.substring(commonPath.length);

2
Ця відповідь в кращому випадку є злом. А що з вікнами?
Qix - МОНІКА ПОМИЛИЛА
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.