How to skip data passing into [NSString stringWithFormat:] - ios

Consider below code:
int count = 1;
NSString* format = count == 1 ? #"One %2$#" : #"%1$d %2$#s";
NSString* result = [NSString stringWithFormat: format, count, #"Bread"];
What count is not 1, the result is valid:
2 Breads
4 Breads
However the count is 1, then it causes EXC_BAD_ACCESS
NSLog(#"%#", [NSString stringWithFormat:#"One %2$#", 1, #"Bread"]);
Xcode compiler complains with upper code:
Data argument not used by format string
I know the reason for this error.
However My approach(Dynamic format that may skips some data) is also useful what if it works.
Is there any workaround for this?

[NSString stringWithFormat:] does not support positional parameters.
That looks like a bug in [NSString stringWithFormat:].
One workaround (hack) would be to use plain printf functions and convert the result to
NSString:
char *format = count == 1 ? "One %2$s" : "%1$d %2$ss";
char *tmp;
asprintf(&tmp, format, count, "Bread");
NSString *result = [NSString stringWithUTF8String:tmp];
free(tmp);
But the proper solution would be to create a "Localizable.strings" file with
language plural rules, as described in
"Handling Noun Plurals and Units of Measurement"
in the Internationalization and Localization Guide.
See also "String Localization"
for documentation and examples.

Replace :-
NSLog(#"%#", [NSString stringWithFormat:#"One %2$#", 1, #"Bread"]);
with Modified below:-
NSLog(#"%#", [NSString stringWithFormat:#"%#,%d,%#",#"One %2$#", 1, #"Bread"]);

Related

How should I localise with multiple parameters?

Let's say I have the following string:
[NSString stringWithFormat:#"Booked for %# at %#", colleagueName, time];
And I realise I've forgotten to localise that string, so I replace it:
[NSString stringWithFormat:NSLocalizedString(#"bookings.bookedFor", "Booked for user at time"), colleagueName, time];
Now when doing translations, I find that the language X needs the parameters the other way round; something closer to:
<time> for booking of <colleague> is done.
What is the best way to address the fact that now I need the second parameter of my formatted string to be time and the third to be colleagueName please?
As is often the case, my colleague found the solution almost as soon as I had asked on here! Apparently Objective-C has positional arguments
The positions are 1-indexed so %1$# refers to the first argument.
NSString *firstParam = #"1st";
NSString *secondParam = #"2nd";
NSLog(#"First %1$# Second: %2$#", firstParam, secondParam);
NSLog(#"Second %2$# First: %1$#", firstParam, secondParam);
This prints:
First 1st Second: 2nd
Second 2nd First: 1st
You can try like this:
NSString * language = [[NSLocale preferredLanguages] objectAtIndex:0];
if ([language isEqualToString:#"X"]) {//X is language code like "fr","de"
[NSString stringWithFormat:NSLocalizedString(#"bookings.bookedFor", "Booked for user at time"), time, colleagueName];
} else {
[NSString stringWithFormat:NSLocalizedString(#"bookings.bookedFor", "Booked for user at time"), colleagueName,time];
}

NSScanner to scan until a character or number is Hit

My problem is that I want to extract the data from the string. The strings prints 1000 lines with some random data fetched from web. The string is like this:
Level
1
2
3
4
5
6
7
8
Score
0
0
0
0
0
0
0
0
I need NSScanner to save data in array like this
int *extractedLevelData = {1, 2, 3, 4, 5, 6, 7, 8}
int *extractedScoreData = {0, 0, 0, 0, 0, 0, 0, 0}
The problem is that the row of number in level and score is dynamic, it goes from 1 then new line then 2 then new line then 3 then new line and so on. The challenge is that sometimes it can be 1 to 5 or sometimes it can be 1 only and sometimes maximum is 1 to 8. They show up in the same style as shown above followed by new line character. Same with the "Score".
I've tried this but the saved data returns null in NSLog, it has been 7 days since I'm learning Objective C and I'm almost finished with the app until this problem came.
Here is what I've tried:
NSString *extractedData;
NSCharacterSet *tripleNewLine = [NSCharacterSet characterSetWithCharactersInString:#"\n\n\n"];
[firstScanner scanString:#"Level" intoString:NULL];
[firstScanner setScanLocation:[firstScanner scanLocation]];
[firstScanner scanUpToCharactersFromSet:tripleNewLine intoString:&extractedData];
NSLog(#"%#", extractedData);
Note that this is just a code snippet and the real problem is really complex but if someone smart enough to solve this problem then my problem will be solved! The logic can be: tell the NSScanner to scan from "Level" text with numbers until it hit any character.
So if you don't really want to use NSScanner another solution can be the following:
Split the whole string by Score this will give you two strings one with the values from score and one with the values from level:
`NSArray *components = [serverString componentsSeparatedByString:#"Score"];`
The above line will split your server string in two strings contained by components array. One will have all the values from Level including Level, and the other one will have the values from Score without Score value.
Now you can split those two strings by \n character, this will result in two arrays with all the values from a row.
if(components.count > 1) { //we better check if we have both substrings
NSArray *levels = [[components objectAtIndex:0] componentsSeparatedByString:#"\n"];
NSArray *scores = [[components objectAtIndex:1] componentsSeparatedByString:#"\n"];
}
Now after you have the all the values we should create a method that will check if a value is an actual number, if not we will remove it from the array.
-(NSArray *)checkAndRemoveNonNumbersFromArray:(NSArray *)checkedArray {
NSCharacterSet *alphaNums = [NSCharacterSet decimalDigitCharacterSet];
checkedArray = [checkedArray filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *evaluatedObject, NSDictionary *bindings) {
NSCharacterSet *inStringSet = [NSCharacterSet characterSetWithCharactersInString:evaluatedObject];
return [alphaNums isSupersetOfSet:inStringSet];
}]];
return checkedArray;
}
Now that we have the method created, the code from point 2 will become:
if(components.count > 1) { //we better check if we have both substrings
NSArray *levels = [[components objectAtIndex:0] componentsSeparatedByString:#"\n"];
NSArray *scores = [[components objectAtIndex:1] componentsSeparatedByString:#"\n"];
levels = [self checkAndRemoveNonNumbersFromArray:levels];
scores = [self checkAndRemoveNonNumbersFromArray:scores];
}
Now levels & scores will have only numerical values from those two arrays. Note, this is not the best solution, is more a solution to show you how you can play with strings, after you understand this implementation I think you can find one that uses NSScaner.
I see that you are passing #"Level" as a string input rather than a variable, which is not correct. I assume Level is the NSString object in this case. You can use this solution. This will print all the data from Level
NSScanner *scanner = [NSScanner scannerWithString:Level];
while([scanner isAtEnd] == NO) {
[scanner setScanLocation:[scanner scanLocation]];
[scanner scanUpToCharactersFromSet:tripleNewLine intoString:&extractedData];
NSLog(#"%#", extractedData);
}

Converting int to NSString

I thought I had nailed converting an int to and NSString a while back, but each time I run my code, the program gets to the following lines and crashes. Can anyone see what I'm doing wrong?
NSString *rssiString = (int)self.selectedBeacon.rssi;
UnitySendMessage("Foo", "RSSIValue", [rssiString UTF8String] );
These lines should take the rssi value (Which is an NSInt) convert it to a string, then pass it to my unity object in a format it can read.
What am I doing wrong?
NSString *rssiString = [NSString stringWithFormat:#"%d", self.selectedBeacon.rssi];
UPDATE: it is important to remember there is no such thing as NSInt. In my snippet I assumed that you meant NSInteger.
If you use 32-bit environment, use this
NSString *rssiString = [NSString stringWithFormat:#"%d", self.selectedBeacon.rssi];
But you cann't use this in 64-bit environment, Because it will give below warning.
Values of type 'NSInteger' should not be used as format arguments; add
an explicit cast to 'long'
So use below code, But below will give warning in 32-bit environment.
NSString *rssiString = [NSString stringWithFormat:#"%ld", self.selectedBeacon.rssi];
If you want to code for both(32-bit & 64-bit) in one line, use below code. Just casting.
NSString *rssiString = [NSString stringWithFormat:#"%ld", (long)self.selectedBeacon.rssi];
I'd like to provide a sweet way to do this job:
//For any numbers.
int iValue;
NSString *sValue = [#(iValue) stringValue];
//Even more concise!
NSString *sValue = #(iValue).stringValue;
NSString *rssiString = [self.selectedBeacon.rssi stringValue];
For simple conversions of basic number values, you can use a technique called casting. A cast forces a value to perform a conversion based on strict rules established for the C language. Most of the rules dictate how conversions between numeric types (e.g., long and short versions of int and float types) are to behave during such conversions.
Specify a cast by placing the desired output data type in parentheses before the original value. For example, the following changes an int to a float:
float myValueAsFloat = (float)myValueAsInt;
One of the rules that could impact you is that when a float or double is cast to an int, the numbers to the right of the decimal (and the decimal) are stripped off. No rounding occurs. You can see how casting works for yourself in Workbench by modifying the runMyCode: method as follows:
- (IBAction)runMyCode:(id)sender {
double a = 12345.6789;
int b = (int)a;
float c = (float)b;
NSLog(#"\ndouble = %f\nint of double = %d\nfloat of int = %f", a, b, c);
}
the console reveals the following log result:
double = 12345.678900
int of double = 12345
float of int = 12345.000000
original link is http://answers.oreilly.com/topic/2508-how-to-convert-objective-c-data-types-within-ios-4-sdk/
If self.selectedBeacon.rssi is an int, and it appears you're interested in providing a char * string to the UnitySendMessage API, you could skip the trip through NSString:
char rssiString[19];
sprintf(rssiString, "%d", self.selectedBeacon.rssi);
UnitySendMessage("Foo", "RSSIValue", rssiString );

Poor compiler warnings

how to get rid of the compiler warning in XCode 5.1
in Stringtable is of course a string with a format specifier (updated: now in front of code)
"fmtDetail" = "Count: %d";
int number = 0;
//Compiler warning: Data argument not used by format string
NSString *text = [NSString stringWithFormat:NSLocalizedString(#"fmtDetail", nil), number];
//this gets no warning
NSString *fmtDetail = NSLocalizedString(#"fmtDetail", nil);
NSString *text2 = [NSString stringWithFormat:fmtDetail, number];
It is not the compiler warning that is poor - you should correct your code.
There seems to be an %d (or similar) missing in the #"fmDetail".
Or you should get rid of the number argument - that is not used.
Depends on what you are actually trying to do...
NSString *text = [NSString stringWithFormat:NSLocalizedString(#"fmtDetail%d", nil), number];
NSString *text = [NSString stringWithFormat:NSLocalizedString(#"fmtDetail %d", nil), number];
NSString *text = [NSString stringWithString:NSLocalizedString(#"fmtDetail", nil)];
Second note: this #"fmtDetail%d" should match the key in the plist dictionary (translated strings). It could also be simly #"theDeatils" - the string that returned from your plist is the one that should actually hold formatting data for the string.
Why would one want to use the %d in the key? Because NSLocalizedString returns the key as the result if it doesn't find string with appropriate key.
EDIT: MartinR found the real reason for why this warning appears. Just a note that might be useful: since localizing strings usually means translation into many languages (duh) you might need to use numbered placeholders - not all languages share the same basic sentence structure.
This seems to be not a bug, but a new feature of the compiler that comes with Xcode 5.1 (beta). It expects now that in
[NSString stringWithFormat:NSLocalizedString(key, ...), arguments... ]
the key itself is a valid format for the given arguments.
(In other words, the key uses the same format specifiers as its value from the strings file).
For example:
// Source code:
[NSString stringWithFormat:NSLocalizedString(#"Count = %d", nil), number]
// Localizable.strings:
"Count = %d" = "Die Anzahl ist %d";
This is an advantage because the compiler can now check that the number and types
of the format specifiers match the actual arguments even with localizable format
strings. That was not possible before (as far as I know).
For example, this will cause a warning in Xcode 5.1 beta, but not in Xcode 5.0.2:
[NSString stringWithFormat:NSLocalizedString(#"fmtDetail %f", nil), 13];
// warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
(And as #rokjarc already had pointed out, using a valid format string as key makes
sense anyway, because NSLocalizedString() returns the key if no matching string
is found in the Localizable.strings file.)

How do I use NSRange with this NSString?

I have the following NSString:
productID = #"com.sortitapps.themes.pink.book";
At the end, "book" can be anything.... "music", "movies", "games", etc.
I need to find the third period after the word pink so I can replace that last "book" word with something else. How do I do this with NSRange? Basically I need this:
partialID = #"com.sortitapps.themes.pink.";
You can try a backward search for the dot and use the result to get the desired range:
NSString *str = #"com.sortitapps.themes.pink.book";
NSUInteger dot = [str rangeOfString:#"." options:NSBackwardsSearch].location;
NSString *newStr =
[str stringByReplacingCharactersInRange:NSMakeRange(dot+1, [str length]-dot-1)
withString:#"something_else"];
You can use -[NSString componentsSeparatedByString:#"."] to split into components, create a new array with your desired values, then use [NSArray componentsJoinedByString:#"."] to join your modified array into a string again.
Well, although this isn't a generic solution for finding characters, in your particular case you can "cheat" and save code by doing this:
[productID stringByDeletingPathExtension];
Essentially, I'm treating the name as a filename and removing the last (and only the last) extension using the NSString method for this purpose.

Resources