フルパスから相対パスを求める

こんどは Scheme で。

(use srfi-13)

(define (init lst)
  (cond ((null? lst) (error "init: empty list"))
        ((null? (cdr lst)) '())
        (else
         (cons (car lst) (init (cdr lst))))))

(define (relative-path-list target base)
  (cond ((null? base) target)
        ((null? target) (map (lambda (x) "..") base))
        ((equal? (car base) (car target)) (relative-path-list (cdr target) (cdr base)))
        (else
         (append
          (map (lambda (x) "..") base)
          target))))

(define (valid-path? path)
  (and (> (string-length path) 0)
       (equal? (string-ref path 0) #\/)
       (not (string-index path #[\\?*:|"<>]))))

(define (relative-path target base)
  (if (and (valid-path? target) (valid-path? base))
      (let ((relative-path-components
             (relative-path-list
              (cdr (string-split target #/\/+/))
              (init (cdr (string-split base #/\/+/))))))
        (string-join
         (if (and (not (null? relative-path-components))
                  (equal? (car relative-path-components) ".."))
             relative-path-components
             (cons "." relative-path-components))
         "/"))
      (error "invalid path")))

(use gauche.test)
(test-start "relative-path")

(test* "a file in the same directory"
       "./to.txt"
       (relative-path "/aaa/bbb/to.txt" "/aaa/bbb/from.txt"))

(test* "a file in the parent directory"
       "../to.txt"
       (relative-path "/aaa/to.txt" "/aaa/bbb/from.txt"))

(test* "a file in the child directory"
       "./ccc/to.txt"
       (relative-path "/aaa/bbb/ccc/to.txt" "/aaa/bbb/from.txt"))

(test* "a file through the grandparent directory"
       "../ccc/ddd/to.txt"
       (relative-path "/aaa/ccc/ddd/to.txt" "/aaa/bbb/from.txt"))

(test* "a file through the root directory"
       "../../ddd/ccc/to.txt"
       (relative-path "/ddd/ccc/to.txt" "/aaa/bbb/from.txt"))

(test* "a file from the directory"
       "../ddd/to.txt"
       (relative-path "/aaa/ddd/to.txt" "/aaa/bbb/"))

(test* "a directory from the directory"
       "../ccc/"
       (relative-path "/aaa/ccc/" "/aaa/bbb/"))

(test* "the same path"
       "./ccc.txt"
       (relative-path "/aaa/bbb/ccc.txt" "/aaa/bbb/ccc.txt"))

(test* "passing empty path"
       *test-error*
       (relative-path "/bbb/to.txt" ""))

(test* "invalid character for filename"
       *test-error*
       (relative-path "/bbb/to.txt" "/aaa/g*"))

(test* "path does not start with slash"
       *test-error*
       (relative-path "./bbb/to.txt" "aaa/bbb/from.txt"))

(test* "consecutive slash"
       "./to.txt"
       (relative-path "/////aaa//////bbb///////to.txt"
                      "//aaa///bbb////from.txt"))

(test-end)