diff --git a/Source/NSString.m b/Source/NSString.m index e446a8ca59..de042f075e 100644 --- a/Source/NSString.m +++ b/Source/NSString.m @@ -6371,7 +6371,7 @@ - (void) enumerateSubstringsInRange: (NSRange)range else if (substringType == NSStringEnumerationByWords || substringType == NSStringEnumerationBySentences) { - #if GS_USE_ICU +#if GS_USE_ICU // These macros may be useful elsewhere. #define GS_U_HANDLE_ERROR(errorCode, description) do { \ if (U_FAILURE(errorCode)) { \ @@ -6389,6 +6389,7 @@ - (void) enumerateSubstringsInRange: (NSRange)range UErrorCode errorCode = U_ZERO_ERROR; const char *locale; UBreakIterator *breakIterator; + int32_t start, end; [self getCharacters: characters range: range]; /* @ss=standard will use lists of common abbreviations, @@ -6405,55 +6406,42 @@ - (void) enumerateSubstringsInRange: (NSRange)range length, // textLength &errorCode); GS_U_HANDLE_ERROR(errorCode, @"opening ICU break iterator"); - ubrk_first(breakIterator); - while (YES) - { - // Make sure it's a valid substring. - BOOL isValidSubstring = YES; - int32_t nextPosition; - NSUInteger nextLocation; - NSRange enclosingRange; - - if (byWords) - { - int32_t ruleStatus = ubrk_getRuleStatus(breakIterator); - /* From ICU User Guide: - * A status value UBRK_WORD_NONE indicates that the boundary - * does not start a word or number. - * However, valid words seem to be UBRK_WORD_NONE, and invalid - * words seem to be UBRK_WORD_NONE_LIMIT. - */ - isValidSubstring = ruleStatus != UBRK_WORD_NONE_LIMIT; -// NSLog(@"Status for position %d (%d): %d", (int)currentLocation, (int)ubrk_current(breakIterator), (int) ruleStatus); - } - - nextPosition = ubrk_next(breakIterator); - if (nextPosition == UBRK_DONE) break; - nextLocation = range.location + nextPosition; - // Same as substringRange - enclosingRange - = NSMakeRange(currentLocation, nextLocation - currentLocation); + // FIXME: Implement reverse enumeration by using ubrk_last and ubrk_previous + start = ubrk_first(breakIterator); + for (end = ubrk_next(breakIterator); end != UBRK_DONE; start = end, end = ubrk_next(breakIterator)) + { + BOOL isValidSubstring = YES; + NSUInteger nextLocation; + NSRange enclosingRange; - if (isValidSubstring) - { + if (byWords) + { + int32_t ruleStatus; + + ruleStatus = ubrk_getRuleStatus(breakIterator); + isValidSubstring = ruleStatus != UBRK_WORD_NONE; + } + + nextLocation = range.location + end; + enclosingRange = NSMakeRange(currentLocation, end - start); + currentLocation = nextLocation; + + if (isValidSubstring) + { CALL_BLOCK(block, - substringNotRequired - ? nil - : [self substringWithRange: enclosingRange], + substringNotRequired ? nil : [self substringWithRange: enclosingRange], enclosingRange, enclosingRange, &stop); if (stop) break; - } - - currentLocation = nextLocation; - } - #else + } + } +#else NSWarnLog(@"NSStringEnumerationByWords and NSStringEnumerationBySentences" @" are not supported when GNUstep-base is compiled without ICU."); return; - #endif +#endif } else if (substringType == NSStringEnumerationByCaretPositions) { diff --git a/Tests/base/NSString/enumerateSubstringsInRange.m b/Tests/base/NSString/enumerateSubstringsInRange.m index dac3f6dc69..3a7e49dca9 100644 --- a/Tests/base/NSString/enumerateSubstringsInRange.m +++ b/Tests/base/NSString/enumerateSubstringsInRange.m @@ -1,97 +1,270 @@ #import "ObjectTesting.h" #import #import +#import #if defined(__has_extension) && __has_extension(blocks) -int main (int argc, const char * argv[]) + +void +testMutationAffectingSubsequentCall() +{ + NSMutableString *mutableString; + NSMutableArray *results; + NSArray *expectedResults; + NSRange range; + + BOOL correctResults; + BOOL correctCallCount; + __block NSUInteger callCount = 0; + + mutableString = [NSMutableString stringWithString:@"Hello World"]; + results = [NSMutableArray array]; + range = NSMakeRange(0, mutableString.length); + expectedResults = @[ @"Hello", @"World" ]; + + [mutableString + enumerateSubstringsInRange:range + options:NSStringEnumerationByWords + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + [results addObject:substring]; + callCount++; + + if ([substring isEqualToString:@"Hello"]) + { + // Simulate a mutation that affects subsequent + // enumeration "Hello " is changed to "Hello" + [mutableString + deleteCharactersInRange:NSMakeRange( + substringRange.location + + substringRange.length, + 1)]; + *stop = YES; + } + }]; + + [mutableString + enumerateSubstringsInRange:NSMakeRange(5, mutableString.length - 5) + options:NSStringEnumerationByWords + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + [results addObject:substring]; + }]; + + correctResults = [results isEqualToArray:expectedResults]; + correctCallCount = (callCount == 1); // Ensure only one call before stopping + + PASS(correctResults && correctCallCount, + "Enumeration should adjust correctly after string mutation and handle " + "subsequent calls appropriately."); +} + +void +testBasicFunctionality() +{ + NSString *string; + NSMutableArray *results; + NSArray *expected; + NSRange range; + BOOL result; + + string = @"Hello World"; + results = [NSMutableArray array]; + range = NSMakeRange(0, string.length); + expected = @[ @"Hello", @"World" ]; + + [string + enumerateSubstringsInRange:range + options:NSStringEnumerationByWords + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + [results addObject:substring]; + }]; + + PASS_EQUAL(results, expected, "Should correctly enumerate words."); +} + +void +testEmptyRange() +{ + NSString *string; + NSMutableArray *results; + NSRange range; + + string = @"Hello World"; + results = [NSMutableArray array]; + range = NSMakeRange(0, 0); + + [string + enumerateSubstringsInRange:range + options:NSStringEnumerationByWords + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + [results addObject:substring]; + }]; + + PASS(results.count == 0, + "No substrings should be enumerated for an empty range."); +} + +void testLocationOffset() { + NSString *string; + NSMutableArray *results; + NSArray *expected; + NSRange range; + + string = @"Hello World Continued"; + results = [NSMutableArray array]; + range = NSMakeRange(6, [string length] - 6); + expected = @[ @"World", @"Continued"]; + + [string + enumerateSubstringsInRange:range + options:NSStringEnumerationByWords + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + [results addObject:substring]; + }]; + + PASS_EQUAL(results, expected, "Should correctly enumerate words with location offset."); +} + +void +testStoppingEnumeration() { - NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + NSString *string; + NSMutableArray *results; + NSRange range; + + string = @"Hello World"; + results = [NSMutableArray array]; + range = NSMakeRange(0, [string length]); + + __block BOOL didStop = NO; + + [string + enumerateSubstringsInRange:range + options:NSStringEnumerationByWords + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + if ([substring isEqualToString:@"Hello"]) + { + *stop = YES; + didStop = YES; + } + [results addObject:substring]; + }]; + + PASS(didStop && [results count] == 1 && [results[0] isEqualToString:@"Hello"], + "Enumeration should stop after 'Hello'."); +} + +int +main(int argc, const char *argv[]) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; START_SET("Enumerate substrings by lines"); - NSString* s1 = @"Line 1\nLine 2"; + NSString *s1 = @"Line 1\nLine 2"; __block NSUInteger currentIteration = 0; - [s1 enumerateSubstringsInRange:(NSRange){ - .location = 0, - .length = [s1 length] - } options: NSStringEnumerationByLines - usingBlock: ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { - NSLog(@"Substring range: {.location=%ld, .length=%ld}", substringRange.location, substringRange.length); - NSLog(@"Enclosing range: {.location=%ld, .length=%ld}", enclosingRange.location, enclosingRange.length); - NSLog(@"Substring: %@", substring); - // *stop = YES; - if(currentIteration == 0) PASS([substring isEqual: @"Line 1"], "First line of \"Line 1\\nLine 2\" is \"Line 1\""); - if(currentIteration == 1) PASS([substring isEqual: @"Line 2"], "Second line of \"Line 1\\nLine 2\" is \"Line 2\""); - currentIteration++; - }]; - PASS(currentIteration == 2, "There are only two lines in \"Line 1\\nLine 2\""); + [s1 + enumerateSubstringsInRange:(NSRange){.location = 0, .length = [s1 length]} + options:NSStringEnumerationByLines + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + // *stop = YES; + if (currentIteration == 0) + PASS([substring isEqual:@"Line 1"], + "First line of \"Line 1\\nLine 2\" is \"Line 1\""); + if (currentIteration == 1) + PASS( + [substring isEqual:@"Line 2"], + "Second line of \"Line 1\\nLine 2\" is \"Line 2\""); + currentIteration++; + }]; + PASS(currentIteration == 2, + "There are only two lines in \"Line 1\\nLine 2\""); END_SET("Enumerate substrings by lines"); START_SET("Enumerate substrings by paragraphs"); - NSString* s1 = @"Paragraph 1\nParagraph 2"; + NSString *s1 = @"Paragraph 1\nParagraph 2"; __block NSUInteger currentIteration = 0; - [s1 enumerateSubstringsInRange:(NSRange){ - .location = 0, - .length = [s1 length] - } options: NSStringEnumerationByParagraphs - usingBlock: ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { - NSLog(@"Substring range: {.location=%ld, .length=%ld}", substringRange.location, substringRange.length); - NSLog(@"Enclosing range: {.location=%ld, .length=%ld}", enclosingRange.location, enclosingRange.length); - NSLog(@"Substring: %@", substring); - // *stop = YES; - if(currentIteration == 0) PASS([substring isEqual: @"Paragraph 1"], "First paragraph of \"Paragraph 1\\nParagraph 2\" is \"Paragraph 1\""); - if(currentIteration == 1) PASS([substring isEqual: @"Paragraph 2"], "Second paragraph of \"Paragraph 1\\nParagraph 2\" is \"Paragraph 2\""); - currentIteration++; - }]; - PASS(currentIteration == 2, "There are only two paragraphs in \"Paragraph 1\\nParagraph 2\""); + [s1 enumerateSubstringsInRange:(NSRange){.location = 0, .length = [s1 length]} + options:NSStringEnumerationByParagraphs + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + // *stop = YES; + if (currentIteration == 0) + PASS([substring isEqual:@"Paragraph 1"], + "First paragraph of \"Paragraph 1\\nParagraph " + "2\" is \"Paragraph 1\""); + if (currentIteration == 1) + PASS([substring isEqual:@"Paragraph 2"], + "Second paragraph of \"Paragraph 1\\nParagraph " + "2\" is \"Paragraph 2\""); + currentIteration++; + }]; + PASS(currentIteration == 2, + "There are only two paragraphs in \"Paragraph 1\\nParagraph 2\""); END_SET("Enumerate substrings by paragraphs"); START_SET("Enumerate substrings by words"); - NSString* s1 = @"Word1 word2."; + testBasicFunctionality(); + testEmptyRange(); + testLocationOffset(); + testStoppingEnumeration(); + testMutationAffectingSubsequentCall(); + + NSString *s1 = @"Word1 word2."; __block NSUInteger currentIteration = 0; - [s1 enumerateSubstringsInRange:(NSRange){ - .location = 0, - .length = [s1 length] - } options: NSStringEnumerationByWords - usingBlock: ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { - NSLog(@"Substring range: {.location=%ld, .length=%ld}", substringRange.location, substringRange.length); - NSLog(@"Enclosing range: {.location=%ld, .length=%ld}", enclosingRange.location, enclosingRange.length); - NSLog(@"Substring: %@", substring); - // *stop = YES; - if(currentIteration == 0) PASS([substring isEqual: @"Word1"], "First word of \"Word1 word2.\" is \"Word1\""); - if(currentIteration == 1) PASS([substring isEqual: @"word2"], "Second word of \"Word1 word2.\" is \"word2\""); - currentIteration++; - }]; + [s1 enumerateSubstringsInRange:(NSRange){.location = 0, .length = [s1 length]} + options:NSStringEnumerationByWords + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + // *stop = YES; + if (currentIteration == 0) + PASS([substring isEqual:@"Word1"], + "First word of \"Word1 word2.\" is \"Word1\""); + if (currentIteration == 1) + PASS([substring isEqual:@"word2"], + "Second word of \"Word1 word2.\" is \"word2\""); + currentIteration++; + }]; PASS(currentIteration == 2, "There are only two words in \"Word1 word2.\""); END_SET("Enumerate substrings by words"); START_SET("Enumerate substrings by sentences"); - NSString* s1 = @"Sentence 1. Sentence 2."; + NSString *s1 = @"Sentence 1. Sentence 2."; __block NSUInteger currentIteration = 0; - [s1 enumerateSubstringsInRange:(NSRange){ - .location = 0, - .length = [s1 length] - } options: NSStringEnumerationBySentences - usingBlock: ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { - NSLog(@"Substring range: {.location=%ld, .length=%ld}", substringRange.location, substringRange.length); - NSLog(@"Enclosing range: {.location=%ld, .length=%ld}", enclosingRange.location, enclosingRange.length); - NSLog(@"Substring: %@", substring); - // *stop = YES; - if(currentIteration == 0) PASS([substring isEqual: @"Sentence 1. "], "First sentence of \"Sentence 1. Sentence 2.\" is \"Sentence 1. \""); - if(currentIteration == 1) PASS([substring isEqual: @"Sentence 2."], "Second sentence of \"Sentence 1. Sentence 2.\" is \"Sentence 2.\""); - currentIteration++; - }]; - PASS(currentIteration == 2, "There are only two sentences in \"Sentence 1. Sentence 2."); + [s1 enumerateSubstringsInRange:(NSRange){.location = 0, .length = [s1 length]} + options:NSStringEnumerationBySentences + usingBlock:^(NSString *substring, NSRange substringRange, + NSRange enclosingRange, BOOL *stop) { + // *stop = YES; + if (currentIteration == 0) + PASS([substring isEqual:@"Sentence 1. "], + "First sentence of \"Sentence 1. Sentence 2.\" " + "is \"Sentence 1. \""); + if (currentIteration == 1) + PASS([substring isEqual:@"Sentence 2."], + "Second sentence of \"Sentence 1. Sentence 2.\" " + "is \"Sentence 2.\""); + currentIteration++; + }]; + PASS(currentIteration == 2, + "There are only two sentences in \"Sentence 1. Sentence 2."); END_SET("Enumerate substrings by sentences"); [pool drain]; - + return 0; } #else -int main (int argc, const char * argv[]) +int +main(int argc, const char *argv[]) { return 0; }