NSString's isEqualToString: seems to erroneously report non-equality - cocoa

I'm trying to compare the equality of two multi-line strings. I'm getting one of the strings from a web service, and the other I'm getting from iTunes via the Scripting Bridge. The strings from the web service are eventually transferred to iTunes, so if I do that and then re-compare the strings, ideally they'd be identical.
However, when comparing strings like this, it seems that isEqualToString: always returns non-equality. I'm testing this by testing equality of a string from iTunes that originally came from the web service, and a string directly from the web service.
Logging both strings to the Console produces output from both strings that appears identical. Logging the lengths of the strings produce identical lengths.
I've also tried comparing the strings using some other methods. For example, I converted them to ASCII strings to make sure it wasn't some Unicode issue:
NSData *iTunesStringData = [[self iTunesString] dataUsingEncoding:NSASCIIStringEncoding
allowLossyConversion:YES];
NSData *webServiceStringData = [[self webServiceString] dataUsingEncoding:NSASCIIStringEncoding
allowLossyConversion:YES];
NSString *newiTunesString = [[[NSString alloc] initWithData:iTunesStringData encoding:NSASCIIStringEncoding] autorelease];
NSString *newWebServiceString = [[[NSString alloc] initWithData:webServiceStringData encoding:NSASCIIStringEncoding] autorelease];
BOOL result = [newiTunesString isEqualToString:newWebServiceString];
Same problem, not equal. I've tried comparing just the first character:
NSComparisonResult result = [newiTunesString compare:newWebServiceString
options:NSLiteralSearch
range:NSMakeRange(0,1)
locale:[NSLocale currentLocale]];
Does not return NSOrderedSame. I've logged these first characters to the Console and they seem identical. I also considered differences in carriage returns, and tried replacing #"\r" with #"" in both strings before comparing, which doesn't work (and besides, that shouldn't affect equality of just the first character). I don't want to remove #"\n" characters because I want to preserve the multiple lines.
What's going on? Any other ideas?

It turns out this problem was related to line endings. But since I'm comparing multi-line strings, I didn't want to completely strip out the newlines. I normalized the line endings like so:
NSString *normalizediTunesString = [[[self iTunesString] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:#"\n"];
NSString *normalizedWebServiceString = [[[self webServiceString] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]] componentsJoinedByString:#"\n"];
Then, comparing the strings via compare: worked as expected.

Just guessing here but maybe use this clean up your strings
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]

Related

Issues using NSNumberFormatter?

I'm trying to make a label to show the temperature, with a maximum of 3 digits for temperatures over a 100, but I don't want any decimals...
NSString *longtempstring = [tmp description];
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setMaximumFractionDigits:3];
[formatter setMinimumFractionDigits:0];
NSString *shorttempstring = [formatter stringForObjectValue:[NSString stringWithString:longtempstring]];
The code above always returns (null)
Any ideas?
You are getting nil because the object you are passing to stringForObjectValue: is not of the type expected.
First of all, don't use stringForObjectValue:. That is a member of the parent class, NSFormatter. NSNumberFormatter has more specific methods that avoid confusion of object types like numberFromString: and stringFromNumber:.
Second, NSNumberFormatter is used to go from number to formatted string or formatted string to number. Not directly from formatted string to formatted string. You will need to use one number formatter to read your original string and produce a number and another to produce a new shorter formatted string from that number.
Of course, you might be able to make the first step (from long string to number) easier by using NSScanner or by taking a substring of your long string (cutting out everything except for the number itself) and then using the NSString method integerValue or doubleValue. A regular expression could also be used to extract the number from the first (longer) string.
The long and short of it is, this is a two step process. The first step (getting a number) can be accomplished any number of ways and a NSNumberFormatter might not be the easiest way. The second step (getting a new shorter string) is what NSNumberFormatter is perfect for.
I fixed it by converting the NSString to an NSNumber and only pulling the integer value from the string, thus removing any decimals.
NSString *longtempstring = [tmp description];
NSNumber *tempintegervalue = [NSNumber numberWithInteger:[longtempstring integerValue]];
NSString *shorttempstring = [tempintegervalue stringValue];
NSString *longtempstring = [tmp description];
What is the value of longtempstring here? Is it even a number?
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setMaximumFractionDigits:3];
[formatter setMinimumFractionDigits:0];
Why do you set maximum fraction digits to 3 if you say that you don't want any fraction digits? Fraction digits are what is "after the dot" and you say you don't want any decimals. If you don't want any decimals, minimum and maximum must be set to 0.
NSString *shorttempstring = [formatter stringForObjectValue:[NSString stringWithString:longtempstring]];
First of all, why are you copying the string? [NSString stringWithString:...] creates a new string that is a copy of the string you provide as argument. Strings are immutable, there is no need to copy them, they won't be modified. If you are afraid that a NSString may in fact be a NSMutableString, after all that is a subclass of NSString and you want to copy it, just copy it by calling [aString copy]. copy is a very smart method here. If the NSString is in fact a mutable string, it will really copy it, so you get an immutable copy. If it is not a mutable string, though, copy just returns the same string with a retain count increased by one (so in that case copy behaves exactly like retain). However, in your case copying makes no sense whatsoever.
And second, what makes you believe, that you can feed a string into a NSNumberFormater? It is called NSNumberFormater because it formats objects of type NSNumber. How is a NSString a NSNumber? It is not called NSStringFormater. If you feed an object of the wrong class to a formater, it will return nil, that's how this method is documented.
If your longtempstring contains a number with fraction, e.g. 12.345, then the correct code would look like this:
NSString * longtempstring = [tmp description];
NSNumberFormatter * formatter = [[NSNumberFormatter alloc] init];
[formatter setMaximumFractionDigits:0];
[formatter setMinimumFractionDigits:0];
NSNumber * tempnumber = [NSNumber numberWithDouble:[longtempstring doubleValue]];
NSString * shorttempstring = [formatter stringForObjectValue:tempnumber];
However, if all you want to do is cut off the decimals, that is a horrible way to do it. This can be done much more effective:
double d;
NSString * longtempstring;
NSString * shorttempstring;
longtempstring = #"12.345";
d = [longtempstring doubleValue];
shorttempstring = [NSString stringWithFormat:#"%d", (int)d];
This is much shorter, needs less memory, and is at least three times faster. If you want to round the number (that means everything with a fraction .5 and above is rounded to the next higher value), just round it:
d = round([longtempstring doubleValue]);

Is alloc+initWithString: same as copy?

Basically, the question is - are the following essentially the same?
NSString *value1 = ...;
NSString *value2 = [[NSString alloc] initWithString:value1];
and
NSString *value1 = ...;
NSString *value2 = [value1 copy];
Conceptually, yes. However, there is one difference: alloc always creates a new string, whereas copy may return the same string.
In particular, immutable objects, such as immutable strings, are likely respond to copy by returning themselves rather than creating and returning a copy. (After all, if you can't change anything about the original, why would you really need a copy?) Mutable strings will respond to it by creating and returning a copy, as you'd expect.
initWithString: is in the middle: It may release the receiver and return the string you gave it, similar to how copy may return the receiver. However, if that happens, it means you wasted the creation of the string you created with alloc. With copy, you may not need to create any additional objects at all.
About the only reason to use alloc and initWithString: is if you have your own subclass of NSString and want to make an instance of it from an existing string. copy won't use your desired subclass. Since subclassing NSString is practically never warranted in Cocoa, the same is true of using initWithString: (or stringWithString:).
So the bottom line is, just use copy (or mutableCopy). It's shorter, clearer about your intent, and can be faster.
Non-mutable strings are treated a bit special, compared to ordinary objects, so in this case, yes, the two operations are the same.
To wit:
NSString *str1 = #"string";
NSString *str2 = [str1 copy];
NSString *str3 = [[NSString alloc] initWithString: str1];
NSLog(#"str1: %p, str2: %p, str3: %p", str1, str2, str3);
Which gives me the following output:
str1: 0x108a960b0, str2: 0x108a960b0, str3: 0x108a960b0
Since the pointer addresses are the same, we are talking about the same object.

cocoa format NSURL string for gmail atom feed

I'm trying to format a url string to retrieve a gmail atom feed but I'm having problems with it. Here's my code:
NSString *urlstring = [NSString stringWithFormat:#"https://%#:%##gmail.google.com/­gmail/­feed/atom", username, userpass];
NSString *encodedString = [urlstring stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:encodedString];
Here's what's in my log.
https://••••••••#gmail.com:•••••••••#gmail.google.com/%C2%ADgmail/%C2%ADfeed/atom
This-->%C2%AD seems to be the problem. It should just be a slash. Any idea how to clean that up? Thanks.
Short answer:
Your urlstring contains soft hyphens.
Comprehensive answer:
In the following code withSoftHyphens and withoutSoftHyphens look equal:
NSString *withSoftHyphens = #"example/­example/­example";
NSString *withoutSoftHyphens = #"example/example/example";
NSLog(#"%#",[withSoftHyphens stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]);
NSLog(#"%#",[withoutSoftHyphens stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]);
The output is however different:
(checkout yourself by copying and executing the code above)
"example/%C2%ADexample/%C2%ADexample"
"example/example/example"
The soft hyphens are basically represented by %C2%AD after encoding the string.
Quote from Wikipedia:
Soft hyphen is a type of hyphen used to specify a place in text where
a hyphenated break is allowed without forcing a line break in an
inconvenient place if the text is re-flowed.
In other words, your urlstring contains soft hyphens.
Simply remove /­g and /­f using the backspace key and type them again.
Notice you actually need THREE backspaces to only remove two characters (/­g).
- The first backspace removes the g.
- The second backspace removes the invisible soft hyphen.
- The third backspace removes the /.
In conclusion, your code works just fine after removing the soft hyphens:
NSString *username = #"Anne";
NSString *userpass = #"Password";
NSString *urlstring = [NSString stringWithFormat:#"https://%#:%##gmail.google.com/mail/feed/atom", username, userpass];
NSString *encodedString = [urlstring stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:encodedString];
NSLog(#"%#", url);
Output:
https://Anne:Password#gmail.google.com/mail/feed/atom

How to change an NSString's encoding?

I have an NSArray of NStrings, I got this from NSLog when printing the array.
Here is the code I have implemented:
NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
.....
NSArray *queryResults = [[query results] copy];
for (NSMetadataItem *item in queryResults)
{
id value = [item valueForAttribute: kMDItemAlbum];
[databaseArray addObject: value];
}
"The Chronicles Of Narnia: Prince Caspian",
"Taste the First Love",
"Once (Original Soundtrack)",
"430 West Presents Detroit Calling",
"O\U0308\U00d0\U00b9u\U0301\U00b0\U00aeA\U0300O\U0308A\U0300O\U0308I\U0301A\U030a-O\U0301a\U0300A\U0302\U00a1",
"\U7ea2\U96e8\U6d41\U884c\U7f51",
"I\U0300\U00ab\U00bc\U00abO\U0303A\U030aE\U0300y\U0301\U00b7a\U0301",
"A\U0303n\U0303\U00b8e\U0300\U00b2I\U0300C\U0327U\U0300",
"\U00bb\U00b3A\U0308i\U0302O\U0303\U00bdO\U0301N\U0303",
"American IV (The Man Comes Aro",
"All That We Needed",
Now how can I change the human-unreadable strings to human-readable strings? Thanks.
Looking past the escaping done by description (e.g., \U0308), the strings are wrong (e.g., “Öйú°®ÀÖÀÖÍÅ-Óà¡”) because the data you got was wrong.
That's probably not Spotlight's fault. (You could verify that by trying a different ID3-tag library.) Most probably, the files themselves contain poorly-encoded tags.
To fix this:
Encode it in the 8-bit encoding that matches the characters. You can't just pick an encoding (like “ASCII”, which Cocoa mapped to ISO Latin 1 the last time I checked) at random; you need to use the encoding that contains all of the characters in the input and encodes them correctly for what you're going to do next. Try ISO Latin 1, ISO Latin 9, Windows codepage 1252, and MacRoman, in that order.
Decode the encoded data as UTF-8. If this fails, go back to step 1 and try a different encoding.
If step 2 succeeds on any attempt, that is your valid data (unless you're very unlucky). If it fails on all attempts, the data is unrecoverable and you may want to warn the user that their input files contain bogus tags.
Parsing these kind of strings aren't particularly easy: See this SO post for background. It's got links to other SO posts with specific ways of handling this problem.
These strings are utf-8 encoded. You can decode them by:
NSString *myDecoded = [NSString stringWithUTF8String:myEscapedString];
So to process your complete array 'completeArray' you can convert to a const char* first and then back into NSString:
NSMutableArray *processed = [NSMutableArray arrayWithCapacity:completeArray.count];
for (NSString* s in completeArray) {
[processed addObject:[NSString stringWithUTF8String:[s cStringUsingEncoding:ASCIIEncoding]]];
}

Easy way to set a single character of an NSString to uppercase

I would like to change the first character of an NSString to uppercase. Unfortunately, - (NSString *)capitalizedString converts the first letter of every word to uppercase. Is there an easy way to convert just a single character to uppercase?
I'm currently using:
NSRange firstCharRange = NSMakeRange(0,1);
NSString* firstCharacter = [dateString substringWithRange:firstCharRange];
NSString* uppercaseFirstChar = [firstCharacter originalString];
NSMutableString* capitalisedSentence = [originalString mutableCopy];
[capitalisedSentence replaceCharactersInRange:firstCharRange withString:uppercaseFirstChar];
Which seems a little convoluted but at least makes no assumptions about the encoding of the underlying unicode string.
Very similar approach to what you have but a little more condense:
NSString *capitalisedSentence =
[dateString stringByReplacingCharactersInRange:NSMakeRange(0,1)
withString:[[dateString substringToIndex:1] capitalizedString]];
Since NSString is immutable, what you have seems to be a good way to do what you want to do. The implementations of (NSString*)uppercaseString and similar methods probably look very much like what you've written, as they return a new NSString instead of modifying the one you sent the message to.
I had a similar requirement, but it was for characters within the string. This assuming i is your index to the character you want to uppercase this worked for me:
curword = [curword stringByReplacingCharactersInRange:NSMakeRange(i,1)
withString:[[curword substringWithRange:NSMakeRange(i, 1)] capitalizedString]];
If you profile these solutions they are much slower then doing this:
NSMutableString *capitolziedString = [NSMutableString stringWithString:originalString];
NSString *firstChar = [[capitolziedString substringWithRange:NSMakeRange(0,1)] uppercaseString];
[capitolziedString replaceCharactersInRange:NSMakeRange(0, 1) withString:firstChar];
in testing on an iphone 4 running iOS 5:
#doomspork's solution ran in 0.115750 ms
while above ran in 0.064250 ms;
in testing on an Simulator running iOS 5:
#doomspork's solution ran in 0.021232 ms
while above ran in 0.007495 ms;
Aiming for maximum readability, make a category on NSString and give it this function:
NSString *capitalizeFirstLetter(NSString *string) {
NSString *firstCapital = [string substringToIndex:1].capitalizedString;
return [string stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCapital];
}
Then in your code where you want it:
NSString *capitalizedSentence = capitalizeFirstLetter(dateString);
This kind of code rarely belongs in the spot where you need it and should generally be factored away into a utility class or a category to improve legibility.

Resources