clojure

function that gives the relative path in clojure?


I need a function that when given a base directory and another path, I've done a simplified version that just matches the absolute path but was hoping to also be able to intelligently deal with '..' and '.' in the path. I'm not sure what the best approach is

some examples:

(relative-path "example" "example/hello") => "hello"

(relative-path "example" "../example/hello") => "hello"

(relative-path "example" "/usr/local") => "../../../usr/local"

Solution

  • I figured it out after a bit of trial and error:

    (require '[clojure.java.io :as io]
             '[clojure.string :as string])
    
    (defn interpret-dots
      ([v] (interpret-dots v []))
      ([v output]
         (if-let [s (first v)]
           (condp = s
             "."  (recur (next v) output)
             ".." (recur (next v) (pop output))
             (recur (next v) (conj output s)))
           output)))
    
    (defn drop-while-matching [u v]
      (cond (or (empty? u) (empty? v)) [u v]
    
            (= (first u) (first v))
            (recur (rest u) (rest v))
    
            :else [u v]))
    
    (defn path-vector [path]
      (string/split (.getAbsolutePath (io/file path))
                    (re-pattern (System/getProperty "file.separator"))))
    
    (defn relative-path [root other]
      (let [[base rel] (drop-while-matching (interpret-dots (path-vector root))
                                            (interpret-dots (path-vector other)))]
        (if (and (empty? base) (empty? rel))
          "."
          (->> (-> (count base)
                   (repeat "..")
                   (concat rel))
               (string/join (System/getProperty "file.separator")))))
    

    Usage:

    (relative-path "example/repack.advance/resources"
               "example/repack.advance/resources/eueueeueu/oeuoeu")
    ;;=> "eueueeueu/oeuoeu"
    
    (relative-path "example/repack.advance/resources"
               "/usr/local")
    ;;=> "../../../../../../../../usr/local"