Unused positional argument skipped in String formatting (Swift) - swift

I want to format a string, in Swift, with two potential arguments (using Format specifiers). The string to format may have a place for only the first argument, only the second argument, or both arguments. If I use the first or both arguments it works, but if I use only the second argument, it does not work. For instance:
let title = "M."
let name = "David"
let greetingFormat = "Hello %1$# %2$#"
print(String(format: greetingFormat, title, name))
// OUTPUT> Hello M. David
// OK
If I use only the first argument in the String to format:
let greetingFormat = "Hello %1$#"
print(String(format: greetingFormat, title, name))
// OUTPUT> Hello M.
// OK
But when using only the second argument
let greetingFormat = "Hello %2$#"
print(String(format: greetingFormat, title, name))
// OUTPUT> Hello M.
// NOT THE EXPECTED RESULT!
In the last case I expected "Hello David". Is it a bug? How can I obtain the intended result for the last case where only the second argument is used?
Remarks:
Please note that this problem occurs in the context of localization (i.e. the string to format comes from a Localizable.strings file), so I don’t have the possibility to remove unused argument directly.
The question does not relate to person’s name formatting. This is just taken as a example.

I answer my own question but all credit to #Martin R that provides the relevant information in comments.
It is not a bug, String(format:) does not support omitting positional parameters.
It is a known behavior since ObjectiveC, see: stackoverflow.com/a/2946880/1187415
If you only have String arguments you can use multiple String substitutions with String.replacingOccurrences(of:, with:) instead of String(format:).
More precision on the last solution. The following will work in the case that only one argument is used and also if both arguments are used in the greetingFormat String:
greetingFormat.replacingOccurrences(
of: "%1$#", with: title)
.replacingOccurrences(
of: "%2$#", with: name)
Of course, with String.replacingOccurrences(of:, with:) you can choose other identifiers for the substitution than %1$# and %2$#.

Related

Why does the behaviour of LocalizedStringKey depend on whether I pass a string interpolation to its initialiser?

While trying to answer this question, I found a strange behaviour.
Text(LocalizedStringKey("Hello \(Image(systemName: "globe"))"))
displays a globe, but
Text(LocalizedStringKey("Hello {world}".replacingOccurrences(of: "{world}", with: "\(Image(systemName: "globe"))")))
Text(LocalizedStringKey("Hello" + "\(Image(systemName: "globe"))"))
displays "Hello" followed by a jumble of SwiftUI's internal jargon mess.
An even more minimal example would be:
let x = "\(Image(systemName: "globe"))"
print(LocalizedStringKey.init(x))
print(LocalizedStringKey.init("\(Image(systemName: "globe"))"))
The values I'm passing to LocalizedStringKey.init should be the same, both "\(Image(systemName: "globe"))", but The first prints
LocalizedStringKey(key: "%#", hasFormatting: true, arguments: [...])
and the second prints
LocalizedStringKey(key: "Image(provider: SwiftUI.ImageProviderBox<SwiftUI.Image.(unknown context at $7ff91ccb3380).NamedImageProvider>)", hasFormatting: false, arguments: [])
It appears that LocalizedStringKey.init changes its behaviour depends on whether the arguments I pass is an (interpolated) string literal or not.
As far as I can see, the two calls to LocalizedStringKey.init are calling the same initialiser. There is only one parameter-label-less initialiser in LocalizedStringKey, which takes a String.
If there were also an initialiser that takes a LocalizedStringKey, the results would be much more understandable. LocalizedStringKey has custom string interpolation rules, and one specifically for Image, after all. But this is the only initialiser with no parameter labels as far as I know.
It would also be somewhat understandable if the initialiser's parameter is #autoclosure () -> String. If the expression I pass in is lazily evaluated, the method might be able to "peek inside" the closure by some means unknown to me. But the parameter isn't an auto closure.
What seems to be happening here is that the compiler is creating a LocalizedStringKey with key being that same pattern as the interpolation you passed in, even though the parameter is a String!
What is actually going on here? Did I miss a hidden initialiser somewhere?
TL;DR: the behaviour you're seeing comes from ExpressibleByStringInterpolation. But read on for more fun!
LocalizedStringKey becomes easier to understand if you think of it purely as a convenience to allow SwiftUI interface elements to be localizable "for free" when using string literals. There's only one real time you'd use it directly.
Consider Text. There are two relevant initializers:
init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)
which will attempt to localize the text passed in, and
init<S>(_ content: S) where S : StringProtocol
which will display the string without altering it.
If you call Text("Hello"), which initializer is used?
String literals conform to StringProtocol, but LocalizedStringKey is also ExpressibleByStringLiteral. The compiler would not know which one to choose.
To get "free" localization, the StringProtocol initializer is marked with #_disfavoredOverload, which tells the compiler to assume that the string literal is a LocalizableStringKey rather than a String.
Therefore, Text("Hello") and Text(LocalizedStringKey("Hello")) are equivalent.
let string = "Hello"
Text(string)
In this case, there is no conflict - the compiler uses the StringProtocol initializer and the string is not localized.
What does this have to do with your question? LocalizedStringKey is also ExpressibleByStringInterpolation, which is where your "hidden initializer" comes from. But like the examples above, this only comes into play if you are initializing it with a single, interpolated string.
Text("Hello \(Image(systemName: "globe"))")
You're passing an interpolated string, so the compiler can deal with it and add the image into the interpolation.
Text("Hello {world}".replacingOccurrences(of: "{world}", with: "\(Image(systemName: "globe"))"))
Here, replacingOccurrences(of: is evaluated first, meaning your argument is a String, which is not treated as a LocalizedStringKey expressed-via-string-interpolation. You're essentially seeing the description of the image.
A similar thing happens with the example with + in it. That implicitly makes a String, so you lose the special image interpolation that LocalizedStringKey gives you.
For your last code example:
let x = "\(Image(systemName: "globe"))"
print(LocalizedStringKey.init(x))
print(LocalizedStringKey.init("\(Image(systemName: "globe"))"))
x is a string containing a description of the image. Remember, only LocalizedStringKey has the magic power to actually understand and represent Image. Any other string interpolation will fall back to the description of the interpolated object.
The first initializer is passing a string (which is treated as a key, that's the only time you'd really directly use LocalizedStringKey, if you were generating keys at run time and wanted to use them for lookup).
The second initializer is using ExpressibleByStringInterpolation and is using LocalizedStringKey.StringInterpolation to insert images into its internal storage, which can then get rendered by Text.

`contains("\n")` of `String` return false while there is a return in the string

let a = """
a
️b
"""
a.contains("\n") // false
It seems there is a strange character in the string right after the return.
a.forEach(){ c in
print(Character(String(c)).asciiValue)
}
Output:
Optional(97)
Optional(10) // the return
nil
Optional(98)
Is this a bug of contains method that results in negatively recognizing the return character while it is followed by some strange ones?
First, I printed out the unicode scalars in your string:
print(a.unicodeScalars.map { $0.value })
// [97, 10, 65039, 98]
And found out that your string indeed contains the line feed character \n, which is the value 10. However, it is followed by U+FE0F (65039), one of those variation selectors in unicode.
The overload of contains you are calling here is contains(StringProtocol), not contains(Character). The former will perform a "smarter" kind of comparison, or as the Xcode's Quick Help documentation calls it, "non-literal":
Summary
Returns true iff other is non-empty and contained within self by case-sensitive, non-literal search.
Discussion
Equivalent to self.rangeOfString(other) != nil
I can't seem to find this documentation online though... All I could find was this discussion showing contains is smart enough to recognise that "ß" means "ss".
Anyway, the point is, contains does not do a character-by-character search. It does whatever it think "makes sense".
Here are a few ways to make it print true:
If you add the variation selector in to the argument to contains, it prints true:
print(a.contains("\n️")) // you can't see it, but there *is* a variation selector after the n
Check whether the unicodeScalars contain the \n character:
print(a.unicodeScalars.contains("\n"))
Use the contains(Character) overload:
print(a.contains("\n" as Character))
Use range(of:) with .literal option:
print(a.rangeOfString("\n", options: [.literal]) != nil)

why do we need the argument labels during the function call in swift

I have just started reading Learn Essentials of Swift
func greet(name: String, day: String) -> String {
return "Hello \(name), today is \(day)."
}
syntax to call
greet("Anna",day : "Tuesday")
My Questions:
What is the real need of sending the name of the parameter
Will it match the name before copying the value in the function
If it matches the names , if i interchange values while i call the function , below code fails
func greet(name: String, day: String,time: String) -> String
{
return "Hello \(name), today is \(day), \(time)."
}
greet("Anna", time: "one forty",day: "Tuesday")
How does the function parameter mechanism work internally ?
As you find in the link in the comment, there are some advantages of named parameters: They make the code clearer.
However, in Objective-C there has been named parameters from the very beginning. To be more honest, they have been akin of named parameters, because the names are part of the method name (selector). In your example, the method in Objective-C would be: greetName:day:. This is different from greetName:day:time:. At least Swift needs the capability to handle that.
What is the real need of sending the name of the parameter
Names parameters make the code more readable. There can be many methods with different parameter names.
Will it match the name before copying the value in the function
Yes. It builds the function name from the argument names in the call and try to match it with the parameter names of a potential method.
If it matches the names , if i interchange values while i call the function , below code fails
In Objective-C and Swift the order of the named parameters is important. You can have two different methods with interchanged names. greetName:day:time: is different to greetName:time:day:.

Swift: Remove text from string [duplicate]

I'm very new to Swift; I've spent the morning reading StackOverflow and trying many strategies, in vain, to accomplish the following:
I have a string, say "12345 is your number!"
I want to extract "12345" to a variable.
In Java, I'd do something like:
String myStr = "12345 is your number!";
return myStr.substring(0, myStr.indexOf(" "));
How do I do something similar in Swift? I don't want to hard-code any assumptions about what the ending index will be. It might be 5 characters in, it might not. I just want to take the substring of everything up to the first occurrence of " ", wherever that might be.
The closest I've gotten so far is:
var myMessage = "12345 is your number!"
myMessage.endIndex.advancedBy(myMessage.characters.count - myMessage.characters.indexOf(" "))
but it doesn't compile for reasons I don't fully yet grok("Binary operator '-' cannot be applied to operands of type Distance (aka 'Int') and 'String.CharacterView.Index?'")
Any help on this is appreciated. Thank you.
Something like this should work:
myMessage.substringToIndex(myMessage.characters.indexOf(" ")!)
Note that in this code I force unwrapped the optional. If you're not guaranteed to have that space in the string, it might make more sense to have the index in a optional binding.
With optional binding, it would look something like this:
if let index = myMessage.characters.indexOf(" ") {
let result = myMessage.substringToIndex(index)
}
You can use a regex, try this code:
var myMessage = "12345 is your number!"
if let match = myMessage.rangeOfString("-?\\d+", options: .RegularExpressionSearch) {
print(myMessage.substringWithRange(match)) // 12345
let myNumber = Int(myMessage.substringWithRange(match)) // Then you can initialize a new variable
}
The advantage is that this method extracts only the numbers wherever they are in the String
Hope this help ;)
With Swift 5 you can use:
myStr.prefix(upTo: myStr.firstIndex(of: " ") ?? myStr.startIndex)
You may need to cast it back to String (String(myStr.prefix(upTo: myStr.firstIndex(of: " ") ?? myStr.startIndex))) since it returns a Substring

Why does Swift's enumerateSubstringsInRange callback take an optional string?

We can extract words from a Swift string like this:
s.enumerateSubstringsInRange(s.characters.indices, options: .ByWords) {
(w,_,_,_) in print(w!)
}
but the forced unwrapping is generally a code smell. It is there because the first parameter of the callback is a String? (an optional). I've tried several possible strings in order to force the function to pass nil to the callback (e.g. the empty string, and strings with no word characters) but with no luck!
So I was wondering why the callback takes an optional string. Is there something I overlooked? Is it because a mutable string can be passed in and modified concurrently? If so, then would it be acceptable practice, if I know my original string is a constant (defined with let), to do the forced unwrap?
(The following information is from the response to my question
https://forums.developer.apple.com/thread/28272 in the
Apple Developer Forum.)
You can pass the .SubstringNotRequired option to enumerateSubstringsInRange(), and then the closure will be called with
substring == nil. This option is documented as
NSStringEnumerationSubstringNotRequired
A way to indicate that the block does not need substring, in which
case nil will be passed. This is simply a performance shortcut.
Example:
let str = "Hello, playground"
str.enumerateSubstringsInRange(str.characters.indices,
options: [.ByWords, .SubstringNotRequired]) {
substring, substringRange, _, _ in
print(substring, substringRange)
}
Output:
nil 0..<5
nil 7..<17
I think it is safe to assume that substring != nil if the
.SubstringNotRequired option is not given.