I have a localized string which needs to take a few variables. However, in localization it is important that the order of the variables can change from language to language.
So this is not a good idea:
NSString *text = NSLocalizedString(#"My birthday is at %# %# in %#", nil);
In some languages some words come before others, while in others it's reverse. I lack of an good example at the moment.
How would I provide NAMED variables in a formatted string? Is there any way to do it without some heavy self-made string replacements? Even some numbered variables like {%#1}, {%#2}, and so on would be sufficient... is there a solution?
This is why NSLocalizedString takes two parameters. Use the second parameter to include a comment describing the native language meaning of the variables. Then, translators can reorder them using the $ + number construct. See Apple's Notes for Localizers.
However you cannot skip parameters in one language. For example, if you have 3 parameters in English and 4 in French and you don't need the third in English, you cannot format like %1$# %2$# and %4$#. You can only skip the last one.
Formatted localized string example:
NSString *today = [MyHandWatch today];
NSString *msg = [NSString stringWithFormat:NSLocalizedString(#"Today is %#", #""), today];
genstrings will generate this line in your Localizable.strings file:
"Today is %#" = "Today is %#";
I solved this in a project a few weeks back by building my own simple template system with NSScanner. The method uses a template system that finds variables with the syntax ${name}. Variables are supplied to the method through an NSDictionary.
- (NSString *)localizedStringFromTemplateString:(NSString *)string variables:(NSDictionary *)variables {
NSMutableString *result = [NSMutableString string];
// Create scanner with the localized string
NSScanner *scanner = [[NSScanner alloc] initWithString:NSLocalizedString(string, nil)];
[scanner setCharactersToBeSkipped:nil];
NSString *output;
while (![scanner isAtEnd]) {
output = NULL;
// Find ${variable} templates
if ([scanner scanUpToString:#"${" intoString:&output]) {
[result appendString:output];
// Skip syntax
[scanner scanString:#"${" intoString:NULL];
output = NULL;
if ([scanner scanUpToString:#"}" intoString:&output]) {
id variable = nil;
// Check for the variable
if ((variable = [variables objectForKey:output])) {
if ([variable isKindOfClass:[NSString class]]) {
// NSString, append
[result appendString:variable];
} else if ([variable respondsToSelector:#selector(description)]) {
// Not a NSString, but can handle description, append
[result appendString:[variable description]];
}
} else {
// Not found, localize the template key and append
[result appendString:NSLocalizedString(output, nil)];
}
// Skip syntax
[scanner scanString:#"}" intoString:NULL];
}
}
}
[scanner release];
return result;
}
With a localize file looking like this:
"born message" = "I was born in ${birthYear} on a ${birthWeekDay}. ${byebye}";
"byebye" = "Cheers!";
We can accomplish the following results...
NSDictionary *variables = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1986], #"birthYear", #"monday", #"birthWeekDay", nil];
NSString *finalString [self localizedStringFromTemplateString:#"born message" variables:variables];
NSLog(#"%#", finalString); // "I was born in 1986 on a monday. Cheers!"
As you can see, I've added some extra functionality too. Firstly, any variables that aren't found (${byebye} in my example) will be localized and appended to the results. I did this because i load in HTML-files from my application bundle and run them through the localize method (when doing that, I don't localize the input string when creating the scanner though). Also, I added the ability to send in other things than just NSString objects, for some extra flexibility.
This code maybe isn't the best performing or prettiest written, but it does the job without any noticeable performance impacts :)
Related
The app I'm currently working on requires me to determine the part of speech of a word in NSString.
So basically is there a library/database/class which you can access in Objective C which allows one to check if a single word (in the form of a NSString) is a noun, an adjective, an adverb or a verb?
Something along the lines of:
NSString *foo="cat";
if ([foo wordIsNoun]) {
//do something
};
On a similar but slightly unrelated note, is it possible to check if two NSString containing verbs of the same stem but different tense (ask, asking, asked, etc) have the same stem? It would be very useful as well.
You can do this with an NSLinguisticTagger! I've never used one before, but I hacked this together:
NSString *str = #"i have a cat";
NSLinguisticTagger *tagger = [[NSLinguisticTagger alloc] initWithTagSchemes:[NSArray arrayWithObject:NSLinguisticTagSchemeLexicalClass] options:~NSLinguisticTaggerOmitWords];
[tagger setString:str];
[tagger enumerateTagsInRange:NSMakeRange(0, [str length])
scheme:NSLinguisticTagSchemeLexicalClass
options:~NSLinguisticTaggerOmitWords
usingBlock:^(NSString *tag, NSRange tokenRange, NSRange sentenceRange, BOOL *stop) {
NSLog(#"found: %# (%#)", [str substringWithRange:tokenRange], tag);
}];
[tagger release];
When you run this, it logs:
found: i (Pronoun)
found: have (Verb)
found: a (Determiner)
found: cat (Noun)
Note, however, that NSLinguisticTagger is only available on iOS 5+ (and Mac OS X 10.7+).
How would I detect if an array displays the following in an if statement? (I tried NULL and it didn't work)...
When I NSLog the description of the array, this is what returns:
NSLog(#"%#", [manager purDesc]description]);
2011-08-30 13:43:20.227 PROJECT[2921:f503] manager purDesc Dump:(
{
amt = "\n ";
desc = "\n ";
}
)
I need to say "If [manager purDesc] looks like that, display a UIAlertView".
Sorry everyone, I feel like I'm having trouble getting my point across this morning. If you don't understand, please comment with your question and I'll try to explain better.
Actually, your structure seems to be a dictionary inside an array. Not sure how that all stacks up. To see if all items in a dictionary are only whitespace.
BOOL empty = YES;
NSCharacterSet* wp = [NSCharacterSet whitespaceAndNewlineCharacterSet];
for(NSString* key in dict)
{
NSString* val = [dict objectForKey: key];
// trim white space and check length
if([[val stringByTrimmingCharactersInSet: wp]length])
{
empty = NO;
break;
}
}
the array version is left as an exercise to the reader. :-)
If those fields are NSString, you might want to check out stringByTrimmingCharactersInSet:.
NSString* trimmedAmount = [amt stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
You can remove whitespace and returns by using the NSString method stringByTrimmingCharactersInSet: along with the static set whitespaceAndNewlineCharacterSet.
However, based on the output of [purDesc description], it looks like you may have an NSDictionary, not an array.
Why don't you check the actual elements of the array? somehting like
[manager objecAtIndex:0] = nil
I am looking for an efficient way to replace URLs in an NSString (or NSMutableString) with the replacement word 'link', for example ...
#"This is a sample with **http://bitbanter.com** within the string and heres another **http://spikyorange.co.uk** for luck"
I would like this to become...
#"This is a sample with **'link'** within the string and heres another **'link'** for luck"
Ideally, I would like this to be some sort of method that accepts regular expressions, but, this needs to work on the iPhone, preferably without any libraries, or, I could be persuaded if the library was tiny.
Other features that would be handy, replace #"OMG" with #"Oh my God", but not when it's part of a word, i.e. #"DOOMGAME" shouldn't be touched.
Any suggestions appreciated.
Regards,
Rob.
This was actually quite a bit of fun to play with and hopefully the solution is somehow what you were looking for. This is flexible enough to cater not only for links, but also other patterns where you may want to replace a word for another using certain conditions:
I have commented most of the code so it should be pretty self explanatory. If not, feel free to leave a comment and I will try my best to help:
- (NSString*)replacePattern:(NSString*)pattern withReplacement:(NSString*)replacement forString:(NSString*)string usingCharacterSet:(NSCharacterSet*)characterSetOrNil
{
// Check if a NSCharacterSet has been provided, otherwise use our "default" one
if (!characterSetOrNil)
characterSetOrNil = [NSCharacterSet characterSetWithCharactersInString:#" !?,()]"];
// Create a mutable copy of the string supplied, setup all the default variables we'll need to use
NSMutableString *mutableString = [[[NSMutableString alloc] initWithString:string] autorelease];
NSString *beforePatternString = nil;
NSRange outputrange = NSMakeRange(0, 0);
// Check if the string contains the "pattern" you're looking for, otherwise simply return it.
NSRange containsPattern = [mutableString rangeOfString:pattern];
while (containsPattern.location != NSNotFound)
// Found the pattern, let's run with the changes
{
// Firstly, we grab the full string range
NSRange stringrange = NSMakeRange(0, [mutableString length]);
NSScanner *scanner = [[NSScanner alloc] initWithString:mutableString];
// Now we use NSScanner to scan UP TO the pattern provided
[scanner scanUpToString:pattern intoString:&beforePatternString];
// Check for nil here otherwise you will crash - you will get nil if the pattern is at the very beginning of the string
// outputrange represents the range of the string right BEFORE your pattern
// We need this to know where to start searching for our characterset (i.e. end of output range = beginning of our pattern)
if (beforePatternString != nil)
outputrange = [mutableString rangeOfString:beforePatternString];
// Search for any of the character sets supplied to know where to stop.
// i.e. for a URL you'd be looking at non-URL friendly characters, including spaces (this may need a bit more research for an exhaustive list)
NSRange characterAfterPatternRange = [mutableString rangeOfCharacterFromSet:characterSetOrNil options:NSLiteralSearch range:NSMakeRange(outputrange.length, stringrange.length-outputrange.length)];
// Check if the link is not at the very end of the string, in which case there will be no characters AFTER it so set the NSRage location to the end of the string (== it's length)
if (characterAfterPatternRange.location == NSNotFound)
characterAfterPatternRange.location = [mutableString length];
// Assign the pattern's start position and length, and then replace it with the pattern
NSInteger patternStartPosition = outputrange.length;
NSInteger patternLength = characterAfterPatternRange.location - outputrange.length;
[mutableString replaceCharactersInRange:NSMakeRange(patternStartPosition, patternLength) withString:replacement];
[scanner release];
// Reset containsPattern for new mutablestring and let the loop continue
containsPattern = [mutableString rangeOfString:pattern];
}
return [[mutableString copy] autorelease];
}
And to use your question as an example, here's how you could call it:
NSString *firstString = #"OMG!!!! this is the best convenience method ever, seriously! It even works with URLs like http://www.stackoverflow.com";
NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:#" !?,()]"];
NSString *returnedFirstString = [self replacePattern:#"OMG" withReplacement:#"Oh my God" forString:firstString usingCharacterSet:characterSet];
NSString *returnedSecondString = [self replacePattern:#"http://" withReplacement:#"LINK" forString:returnedFirstString usingCharacterSet:characterSet];
NSLog (#"Original string = %#\nFirst returned string = %#\nSecond returned string = %#", firstString, returnedFirstString, returnedSecondString);
I hope it helps!
Cheers,
Rog
As of iOS 4, NSRegularExpression is available. Amongst other things, you can enumerate all matches within a string via a block, allowing you to do whatever you want to each, or have the regular expression perform some kinds of substitution directly for you.
Direct string substitutions (like 'OMG' -> 'Oh my God') can be performed directly by an NSString, using -stringByReplacingOccurencesOfString:withString:, or replaceOccurrencesOfString:withString:options:range: if your string is mutable.
I’m making a languages application, and I have a long list of vocabulary relating to that language (German, in case anyone was interested). I have the functionality in my app to switch between sorting the tableview by German words, or by english words.
When I use the following:
NSString *path = [[NSBundle mainBundle] pathForResource:name ofType:type];
NSString *string = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
NSArray *array = [[string componentsSeparatedByString:#"\n"] sortedArrayUsingSelector:#selector(compare:)];
it works absolutely perfectly - by which I mean, exactly as expected. What I would like to improve on this however, is that there are certain words, such as verbs or nouns, which are always preceded by prefixes, like “to”, as in “to do something”, or “the” in front of nouns. So what I would like to do is somehow exclude these from my sort, because otherwise I end up with all the verbs being sorted alphabetically under the “t” section in my array, which is not very user friendly.
I’ve looked through the Apple documentation about NSString and NSArray, as this is where the compare function is (unless I’m very much mistaken), and I haven’t found any way that makes sense to me. This is the first time I have done any data handling like this so I may be missing something simple, and so I would really appreciate some help.
Thanks very much
Michaeljvdw
You're on the right track. What you want to use instead of the (built-in) compare method is to write your own method, which can eliminate the "to" or "the" bits if they exist, and then use the existing compare method.
Your call would look something like this:
NSArray *array = [[string componentsSeparatedByString:#"\n"] sortedArrayUsingSelector:#selector(myCompare:)];
Using a custom category you give to NSString with the following methods:
// This method can be exposed in a header
- (NSComparisonResult)myCompare:(NSString*)aString
{
NSString* selfTrimmed = [self removeArticles];
NSString* aStringTrimmed = [s2 removeArticles];
return [self compare:aString];
}
// This method can be kept private in the .m implementation
- (NSString*)removeArticles
{
NSRange range = NSMakeRange(NSNotFound, 0);
if ([self hasPrefix:#"to "])
{
range = [self rangeOfString:#"to "];
}
else if ([self hasPrefix:#"the "])
{
range = [self rangeOfString:#"the "];
}
if (range.location != NSNotFound)
{
return [self substringFromIndex:range.length];
}
else
{
return self;
}
}
You might have some luck with localizedCompare: or localizedStandardCompare:, but I don't think that either of these will strip out articles and prepositions like you want. Instead, you will probably have to define a category on NSString that provides the specific style of sorting you're looking for:
#interface NSString (MySortAdditions)
- (NSComparisonResult)compareWithoutArticles:(NSString *)other;
#end
#implementation NSString (MySortAdditions)
- (NSComparisonResult)compareWithoutArticles:(NSString *)other {
NSMutableString *mutableSelf = [NSMutableString stringWithString:self];
[mutableSelf
replaceOccurrencesOfString:#"das"
withString:#""
options:NSCaseInsensitiveSearch
range:NSMakeRange(0, [mutableSelf length])
];
...
// delete articles from 'other' too
NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSString *trimmedSelf = [mutableSelf stringByTrimmingCharactersInSet:trimSet];
NSString *trimmedOther = ...;
return [trimmedSelf localizedCaseInsensitiveCompare:trimmedOther];
}
#end
You can then use #selector(compareWithoutArticles:) as your sort selector for NSArray.
First, don't use compare:. Use localizedCompare: instead. This is important, because whether á appears just after a or after z as a separate letter depends on the language. localizedCompare: takes care of that.
--edit
As Justin says, localizedStandardCompare: is the selector to be used! I didn't know that method. As written in the documentation, localizedStandardCompare: does more than localizedCompare:, although the document doesn't say exactly what it does.
--end of edit
If you want more, you need to implement that yourself. You can use category for that purpose. First declare it
#interface NSString (MichaelsSuperCompareCategory)
-(NSComparisonResult)michaelsSuperCompare:(NSString*)string;
#end
and then implement it
#interface NSString (MichaelsSuperCompareCategory)
-(NSComparisonResult)michaelsSuperCompare:(NSString*)string{
...
}
#end
This way you can add methods to an existing class. Then you can use
NSArray *array = [[string componentsSeparatedByString:#"\n"]
sortedArrayUsingSelector:#selector(michaelsSuperCompare:)];
It is important to prefix the method name with something distinctive, not to accidentally crash with internal methods used by Apple.
As for the functionality, you need to implement that yourself, as far as I know. You can get the current locale with [NSLocale currentLocale]. You can implement a nicer behavior for the languages you know, and then default to localizedCompare: for unknown languages.
I would somehow do -replaceOccurancesOfStrings on all the data eg "To" -> "" - and then reload the data. (or this can in a text editor)
Another thing to think about is having eg 'to walk' changed to 'walk (to)' which can be done ahead of time (and will also create less confusion for the user as they are scrolling alphabetically).
NSDictionary *story = [stories objectAtIndex: indexPath.row];
cell.text=[NSString stringwithFormat:[story objectForKey#"message];
i dont knw what exaclty "message " contains (what is the meaning of objectForKey#"message")
EDIT CODE
NSString *key =[appDelegate.books objectAtIndex:indexPath.row];
//dict y=#"Name";
NSArray *nameSection = [dict objectForKey:key];
NSDictionary *story = [nameSection objectAtIndex: indexPath.row];
cell.text=[NSString stringwithFormat:[story objectForKey:key]];
NSLog(#"Value Of message: %#", [dict objectForKey:key]);
why my code crashes
If you are more familiar with Java or C# the code is equivalent to something like this:
// Assuming stories is declared as: List<Dictionary<string, string> stories;
Dictionary<string, string> story = stories[indexPath.row];
cell.Text = String.Format(story["message"]);
In Smalltalk-style (and therefore Objective-C too) Object Oriented programming, methods are more like messages to other objects. So a good Objective-C method name should read like an English sentence (Subject-Verb-Object). Because of this working with dictionaries (hash tables) looks like this:
[myDictionary setObject:#"Value" forKey:#"someKey"];
[myDictionary objectForKey:#"someKey"]; // == #"Value"
In Java it would be:
myDictionary.put("someKey", "Value");
myDictionary.get("someKey"); // == "Value"
Notice how the key ("someKey") was the first argument in the Java example. In Objective-C you name your arguments with the method name, hence setObject: forKey:. Also notice that in Objective-C strings start with an # symbol. That's because Objective-C strings are different from regular C strings. When using Objective-C you almost always use Objective-C's # strings.
In C# there is a special syntax for Dictionaries so it becomes:
myDictionary["someKey"] = "Value";
myDictionary["someKey"]; // == "Value"
One important problem that you might encounter if you're new is the problem of native types.
In Java to add an int to a Dictionary you used to have to do:
myDictionary.put("someKey", new Integer(10));
Because the primitive types (int, char/short, byte, boolean) aren't real Objects. Objective-C has this problem too. So if you want to put an int into a dictionary you must use NSNumber like so:
[myDictionary setObject:[NSNumber numberForInt:10]
forKey:#"someKey"];
And you pull out the integer like so:
NSNumber *number = [myDictionary objectForKey:#"someKey"];
[number intValue]; // == 10
EDIT:
Your code might be crashing if you have a '%' character in your string, since stringWithFormat is just like NSLog in that it takes many arguments. So if story["message"] is "Hello" then it'll work fine without extra arguments but if it's "Hello %#" you need to add one argument to stringWithFormat.
NSString *message = #"Hello %#";
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:message forKey:#"message"];
NSString *output = [NSString stringWithFormat:[dict objectForKey:#"message"], #"World!"];
// output is now #"Hello World!".
#"message" is a key for a value stored in the NSDictionary object. The first line declares an NSDictionary named story that appears to come from an array.
If you want to find what value is stored for the key:#"message", consider using:
NSLog(#"Value Of message: %#", cell.text);
Run and check the console to see the output. (SHIFT + COMMAND + Y) in XCode will bring up the console, if that's what you are using. If you are unfamiliar with NSArrays/NSDictionaries, give Apple's Documentation a look.
I'm just guessing at all of this since that is a very limited sample of code. Try submit more code when you ask a question so that the viewers can get a better idea of your questions.
That is an example of key-value coding, and a lot of information is available on the Apple dev site if you're interested:
http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/KeyValueCoding/KeyValueCoding.html