I'm trying to get an understanding of some code I came across recently.
In an answer to a question here https://stackoverflow.com/a/51173170/1162328, the author made use of a String with a format specifier when looping over files in the documentDirectory. Can anyone shed some light on what %#/%# is actually doing?
for fileName in fileNames {
let tempPath = String(format: "%#/%#", path, fileName)
// Check for specific file which you don't want to delete. For me .sqlite files
if !tempPath.contains(".sql") {
try fileManager.removeItem(atPath: tempPath)
}
}
Reading the Apple documentation archive for Formatting Basics I came across this:
In format strings, a ‘%’ character announces a placeholder for a value, with the characters that follow determining the kind of value expected and how to format it. For example, a format string of "%d houses" expects an integer value to be substituted for the format expression '%d'. NSString supports the format characters defined for the ANSI C functionprintf(), plus ‘#’ for any object.
What exactly then, is %#/%# doing?
Each format specifier is replaced by one of the following arguments (usually in the same order, although that can be controlled with positional arguments). So in your case, the first %# is replaced by path and the second %# is replaced by fileName. Example:
let path = "/path/to/dir"
let fileName = "foo.txt"
let tempPath = String(format: "%#/%#", path, fileName)
print(tempPath) // /path/to/dir/foo.txt
The preferred way to build file names and paths is to use the corresponding URL methods instead of string manipulation. Example:
let pathURL = URL(fileURLWithPath: path)
let tempURL = pathURL.appendingPathComponent(fileName)
if tempURL.pathExtension != "sql" {
try FileManager.default.removeItem(at: tempURL)
}
%# is something similar to %d or anything like that. This is the way of string interpolation in Swift.
To be exact %# is placeholder for object - used in Objective-C A LOT. Since NSString * was object (now it is only String), it was used to insert NSString * into another NSString *.
Also given code is just rewritten objective-c code which was something like
NSString *tempPath = [NSString stringWithFormat:#"%#/%#", path, filename];
which can be rewritten in swift:
let tempPath = path + "/" + fileName
Also, given path = "Test" and fileName = "great" will give output Test/great.
One more note: %# is as good as dangerous. You can put UITableView as well as String in it. It will use description property for inserting into string.
Related
I'm loading a lot of image paths into a NSURL.
The images are in a folder ordered from 1.PNG, 2.PNG, 3.PNG to 1500.PNG. When I trie to load them:
let imagePath = path + "/images"
let url = NSURL(fileURLWithPath: imagePath)
print(url)
let fileManager = NSFileManager.defaultManager()
let properties = [NSURLLocalizedLabelKey,
NSURLCreationDateKey, NSURLLocalizedTypeDescriptionKey]
do {
imageURLs = try fileManager.contentsOfDirectoryAtURL(url, includingPropertiesForKeys: properties, options:NSDirectoryEnumerationOptions.SkipsHiddenFiles)
} catch let error1 as NSError {
print(error1.description)
}
The imageURLs array gets filled with:
imageURLs[0] = ...\0.PNG
imageURLs[1] = ...\1.PNG
imageURLs[2] = ...\100.PNG
imageURLs[3] = ...\1000.PNG
and not in the numeric order!
Can someone help to sort the imageURLs or while i load the image paths on it or after they are loaded?
As you want to sort the files by the number you have to parse first the path to achieve it, so let's suppose we have the following array of NSURL objects:
var urls = [NSURL(string: "file:///path/to/user/folder/2.PNG")!, NSURL(string: "file:///path/to/user/folder/100.PNG")!, NSURL(string: "file:///path/to/user/folder/101.PNG")!, NSURL(string: "file:///path/to/user/folder/1.PNG")! ]
We can use the pathComponents property to extract an array with all the components in the path for a NSURL (e.g ["/", "path", "to", "user", "folder", "2.PNG"]).
If we see we can order the files by the last element in the array that is the filename removing the extension and the dot("."), in this case the number. Let's see how to do it in the following code:
urls.sortInPlace {
// number of elements in each array
let c1 = $0.pathComponents!.count - 1
let c2 = $1.pathComponents!.count - 1
// the filename of each file
var v1 = $0.pathComponents![c1].componentsSeparatedByString(".")
var v2 = $1.pathComponents![c2].componentsSeparatedByString(".")
return Int(v1[0]) < Int(v2[0])
}
In the above code we use the function sortInPlace to avoid create another array with the elements sorted, but can you use sort instead if you want. The another important point in the code is the line return Int(v1[0]) < Int(v2[0]), in this line we have to convert the number in the string to a real number, because if we compare the two strings "2" and "100" the second one is less than greater than because the string are compared lexicographically.
So the the array urls should be like the following one:
[file:///path/to/user/folder/1.PNG, file:///path/to/user/folder/2.PNG, file:///path/to/user/folder/100.PNG, file:///path/to/user/folder/101.PNG]
EDIT:
The two functions pathComponents and componentsSeparatedByString increase the space complexity of the sortInPlace algorithm, if you can asure that the path for the files always will be the same except it's filename that should be a number you can use instead this code:
urls.sortInPlace { $0.absoluteString.compare(
$1.absoluteString, options: .NumericSearch) == .OrderedAscending
}
I hope this help you.
Its a bit late, anyway this worked for me in Swift 5
imageURLs.sort {
($0.pathComponents.last?.components(separatedBy: ".").first)! < ($1.pathComponents.last?.components(separatedBy: ".").first)!
}
This takes path components from url(separated by /), then take the last part and take components of it ( separated by . ). Then take first part and compare with the other urls similarly.
Complexity: O(n log n), where n is the length of the collection[ as per Apple doc]
I'm using NSURLComponents and I can't seem to get the query values to encode correctly. I need the final URL to represent a + as %2B.
let baseUrl = NSURL(string: "http://www.example.com")
let components = NSURLComponents(URL: baseUrl, resolvingAgainstBaseURL: true)
components.queryItems = [ NSURLQueryItem(name: "name", value: "abc+def") ]
XCTAssertEqual(components!.string!, "http://www.example.com?connectionToken=abc%2Bdef")
Failed!
Output equals:
http://www.example.com?connectionToken=abc+def
NOT
http://www.example.com?connectionToken=abc%2Bdef
I've tried several variations and I just can't seem to get it to output %2B at all.
My answer from Radar 24076063 with an explanation of why it works the way it does (with a little cleanup of the text):
The '+' character is legal in the query component so it does not need to be percent-encoded.
Some systems use the '+' as a space and require '+' the plus character to be percent-encoded. However, that kind of two stage encoding (converting plus sign to %2B and then converting space to plus sign) is prone to errors because it easily leads to encoding problems. It also breaks if the URL is normalized (syntax normalization of URLs includes the removal of all unnecessary percent-encoding — see rfc3986 section 6.2.2.2).
So, if you need that behavior because of the server your code is talking to, you'll handle the extra transformation(s) yourself. Here's a snippet of code that shows what you need to do both ways:
NSURLComponents *components = [[NSURLComponents alloc] init];
NSArray *items = [NSArray arrayWithObjects:[NSURLQueryItem queryItemWithName:#"name" value:#"Value +"], nil];
components.queryItems = items;
NSLog(#"URL queryItems: %#", [components queryItems]);
NSLog(#"URL string before: %#", [components string]);
// Replace all "+" in the percentEncodedQuery with "%2B" (a percent-encoded +) and then replace all "%20" (a percent-encoded space) with "+"
components.percentEncodedQuery = [[components.percentEncodedQuery stringByReplacingOccurrencesOfString:#"+" withString:#"%2B"] stringByReplacingOccurrencesOfString:#"%20" withString:#"+"];
NSLog(#"URL string after: %#", [components string]);
// This is the reverse if you receive a URL with a query in that form and want to parse it with queryItems
components.percentEncodedQuery = [[components.percentEncodedQuery stringByReplacingOccurrencesOfString:#"+" withString:#"%20"] stringByReplacingOccurrencesOfString:#"%2B" withString:#"+"];
NSLog(#"URL string back: %#", [components string]);
NSLog(#"URL queryItems: %#", [components queryItems]);
The output is:
URL queryItems: (
"<NSURLQueryItem 0x100502460> {name = name, value = Value +}"
)
URL string before: ?name=Value%20+
URL string after: ?name=Value+%2B
URL string back: ?name=Value%20+
URL queryItems: (
"<NSURLQueryItem 0x1002073e0> {name = name, value = Value +}"
)
As the other answers mention, "+" isn't encoded on iOS by default. But if your server requires that to be encoded, here's how to do it:
var comps = URLComponents(url: self, resolvingAgainstBaseURL: true)
// a local var is needed to fix a swift warning about "overlapping accesses" caused by writing to the same property that's being read.
var compsCopy = comps
compsCopy?.queryItems = [URLQueryItem(name: "name", value: "abc+def")]
comps?.percentEncodedQuery = compsCopy?.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
+ may be a valid character when the content-type is application/x-www-form-urlencoded, see the link, so NSURLComponents doesn't encode it.
Apple also mention this:
RFC 3986 specifies which characters must be percent-encoded in the
query component of a URL, but not how those characters should be
interpreted. The use of delimited key-value pairs is a common
convention, but isn't standardized by a specification. Therefore, you
may encounter interoperability problems with other implementations
that follow this convention.
One notable example of potential interoperability problems is how the
plus sign (+) character is handled:
According to RFC 3986, the plus sign is a valid character within a
query, and doesn't need to be percent-encoded. However, according to
the W3C recommendations for URI addressing, the plus sign is reserved
as shorthand notation for a space within a query string (for example,
?greeting=hello+world).
If a URL query component contains a date formatted according to RFC
3339 with a plus sign in the timezone offset (for example,
2013-12-31T14:00:00+00:00), interpreting the plus sign as a space
results in an invalid time format. RFC 3339 specifies how dates should
be formatted, but doesn't advise whether the plus sign must be
percent-encoded in a URL. Depending on the implementation receiving
this URL, you may need to preemptively percent-encode the plus sign
character.
As an alternative, consider encoding complex and/or potentially
problematic data in a more robust data-interchange format, such as
JSON or XML.
The conclude is you may or may not encode '+'.
In my opinion, NSURLComponents only encode character which it make sure that should be encoded, such as '&', '=' or Chinese characters like '你' '好', it doesn't encode the character may be encode or not according to the content-type, like '+' I mentioned above. So if you find you have to encode '+' or your server can't parse correctly, you can use the code below.
I don't know swift, so I just provide objective-c code, sorry for that.
- (NSString *)URLEncodingValue:(NSString *)value
{
NSCharacterSet *set = [NSCharacterSet URLQueryAllowedCharacterSet];
NSMutableCharacterSet *mutableQueryAllowedCharacterSet = [set mutableCopy];
[mutableQueryAllowedCharacterSet removeCharactersInString:#"!*'();:#&=+$,/?%#[]"];
return [value stringByAddingPercentEncodingWithAllowedCharacters:mutableQueryAllowedCharacterSet];
}
!*'();:#&=+$,/?%#[] are reserved characters defined in RFC 3986, the code will encode all of them appear in the value parameter, if you just want to encode '+', just replace !*'();:#&=+$,/?%#[] with +.
This is an Apple bug. Instead use
NSString -stringByAddingPercentEncodingWithAllowedCharacters:
with
NSCharacterSet +URLQueryAllowedCharacterSet
Help me write correct code, I need convert string from utf8 to cp1251. Using the library Uutf.
my code is not work
let str = "русский текст" in
let decode = Uutf.encoding_of_string str in
Uutf.encoding_to_string decode;;
I found another solution. Convert string via library Tk.
open Tk;;
let top = openTk ();;
let str = "abracadabra" in
let x = Encoding.convertfrom ~encoding:"utf-8" abracadabra in
print_endline(x);;
Neither Batteries Included nor Uutf handles the CP1251 encoding, as far as I can tell. You might look at Camomile.
(It's interesting to ask yourself what encoding is being used in the quoted text of your source code.)
I cannot figure out how to replace all special characters in a string and convert it to a string I can use in a URL.
What I am using it for:
I am uploading an image, converting it to base64, and then passing it to the Laravel framework, however the base64 string can contain +, /, \, etc. which changes the meaning of the URL.
I can replace the + sign with the following code:
let withoutPlus = image.stringByReplacingCharactersInRange("+", withString: "%2B")
however then I cannot use that as a NSString to try and change the other characters.
Surely there is a way to just target every single special character and convert it something usable in a URL?
You can use stringByAddingPercentEncodingWithAllowedCharacters to escape characters as needed. You pass it an NSCharacterSet containing the characters that are valid for that string (i.e. the ones you don't want replaced). There's a built-in NSCharacterSet for characters allowed in URL query strings that will get you most of the way there, but it includes + and / so you'll need to remove those from the set. You can do that by making a mutable copy of the set and then calling removeCharactersInString:
let allowedCharacters = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as NSMutableCharacterSet
allowedCharacters.removeCharactersInString("+/=")
Then you can call stringByAddingPercentEncodingWithAllowedCharacters on your string, passing in allowedCharacters:
let encodedImage = image.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters)
Note that it will return an optional String (String?) so you'll probably want to use optional binding:
if let encodedImage = image.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters) {
/* use encodedImage here */
} else {
/* stringByAddingPercentEncodingWithAllowedCharacters failed for some reason */
}
Example:
let unencodedString = "abcdef/+\\/ghi"
let allowedCharacters = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy() as NSMutableCharacterSet
allowedCharacters.removeCharactersInString("+/=")
if let encodedString = unencodedString.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters) {
println(encodedString)
}
Prints:
abcdef%2F%2B%5C%2Fghi
Use
let withoutPlus = image.stringByReplacingOccurrencesOfString("+", withString: "%2B")
rather than image.stringByReplacingCharactersInRange. Note that your call as posted doesn't work, as that method is declared as
func stringByReplacingCharactersInRange(range: Range<String.Index>, withString replacement: String) -> String
and you are not supplying the correct parameters.
You might do better to use POST to send a file, rather than encode it into your URL
I try to get NSString from NSURL with this method:
NSString *tmp2 = [item.path absoluteString];
Unfortunately I get instead of the NSURL:
<CFURL 0x173c50 [0x3f1359f8]>{type = 0, string = /var/mobile/Applications/A30FD2E4-A273-4522-AFD5-A981EFD3C2AA/Documents/*** *** - *** ***.***, encoding = 134217984, base = (null)}
I get :
file://localhost/var/mobile/Applications/A30FD2E4-A273-4522-AFD5-A981EFD3C2AA/Documents/***%20***%20-%20***%20***.***
any idea why?
The NSURL documentation clearly states that absoluteString returns an NSString, just like your code above. This is the string representation of the absolute path, so what you are getting is what you should be getting.
However, looking at the documentation you could also use path, relativePath or relativeString to get a string representation of the url in other formats (absolute or relative paths that either do or do not conform to RFC 1808 (a now obsolete percent encoding).