Swift equivalent of Ruby's "Pathname.relative_path_from"
There is no such method in the Swift standard library or in the Foundation framework, as far as I know.
Here is a possible implementation as an extension method of URL
:
extension URL {
func relativePath(from base: URL) -> String? {
// Ensure that both URLs represent files:
guard self.isFileURL && base.isFileURL else {
return nil
}
// Remove/replace "." and "..", make paths absolute:
let destComponents = self.standardized.pathComponents
let baseComponents = base.standardized.pathComponents
// Find number of common path components:
var i = 0
while i < destComponents.count && i < baseComponents.count
&& destComponents[i] == baseComponents[i] {
i += 1
}
// Build relative path:
var relComponents = Array(repeating: "..", count: baseComponents.count - i)
relComponents.append(contentsOf: destComponents[i...])
return relComponents.joined(separator: "/")
}
}
My test code:
func test(_ p1: String, _ p2: String) {
let u1 = URL(fileURLWithPath: p1)
let u2 = URL(fileURLWithPath: p2)
print(u1.relativePath(from: u2) ?? "<ERROR>")
}
test("/usr/X11/agent/47.gz", "/usr/X11") // "agent/47.gz"
test("/usr/share/man/meltdown.1", "/usr/share/cups") // "../man/meltdown.1"
test("/var/logs/x/y/z/log.txt", "/var/logs") // "x/y/z/log.txt"
Remarks:
- "." and ".." in the given URLs are removed, and relative file URLs
are made absolute (both using the
standardized
method ofURL
). - Case (in)sensitivity is not handled.
- It is assumed that the base URL represents a directory.
Addendum: @neoneye wrapped this into a Swift package: SwiftyRelativePath.
Martin R had the right answer. However, I had an issue when the base URL is a file itself. Hence, I did a bit of tweaking:
func relativePath(from base: URL) -> String? {
// Ensure that both URLs represent files:
guard self.isFileURL && base.isFileURL else {
return nil
}
//this is the new part, clearly, need to use workBase in lower part
var workBase = base
if workBase.pathExtension != "" {
workBase = workBase.deletingLastPathComponent()
}
// Remove/replace "." and "..", make paths absolute:
let destComponents = self.standardized.resolvingSymlinksInPath().pathComponents
let baseComponents = workBase.standardized.resolvingSymlinksInPath().pathComponents
// Find number of common path components:
var i = 0
while i < destComponents.count &&
i < baseComponents.count &&
destComponents[i] == baseComponents[i] {
i += 1
}
// Build relative path:
var relComponents = Array(repeating: "..", count: baseComponents.count - i)
relComponents.append(contentsOf: destComponents[i...])
return relComponents.joined(separator: "/")
}
My test case got a bit extended. Case 4 was my trigger for this small change.
func testRelativePath() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
func test(_ p1: String, _ p2: String,_ result: String,_ nr: Int) {
let u1 = URL(fileURLWithPath: p1)
let u2 = URL(fileURLWithPath: p2)
let r = u1.relativePath(from: u2)!
XCTAssert( r == result,"\(nr): '\(r)' != '\(result)'")
}
test("/usr/X11/agent/47.gz", "/usr/X11","agent/47.gz", 1)
test("/usr/share/man/meltdown.1", "/usr/share/cups", "../man/meltdown.1",2 )
test("/var/logs/x/y/z/log.txt", "/var/logs", "x/y/z/log.txt",3)
test("/usr/embedded.jpg", "/usr/main.html", "embedded.jpg",4)
test("/usr/embedded.jpg", "/usr", "embedded.jpg",5)
test("~/Downloads/resources", "~/", "Downloads/resources",6)
test("~/Downloads/embedded.jpg", "~/Downloads/main.html", "embedded.jpg",7)
test("/private/var/logs/x/y/z/log.txt", "/var/logs", "x/y/z/log.txt",8)
}