0

I'm breaking my head on why descending order sort is not working with the following code. I wanted to limit by top 5 scores and other logic. The scores would look like this: 22/30, 12/18, 34/38, 23/32 etc. I added/removed SortDescriptor to sort by descending order and it seems to work for the first 3 items but then is not sorting properly. Can somebody help?

- (NSMutableArray*) method1:(NSString *) mode byScore: (NSString *) score
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSMutableArray *array = [[defaults objectForKey:mode]mutableCopy];

    if (!array)
    {
        array = [[NSMutableArray alloc] init];
    }
    NSLog(@"The content of array is%@", array);

    if ([array count] < 5)
    {
      if (![array containsObject:score])
      {
          [array addObject:score];
          // Need to sort here. But not too sure this is the right place
          NSLog(@"The content of the sorted array upto 5 is%@", array); 
      }
    }
    else
    {
       if (![array containsObject:score])
       {
           if ([array lastObject] < score)
           {
               [array addObject:score];
               // Need to sort here before I remove the last object                   
               [array removeLastObject];
               NSLog(@"The content of the sorted array else is%@",array);
           }
       }
    }
    [defaults setObject:array forKey:mode];
    [defaults synchronize];
    // I want the array in NSUserDefaults to be sorted in desc order
    // don't know what to return here ==> the array object or the defaults object cast    to NSMutableArray?
}
Hema
  • 947
  • 1
  • 8
  • 13
  • 1
    Were is the code for sorting? – Anoop Vaidya Dec 30 '12 at 06:58
  • 1
    Definitely do not return "the defaults object cast to NSMutableArray". Returning `array` makes some sense, but the fact that you're unsure about what should be returned indicates that you may not need to return anything at all and that the return type should be `void`. – Nate Chandler Dec 30 '12 at 07:01
  • I updated my question. I was using: NSSortDescriptor class to sort in descending order but didn't work – Hema Dec 30 '12 at 07:01

2 Answers2

1

Helper function

static NSComparisonResult CompareFloats( float a, float b )
{
    if ( a < b ) { return NSOrderedAscending ; }
    else if ( a > b ) { return NSOrderedDescending ; }
    return NSOrderedSame ;
}

Category on NSString

@implementation NSString (Stuff)

-(float)floatValueForFraction
{
    NSArray * components = [ self componentsSeparatedByString:@"/" ] ;
    return [ components[0] floatValue ] / [ components[1] floatValue ] ;
}

@end

Your method:

- (void)addScore:(NSString*)score forMode:(NSString*)mode
{
    NSUserDefaults * defaults = [ NSUserDefaults standardUserDefaults ] ;
    NSArray * scores = [ defaults objectForKey:mode ] ;

    scores = scores ? [ scores arrayByAddingObject:score ] : @[ score ] ;
    scores = [ scores sortedArrayUsingComparator:^(NSString * a, NSString * b){
        return CompareFloats( [ a floatValueForFraction ], [ b floatValueForFraction ] ) ;
    }]

    if ( scores.count > 5 ) { scores = [ scores subarrayWithRange:(NSRange){ .length = 5 } ] ; }

    [ default setObject:scores forKey:mode ] ;
}

If you want the updated high scores after calling this method, just use [ [ NSUserDefaults standardUserDefaults ] objectForKey:<mode> ]. It's better to have your methods just do one thing.

nielsbot
  • 15,922
  • 4
  • 48
  • 73
0

One approach to sorting array:

First define a block getNumeratorAndDenominatorFromScoreString as follows:

BOOL (^getNumeratorAndDenominatorFromScoreString)(NSString *, NSInteger *, NSInteger *) = ^(NSString *scoreString, NSInteger *numeratorOut, NSInteger *denominatorOut) {
    BOOL res = NO;
    NSArray *components = [scoreString componentsSeparatedByString:@"/"];
    if (components && 
        [components count] == 2) {
        res = YES;
        if (numeratorOut) {
            NSNumber *numeratorNumber = [components objectAtIndex:0];
            *numeratorOut = [numeratorNumber integerValue];
        }
        if (denominatorOut) {
            NSNumber *denominatorNumber = [components objectAtIndex:1];
            *denominatorOut = [denominatorNumber integerValue];
        }
    }
    return res;
};

Then use this block together with -[NSArray sortedArrayUsingComparator] to sort array:

NSArray *sortedArray = [array sortedArrayUsingComparator: ^(id obj1, id obj2) {
    NSComparisonResult res = NSOrderedSame;

    NSString *score1 = (NSString *)obj1;
    NSString *score2 = (NSString *)obj2;

    NSInteger numerator1, denominator1, numerator2, denominator2;
    BOOL res1, res2;

    res1 = getNumeratorAndDenominatorFromScoreString(score1, &numerator1, &denominator1);
    res2 = getNumeratorAndDenominatorFromScoreString(score2, &numerator2, &denominator2);

    if (res1
        && res2) {
        CGFloat value1 = ((CGFloat)numerator1)/((CGFloat)denominator1);
        CGFloat value2 = ((CGFloat)numerator2)/((CGFloat)denominator2);

        if (value1 > value2) {
            res = NSOrderedDescending;
        } else if (value1 < value2) {
            res = NSOrderedAscending;
        }
    }
    return res;
}];

This will order array from least to greatest. To order from greatest to least, just replace

if (value1 > value2) {
    res = NSOrderedDescending;
} else if (value1 < value2) {
    res = NSOrderedAscending;
}

with

if (value1 > value2) {
    res = NSOrderedAscending;
} else if (value1 < value2) {
    res = NSOrderedDescending;
}

A readable structure for this method would be, in [mostly not] pseudocode

- (void)addScoreToHighscores:(NSString *)score withMethod:(NSString *)mode
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSArray *currentHighscores = [defaults arrayForKey:mode];
    if (!currentHighscores) currentHighscores = [NSArray array];

    if (![currentHighscores containsObject:score]) {
        currentHighscores = [currentHighscores arrayByAddingObject:score];

        //sort currentHighscores: adapt the above code so that we have
        BOOL (^getNumeratorAndDenominatorFromScoreString)(NSString *, NSInteger *, NSInteger *) = //as above
        NSArray *newHighscores = [currentHighscores sortedArrayUsingComparator:^(id obj1, id obj2) {
            //as above
        }];

        //truncate newHighscores
        if ([newHighscores count] > 5) {
            newHighscores = [newHighscores subarrayWithRange:NSMakeRange(0,5)];
        }

        [defaults setObject:newHighscores forKey:mode];
    } else {
        //since score is already in currentHighscores, we're done.
        return;
    }
}

If you need to screen out scores the strings for which are not equal but the evaluations of the fractions for which are equal (@"1/2" and @"5/10"), you'll need to be more clever.


Here is the full code sketched out above:

- (void)addScoreToHighscores:(NSString *)score withMethod:(NSString *)mode
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    NSArray *currentHighscores = [defaults arrayForKey:mode];
    if (!currentHighscores) currentHighscores = [NSArray array];

    if (![currentHighscores containsObject:score]) {
        currentHighscores = [currentHighscores arrayByAddingObject:score];

        //sort currentHighscores: adapt the above code so that we have
        BOOL (^getNumeratorAndDenominatorFromScoreString)(NSString *, NSInteger *, NSInteger *) = ^(NSString *scoreString, NSInteger *numeratorOut, NSInteger *denominatorOut) {
            BOOL res = NO;
            NSArray *components = [scoreString componentsSeparatedByString:@"/"];
            if (components && 
                [components count] == 2) {
                res = YES;
                if (numeratorOut) {
                    NSNumber *numeratorNumber = [components objectAtIndex:0];
                    *numeratorOut = [numeratorNumber integerValue];
                }
                if (denominatorOut) {
                    NSNumber *denominatorNumber = [components objectAtIndex:1];
                    *denominatorOut = [denominatorNumber integerValue];
                }
            }
            return res;
        };

        NSArray *newHighscores = [currentHighscores sortedArrayUsingComparator:^(id obj1, id obj2) {
            NSComparisonResult res = NSOrderedSame;

            NSString *score1 = (NSString *)obj1;
            NSString *score2 = (NSString *)obj2;

            NSInteger numerator1, denominator1, numerator2, denominator2;
            BOOL res1, res2;

            res1 = getNumeratorAndDenominatorFromScoreString(score1, &numerator1, &denominator1);
            res2 = getNumeratorAndDenominatorFromScoreString(score2, &numerator2, &denominator2);

            if (res1
                && res2) {
                CGFloat value1 = ((CGFloat)numerator1)/((CGFloat)denominator1);
                CGFloat value2 = ((CGFloat)numerator2)/((CGFloat)denominator2);

                if (value1 > value2) {
                    res = NSOrderedDescending;
                } else if (value1 < value2) {
                    res = NSOrderedAscending;
                }
            }
            return res;
        }];

        //truncate newHighscores
        if ([newHighscores count] > 5) {
            newHighscores = [newHighscores subarrayWithRange:NSMakeRange(0,5)];
        }

        [defaults setObject:newHighscores forKey:mode];
    } else {
        //since score is already in currentHighscores, we're done.
        return;
    }
}
Nate Chandler
  • 4,533
  • 1
  • 23
  • 32
  • I think this solution might better work for me except that I have condensed the code. As Nate Chandler mentioned, currentHighscores = [currentHighscores arrayByAddingObject:score]; is not doing anything. Please help – Hema Dec 31 '12 at 04:25
  • In the latest edit, I added the full code for the method `-addScoreToHighscores:withMethod:` that I'd sketched above. – Nate Chandler Dec 31 '12 at 04:37
  • An improvement which could be made would be to add a category method to `NSArray` named something like `-pfx_containsFractionStringWithFloatValueEqualToFloatValueForFractionString:` (replace `pfx` with your project's prefix) which would return `YES` for @"1/2" not just if `@"1/2"` were in `currentHighscores` but also if `@"2/4"` or `@"9/18"`, etc., were in `currentHighscores`. The line `if (![currentHighscores containsObject:score]) {` would then be replaced with `if (![currentHighscores pfx_containsFractionStringWithFloatValueEqualToFloatValueForFractionString:score]) {`. – Nate Chandler Dec 31 '12 at 04:44
  • Excellent. Works wonders!. Since I'm not familiar with blocks yet, I have consolidated the code. Thanks anyways! – Hema Dec 31 '12 at 06:20
  • Just a small change. I'm keeping scores that can be still be reduced that match the same existing scores. Say for example, if I have a high score of 1/2, when I get 11/22, 2/4, 9/18 etc, I take everything because I consider these are distinct scores. But, when I get the same score (9/18, 1/2, 11/22, 2/4 etc), I disregard them – Hema Dec 31 '12 at 06:22
  • Ahh, excellent! Then that's a wrinkle that doesn't need to be dealt with. Do you have a preference for ordering scores like these (10/20, 3/6, 2/4, etc.)? – Nate Chandler Dec 31 '12 at 06:25
  • I'll let the runtime decide. I tested it with 3 scores along with 11/22 and 1/2 and it ordered all others in desc order and then 11/22 and then 1/2. But then when I added 3/4, it removed 1/2 which is good. Btw, what about memory leaks?. I'm using ARC. Do I have set the array objects to release in the method? – Hema Dec 31 '12 at 06:30
  • Btw, I'm a good C# developer and lot of these things are piece of cake. I'm struggling with Objective-C and always admire C# – Hema Dec 31 '12 at 06:31
  • Yeah, I admit I'm more than a bit envious of C#'s `await`. For _most_ things, ARC completely takes care of memory management. That's the case here, but it never hurts to run the [static analyzer](http://developer.apple.com/library/mac/#recipes/xcode_help-source_editor/Analyze/Analyze.html) or to use the [leaks instrument](http://stackoverflow.com/questions/5305906/xcode-4-running-the-leaks-instrument). – Nate Chandler Dec 31 '12 at 06:38
  • By the way, the `getNumeratorAndDenominatorFromScoreString` block behaves like and is being used as a function with limited scope: it's not capturing any variables. You could easily transform it into a regular old C function, or alternatively a category method on `NSString`. Seemed sensible to go with the extremely limited scope, though, since it doesn't seem like the kind of thing that will get used outside this method. – Nate Chandler Dec 31 '12 at 06:43