From c35ccde68215c24b1d7a4f35fa34d019c0772e1b Mon Sep 17 00:00:00 2001 From: Martin Carlberg Date: Fri, 11 May 2012 15:32:18 +0200 Subject: [PATCH] Added stringByAppendingPathComponent: and stringByAppendingPathExtension: to CPString. Also, changed CPString path behaviour to correspond to Cocoa behaviour. --- Foundation/CPString.j | 120 ++++++++++++++++++++++++++++---- Tests/Foundation/CPStringTest.j | 91 ++++++++++++++++++++++-- 2 files changed, 193 insertions(+), 18 deletions(-) diff --git a/Foundation/CPString.j b/Foundation/CPString.j index 6b8779ccdd..ae04b9ad84 100644 --- a/Foundation/CPString.j +++ b/Foundation/CPString.j @@ -706,14 +706,67 @@ var CPStringRegexSpecialCharacters = [ Returns an the path components of this string. This method assumes that the string's content is a '/' separated file system path. + Multiple '/' separators between components are truncated to a single one. */ - (CPArray)pathComponents { + if (length === 0) return [""]; + if (self === "/") return ["/"]; var result = split('/'); if (result[0] === "") result[0] = "/"; - if (result[result.length - 1] === "") - result.pop(); + var index = result.length - 1; + if (index > 0) + { + if (result[index] === "") + result[index] = "/"; + while (index--) + { + while (result[index] === "") + { + result.splice(index--, 1); + } + } + } + return result; +} + +/*! + Returns a string built from the strings in a given array by + concatenating them with a path separator between each pair. + This method assumes that the string's content is a '/' + separated file system path. + Multiple '/' separators between components are truncated to a single one. +*/ ++ (CPString)pathWithComponents:(CPArray)components +{ + var size = components.length, + result = "", + i = -1, + firstRound = true, + firstIsSlash = false; + + while (++i < size) + { + var component = components[i], + lenMinusOne = component.length - 1; + if (lenMinusOne >= 0 && (component !== "/" || firstRound)) // Skip "" and "/" (not first time) + { + if (lenMinusOne > 0 && component.indexOf("/",lenMinusOne) === lenMinusOne) // Ends with "/" + component = component.substring(0, lenMinusOne); + if (firstRound) + { + if (component === "/") + firstIsSlash = true; + firstRound = false; + } + else if (!firstIsSlash) + result += "/"; + else + firstIsSlash = false; + result += component; + } + } return result; } @@ -737,33 +790,72 @@ var CPStringRegexSpecialCharacters = [ */ - (CPString)lastPathComponent { - var components = [self pathComponents]; - return components[components.length - 1]; + var components = [self pathComponents], + lastIndex = components.length - 1, + lastComponent = components[lastIndex]; + return lastIndex > 0 && lastComponent === "/" ? components[lastIndex - 1] : lastComponent; } /*! - Deletes the last path component of a string. + Returns a new string made by appending to the receiver a given string This method assumes that the string's content is a '/' separated file system path. + Multiple '/' separators between components are truncated to a single one. */ -- (CPString)stringByDeletingLastPathComponent +- (CPString)stringByAppendingPathComponent:(CPString)aString +{ + var components = [self pathComponents], + addComponents = aString && aString !== "/" ? [aString pathComponents] : []; + return [CPString pathWithComponents:components.concat(addComponents)]; +} + +/*! + Returns a new string made by appending to the receiver an extension separator followed by a given extension + This method assumes that the extension separator is a '.' + Extension can't include a '/' character, receiver can't be empty or be just a '/'. If so the + result will be the receiver itself. + Multiple '/' separators between components are truncated to a single one. +*/ +- (CPString)stringByAppendingPathExtension:(CPString)ext { - var path = self, - start = length - 1; + if (ext.indexOf('/') >= 0 || length === 0 || self === "/") // Can't handle these + return self; + var components = [self pathComponents], + last = components.length - 1; + + if (last > 0 && components[last] === "/") + components.splice(last--, 1); + + components[last] = components[last] + "." + ext; - while (path.charAt(start) === '/') - start--; + return [CPString pathWithComponents:components]; +} - path = path.substr(0, path.lastIndexOf('/', start)); +/*! + Deletes the last path component of a string. + This method assumes that the string's content is a '/' + separated file system path. + Multiple '/' separators between components are truncated to a single one. +*/ +- (CPString)stringByDeletingLastPathComponent +{ + if (length === 0) return ""; + if (self === "/") return "/"; + var components = [self pathComponents], + last = components.length - 1; - if (path === "" && charAt(0) === '/') - return '/'; + if (components[last] === "/") + last--; + components.splice(last, components.length - last); - return path; + return [CPString pathWithComponents:components]; } /*! Deletes the extension of a string. + This method assumes that the string's content is a '/' + separated file system path. + Multiple '/' separators between components are truncated to a single one. */ - (CPString)stringByDeletingPathExtension { diff --git a/Tests/Foundation/CPStringTest.j b/Tests/Foundation/CPStringTest.j index 678a560ced..67f06f2990 100644 --- a/Tests/Foundation/CPStringTest.j +++ b/Tests/Foundation/CPStringTest.j @@ -225,6 +225,52 @@ [self assert:"ffffff" equals:[CPString stringWithHash:16777215]]; } +- (void)testStringByAppendingPathComponent +{ + var testStrings = [ + ["/tmp/", "scratch.tiff", "/tmp/scratch.tiff"], + ["/tmp///", "scratch.tiff", "/tmp/scratch.tiff"], + ["/tmp///", "///scratch.tiff", "/tmp/scratch.tiff"], + ["/tmp", "scratch.tiff", "/tmp/scratch.tiff"], + ["/tmp///", "scratch.tiff", "/tmp/scratch.tiff"], + ["/tmp///", "///scratch.tiff", "/tmp/scratch.tiff"], + ["/", "scratch.tiff", "/scratch.tiff"], + ["", "scratch.tiff", "scratch.tiff"], + ["", "", ""], + ["", "/", ""], + ["/", "/", "/"], + ["/tmp", nil, "/tmp"], + ["/tmp", "/", "/tmp"], + ["/tmp/", "", "/tmp"] + ]; + + for (var i = 0; i < testStrings.length; i++) + { + var result = [testStrings[i][0] stringByAppendingPathComponent:testStrings[i][1]]; + + [self assertTrue:result === testStrings[i][2] message:"Value <" + testStrings[i][0] + "> Adding <" + testStrings[i][1] + "> Expected <" + testStrings[i][2] + "> was <" + result + ">"]; + } +} + +- (void)testStringByAppendingPathExtension +{ + var testStrings = [ + ["/tmp/scratch.old", "tiff", "/tmp/scratch.old.tiff"], + ["/tmp/scratch.", "tiff", "/tmp/scratch..tiff"], + ["/tmp///", "tiff", "/tmp.tiff"], + ["scratch", "tiff", "scratch.tiff"], + ["/", "tiff", "/"], + ["", "tiff", ""] + ]; + + for (var i = 0; i < testStrings.length; i++) + { + var result = [testStrings[i][0] stringByAppendingPathExtension:testStrings[i][1]]; + + [self assertTrue:result === testStrings[i][2] message:"Value <" + testStrings[i][0] + "> Adding <" + testStrings[i][1] + "> Expected <" + testStrings[i][2] + "> was <" + result + ">"]; + } +} + - (void)testStringByDeletingLastPathComponent { var testStrings = [ @@ -235,22 +281,58 @@ ["/", "/"], ["scratch.tiff", ""], ["a/b/c/d//////", "a/b/c"], + ["a/b/////////c/d//////", "a/b/c"], ["a/b/././././c/d/./././", "a/b/././././c/d/./."], [@"a/b/././././d////", "a/b/./././."], [@"~/a", "~"], [@"~/a/", "~"], - [@"../../", ".."] + [@"../../", ".."], + [@"", ""] ]; for (var i = 0; i < testStrings.length; i++) - [self assert:[testStrings[i][0] stringByDeletingLastPathComponent] equals:testStrings[i][1]]; + { + var result = [testStrings[i][0] stringByDeletingLastPathComponent]; + + [self assertTrue:result === testStrings[i][1] message:"Value <" + testStrings[i][0] + "> Expected <" + testStrings[i][1] + "> was <" + result + ">"]; + } +} + +- (void)testPathWithComponents +{ + var testStrings = [ + [["tmp", "scratch"], "tmp/scratch"], + [["/", "tmp", "scratch"], "/tmp/scratch"], + [["/", "tmp", "/", "scratch"], "/tmp/scratch"], + [["/", "tmp", "scratch", "/"], "/tmp/scratch"], + [["/", "tmp", "scratch", ""], "/tmp/scratch"], + [["", "/tmp", "scratch", ""], "/tmp/scratch"], + [["", "tmp", "scratch", ""], "tmp/scratch"], + [["/"], "/"], + [["/", "/", "/"], "/"], + [["", "", ""], ""], + [[""], ""] + ]; + + for (var i = 0; i < testStrings.length; i++) + { + var result = [CPString pathWithComponents:testStrings[i][0]]; + + [self assertTrue:result === testStrings[i][1] message:"Value <" + testStrings[i][0] + "> Expected [" + testStrings[i][1] + "] was [" + result + "]"]; + } } - (void)testPathComponents { var testStrings = [ ["tmp/scratch", ["tmp", "scratch"]], - ["/tmp/scratch", ["/", "tmp", "scratch"]] + ["/tmp/scratch", ["/", "tmp", "scratch"]], + ["/tmp/scratch/", ["/", "tmp", "scratch", "/"]], + ["/tmp/", ["/", "tmp", "/"]], + ["/////tmp/////scratch///", ["/", "tmp", "scratch", "/"]], + ["scratch.tiff", ["scratch.tiff"]], + ["/", ["/"]], + ["", [""]] ]; for (var i = 0; i < testStrings.length; i++) @@ -268,7 +350,8 @@ ["/tmp/scratch", "scratch"], ["/tmp/", "tmp"], ["scratch", "scratch"], - ["/", "/"] + ["/", "/"], + ["", ""] ]; for (var i = 0; i < testStrings.length; i++)