Sort NSmutablearray with two special keys - objective-c

I have a tableview, its header is stored in a mutablearray, the array looks like
(2005 fall, 2005 spring, 2007 summer...)
When I output the tableview, I want the header in time ascending displayed.
2005 spring
2005 fall
2007 summer
I used the code here:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
[self.sectionKeys sortUsingSelector:#selector(localizedCaseInsensitiveCompare:)];
NSString *key = [self.sectionKeys objectAtIndex:section];
return key;
}
It works fine with year. However, fall comes before spring and summer because of alphabetreason , what to do to fix it please?

Use a custom comparator to get a custom sort order:
NSMutableArray *array = [#[ #"2005 fall", #"2005 spring", #"2007 summer" ] mutableCopy];
NSArray *seasons = #[ #"spring", #"summer", #"fall", #"winter" ];
[array sortUsingComparator:^NSComparisonResult(NSString *str1, NSString *str2) {
NSArray *parts1 = [str1 componentsSeparatedByString:#" "];
NSArray *parts2 = [str1 componentsSeparatedByString:#" "];
NSString *year1 = parts1[0];
NSString *year2 = parts2[0];
NSComparisonResult yearRes = [year1 compare:year2 options:NSNumericSearch];
if (yearRes == NSOrderedSame) {
NSString *season1 = parts1[1];
NSString *season2 = parts2[1];
NSUInteger index1 = [seasons indexOfObject:season1];
NSUInteger index2 = [seasons indexOfObject:season2];
if (index1 < index2) {
return NSOrderedAscending;
} else if (index1 > index2) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
} else {
return yearRes;
}
}];
Note - I might have the NSOrderedAscending and NSOrderedDescending backwards. Swap them if the sort of the seasons in the same year come out in the reverse order.

You need a lookup mechanism to define the ordering of the seasons
NSArray *seasons = #[#"spring", #"summer", #"fall", #"winter"];
NSArray *strings = #[#"2005 fall",#"2007 spring", #"2005 spring", #"2007 winter", #"2005 winter"];
strings = [strings sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
NSArray *string1Comps = [obj1 componentsSeparatedByString:#" "];
NSArray *string2Comps = [obj2 componentsSeparatedByString:#" "];
NSComparisonResult compareYearResult = [#([string1Comps[0] integerValue]) compare:#([string2Comps[0] integerValue]) ];
if (compareYearResult == NSOrderedSame) {
return [#([seasons indexOfObject:string1Comps[1]]) compare:#([seasons indexOfObject:string2Comps[1]])];
}
return compareYearResult;
}];
result
(
2005 spring,
2005 fall,
2005 winter,
2007 spring,
2007 winter
)
Another look up mechanism could be a block
NSNumber* (^lookUpSeason)(NSString *) = ^(NSString *seasonname){
static NSArray *seasons;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
seasons = #[#"spring", #"summer", #"fall", #"winter"];
});
return #([seasons indexOfObject:seasonname]);
};
This might look a bit cumbersome at first, but increases readability when used.
return [#([seasons indexOfObject:string1Comps[1]]) compare:#([seasons indexOfObject:string2Comps[1]])];
becomes
return [lookUpSeason(string1Comps[1]) compare:lookUpSeason(string2Comps[1])];
in both cases you could also give the lookup code into the comparator block, this will give you the opportunity to remove the same comparator with the lookup in other places.
like:
NSArray *strings = #[#"2005 fall", #"2007 spring", #"2005 spring", #"2007 winter", #"2005 winter", #"2005 summer", #"2000 hhh"];
NSComparisonResult (^yearAndSeasonComparator)(id,id) = ^NSComparisonResult(NSString *obj1, NSString *obj2) {
NSNumber* (^lookUpSeason)(NSString *) = ^(NSString *seasonname){
static NSArray *seasons;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
seasons = #[#"spring", #"summer", #"fall", #"winter"];
});
return #([seasons indexOfObject:seasonname]);
};
NSArray *string1Comps = [obj1 componentsSeparatedByString:#" "];
NSArray *string2Comps = [obj2 componentsSeparatedByString:#" "];
NSComparisonResult compareYearResult = [#([string1Comps[0] integerValue]) compare:#([string2Comps[0] integerValue]) ];
if (compareYearResult == NSOrderedSame) {
return [lookUpSeason(string1Comps[1]) compare:lookUpSeason(string2Comps[1])];
}
return compareYearResult;
};
strings = [strings sortedArrayUsingComparator:yearAndSeasonComparator];
The block assigned to yearAndSeasonComparator could now be reused in other places that would sort similar strings.

So you have an array with section keys. But the sections are not in order of the array, they need to be sorted. You will notice that cellForRowAtIndexPath: needs the exact same information. So sorting in this place is wrong.
What I do to handle this: I have a property "unsortedSectionKeys" and a property "sortedSectionKeys". sortedSectionKeys has a getter that checks for nil and stores a sorted copy of unsortedSectionKeys if it is nil. And whenever unsortedSectionKeys changes, you just set sortedSectionKeys to nil. (That solves at least some problems).
For your sorting, you need to write proper code. Use (void)sortUsingComparator:(NSComparator)cmptr to sort a mutable, or - (NSArray *)sortedArrayUsingComparator:(NSComparator)cmptr to get a sorted copy of an array.
Example:
[self.sectionKeys sortArrayUsingComparator:^NSComparisonResult(NSString* obj1, NSString* obj2) {
NSInteger year1 = obj1.integerValue;
NSInteger year2 = obj2.integerValue;
if (year1 < year2) return NSOrderedAscending;
if (year1 > year2) return NSOrderedDescending;
NSInteger season1 = 0;
if ([obj1 rangeOfString:#"spring" options:NSCaseInsensitiveSearch].location != NSNotFound)
season1 = 1;
if ([obj1 rangeOfString:#"summer" options:NSCaseInsensitiveSearch].location != NSNotFound)
season1 = 2;
if ([obj1 rangeOfString:#"fall" options:NSCaseInsensitiveSearch].location != NSNotFound)
season1 = 3;
if ([obj1 rangeOfString:#"winter" options:NSCaseInsensitiveSearch].location != NSNotFound)
season1 = 4;
NSInteger season2 = 0;
if ([obj2 rangeOfString:#"spring" options:NSCaseInsensitiveSearch].location != NSNotFound)
season2 = 1;
if ([obj2 rangeOfString:#"summer" options:NSCaseInsensitiveSearch].location != NSNotFound)
season2 = 2;
if ([obj2 rangeOfString:#"fall" options:NSCaseInsensitiveSearch].location != NSNotFound)
season2 = 3;
if ([obj2 rangeOfString:#"winter" options:NSCaseInsensitiveSearch].location != NSNotFound)
season2 = 4;
if (season1 < season2) return NSOrderedAscending;
if (season1 > season2) return NSOrderedDescending;
return NSOrderedSame;
}];
Your decision if winter is the first or last season in the year, since usually it's December to February.

Related

Simplest way to concatenate objective-C strings and achieve grammatical correctness (with 'and' conjunction)

What is the easiest and best way in Objective-C to combine a list (NSArray) of NSStrings into a single NSString separated by commas, with the grammatically correct terminal conjunction ", and " before the final item of the list?
NSArray *anArray = [NSArray arrayWithObjects:#"milk", #"butter", #"eggs", #"spam", nil];
From this array, I want the NSString #"milk, butter, eggs, and spam".
More generally, if the list is more than two items long, I want ", " between every item except the last and second-to-last (which should have ", and "). If the list is two items long, I want just the ' and ' with no comma. If the list is one item long, I want the single string from the array.
I like something as simple as:
NSString *newString = [anArray componentsJoinedByString:#", "];
But this of course omits the 'and' conjunction.
Is there a simpler and/or faster Objective-C way than the following:
- (NSString *)grammaticallyCorrectStringFromArrayOfStrings:(NSArray *)anArray {
if (anArray == nil) return nil;
int arrayCount = [anArray count];
if (arrayCount == 0) return #"";
if (arrayCount == 1) return [anArray objectAtIndex:0];
if (arrayCount == 2) return [anArray componentsJoinedByString:#" and "];
// arrayCount > 2
NSString *newString = #"";
for (NSString *thisString in anArray) {
if (thisString != [anArray objectAtIndex:0] && thisString != [anArray lastObject]) {
newString = [newString stringByAppendingString:#", "];
}
else if (thisString == [anArray lastObject]) {
newString = [newString stringByAppendingString:#", and "];
}
newString = [newString stringByAppendingString:thisString];
}
return newString;
}
For the loop, I'd probably do something like
NSMutableString *newString = [NSMutableString string];
NSUInteger lastIndex = arrayCount - 1;
[anArray enumerateObjectsUsingBlock:^(NSString *thisString, NSUInteger idx, BOOL *stop) {
if (idx != 0)
[newString appendString:#","];
if (idx == lastIndex)
[newString appendString:#" and "];
[newString appendString:thisString];
}];
Though I guess that's not really less lines.

Check for substring in string with NSString from NSArray?

So pretty much I want to check if my NSString from my NSArray is a substring of my string named imageName.
So lets say this:
My Image name is: picture5of-batman.png
My Array contains strings and one of them is: Batman
So pretty much I want to eliminate the: picture5of- part of the image name and replace it with the NSString from the NSArray.
This is how I try to do it but it never makes it to the if statement. And no my Array is not nil either. Here is the code:
for (NSString *string in superheroArray) {
if ([string rangeOfString:imageName].location != NSNotFound) {
//Ok so some string in superheroArray is equal to the file name of the image
imageName = [imageName stringByReplacingOccurrencesOfString:#"" withString:string
options:NSCaseInsensitiveSearch range:NSMakeRange(0, string.length)];
}
}
Edit1: This still does not work
for (NSString *string in superheroArray) {
if ([imageName rangeOfString:string options:NSCaseInsensitiveSearch].location != NSNotFound) {
//Ok so some string in superheroArray is equal to the file name of the image
imageName = string;
//HOW ABOUT THAT FOR EFFICIENCY :P
}
}
[imageName rangeOfString:string options: NSCaseInsensitiveSearch]
I don't see why it's not working in your code, maybe split the NSString stuff from the NSRage test.
but this work here :
NSArray *ar = [NSArray arrayWithObjects:#"Batman", #"Maurice", nil];
__block NSString *imageName = #"picture5of-batman.png";
__block NSUInteger theIndex = -1;
[ar enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSRange r = [imageName rangeOfString: obj
options: NSCaseInsensitiveSearch];
if (r.location != NSNotFound)
{
theIndex = idx;
NSString *str = [imageName pathExtension];
imageName = [(NSString *)obj stringByAppendingPathExtension:str];
// you found it, so you can stop now
*stop = YES;
}
}];
if (theIndex != -1)
{
NSLog(#"The index is : %d and new imageName == %#", theIndex, imageName);
}
And here is the NSLog statement :
2011-12-10 23:04:28.967 testSwitch1[2493:207] The index is : 0 and new imageName == Batman.png

How to sort array controller alphabetically with numbers last in objective-c?

I have an NSArrayController and I would like to sort the contents so that anything with English alphabets are sorted first and then anything with numbers and non English characters are sorted last.
For example: A, B , C ... Z, 1 , 2, 3 ... 9, 구, 결, ...
Currently I only know how to sort items in alphabetical order. Suggestions?
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[dataController setSortDescriptors: [NSArray arrayWithObject: sort]];
You can use sortedArrayUsingComparator to customize the sort algorithm to your needs. For instance, you can give precedence to symbols with this lines:
NSArray *assorted = [#"1 2 3 9 ; : 구 , 결 A B C Z ! á" componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSArray *sorted = [assorted sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
/* NSOrderedAscending, NSOrderedSame, NSOrderedDescending */
BOOL isPunct1 = [[NSCharacterSet punctuationCharacterSet] characterIsMember:[(NSString*)obj1 characterAtIndex:0]];
BOOL isPunct2 = [[NSCharacterSet punctuationCharacterSet] characterIsMember:[(NSString*)obj2 characterAtIndex:0]];
if (isPunct1 && !isPunct2) {
return NSOrderedAscending;
} else if (!isPunct1 && isPunct2) {
return NSOrderedDescending;
}
return [(NSString*)obj1 compare:obj2 options:NSDiacriticInsensitiveSearch|NSCaseInsensitiveSearch];
}];
To put English characters before non-English ones, it'd be enough to use NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch as options, no fancy algorithm required.
If you need to support iOS without blocks try sortedArrayUsingSelector.
Another solution by testing, if a string is latin1-encodeable:
test for both strings, if the first character is a latin (aka english) character
if both are, test if both starts either with a letter or a number. In both cases, leave it up the compare:
else, if one starts with the number and one with a letter, return NSOrderingAscending, if the one with letter its first, otherwise NSOrderingDescending
If both strings aren't latin, let compare: decide again
if one is latin, and one not, return NSOrderingAscending if the latin is first, otherwise NSOrderingDescending
the code
NSArray *array = [NSArray arrayWithObjects:#"peach",#"apple",#"7",#"banana",#"ananas",#"5", #"papaya",#"4",#"구",#"결",#"1" ,nil];
array = [array sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSString *s1 = [obj1 substringToIndex:1];
NSString *s2 = [obj2 substringToIndex:1];
BOOL b1 = [s1 canBeConvertedToEncoding:NSISOLatin1StringEncoding];
BOOL b2 = [s2 canBeConvertedToEncoding:NSISOLatin1StringEncoding];
if ((b1 == b2) && b1) {//both number or latin char
NSRange r1 = [s1 rangeOfCharacterFromSet:[NSCharacterSet decimalDigitCharacterSet]];
NSRange r2 = [s2 rangeOfCharacterFromSet:[NSCharacterSet decimalDigitCharacterSet]];
if (r1.location == r2.location ) { // either both start with a number or both with a letter
return [obj1 compare:obj2 options:NSDiacriticInsensitiveSearch|NSCaseInsensitiveSearch];
} else { // one starts wit a letter, the other with a number
if ([s1 rangeOfCharacterFromSet:[NSCharacterSet decimalDigitCharacterSet]].location == NSNotFound) {
return NSOrderedAscending;
}
return NSOrderedDescending;
}
} else if((b1 == b2) && !b1){ // neither latin char
return [obj1 compare:obj2 options:NSDiacriticInsensitiveSearch|NSCaseInsensitiveSearch];
} else { //one is latin char, other not
if (b1) return NSOrderedAscending;
return NSOrderedDescending;
}
}];
for (NSString *s in array) NSLog(#"%#", s);
result
ananas
apple
banana
papaya
peach
1
4
5
7
구
결
I don't think you can do that kind of sorting without defining your own comparison function.
To this aim, you could use sortedArrayUsingFunction:
[array sortedArrayUsingFunction:f context:userContext];
where f is defined as:
NSInteger f(id num1, id num2, void *context)
{
int v1 = [num1 intValue];
int v2 = [num2 intValue];
if (...)
return NSOrderedAscending;
else if (...)
return NSOrderedDescending;
else
return NSOrderedSame;
}
If you prefer not creating function for doing this you could use the block-version of the method, sortedArrayUsingComparator:
[array sortedArrayUsingComparator: ^(id obj1, id obj2) {
return NSOrderedSame;
}];
A sort descriptor based on a comparator should do the trick (note: not tested).
NSComparator cmp = ^(id str1, id str2) {
// Make your sorting
if ( /* str1 before str2 */ )
return NSOrderedAscending
else if ( /* str2 after str1 */ )
return NSOrderedDescending
else
return NSOrderedSame
};
NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey: sortKey ascending: YES comparator: cmp];
NSArrayController *ac = // ...
[ac setSortDescriptor: sd];
You of course have to define your own sort order algorithm - but this example should show how to use a sort descriptor for an array controller.
One thing is missing to answer properly to the question : NSNumericSearch
NSArray *assorted = [#"1 2 3 9 ; : 구 , 결 A B C Z ! á" componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSArray *sorted = [assorted sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
/* NSOrderedAscending, NSOrderedSame, NSOrderedDescending */
BOOL isPunct1 = [[NSCharacterSet punctuationCharacterSet] characterIsMember:[(NSString*)obj1 characterAtIndex:0]];
BOOL isPunct2 = [[NSCharacterSet punctuationCharacterSet] characterIsMember:[(NSString*)obj2 characterAtIndex:0]];
if (isPunct1 && !isPunct2) {
return NSOrderedAscending;
} else if (!isPunct1 && isPunct2) {
return NSOrderedDescending;
}
return [(NSString*)obj1 compare:obj2 options:NSDiacriticInsensitiveSearch|NSCaseInsensitiveSearch|NSNumericSearch]|;
}];

How to merge paths (NSString) in xcode?

Imagine you have two paths.
http://myserver.com/path1/path2 + /path1/path2/cache/image1.jpg = http://myserver.com/path1/path2/cache/image1.jpg
Both path strings could have more or less path components.
So what I'm asking is how to find the equal part in the strings and then remove that part from one of the strings?
You have no idea whether the "equal" parts are really equal or not. It's not uncommon to have, eg, paths like .../part1/part1/part1/...
For the mechanics of dealing with the paths, though, NSString has some nice methods -- lastPathComponent, stringByAppendindPathComponent, pathComponents, pathWithComponents, etc.
What about this:
- (NSString *)mergeStringsPrefix:(NSString *)prefix suffix:(NSString *)suffix
{
NSString *string = [NSString stringWithFormat:#"%#", prefix];
NSArray *prefixComponents = [prefix pathComponents];
NSArray *suffixComponents = [suffix pathComponents];
if ([prefixComponents count] == 0) return [string retain];
int rootIndex = [suffixComponents indexOfObject:#"/"];
int index = 1;
if (rootIndex == NSNotFound || rootIndex != 0) index = 0;
int startIndex = [prefixComponents indexOfObject:[suffixComponents objectAtIndex:index]];
if (startIndex == NSNotFound) return nil;
if ([suffixComponents count] - index < [prefixComponents count] - startIndex) return nil;
// fing length and check merge compatability
BOOL equalParts = YES;
for (int i=startIndex; i<[prefixComponents count] && equalParts; i++, index++)
{
NSString *el1 = [prefixComponents objectAtIndex:i];
NSString *el2 = [suffixComponents objectAtIndex:index];
if ([el1 compare:el2] != NSOrderedSame) equalParts = NO;
}
if (!equalParts) return nil;
// merge
for (int i=index; i<[suffixComponents count]; i++)
{
string = [string stringByAppendingFormat:#"/%#", [suffixComponents objectAtIndex:i]];
}
return [string retain];
}
This should do for you:
NSString* path1 = #"http://myserver.com/path1/path2";
NSString* path2 = #"/path1/path2/cache/image1.jpg";
NSMutableArray* path1Components = [NSMutableArray arrayWithArray:[path1 componentsSeparatedByString:#"/"]];
NSMutableArray* path2Components = [NSMutableArray arrayWithArray:[path2 componentsSeparatedByString:#"/"]];
[path2Components removeObjectAtIndex:0];
if ([path1Components containsObject:[path2Components objectAtIndex:0]]) {
NSUInteger objectIndex = [path1Components indexOfObject:[path2Components objectAtIndex:0]];
[path1Components removeObjectsInRange:NSMakeRange(objectIndex, [path1Components count]-objectIndex)];
[path1Components addObjectsFromArray:path2Components];
NSString* mergedPath = [path1Components componentsJoinedByString:#"/"];
NSLog(#"%#",mergedPath);
}

Create Generic Function For Database Selects

I'm developing an iPhone app. I've got a function that reads data from a sqlite database and puts the results into an array. Everything works fine. Here is part of the function that fills the array:
while(sqlite3_step(compiledStatement) == SQLITE_ROW) {
NSString *aVar1 = [NSString stringWithUTF8String(char*)sqlite3_column_text(compiledStatement, 0)];
NSString *aVar2 = [NSString stringWithUTF8String(char*)sqlite3_column_text(compiledStatement, 1)];
NSArray *anArray = [NSArray arrayWithObjects:aVar1,aVar2,nil];
[returnArray addObject:anArray]
[anArray release];
}
//return the array
I want to make this function more generic so that it takes a sql statement string as a parameter, and returns a mutablearray of arrays, no matter how many columns are in the result set.
Is there a way to do this? The solution doesn't have to include arrays -- could be any collection object. I'm just looking for a way to make the function re-usable for other queries to the same database.
Couldn't you just do something like:
int numCols = sqlite3_column_count(compiledStatement);
NSMutableArray *result = [NSMutableArray array];
while(sqlite3_step(compiledStatement) == SQLITE_ROW) {
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < numCols; i++) {
[array addObject:
[NSString stringWithUTF8String:
(char *)sqlite3_column_text(compiledStatement, i)]];
}
[result addObject:array];
}
+(NSArray *)executeQueryAndReturnArray:(NSString *)query {
sqlite3_stmt *statement = nil;
const char *sql = [query UTF8String];
if (sqlite3_prepare_v2(database, sql, -1, &statement, NULL) != SQLITE_OK) {
NSLog(#"[SQLITE] Error when preparing query!");
} else {
NSMutableArray *result = [NSMutableArray array];
while (sqlite3_step(statement) == SQLITE_ROW) {
NSMutableArray *row = [NSMutableArray array];
for (int i = 0; i < sqlite3_column_count(statement); i++) {
int colType = sqlite3_column_type(statement, i);
id value;
if (colType == SQLITE_TEXT) {`enter code here`
const unsigned char *col = sqlite3_column_text(statement, i);
value = [NSString stringWithFormat:#"%s", col];
} else if (colType == SQLITE_INTEGER) {
int col = sqlite3_column_int(statement, i);
value = [NSNumber numberWithInt:col];
} else if (colType == SQLITE_FLOAT) {
double col = sqlite3_column_double(statement, i);
value = [NSNumber numberWithDouble:col];
} else if (colType == SQLITE_NULL) {
value = [NSNull null];
} else {
NSLog(#"[SQLITE] UNKNOWN DATATYPE");
}
[row addObject:value];
}
[result addObject:row];
}
return result;
}
return nil;
}