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\$
10
  • \$\begingroup\$ Regarding rule #3 - is a mixture ok? E.g. ../../vim\vim73\ftplugin. \$\endgroup\$ Jun 26, 2014 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, 2014 at 10:54
  • \$\begingroup\$ @Duncan Yes, a mix is ok. \$\endgroup\$
    – Rynant
    Jun 26, 2014 at 13:06
  • 1
    \$\begingroup\$ @Howard, it must be the shortest relative path. \$\endgroup\$
    – Rynant
    Jun 26, 2014 at 13:07
  • \$\begingroup\$ shouldn't the first example be ../vim/vim73/ftplugin? \$\endgroup\$
    – Martijn
    Jun 26, 2014 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\$
2
  • 1
    \$\begingroup\$ It has a bug. Try /aa/x with /ab/y. \$\endgroup\$
    – jimmy23013
    Jul 3, 2014 at 15:56
  • \$\begingroup\$ @user23013: Fixed. \$\endgroup\$
    – Dennis
    Jul 3, 2014 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\$
7
  • \$\begingroup\$ Looks interesting! Can you add an explanation? \$\endgroup\$ Jun 27, 2014 at 12:41
  • 1
    \$\begingroup\$ @DigitalTrauma Added. But basically they are just regular expressions. \$\endgroup\$
    – jimmy23013
    Jun 27, 2014 at 13:23
  • \$\begingroup\$ Thanks! I'm going to play with this next time I'm at a terminal \$\endgroup\$ Jun 27, 2014 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, 2014 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, 2014 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\$
2
  • \$\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, 2014 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, 2014 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\$
4
  • \$\begingroup\$ Might there be a shorter way to do line 1 with exec and string operations? \$\endgroup\$
    – xnor
    Jun 27, 2014 at 13:29
  • \$\begingroup\$ @xnor Maybe, but I can't see it. \$\endgroup\$
    – grc
    Jun 27, 2014 at 14:43
  • \$\begingroup\$ Might map(input,' ') work for `(input(),input())? (I can't test it myself) \$\endgroup\$
    – xnor
    Jun 27, 2014 at 14:50
  • \$\begingroup\$ @xnor Yeah that works thanks! \$\endgroup\$
    – grc
    Jun 27, 2014 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\$
6
  • 3
    \$\begingroup\$ This fails for arguments like /foo/bar/foo/bar and /foo/qux/foo/bar. \$\endgroup\$
    – Ventero
    Jun 26, 2014 at 9:27
  • \$\begingroup\$ And fails for windows style paths \$\endgroup\$
    – edc65
    Jun 26, 2014 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, 2014 at 18:49
  • \$\begingroup\$ @nderscore Rule 2 Both Windows and Unix style paths must be supported. \$\endgroup\$
    – edc65
    Jun 26, 2014 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, 2014 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\$
1
  • \$\begingroup\$ Your answer isn't correct. Try making this dirs and cd form one to another :) \$\endgroup\$
    – core1024
    Jul 3, 2014 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 and acknowledge you have read our privacy policy.

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