15
\$\begingroup\$

Description

Given a source path and a destination path, output the relative path to the destination with respect to the source.

Rules

  1. The input can come from stdin or as arguments to the program/function.

  2. Both Windows and Unix style paths must be supported.

  3. The output path can use / and/or \ for the path separator (your choice and combination of both is OK).

  4. You can assume a relative path is possible.

  5. The use of external programs, built-in or library functions made for computing relative paths is prohibited (e.g. Python's os.path.relpath)

  6. This is

    Edit: New rule from comments.

  7. The relative path must be the shortest relative path possible.

  8. Assume the destination path is different from the source path.

Example 1

# In
/usr/share/geany/colorschemes
/usr/share/vim/vim73/ftplugin

# Out
../../vim/vim73/ftplugin

Example 2

# In
C:\Windows\System32\drivers
C:\Windows\System32\WindowsPowerShell\v1.0

# Out
..\WindowsPowerShell\v1.0
\$\endgroup\$
  • \$\begingroup\$ Regarding rule #3 - is a mixture ok? E.g. ../../vim\vim73\ftplugin. \$\endgroup\$ – Duncan Jones Jun 26 '14 at 7:55
  • 1
    \$\begingroup\$ Do we have to return the shortest relative path or is it okay to yield any path? \$\endgroup\$ – Howard Jun 26 '14 at 10:54
  • \$\begingroup\$ @Duncan Yes, a mix is ok. \$\endgroup\$ – Rynant Jun 26 '14 at 13:06
  • 1
    \$\begingroup\$ @Howard, it must be the shortest relative path. \$\endgroup\$ – Rynant Jun 26 '14 at 13:07
  • \$\begingroup\$ shouldn't the first example be ../vim/vim73/ftplugin? \$\endgroup\$ – Martijn Jun 26 '14 at 13:39

12 Answers 12

2
\$\begingroup\$

CJam, 46 bytes

ll]{'\/'/f/:~}/W{)__3$=4$@==}g@,1$-"../"*o>'/*

Try it online.

Examples

$ echo '/usr/share/geany/colorschemes
> /usr/share/vim/vim73/ftplugin' | cjam path.cjam; echo
../../vim/vim73/ftplugin
$ echo 'C:\Windows\System32\drivers
> C:\Windows\System32\WindowsPowerShell\v1.0' | cjam path.cjam; echo
../WindowsPowerShell/v1.0

How it works

ll]         " Read two lines from STDIN and wrap them in an array.                       ";
{           " For each line:                                                             ";
  '\/       " Split by “\”.                                                              ";
  '/f/      " Split each chunk by “/”.                                                   ";
  :~        " Flatten the array of chunks.                                               ";
}/          "                                                                            ";
W           " Push -1 (accumulator).                                                     ";
{           "                                                                            ";
  )__       " Increment and duplicate twice.                                             ";
  3$=       " Extract the corresponding chunk from the first line.                       ";
  4$@=      " Extract the corresponding chunk from the second line.                      ";
  =         " If the are equal,                                                          ";
}g          " repeat the loop.                                                           ";
@,          " Rotate the array of chunks of the first line on top and get its length.    ";
1$-         " Subtract the value of the accumulator.                                     ";
"../"*o     " Print the string “../” repeated that many times.                           ";
>           " Remove all chunks with index less than the accumulator of the second line. ";
'/*         " Join the chunks with “/”.                                                  ";
\$\endgroup\$
  • 1
    \$\begingroup\$ It has a bug. Try /aa/x with /ab/y. \$\endgroup\$ – jimmy23013 Jul 3 '14 at 15:56
  • \$\begingroup\$ @user23013: Fixed. \$\endgroup\$ – Dennis Jul 3 '14 at 20:07
2
\$\begingroup\$

Bash + coreutils, 116

Here's a shell script to get the ball rolling. Pretty sure there'll be shorter answers:

n=`cmp <(echo $1) <(echo $2)|grep -Po "\d+(?=,)"`
printf -vs %`grep -o /<<<${1:n-1}|wc -l`s
echo ${s// /../}${2:n-1}

Output:

$ ./rel.sh /usr/share/geany/colorschemes /usr/share/vim/vim73/ftplugin
../vim/vim73/ftplugin
$ ./rel.sh /usr/share/geany/colorschemes/ /usr/share/vim/vim73/ftplugin/
../../vim/vim73/ftplugin/
$ ./rel.sh /usr/share/vim/vim73/ftplugin /usr/share/geany/colorschemes
../../geany/colorschemes
$ 

Note there is no way for the script to tell if the string ftplugin is a file or a directory. You may explicitly provide a directory by appending it with an / as in the example above.

Won't handle paths containing whitespace or other funny characters. Not sure if that is a requirement or not. Just a few extra quotes would be needed.

\$\endgroup\$
2
\$\begingroup\$

Javascript (E6) 104

Edit Added alert for output

R=(s,d)=>alert(s.split(x=/\/|\\/).map(a=>a==d[0]?d.shift()&&'':'../',d=d.split(x)).join('')+d.join('/'))

Ungolfed

R (s,d) => // a single espression is returned, no {} or () needed
  s.split(x=/\/|\\/) // split string at / or \, save regexp in X for later
  .map( // create a new array from s
     a => a == d[0] // check if current of s and d equals
          ? d.shift() && '' // map to '' and cut 1 element of d
          : '../', // else map to '../'
     d=d.split(x)) // second param of map is useless, so split d here
  .join('')+d.join('/') // join map and concat to rest of d adding separators

Test

R('C:\\Windows\\System32\\drivers','C:\\Windows\\System32\\WindowsPowerShell\\v1.0')

../WindowsPowerShell/v1.0

R('/usr/share/geany/colorschemes','/usr/share/vim/vim73/ftplugin')

../../vim/vim73/ftplugin

\$\endgroup\$
2
\$\begingroup\$

Ruby >= 1.9, 89 94 characters

$;=/\\|\//
a,b=$*.map &:split
puts"../"*(a.size-r=a.index{a[$.+=1]!=b[$.]}+1)+b[r..-1]*?/

Input via command line arguments. Works for both UNIX- and Windows-style paths, including paths with repeated folder names:

$ ruby relpath.rb /usr/share/geany/colorschemes /usr/share/vim/vim73/ftplugin
../../vim/vim73/ftplugin
$ ruby relpath.rb 'C:\Windows\System32\drivers' 'C:\Windows\System32\WindowsPowerShell\v1.0'
../WindowsPowerShell/v1.0
$ ruby relpath.rb /foo/bar/foo/bar /foo/qux/foo/bar
../../../qux/foo/bar
\$\endgroup\$
2
\$\begingroup\$

J - 63 char

A function taking the old path on the left and the new path on the right.

}.@;@(c=.c&}.`(,~(<'/..')"0)@.(~:&{.))&('/'<;.1@,'\/'&charsub)~

This solution comes in three parts, looking like post@loop&pre~. Explained by explosion:

post @ loop & pre ~   NB. the full golf
                  ~   NB. swap the arguments: new on left, old on right
            & pre     NB. apply pre to each argument
       loop           NB. run the recursive loop on both
post @                NB. apply post to the final result

'/'<;.1@,'\/'&charsub  NB. pre
         '\/'&charsub  NB. replace every \ char with /
'/'     ,              NB. prepend a / char
   <;.1@               NB. split string on the first char (/)

c=.c&}.`(,~(<'/..')"0)@.(~:&{.)  NB. loop
                      @.(~:&{.)  NB. if the top folders match:
    &}.                          NB.   chop off the top folders
   c                             NB.   recurse
       `                         NB. else:
           (<'/..')"0            NB.   change remaining old folders to /..
         ,~                      NB.   append to front of remaining new folders
c=.                              NB. call this loop c to recurse later

}.@;  NB. post
   ;  NB. turn the list of folders into a string
}.@   NB. chop off the / in the front

Note that we add a leading / to each path before splitting, so that we handle Windows-style paths by making C: into a "folder". This results in an empty folder at the start of Unix-style paths, but that always gets removed by the loop.

See it in action:

   NB. you can use it without a name if you want, we will for brevity
   relpath =. }.@;@(c=.c&}.`(,~(<'/..')"0)@.(~:&{.))&('/'<;.1@,'\/'&charsub)~
   '/usr/share/geany/colorschemes' relpath '/usr/share/vim/vim73/ftplugin'
../../vim/vim73/ftplugin
   'C:\Windows\System32\drivers' relpath 'C:\Windows\System32\WindowsPowerShell\v1.0'
../WindowsPowerShell/v1.0

You can also try it yourself at tryj.tk.

\$\endgroup\$
2
\$\begingroup\$

Bash, 69 66

I didn't post this one because I thought someone must be able to do it much better. But apparently it is not that easy.

sed -r 'N;s/(.*[/\])(.*)\n\1/\2\n/'|sed '1s/[^/\]*/../g;N;s!\n!/!'

N makes sed match two lines together. The first expression removes the common prefix ending with / or \. The second expression replaces directory names with .. in the first line. Finally it concatenates the two lines with the separator.

Thanks to Hasturkun for 3 characters.

\$\endgroup\$
  • \$\begingroup\$ Looks interesting! Can you add an explanation? \$\endgroup\$ – Digital Trauma Jun 27 '14 at 12:41
  • 1
    \$\begingroup\$ @DigitalTrauma Added. But basically they are just regular expressions. \$\endgroup\$ – jimmy23013 Jun 27 '14 at 13:23
  • \$\begingroup\$ Thanks! I'm going to play with this next time I'm at a terminal \$\endgroup\$ – Digital Trauma Jun 27 '14 at 13:27
  • \$\begingroup\$ You don't really need to run sed twice, you can do this with a single script. \$\endgroup\$ – Hasturkun Jul 2 '14 at 12:36
  • \$\begingroup\$ @Hasturkun But I didn't find a way to have it work with N. Maybe you can edit this answer if you know how. \$\endgroup\$ – jimmy23013 Jul 2 '14 at 14:50
1
\$\begingroup\$

C, 119 106

void p(char*s,char* d){while(*s==*d){s++;d++;}s--;while(*s){if(*s==47||*s==92)printf("../");s++;}puts(d);}
\$\endgroup\$
  • \$\begingroup\$ p(char*s,char*d){for(;*s;)*s++-*d?*s-47||printf("../"):d++;puts(d);} 68 chars w/o backslash \$\endgroup\$ – bebe Jun 27 '14 at 11:59
  • \$\begingroup\$ Thanks! But rule 2 says both must be supported. It is in the output where I can pick one or the other(rule 3). \$\endgroup\$ – kwokkie Jun 27 '14 at 12:43
1
\$\begingroup\$

Python 3, 120

a,b=(i.split('\\/'['/'in i])for i in map(input,'  '))
while[]<a[:1]==b[:1]:del a[0],b[0]
print('../'*len(a)+'/'.join(b))

Example:

$ python3 path.py
 /usr/share/geany/colorschemes
/usr/share/vim/vim73/ftplugin 
../../vim/vim73/ftplugin
\$\endgroup\$
  • \$\begingroup\$ Might there be a shorter way to do line 1 with exec and string operations? \$\endgroup\$ – xnor Jun 27 '14 at 13:29
  • \$\begingroup\$ @xnor Maybe, but I can't see it. \$\endgroup\$ – grc Jun 27 '14 at 14:43
  • \$\begingroup\$ Might map(input,' ') work for `(input(),input())? (I can't test it myself) \$\endgroup\$ – xnor Jun 27 '14 at 14:50
  • \$\begingroup\$ @xnor Yeah that works thanks! \$\endgroup\$ – grc Jun 27 '14 at 15:00
1
\$\begingroup\$

Ruby - 89

r=/\/|\\/
s = ARGV[0].split r
d = ARGV[1].split r
puts ("../"*(s-d).size)+((d-s).join"/")

Usage:

ruby relative.rb working/directory destination/directory
\$\endgroup\$
  • 3
    \$\begingroup\$ This fails for arguments like /foo/bar/foo/bar and /foo/qux/foo/bar. \$\endgroup\$ – Ventero Jun 26 '14 at 9:27
  • \$\begingroup\$ And fails for windows style paths \$\endgroup\$ – edc65 Jun 26 '14 at 11:01
  • \$\begingroup\$ @edc65 The rules don't say it's necessary to support both path formats, you can do either one. \$\endgroup\$ – nderscore Jun 26 '14 at 18:49
  • \$\begingroup\$ @nderscore Rule 2 Both Windows and Unix style paths must be supported. \$\endgroup\$ – edc65 Jun 26 '14 at 18:51
  • 1
    \$\begingroup\$ @Jwosty: Well, that's the beauty, isn't it? Coming up with a solution that's both short and correct. I have in the past had cases where I had to revise the answer completely because of an overlooked edge case. Now, in this case I do put the blame partly on the task as well, because I believe a solid set of test cases should accompany every task, but well. \$\endgroup\$ – Joey Jun 28 '14 at 9:54
0
\$\begingroup\$

JavaScript - 155

function p(s,d){s=s.split(/\\|\//g);d=d.split(/\\|\//g);o=[];for(i=0;s[i]==d[i];i++);for(j=s.length-i;j--;)o[j]="..";return o.concat(d.slice(i)).join("/")}

Parses either path format but outputs with / separator.

console.log(p("/usr/share/geany/colorschemes","/usr/share/vim/vim73/ftplugin"));
../../vim/vim73/ftplugin
console.log(p("/usr/share/geany/colorschemes/test/this","/usr/share/vim/vim73/ftplugin/this/is/a/test"));
../../../../vim/vim73/ftplugin/this/is/a/test
console.log(p("c:\\windows\\system32\\drivers\\etc\\host","c:\\windows\\system\\drivers\\etc\host"));
../../../../system/drivers/etchost
\$\endgroup\$
0
\$\begingroup\$

PHP, 158 151

function r($a,$b){$a=explode('/',$a);$b=explode('/',$b);foreach($a as $k=>$v){if($v==$b[$k])$b[$k]='..';else break;}unset($b[0]);echo implode('/',$b);}

Ungolfed:

function r($a,$b){
    $a=explode('/',$a);
    $b=explode('/',$b);
    foreach($a as $k=>$v){
        if($v==$b[$k])$b[$k]='..';
        else break; 
    }
    unset($b[0]);
    echo implode('/',$b);
}
// these lines are not included in count:
r('/test/test2/abc','/test/test3/abcd'); // ../test3/abcd
r('/test/test2/abc','/test/test2/abcd'); // ../../abcd
\$\endgroup\$
  • \$\begingroup\$ Your answer isn't correct. Try making this dirs and cd form one to another :) \$\endgroup\$ – core1024 Jul 3 '14 at 16:15
0
\$\begingroup\$

Groovy - 144 chars

One solution:

x=args[0][1]!=':'?'/':'\\'
f={args[it].tokenize x}
s=f 0
d=f 1
n=0
while(s[n]==d[n++]);
u="..$x"*(s.size-(--n))
println "$u${d.drop(n).join x}"

example output:

bash$ groovy P.groovy C:\\Windows\\System32\\drivers C:\\Windows\\System32\\WindowsPowerShell\\v1.0
..\WindowsPowerShell\v1.0

bash$ groovy P.groovy /usr/share/geany/colorschemes /usr/share/vim/vim73/ftplugin
../../vim/vim73/ftplugin

bash$ groovy P.groovy /foo/bar/foo/bar /foo/qux/foo/bar
../../../qux/foo/bar

ungolfed:

// fs = file seperator, / or \
fs = args[0][1]!=':'?'/':'\\'

s = args[0].tokenize fs
d = args[1].tokenize fs

// n = # of matching dirs from root + 1
n = 0
while (s[n] == d[n++]) ;

// up = the up-prefix. e.g. "../../..", for instance 
n--
up = "..${fs}" * (s.size-n)

println "$up${d.drop(n).join fs}"
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.