objective-cnsmutablestringnsscanner

Why does my NSMutableString edit sometimes not work?


I'm trying to repair some mis-numbered movie subtitle files (each sub is separated by a blank line). The following code scans up to the faulty subtitle index number in a test file. If I just 'printf' the faulty old indices and replacement new indices, everything appears just as expected.

//######################################################################
-(IBAction)scanToSubIndex:(id)sender
{
    NSMutableString* tempString = [[NSMutableString alloc] initWithString:[theTextView string]];
    int textLen = (int)[tempString length];
    
    NSScanner *theScanner = [NSScanner scannerWithString:tempString];
    while ([theScanner isAtEnd] == NO)
        {
        [theScanner scanUpToString:@"\r\n\r\n" intoString:NULL];
        [theScanner scanString:@"\r\n\r\n" intoString:NULL];
        
        if([theScanner scanLocation] >= textLen)
            break;
        else
            { // remove OLD subtitle index...
            NSString *oldNumStr;
            [theScanner scanUpToString:@"\r\n" intoString:&oldNumStr];
printf("old number:%s\n", [oldNumStr UTF8String]);                
            NSRange range = [tempString rangeOfString:oldNumStr];
                [tempString deleteCharactersInRange:range];

            // ...and insert SEQUENTIAL index
            NSString *newNumStr = [self changeSubIndex];
printf("new number:%s\n\n", [newNumStr UTF8String]);
                [tempString insertString:newNumStr atIndex:range.location];
            }
        }
printf("\ntempString\n\n:%s\n", [tempString UTF8String]);
}

//######################################################################
-(NSString*)changeSubIndex
{
    static int newIndex = 1;
    // convert int to string and return...
    NSString *numString = [NSString stringWithFormat:@"%d", newIndex];
        ++newIndex;
    
return numString;
}

When I attempt to write the new indices to the mute string however, I end up with disordered results like this:

sub 1

sub 2

sub 3

sub 1

sub 5

sub 6

sub 7

sub 5

sub 9

sub 7

sub 8

An interesting observation (and possible clue?) is that when I reach subtitle number 1000, every number gets written to the mutable string in sequential order as required. I've been struggling with this for a couple of weeks now, and I can't find any other similar questions on SO. Any help much appreciated :-)


Solution

  • NSScanner & NSMutableString

    NSMutableString is a subclass of NSString. In other words, you can pass NSMutableString at places where the NSString is expected. But it doesn't mean you're allowed to modify it.

    scannerWithString: expects NSString. Translated to human language - I expect a string and I also do expect that the string is read-only (wont be modified).

    In other words - your code is considered to be a programmer error - you give something to the NSScanner, NSScanner expects immutable string and you're modifying it.

    We don't know what the NSScanner class is doing under the hood. There can be buffering or any other kind of optimization.

    Even if you will be lucky with the mentioned scanLocation fix (in the comments), you shouldn't rely on it, because the under the hood implementation can change with any new release.

    Don't do this. Not just here, but everywhere where you see immutable data type.

    (There're situations where you can do it, but then you should really know what the under the hood implementation is doing, be certain that it wont be modified, etc. But generally speaking, it's not a good idea unless you know what you're doing.)

    Sample

    This sample code is based on the following assumptions:

    @import Foundation;
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface SubRipText : NSObject
    
    + (NSString *)fixSubtitleIndexes:(NSString *)string;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    @implementation SubRipText
    
    + (NSString *)fixSubtitleIndexes:(NSString *)string {
        NSMutableString *result = [@"" mutableCopy];
        
        __block BOOL nextLineIsIndex = YES;
        __block NSUInteger index = 1;
        
        [string enumerateLinesUsingBlock:^(NSString * _Nonnull line, BOOL * _Nonnull stop) {
            if (nextLineIsIndex) {
                [result appendFormat:@"%lu\r\n", (unsigned long)index];
                index++;
                nextLineIsIndex = NO;
                return;
            }
            
            [result appendFormat:@"%@\r\n", line];
            
            nextLineIsIndex = line.length == 0;
        }];
        
        return result;
    }
    
    @end
    

    Usage:

    NSString *test = @"29\r\n"
    "00:00:00,498 --> 00:00:02,827\r\n"
    "Hallo\r\n"
    "\r\n"
    "4023\r\n"
    "00:00:02,827 --> 00:00:06,383\r\n"
    "This is two lines,\r\n"
    "subtitles rocks!\r\n"
    "\r\n"
    "1234\r\n"
    "00:00:06,383 --> 00:00:09,427\r\n"
    "Maybe not,\r\n"
    "just learn English :)\r\n";
    
    NSString *result = [SubRipText fixSubtitleIndexes:test];
    
    NSLog(@"%@", result);
    

    Output:

    1
    00:00:00,498 --> 00:00:02,827
    Hallo
    
    2
    00:00:02,827 --> 00:00:06,383
    This is two lines,
    subtitles rocks!
    
    3
    00:00:06,383 --> 00:00:09,427
    Maybe not,
    just learn English :)
    

    There're other ways how to achieve this, but you should think about readability, speed of writing, speed of running, ... Depends on your usage - how many of them are you going to fix, etc.