// Copyright 1997-2000 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import <OmniFoundation/NSString-OFExtensions.h>

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>

#import <OmniFoundation/OFStringScanner.h>
#import <OmniFoundation/NSMutableString-OFExtensions.h>
#import <OmniFoundation/NSThread-OFExtensions.h>
#import <OmniFoundation/NSFileManager-OFExtensions.h>
#import "NSObject-OFExtensions.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniFoundation/OpenStepExtensions.subproj/NSString-OFExtensions.m,v 1.40 2000/12/19 21:55:32 wiml Exp $")

@implementation NSString (OFExtensions)

+ (NSString *)stringWithData:(NSData *)data encoding:(NSStringEncoding)encoding;
{
    return [[[self alloc] initWithData:data encoding:encoding] autorelease];
}

+ (NSString *)abbreviatedStringForBytes:(unsigned long long)bytes;
{
    double kb, mb, gb, tb, pb;
    
    if (bytes < 1024)
        return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%d bytes", @"OmniFoundation", [self bundle], abbreviated string for bytes format), (int)bytes];
    kb = bytes / 1024.0;
    if (kb < 1024.0)
        return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%0.1f KB", @"OmniFoundation", [self bundle], abbreviated string for bytes format), kb];
    mb = kb / 1024.0;
    if (mb < 1024.0)
        return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%0.1f MB", @"OmniFoundation", [self bundle], abbreviated string for bytes format), mb];
    gb = mb / 1024.0;
    if (gb < 1024.0)
        return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%0.1f GB", @"OmniFoundation", [self bundle], abbreviated string for bytes format), gb];
    tb = gb / 1024.0;
    if (tb < 1024.0)
        return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%0.1f TB", @"OmniFoundation", [self bundle], abbreviated string for bytes format), tb];
    pb = tb / 1024.0;
    return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%0.1f PB", @"OmniFoundation", [self bundle], abbreviated string for bytes format), pb];
}

+ (NSString *)humanReadableStringForTimeInterval:(NSTimeInterval)timeInterval;
{
    NSString *intervalString;
    unsigned long int days, hours, minutes, seconds;
    BOOL inThePast = NO;

    timeInterval = rint(timeInterval);
    if (timeInterval < 0) {
        inThePast = YES;
        timeInterval = -timeInterval;
    }
    days = timeInterval / (24 * 60 * 60);
    timeInterval -= days * (24 * 60 * 60);
    hours = timeInterval / (60 * 60);
    timeInterval -= hours * (60 * 60);
    minutes = timeInterval / 60;
    timeInterval -= minutes * 60;
    seconds = timeInterval;
    
    if (days > 1)
        intervalString = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%ld days, %ld:%02ld:%02ld", @"OmniFoundation", [self bundle], humanReadableStringForTimeInterval), days, hours, minutes, seconds];
    else if (days == 1)
        intervalString = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%ld day, %ld:%02ld:%02ld", @"OmniFoundation", [self bundle], humanReadableStringForTimeInterval), days, hours, minutes, seconds];
    else if (hours > 0)
        intervalString = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%ld:%02ld:%02ld", @"OmniFoundation", [self bundle], humanReadableStringForTimeInterval), hours, minutes, seconds];
    else if (minutes > 0)
        intervalString = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%ld:%02ld", @"OmniFoundation", [self bundle], humanReadableStringForTimeInterval), minutes, seconds];
    else if (seconds > 0)
        intervalString = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%ld seconds", @"OmniFoundation", [self bundle], humanReadableStringForTimeInterval), seconds];
    else
        intervalString = NSLocalizedStringFromTableInBundle(@"right now", @"OmniFoundation", [self bundle], humanReadableStringForTimeInterval);

    if (!inThePast)
        return intervalString;
    else
        return [intervalString stringByAppendingString:NSLocalizedStringFromTableInBundle(@" ago", @"OmniFoundation", [self bundle], humanReadableStringForTimeInterval)];
}

+ (NSString *)spacesOfLength:(unsigned int)aLength;
{
    static NSMutableString *spaces = nil;
    static NSLock *spacesLock;
    static unsigned int spacesLength;

    if (!spaces) {
	spaces = [@"                " mutableCopy];
	spacesLength = [spaces length];
        spacesLock = [[NSLock alloc] init];
    }
    if (spacesLength < aLength) {
        [spacesLock lock];
        while (spacesLength < aLength) {
            [spaces appendString:spaces];
            spacesLength += spacesLength;
        }
        [spacesLock unlock];
    }
    return [spaces substringToIndex:aLength];
}

+ (NSString *)stringWithCharacter:(unichar)aCharacter;
{
    return [[[NSString alloc] initWithCharacters:&aCharacter length:1] autorelease];
}

+ (NSString *)stringWithStrings:(NSString *)first, ...
{
    NSMutableString *buffer;
    NSString *prev;
    NSString *returnValue;
    va_list argList;

    buffer = [[NSMutableString alloc] init];

    va_start(argList, first);
    prev = first;
    while(prev != nil) {
        [buffer appendString:prev];
        prev = va_arg(argList, NSString *);
    }
    va_end(argList);

    returnValue = [buffer copy];
    [buffer release];
    return [returnValue autorelease];
}

+ (NSString *)horizontalEllipsisString;
{
    static NSString *string = nil;

    if (!string)
        string = [[self stringWithCharacter:0x2026] retain];

    OBPOSTCONDITION(string);

    return string;
}

+ (NSString *)leftPointingDoubleAngleQuotationMarkString;
{
    static NSString *string = nil;

    if (!string)
        string = [[self stringWithCharacter:0xab] retain];

    OBPOSTCONDITION(string);

    return string;
}

+ (NSString *)rightPointingDoubleAngleQuotationMarkString;
{
    static NSString *string = nil;

    if (!string)
        string = [[self stringWithCharacter:0xbb] retain];

    OBPOSTCONDITION(string);

    return string;
}

+ (NSString *)emdashString;
{
    static NSString *string = nil;

    if (!string)
        string = [[self stringWithCharacter:0x2014] retain];

    OBPOSTCONDITION(string);

    return string;
}

+ (NSString *)endashString;
{
    static NSString *string = nil;

    if (!string)
        string = [[self stringWithCharacter:0x2013] retain];

    OBPOSTCONDITION(string);

    return string;
}

- (BOOL)containsCharacterInSet:(NSCharacterSet *)searchSet;
{
    NSRange characterRange;

    characterRange = [self rangeOfCharacterFromSet:searchSet];
    return characterRange.length != 0;
}

- (BOOL)containsString:(NSString *)searchString options:(unsigned int)mask;
{
    return !searchString || [searchString length] == 0 || [self rangeOfString:searchString options:mask].length > 0;
}

- (BOOL)containsString:(NSString *)searchString;
{
    return !searchString || [searchString length] == 0 || [self rangeOfString:searchString].length > 0;
}

- (BOOL)isEqualToCString:(const char *)cString;
{
    if (!cString)
	return NO;
    return [self isEqualToString:[NSString stringWithCString:cString]];
}

- (BOOL)hasLeadingWhitespace;
{
    if ([self length] == 0)
	return NO;
    switch ([self characterAtIndex:0]) {
        case ' ':
        case '\t':
        case '\r':
        case '\n':
            return YES;
        default:
            return NO;
    }
}

- (BOOL)boolValue;
{
    // Should maybe later add a configurable dictionary that contains the valid YES and NO values
    if ([self isEqualToString:@"YES"] || [self isEqualToString:@"Y"] || [self isEqualToString:@"yes"] || [self isEqualToString:@"y"] || [self isEqualToString:@"1"])
        return YES;
    else
        return NO;
}

- (long long int)longLongValue;
{
    long long int longLongValue;
    NSScanner *scanner;

    scanner = [[NSScanner alloc] initWithString:self];
    if (![scanner scanLongLong:&longLongValue])
        longLongValue = 0;
    [scanner release];
    return longLongValue;
}

- (unsigned int)unsignedIntValue;
{
    return (unsigned int)[self intValue];
}

- (NSDecimal)decimalValue;
{
    return [[NSDecimalNumber decimalNumberWithString:self] decimalValue];
}

- (NSDecimalNumber *)decimalNumberValue;
{
    return [NSDecimalNumber decimalNumberWithString:self];
}

- (NSNumber *)numberValue;
{
    return [NSNumber numberWithInt:[self intValue]];
}

- (NSArray *)arrayValue;
{
    return [NSArray arrayWithObject:self];
}

- (NSDictionary *)dictionaryValue;
{
    return (NSDictionary *)[self propertyList];
}

- (NSData *)dataValue;
{
    return [self dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
}

- (NSCalendarDate *)dateValue;
{
    return [NSCalendarDate dateWithNaturalLanguageString:self];
}

#define MAX_HEX_TEXT_LENGTH 40

static inline unsigned int parseHexString(NSString *hexString, unsigned long long int *parsedHexValue)
{
    unsigned int hexLength;
    unichar hexText[MAX_HEX_TEXT_LENGTH];
    unichar hexDigit;
    unsigned int textIndex;
    unsigned long long int hexValue;
    unsigned int hexDigitsFound;

    hexLength = [hexString length];
    if (hexLength > MAX_HEX_TEXT_LENGTH)
        hexLength = MAX_HEX_TEXT_LENGTH;
    [hexString getCharacters:hexText range:NSMakeRange(0, hexLength)];

    textIndex = 0;
    hexValue = 0;
    hexDigitsFound = 0;

    while (textIndex < hexLength && isspace(hexText[textIndex])) {
        // Skip leading whitespace
        textIndex++;
    }

    if (hexText[textIndex] == '0' && hexText[textIndex + 1] == 'x') {
        // Skip leading "0x"
        textIndex += 2;
    }

    while (textIndex < hexLength) {
        hexDigit = hexText[textIndex++];

        if (hexDigit >= '0' && hexDigit <= '9') {
            hexDigit = hexDigit - '0';
        } else if (hexDigit >= 'A' && hexDigit <= 'F') {
            hexDigit = hexDigit - 'A' + 10;
        } else if (hexDigit >= 'a' && hexDigit <= 'f') {
            hexDigit = hexDigit - 'a' + 10;
        } else if (isspace(hexDigit)) {
            continue;
        } else {
            hexDigitsFound = 0;
            break;
        }
        hexDigitsFound++;
        hexValue <<= 4;
        hexValue |= hexDigit;
    }

    *parsedHexValue = hexValue;
    return hexDigitsFound;
}

- (unsigned int)hexValue;
{
    unsigned int hexDigitsParsed;
    unsigned long long int hexValue;

    hexDigitsParsed = parseHexString(self, &hexValue);
    if (hexDigitsParsed > 0) {
        // More than one hex digit parsed
        // Since we return a long and we just parsed a long long, let's be explicit about throwing away the high bits.
        return (unsigned int)(hexValue & 0xffffffff);
    } else {
        // No hex digits, use the default return value
        return 0;
    }
}

- (NSString *)stringByUppercasingAndUnderscoringCaseChanges;
{
    static NSCharacterSet *lowercaseSet, *uppercaseSet, *numberSet;
    static OFFastCharacterSet lowercaseCSBitmap, uppercaseCSBitmap, numberCSBitmap, currentCSBitmap;
    NSMutableArray *words;
    OFStringScanner *scanner;
    unsigned int wordStartIndex = 0;
    static BOOL hasInitialized = NO;

    if (![self length])
        return nil;
    
    if (!hasInitialized) {
        // Potential minor memory leak here due to multithreading
        lowercaseSet = [[NSCharacterSet lowercaseLetterCharacterSet] retain];
        uppercaseSet = [[NSCharacterSet uppercaseLetterCharacterSet] retain];
        numberSet = [[NSCharacterSet decimalDigitCharacterSet] retain];
        lowercaseCSBitmap = bitmapForCharacterSetDoRetain(lowercaseSet, YES);
        uppercaseCSBitmap = bitmapForCharacterSetDoRetain(uppercaseSet, YES);
        numberCSBitmap = bitmapForCharacterSetDoRetain(numberSet, YES);

        hasInitialized = YES;
    }

    words = [NSMutableArray array];
    scanner = [[[OFStringScanner alloc] initWithString:self] autorelease];
    
    while (scannerHasData(scanner)) {
        unichar peekedChar;

        peekedChar = scannerPeekCharacter(scanner);
        if ([lowercaseSet characterIsMember:peekedChar])
            currentCSBitmap = lowercaseCSBitmap;
        else if ([uppercaseSet characterIsMember:peekedChar])
            currentCSBitmap = uppercaseCSBitmap;
        else if ([numberSet characterIsMember:peekedChar])
            currentCSBitmap = numberCSBitmap;
        else {
            [NSException raise:NSInvalidArgumentException format:@"Character: %@, at index: %d, not found in lowercase, uppercase, or decimal digit character sets", [NSString stringWithCharacters:&peekedChar length:1], scannerScanLocation];
        }

        if (scannerScanUpToCharacterNotInCSBitmap(scanner, currentCSBitmap)) {
            unsigned int scanLocation;

            scanLocation = scannerScanLocation(scanner);
            if (currentCSBitmap == lowercaseCSBitmap || currentCSBitmap == numberCSBitmap) {
                [words addObject:[self substringWithRange:NSMakeRange(wordStartIndex, scanLocation - wordStartIndex)]];
                wordStartIndex = scanLocation;
            } else if (currentCSBitmap == uppercaseCSBitmap) {
                if (scanLocation - wordStartIndex == 1) {
                    continue;
                } else if ([numberSet characterIsMember:scannerPeekCharacter(scanner)]) {
                    [words addObject:[self substringWithRange:NSMakeRange(wordStartIndex, scanLocation - wordStartIndex)]];
                    wordStartIndex = scanLocation;
                } else {
                    scanLocation--;
                    [scanner setScanLocation:scanLocation];
                    [words addObject:[self substringWithRange:NSMakeRange(wordStartIndex, scanLocation - wordStartIndex)]];
                    wordStartIndex = scanLocation;
                }
            } else {
                OBASSERT(NO);
            }
        }
    }

    [words addObject:[self substringWithRange:NSMakeRange(wordStartIndex, scannerScanLocation(scanner) - wordStartIndex)]];

    return [[words componentsJoinedByString:@"_"] uppercaseString];
}

- (NSString *)stringByRemovingSurroundingWhitespace;
{
    static NSCharacterSet *nonWhitespace = nil;
    NSRange firstValidCharacter, lastValidCharacter;

    if (!nonWhitespace) {
        nonWhitespace = [[[NSCharacterSet characterSetWithCharactersInString:
            @" \t\r\n"] invertedSet] retain];
    }
    
    firstValidCharacter = [self rangeOfCharacterFromSet:nonWhitespace];
    if (firstValidCharacter.length == 0)
	return @"";
    lastValidCharacter = [self rangeOfCharacterFromSet:nonWhitespace options:NSBackwardsSearch];

    if (firstValidCharacter.location == 0 && lastValidCharacter.location == [self length] - 1)
	return self;
    else
	return [self substringWithRange:NSUnionRange(firstValidCharacter, lastValidCharacter)];
}

- (NSString *)stringByCollapsingWhitespaceAndRemovingSurroundingWhitespace;
{
    static BOOL initialized = NO;
    static CSBitmap whitespaceCSBitmap, nonWhitespaceCSBitmap;
    OFStringScanner *stringScanner;
    NSMutableString *collapsedString;
    BOOL firstSubstring;
    unsigned int length;

    if (!initialized) {
        NSCharacterSet *whitespace, *nonWhitespace;
        
        // Potential multithreaded leak here, since we don't lock
        whitespace = [[NSCharacterSet characterSetWithCharactersInString:@" \t\r\n"] retain];
        nonWhitespace = [[whitespace invertedSet] retain];
        whitespaceCSBitmap = bitmapForCharacterSetDoRetain(whitespace, YES);
        nonWhitespaceCSBitmap = bitmapForCharacterSetDoRetain(nonWhitespace, YES);
        initialized = YES;
    }

    length = [self length];
    if (length == 0)
        return @""; // Trivial optimization

    stringScanner = [[OFStringScanner alloc] initWithString:self];
    collapsedString = [[NSMutableString alloc] initWithCapacity:length];
    firstSubstring = YES;
    while (scannerScanUpToCharacterInCSBitmap(stringScanner, nonWhitespaceCSBitmap)) {
        NSString *nonWhitespaceSubstring;

        nonWhitespaceSubstring = [stringScanner readFullTokenWithDelimiterCSBitmap:whitespaceCSBitmap forceLowercase:NO];
        if (nonWhitespaceSubstring) {
            if (firstSubstring) {
                firstSubstring = NO;
            } else {
                [collapsedString appendString:@" "];
            }
            [collapsedString appendString:nonWhitespaceSubstring];
        }
    }
    [stringScanner release];
    return [collapsedString autorelease];
}

- (NSString *)stringByRemovingReturns;
{
    return [[self stringByRemovingString:@"\n"] stringByRemovingString:@"\r"];
}

- (NSString *)stringByRemovingString:(NSString *)removeString
{
    NSArray *lines;
    NSMutableString *newString;
    NSString *returnValue;
    unsigned int lineIndex, lineCount;

    if (![self containsString:removeString])
	return [[self copy] autorelease];
    newString = [[NSMutableString alloc] initWithCapacity:[self length]];
    lines = [self componentsSeparatedByString:removeString];
    lineCount = [lines count];
    for (lineIndex = 0; lineIndex < lineCount; lineIndex++)
	[newString appendString:[lines objectAtIndex:lineIndex]];
    returnValue = [newString copy];
    [newString release];
    return [returnValue autorelease];
}

- (NSString *)stringByPaddingToLength:(unsigned int)aLength;
{
    unsigned int currentLength;

    currentLength = [self length];

    if (currentLength == aLength)
	return self;
    if (currentLength > aLength)
	return [self substringToIndex:aLength];
    return [self stringByAppendingString:[[self class] spacesOfLength:aLength - currentLength]];
}

- (NSString *)stringByNormalizingPath;
{
    NSArray *pathElements;
    NSMutableArray *newPathElements;
    unsigned int preserveCount;
    unsigned int elementIndex, elementCount;

    // Split on slashes and chop out '.' and '..' correctly.

#warning -stringByNormalizingPath does not work properly on Windows
    pathElements = [self componentsSeparatedByString:@"/"];
    elementCount = [pathElements count];
    newPathElements = [NSMutableArray arrayWithCapacity:elementCount];
    if (elementCount > 0 && [[pathElements objectAtIndex:0] isEqualToString:@""])
	preserveCount = 1;
    else
        preserveCount = 0;
    for (elementIndex = 0; elementIndex < elementCount; elementIndex++) {
	NSString *pathElement;

	pathElement = [pathElements objectAtIndex:elementIndex];
	if ([pathElement isEqualToString:@".."]) {
	    if ([pathElements count] > preserveCount)
		[newPathElements removeLastObject];
	} else if (![pathElement isEqualToString:@"."])
	    [newPathElements addObject:pathElement];
    }
    return [newPathElements componentsJoinedByString:@"/"];
}

- (unichar)firstCharacter;
{
    if ([self length] == 0)
	return '\0';
    return [self characterAtIndex:0];
}

- (unichar)lastCharacter;
{
    unsigned int length;

    length = [self length];
    if (length == 0)
        return '\0';
    return [self characterAtIndex:length - 1];
}

#if 0  /* nobody uses this, and it's semi-broken, so I'm commenting it out -wim */
- (NSString *)lowercaseSubstringFromRange:(NSRange)range;
{
    unichar *inputBuffer, *inputPointer;
    unsigned int characterCount;

    inputBuffer = (unichar *)NSZoneMalloc(NULL, range.length * sizeof(unichar));
    [self getCharacters:inputBuffer range:range];
    inputPointer = inputBuffer;
    characterCount = range.length;
    while (characterCount--) {
	unichar ch;

        ch = *inputPointer;
#warning -lowercaseSubstringFromRange: only handles ASCII characters
	if (isupper(ch))
            *inputPointer = tolower(ch);
        inputPointer++;
    }
    return [[[NSString alloc] initWithCharactersNoCopy:inputBuffer length:range.length freeWhenDone:YES] autorelease];
}
#endif

- (NSString *)stringByReplacingCharactersInSet:(NSCharacterSet *)set withString:(NSString *)replaceString;
{
    NSMutableString *newString;

    if (![self containsCharacterInSet:set])
	return self;
    newString = [[self mutableCopy] autorelease];
    [newString replaceAllOccurrencesOfCharactersInSet:set withString:replaceString];
    return newString;
}

- (NSString *)stringByReplacingKeysInDictionary:(NSDictionary *)keywordDictionary startingDelimiter:(NSString *)startingDelimiterString endingDelimiter:(NSString *)endingDelimiterString removeUndefinedKeys: (BOOL) removeUndefinedKeys;
{
    NSScanner *scanner = [NSScanner scannerWithString:self];
    NSMutableString *interpolatedString = [NSMutableString string];
    NSString *scannerOutput;
    BOOL didInterpolate = NO;

    while (![scanner isAtEnd]) {
        NSString *key = nil;
        NSString *value;
        BOOL gotInitialString, gotStartDelimiter, gotEndDelimiter;
        BOOL gotKey;

        gotInitialString = [scanner scanUpToString:startingDelimiterString intoString:&scannerOutput];
        if (gotInitialString) {
            [interpolatedString appendString:scannerOutput];
        }

        gotStartDelimiter = [scanner scanString:startingDelimiterString intoString:NULL];
        gotKey = [scanner scanUpToString:endingDelimiterString intoString:&key];
        gotEndDelimiter = [scanner scanString:endingDelimiterString intoString:NULL];

        if (gotKey) {
            value = [keywordDictionary objectForKey:key];
            if (!value && removeUndefinedKeys)
                value = @"";
            if (value != nil && [value isKindOfClass:[NSString class]]) {
                [interpolatedString appendString:value];
                didInterpolate = YES;
            }
        } else {
            if (gotStartDelimiter)
                [interpolatedString appendString:startingDelimiterString];
            if (gotKey)
                [interpolatedString appendString:key];
            if (gotEndDelimiter)
                [interpolatedString appendString:endingDelimiterString];
        }
    }
    return didInterpolate ? [[interpolatedString copy] autorelease] : self;
}

- (NSString *)stringByReplacingKeysInDictionary:(NSDictionary *)keywordDictionary startingDelimiter:(NSString *)startingDelimiterString endingDelimiter:(NSString *)endingDelimiterString;
{
    return [self stringByReplacingKeysInDictionary: keywordDictionary
                                 startingDelimiter: startingDelimiterString
                                   endingDelimiter: endingDelimiterString
                               removeUndefinedKeys: NO];
}

- (NSString *)stringByReplacingOccurancesOfString:(NSString *)targetString withObjectsFromArray:(NSArray *)sourceArray;
{
    NSMutableString *resultString;
    OFStringScanner *replacementScanner;
    unsigned int occurranceIndex = 0;
    unsigned int lastAppendedIndex = 0;
    unsigned int sourceCount;
    unsigned int targetStringLength;

    replacementScanner = [[[OFStringScanner alloc] initWithString:self] autorelease];
    resultString = [NSMutableString string];

    targetStringLength = [targetString length];
    sourceCount = [sourceArray count];
    
    while ([replacementScanner scanUpToString:targetString]) {
        NSRange beforeMatchRange;
        NSString *itemDescription;
        unsigned int scanLocation;

        scanLocation = [replacementScanner scanLocation];
        beforeMatchRange = NSMakeRange(lastAppendedIndex, scanLocation - lastAppendedIndex);
        if (beforeMatchRange.length > 0)
            [resultString appendString:[self substringWithRange:beforeMatchRange]];

        if (occurranceIndex >= sourceCount) {
            [NSException raise:NSInvalidArgumentException format:@"The string being scanned has more occurrances of the target string than the source array has items (scannedString = %@, targetString = %@, sourceArray = %@)."];
        }
        
        itemDescription = [[sourceArray objectAtIndex:occurranceIndex] description];
        [resultString appendString:itemDescription];

        occurranceIndex++;
        [replacementScanner setScanLocation:scanLocation + targetStringLength];
        lastAppendedIndex = [replacementScanner scanLocation];
    }

    if (lastAppendedIndex < [self length])
        [resultString appendString:[self substringFromIndex:lastAppendedIndex]];

    return [[resultString copy] autorelease];
}

- (NSString *) stringBySeparatingSubstringsOfLength:(unsigned int)substringLength                                          withString:(NSString *)separator startingFromBeginning:(BOOL)startFromBeginning;
{
    unsigned int     lengthLeft, offset = 0;
    NSMutableString *result;

    lengthLeft = [self length];
    if (lengthLeft <= substringLength)
        // Use <= since you have to have more than one group to need a separator.
        return self;

    if (!substringLength)
        [NSException raise: NSInvalidArgumentException
                    format: @"-[%@ %@], substringLength must be non-zero.",
            NSStringFromClass(isa), NSStringFromSelector(_cmd), substringLength];
    
    result = [NSMutableString string];
    if (!startFromBeginning) {
        unsigned int mod;
        
        // We'll still really start from the beginning, but first we'll trim off
        // whatever the extra count is that would have gone on the end.  This
        // produces the same effect.

        mod = lengthLeft % substringLength;
        if (mod) {
            [result appendString: [self substringWithRange: NSMakeRange(offset, mod)]];
            [result appendString: separator];
            offset     += mod;
            lengthLeft -= mod;
        }
    }

    while (lengthLeft) {
        unsigned int lengthToCopy;

        lengthToCopy = MIN(lengthLeft, substringLength);
        [result appendString: [self substringWithRange: NSMakeRange(offset, lengthToCopy)]];
        lengthLeft -= lengthToCopy;
        offset     += lengthToCopy;

        if (lengthLeft)
            [result appendString: separator];
    }

    return result;
}

- (NSString *)substringStartingWithString:(NSString *)startString;
{
    NSRange startRange;

    startRange = [self rangeOfString:startString];
    if (startRange.length == 0)
        return nil;
    return [self substringFromIndex:startRange.location];
}

- (NSString *)substringStartingAfterString:(NSString *)aString;
{
    NSRange aRange;

    aRange = [self rangeOfString:aString];
    if (aRange.length == 0)
        return nil;
    return [self substringFromIndex:aRange.location + aRange.length];
}

- (NSString *)stringByRemovingPrefix:(NSString *)prefix;
{
    NSRange aRange;

    aRange = [self rangeOfString:prefix];
    if ((aRange.length == 0) || (aRange.location != 0))
        return self;
    return [self substringFromIndex:aRange.location + aRange.length];
}

- (NSString *)stringByRemovingSuffix:(NSString *)suffix;
{
    if (![self hasSuffix:suffix])
        return self;
    return [self substringToIndex:[self length] - [suffix length]];
}

- (NSRange)findString:(NSString *)string selectedRange:(NSRange)selectedRange options:(unsigned int)options wrap:(BOOL)wrap;
{
    BOOL forwards;
    unsigned int length;
    NSRange searchRange, range;

    length = [self length];
    forwards = (options & NSBackwardsSearch) == 0;
    if (forwards) {
	searchRange.location = NSMaxRange(selectedRange);
	searchRange.length = length - searchRange.location;
	range = [self rangeOfString:string options:options range:searchRange];
        if ((range.length == 0) && wrap) {
            // If not found look at the first part of the string
	    searchRange.location = 0;
            searchRange.length = selectedRange.location;
            range = [self rangeOfString:string options:options range:searchRange];
        }
    } else {
	searchRange.location = 0;
	searchRange.length = selectedRange.location;
        range = [self rangeOfString:string options:options range:searchRange];
        if ((range.length == 0) && wrap) {
            searchRange.location = NSMaxRange(selectedRange);
            searchRange.length = length - searchRange.location;
            range = [self rangeOfString:string options:options range:searchRange];
        }
    }
    return range;
}        

- (NSRange)rangeOfCharactersAtIndex:(unsigned)pos delimitedBy:(NSCharacterSet *)delim;
{
    unsigned int myLength;
    NSRange searchRange, foundRange;
    unsigned int first, after;

    myLength = [self length];
    searchRange.location = 0;
    searchRange.length = pos;
    foundRange = [self rangeOfCharacterFromSet:delim options:NSBackwardsSearch range:searchRange];
    if (foundRange.length > 0)
      first = foundRange.location + foundRange.length;
    else
      first = 0;

    searchRange.location = pos;
    searchRange.length = myLength - pos;
    foundRange = [self rangeOfCharacterFromSet:delim options:0 range:searchRange];
    if (foundRange.length > 0)
      after = foundRange.location;
    else
      after = myLength;

    foundRange.location = first;
    foundRange.length = after - first;
    return foundRange;
}

- (NSRange)rangeOfWordContainingCharacter:(unsigned int)pos;
{
    NSCharacterSet *wordSep;
    unichar ch;

    // XXX TODO: This should depend on what your notion of a "word" is.
    wordSep = [NSCharacterSet whitespaceAndNewlineCharacterSet];
    ch = [self characterAtIndex:pos];
    if ([wordSep characterIsMember:ch])
        return [self rangeOfCharactersAtIndex:pos delimitedBy:[wordSep invertedSet]];
    else
        return [self rangeOfCharactersAtIndex:pos delimitedBy:wordSep];
}

- (NSRange)rangeOfWordsIntersectingRange:(NSRange)range;
{
    unsigned int first, last;
    NSRange firstRange, lastRange;

    if (range.length == 0)
        return NSMakeRange(0, 0);

    first = range.location;
    last = NSMaxRange(range) - 1;
    firstRange = [self rangeOfWordContainingCharacter:first];
    lastRange = [self rangeOfWordContainingCharacter:last];
    return NSMakeRange(firstRange.location, NSMaxRange(lastRange) - firstRange.location);
}

- (unsigned)indexOfCharacterNotRepresentableInCFEncoding:(CFStringEncoding)anEncoding
{
    CFIndex usedBufLen;
    CFIndex thisBufferCharacters;
    CFRange scanningRange;
    CFIndex bufLen = 1024;  // warning: this routine will fail if any single character requires more than 1024 bytes to represent! (ha, ha)
    
    scanningRange.location = 0;
    scanningRange.length = [self length];
    while(1) {
        if (!(scanningRange.length))
            return NSNotFound;
            
        usedBufLen = 0;
        thisBufferCharacters = CFStringGetBytes((CFStringRef)self, scanningRange, anEncoding, 0, FALSE, NULL, bufLen, &usedBufLen);
        if (thisBufferCharacters == 0)
            break;
        OBASSERT(thisBufferCharacters <= scanningRange.length);
        scanningRange.location += thisBufferCharacters;
        scanningRange.length -= thisBufferCharacters;
    }
    
    return scanningRange.location;
}

- (NSRange)rangeOfCharactersNotRepresentableInCFEncoding:(CFStringEncoding)anEncoding
{
    unsigned firstBad;
    CFIndex thisBad;
    CFIndex charactersConverted;
    NSRange testNSRange;
    CFRange testRange;
    CFIndex bufLen = 1024;
    CFIndex usedBufLen;
    
    firstBad = [self indexOfCharacterNotRepresentableInCFEncoding:anEncoding];
    if (firstBad == NSNotFound) 
        return NSMakeRange([self length], 0);
 
    thisBad = firstBad;
    
    for(thisBad = firstBad;
        thisBad < [self length];
        thisBad += testRange.length) {
        
        // there's no CoreFoundation function for this, sigh
        testNSRange = [self rangeOfComposedCharacterSequenceAtIndex:thisBad];
        testRange.location = testNSRange.location;
        testRange.length = testNSRange.length;
        
        testRange.location = thisBad;
        usedBufLen = 0;
        charactersConverted = CFStringGetBytes((CFStringRef)self, testRange, anEncoding, 0, FALSE, NULL, bufLen, &usedBufLen);
        if (charactersConverted > 0)
            break;
    };
    
    return NSMakeRange(firstBad, testRange.location - firstBad);
}

- (NSData *)dataUsingCFEncoding:(CFStringEncoding)anEncoding;
{
    return [(id)CFStringCreateExternalRepresentation(kCFAllocatorDefault, (CFStringRef)self, anEncoding, FALSE) autorelease];
}

- (NSData *)dataUsingCFEncoding:(CFStringEncoding)anEncoding allowLossyConversion:(BOOL)lossy;
{
    return [(id)CFStringCreateExternalRepresentation(kCFAllocatorDefault, (CFStringRef)self, anEncoding, lossy?TRUE:FALSE) autorelease];
}

- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile createDirectories:(BOOL)shouldCreateDirectories;
    // Will raise an exception if it can't create the required directories.
{
    if (shouldCreateDirectories) {
        // TODO: We ought to make the attributes configurable
        [[NSFileManager defaultManager] createPathToFile:path attributes:nil];
    }

    return [self writeToFile:path atomically:useAuxiliaryFile];
}

@end
