diff options
Diffstat (limited to 'Source/SPCSVExporter.m')
-rw-r--r-- | Source/SPCSVExporter.m | 483 |
1 files changed, 228 insertions, 255 deletions
diff --git a/Source/SPCSVExporter.m b/Source/SPCSVExporter.m index b508172b..82f43a79 100644 --- a/Source/SPCSVExporter.m +++ b/Source/SPCSVExporter.m @@ -26,16 +26,8 @@ #import "SPCSVExporter.h" #import "SPArrayAdditions.h" -@interface SPCSVExporter (PrivateAPI) - -- (void)_startCSVExportInBackgroundThread; - -@end - @implementation SPCSVExporter -@synthesize csvFileHandle; - @synthesize csvDataArray; @synthesize csvDataResult; @@ -48,292 +40,273 @@ @synthesize csvTableColumnNumericStatus; /** - * Start the CSV export process. + * Start the CSV data conversion process. This method is automatically called when an instance of this object + * is placed on an NSOperationQueue. Do not call it directly as there is no manual multithreading. */ -- (BOOL)startExportProcess -{ - // Check that we have all the required info before starting the export - if ((![self csvFileHandle]) || - (![self csvOutputFieldNames]) || - (![self csvFieldSeparatorString]) || - (![self csvEscapeString]) || - (![self csvLineEndingString]) || - (![self csvTableColumnNumericStatus])) - { - return NO; - } - - // Check that the CSV output options are not just empty strings or empty arrays - if ((![[self csvFieldSeparatorString] isEqualToString:@""]) || - (![[self csvEscapeString] isEqualToString:@""]) || - (![[self csvLineEndingString] isEqualToString:@""]) || - ([[self csvTableColumnNumericStatus] count] != 0)) - { - return NO; - } - - // Check that we have at least some data to export - if ((![self csvDataArray]) && (![self csvDataResult])) return NO; - - // Tell the delegate that we are starting the export process - if (delegate && [delegate respondsToSelector:@selector(exportProcessDidStart:)]) { - [delegate exportProcessDidStart:self]; - } - - [self setExportProcessIsRunning:YES]; +- (void)main +{ + @try { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - // Start the export in a new thread - [NSThread detachNewThreadSelector:@selector(_startCSVExportInBackgroundThread) toTarget:self withObject:nil]; - - [self setExportProcessIsRunning:NO]; - - // Tell the delegate that the export process has ended - if (delegate && [delegate respondsToSelector:@selector(exportProcessDidEnd:)]) { - [delegate exportProcessDidEnd:self]; - } - - return YES; -} - -/** - * Stop the CSV export process by killing the export thread and cleaning up if its running. - */ -- (BOOL)stopExportProcess -{ - if (![self exportProcessIsRunning]) return NO; - - // Kill the running thread here - - return YES; -} - -/** - * Dealloc - */ -- (void)dealloc -{ - [csvFileHandle release], csvFileHandle = nil; - [csvDataArray release], csvDataArray = nil; - [csvDataResult release], csvDataResult = nil; - [csvFieldSeparatorString release], csvFieldSeparatorString = nil; - [csvEnclosingCharacterString release], csvEnclosingCharacterString = nil; - [csvEscapeString release], csvEscapeString = nil; - [csvLineEndingString release], csvLineEndingString = nil; - [csvNULLString release], csvNULLString = nil; - [csvTableColumnNumericStatus release], csvTableColumnNumericStatus = nil; - - [super dealloc]; -} - -@end - -@implementation SPCSVExporter (PrivateAPI) - -/** - * Starts the export process in a background thread. - */ -- (void)_startCSVExportInBackgroundThread -{ - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - - NSMutableArray *csvRow = [NSMutableArray array]; - NSMutableString *csvCell = [NSMutableString string]; - NSMutableString *csvString = [NSMutableString string]; - - NSScanner *csvNumericTester; - NSString *escapedEscapeString, *escapedFieldSeparatorString, *escapedEnclosingString, *escapedLineEndString, *dataConversionString; - - BOOL csvCellIsNumeric; - BOOL quoteFieldSeparators = [[self csvEnclosingCharacterString] isEqualToString:@""]; - - NSUInteger i, j, startingRow, totalRows; - - if ([self csvDataResult] != nil && [[self csvDataResult] numOfRows]) [[self csvDataResult] dataSeek:0]; - - // Detect and restore special characters being used as terminating or line end strings - NSMutableString *tempSeparatorString = [NSMutableString stringWithString:[self csvFieldSeparatorString]]; + NSMutableArray *csvRow = [NSMutableArray array]; + NSMutableString *csvCell = [NSMutableString string]; + NSMutableString *csvString = [NSMutableString string]; + NSMutableString *csvData = [NSMutableString string]; - // Escape tabs, line endings and carriage returns - [tempSeparatorString replaceOccurrencesOfString:@"\\t" withString:@"\t" - options:NSLiteralSearch - range:NSMakeRange(0, [tempSeparatorString length])]; - - [tempSeparatorString replaceOccurrencesOfString:@"\\n" withString:@"\n" - options:NSLiteralSearch - range:NSMakeRange(0, [tempSeparatorString length])]; - - [tempSeparatorString replaceOccurrencesOfString:@"\\r" withString:@"\r" - options:NSLiteralSearch - range:NSMakeRange(0, [tempSeparatorString length])]; - - // Set the new field separator string - [self setCsvFieldSeparatorString:[NSString stringWithString:tempSeparatorString]]; - - NSMutableString *tempLineEndString = [NSMutableString stringWithString:[self csvLineEndingString]]; + NSScanner *csvNumericTester; + NSString *escapedEscapeString, *escapedFieldSeparatorString, *escapedEnclosingString, *escapedLineEndString, *dataConversionString; - // Escape tabs, line endings and carriage returns - [tempLineEndString replaceOccurrencesOfString:@"\\t" withString:@"\t" - options:NSLiteralSearch - range:NSMakeRange(0, [tempLineEndString length])]; - - - [tempLineEndString replaceOccurrencesOfString:@"\\n" withString:@"\n" - options:NSLiteralSearch - range:NSMakeRange(0, [tempLineEndString length])]; - - [tempLineEndString replaceOccurrencesOfString:@"\\r" withString:@"\r" - options:NSLiteralSearch - range:NSMakeRange(0, [tempLineEndString length])]; - - // Set the new line ending string - [self setCsvLineEndingString:[NSString stringWithString:tempLineEndString]]; - - // Set up escaped versions of strings for substitution within the loop - escapedEscapeString = [[self csvEscapeString] stringByAppendingString:[self csvEscapeString]]; - escapedFieldSeparatorString = [[self csvEscapeString] stringByAppendingString:[self csvFieldSeparatorString]]; - escapedEnclosingString = [[self csvEscapeString] stringByAppendingString:[self csvEnclosingCharacterString]]; - escapedLineEndString = [[self csvEscapeString] stringByAppendingString:[self csvLineEndingString]]; - - // Determine the total number of rows and starting row depending on supplied data format - if ([self csvDataArray] == nil) { - startingRow = [self csvOutputFieldNames] ? -1 : 0; - totalRows = [[self csvDataResult] numOfRows]; - } - else { - startingRow = [self csvOutputFieldNames] ? 0 : 1; - totalRows = [[self csvDataArray] count]; - } - - // Walk through the supplied data constructing the CSV string - for (i = startingRow; i < totalRows; i++) - { - // Check if we should stop and exit the export operation - if ([self exportProcessShouldExit]) { - [pool release]; - + BOOL csvCellIsNumeric; + BOOL quoteFieldSeparators = [[self csvEnclosingCharacterString] isEqualToString:@""]; + + NSUInteger i, j, startingRow, totalRows; + + // Check that we have all the required info before starting the export + if ((![self csvOutputFieldNames]) || + (![self csvFieldSeparatorString]) || + (![self csvEscapeString]) || + (![self csvLineEndingString]) || + (![self csvTableColumnNumericStatus])) + { return; } + + // Check that the CSV output options are not just empty strings or empty arrays + if (([[self csvFieldSeparatorString] isEqualToString:@""]) || + ([[self csvEscapeString] isEqualToString:@""]) || + ([[self csvLineEndingString] isEqualToString:@""]) || + ([[self csvTableColumnNumericStatus] count] == 0)) + { + return; + } + + // Check that we have at least some data to export + if ((![self csvDataArray]) && (![self csvDataResult])) return; + + // Tell the delegate that we are starting the export process + if (delegate && [delegate respondsToSelector:@selector(exportProcessDidStart:)]) { + [delegate exportProcessDidStart:self]; + } - // Update the progress value - if (totalRows) [self setExportProgressValue:(((i + 1) * 100) / totalRows)]; + // Mark the process as running + [self setExportProcessIsRunning:YES]; - // Retrieve the row from the supplied data + if ([self csvDataResult] != nil && [[self csvDataResult] numOfRows]) [[self csvDataResult] dataSeek:0]; + + // Detect and restore special characters being used as terminating or line end strings + NSMutableString *tempSeparatorString = [NSMutableString stringWithString:[self csvFieldSeparatorString]]; + + // Escape tabs, line endings and carriage returns + [tempSeparatorString replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, [tempSeparatorString length])]; + + [tempSeparatorString replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, [tempSeparatorString length])]; + + [tempSeparatorString replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, [tempSeparatorString length])]; + + // Set the new field separator string + [self setCsvFieldSeparatorString:[NSString stringWithString:tempSeparatorString]]; + + NSMutableString *tempLineEndString = [NSMutableString stringWithString:[self csvLineEndingString]]; + + // Escape tabs, line endings and carriage returns + [tempLineEndString replaceOccurrencesOfString:@"\\t" withString:@"\t" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEndString length])]; + + + [tempLineEndString replaceOccurrencesOfString:@"\\n" withString:@"\n" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEndString length])]; + + [tempLineEndString replaceOccurrencesOfString:@"\\r" withString:@"\r" + options:NSLiteralSearch + range:NSMakeRange(0, [tempLineEndString length])]; + + // Set the new line ending string + [self setCsvLineEndingString:[NSString stringWithString:tempLineEndString]]; + + // Set up escaped versions of strings for substitution within the loop + escapedEscapeString = [[self csvEscapeString] stringByAppendingString:[self csvEscapeString]]; + escapedFieldSeparatorString = [[self csvEscapeString] stringByAppendingString:[self csvFieldSeparatorString]]; + escapedEnclosingString = [[self csvEscapeString] stringByAppendingString:[self csvEnclosingCharacterString]]; + escapedLineEndString = [[self csvEscapeString] stringByAppendingString:[self csvLineEndingString]]; + + // Determine the total number of rows and starting row depending on supplied data format if ([self csvDataArray] == nil) { - // Header row - [csvRow setArray:(i == -1) ? [[self csvDataResult] fetchFieldNames] : [[self csvDataResult] fetchRowAsArray]]; + startingRow = [self csvOutputFieldNames] ? 1 : 0; + totalRows = [[self csvDataResult] numOfRows]; } else { - [csvRow setArray:NSArrayObjectAtIndex([self csvDataArray], i)]; + startingRow = [self csvOutputFieldNames] ? 0 : 1; + totalRows = [[self csvDataArray] count]; } - [csvString setString:@""]; - - for (j = 0; j < [csvRow count]; j++) - { - // For NULL objects supplied from a queryResult, add an unenclosed null string as per prefs - if ([[csvRow objectAtIndex:j] isKindOfClass:[NSNull class]]) { - [csvString appendString:[self csvNULLString]]; - - if (j < [csvRow count] - 1) [csvString appendString:[self csvFieldSeparatorString]]; + [csvData setString:@""]; - continue; + // Walk through the supplied data constructing the CSV string + for (i = startingRow; i < totalRows; i++) + { + // Check to see if the operation has been cancelled. If so exit the loop. + if ([self isCancelled]) { + break; } - // Retrieve the contents of this cell - if ([NSArrayObjectAtIndex(csvRow, j) isKindOfClass:[NSData class]]) { - dataConversionString = [[NSString alloc] initWithData:NSArrayObjectAtIndex(csvRow, j) encoding:[self exportOutputEncoding]]; - - if (dataConversionString == nil) { - dataConversionString = [[NSString alloc] initWithData:NSArrayObjectAtIndex(csvRow, j) encoding:NSASCIIStringEncoding]; - } - - [csvCell setString:[NSString stringWithString:dataConversionString]]; - [dataConversionString release]; + // Update the progress value + if (totalRows) [self setExportProgressValue:(((i + 1) * 100) / totalRows)]; + + // Retrieve the row from the supplied data + if ([self csvDataArray] == nil) { + // Header row + [csvRow setArray:(i == -1) ? [[self csvDataResult] fetchFieldNames] : [[self csvDataResult] fetchRowAsArray]]; } else { - [csvCell setString:[NSArrayObjectAtIndex(csvRow, j) description]]; + [csvRow setArray:NSArrayObjectAtIndex([self csvDataArray], i)]; } - // For NULL values supplied via an array add the unenclosed null string as set in preferences - if ([csvCell isEqualToString:[self csvNULLString]]) { - [csvString appendString:[self csvNULLString]]; - } - // Add empty strings as a pair of enclosing characters. - else if ([csvCell length] == 0) { - [csvString appendString:[self csvEnclosingCharacterString]]; - [csvString appendString:[self csvEnclosingCharacterString]]; + [csvString setString:@""]; + + for (j = 0; j < [csvRow count]; j++) + { + // For NULL objects supplied from a queryResult, add an unenclosed null string as per prefs + if ([[csvRow objectAtIndex:j] isKindOfClass:[NSNull class]]) { + [csvString appendString:[self csvNULLString]]; + + if (j < [csvRow count] - 1) [csvString appendString:[self csvFieldSeparatorString]]; + + continue; + } - } - else { - // Test whether this cell contains a number + // Retrieve the contents of this cell if ([NSArrayObjectAtIndex(csvRow, j) isKindOfClass:[NSData class]]) { - csvCellIsNumeric = NO; + dataConversionString = [[NSString alloc] initWithData:NSArrayObjectAtIndex(csvRow, j) encoding:[self exportOutputEncoding]]; + + if (dataConversionString == nil) { + dataConversionString = [[NSString alloc] initWithData:NSArrayObjectAtIndex(csvRow, j) encoding:NSASCIIStringEncoding]; + } + + [csvCell setString:[NSString stringWithString:dataConversionString]]; + [dataConversionString release]; } - // If an array of bools supplying information as to whether the column is numeric has been supplied, use it. - else if ([self csvTableColumnNumericStatus] != nil) { - csvCellIsNumeric = [NSArrayObjectAtIndex([self csvTableColumnNumericStatus], j) boolValue]; - } - // Or fall back to testing numeric content via an NSScanner. else { - csvNumericTester = [NSScanner scannerWithString:csvCell]; - csvCellIsNumeric = [csvNumericTester scanFloat:nil] && - [csvNumericTester isAtEnd] && - ([csvCell characterAtIndex:0] != '0' || - [csvCell length] == 1 || - ([csvCell length] > 1 && - [csvCell characterAtIndex:1] == '.')); + [csvCell setString:[NSArrayObjectAtIndex(csvRow, j) description]]; } - // Escape any occurrences of the escaping character - [csvCell replaceOccurrencesOfString:[self csvEscapeString] - withString:escapedEscapeString - options:NSLiteralSearch - range:NSMakeRange(0, [csvCell length])]; - - // Escape any occurrences of the enclosure string - if (![[self csvEscapeString] isEqualToString:[self csvEnclosingCharacterString]]) { - [csvCell replaceOccurrencesOfString:[self csvEnclosingCharacterString] - withString:escapedEnclosingString + // For NULL values supplied via an array add the unenclosed null string as set in preferences + if ([csvCell isEqualToString:[self csvNULLString]]) { + [csvString appendString:[self csvNULLString]]; + } + // Add empty strings as a pair of enclosing characters. + else if ([csvCell length] == 0) { + [csvString appendString:[self csvEnclosingCharacterString]]; + [csvString appendString:[self csvEnclosingCharacterString]]; + + } + else { + // Test whether this cell contains a number + if ([NSArrayObjectAtIndex(csvRow, j) isKindOfClass:[NSData class]]) { + csvCellIsNumeric = NO; + } + // If an array of bools supplying information as to whether the column is numeric has been supplied, use it. + else if ([self csvTableColumnNumericStatus] != nil) { + csvCellIsNumeric = [NSArrayObjectAtIndex([self csvTableColumnNumericStatus], j) boolValue]; + } + // Or fall back to testing numeric content via an NSScanner. + else { + csvNumericTester = [NSScanner scannerWithString:csvCell]; + csvCellIsNumeric = [csvNumericTester scanFloat:nil] && + [csvNumericTester isAtEnd] && + ([csvCell characterAtIndex:0] != '0' || + [csvCell length] == 1 || + ([csvCell length] > 1 && + [csvCell characterAtIndex:1] == '.')); + } + + // Escape any occurrences of the escaping character + [csvCell replaceOccurrencesOfString:[self csvEscapeString] + withString:escapedEscapeString options:NSLiteralSearch range:NSMakeRange(0, [csvCell length])]; - } - - // Escape occurrences of the line end character - [csvCell replaceOccurrencesOfString:[self csvLineEndingString] - withString:escapedLineEndString - options:NSLiteralSearch - range:NSMakeRange(0, [csvCell length])]; - - // If the string isn't quoted or otherwise enclosed, escape occurrences of the field separators - if (quoteFieldSeparators || csvCellIsNumeric) { - [csvCell replaceOccurrencesOfString:[self csvFieldSeparatorString] - withString:escapedFieldSeparatorString + + // Escape any occurrences of the enclosure string + if (![[self csvEscapeString] isEqualToString:[self csvEnclosingCharacterString]]) { + [csvCell replaceOccurrencesOfString:[self csvEnclosingCharacterString] + withString:escapedEnclosingString + options:NSLiteralSearch + range:NSMakeRange(0, [csvCell length])]; + } + + // Escape occurrences of the line end character + [csvCell replaceOccurrencesOfString:[self csvLineEndingString] + withString:escapedLineEndString options:NSLiteralSearch range:NSMakeRange(0, [csvCell length])]; + + // If the string isn't quoted or otherwise enclosed, escape occurrences of the field separators + if (quoteFieldSeparators || csvCellIsNumeric) { + [csvCell replaceOccurrencesOfString:[self csvFieldSeparatorString] + withString:escapedFieldSeparatorString + options:NSLiteralSearch + range:NSMakeRange(0, [csvCell length])]; + } + + // Write out the cell data by appending strings - this is significantly faster than stringWithFormat. + if (csvCellIsNumeric) { + [csvString appendString:csvCell]; + } + else { + [csvString appendString:[self csvEnclosingCharacterString]]; + [csvString appendString:csvCell]; + [csvString appendString:[self csvEnclosingCharacterString]]; + } } - // Write out the cell data by appending strings - this is significantly faster than stringWithFormat. - if (csvCellIsNumeric) { - [csvString appendString:csvCell]; - } - else { - [csvString appendString:[self csvEnclosingCharacterString]]; - [csvString appendString:csvCell]; - [csvString appendString:[self csvEnclosingCharacterString]]; - } + if (j < ([csvRow count] - 1)) [csvString appendString:[self csvFieldSeparatorString]]; } - if (j < ([csvRow count] - 1)) [csvString appendString:[self csvFieldSeparatorString]]; + // Append the line ending to the string for this row + [csvString appendString:[self csvLineEndingString]]; + [csvData appendString:csvString]; + } + + // Mark the process as not running + [self setExportProcessIsRunning:NO]; + + // Tell the delegate that the export process has ended + if (delegate && [delegate respondsToSelector:@selector(exportProcessDidEnd:)]) { + [delegate exportProcessDidEnd:self]; } - // Append the line ending to the string for this row - [csvString appendString:[self csvLineEndingString]]; + // Pass the resulting CSV data back to the delegate by calling the specified didEndSelector + [[self delegate] performSelectorOnMainThread:[self didEndSelector] withObject:csvData waitUntilDone:YES]; - // Write it to the fileHandle - [csvFileHandle writeData:[csvString dataUsingEncoding:[self exportOutputEncoding]]]; + [pool release]; } + @catch(NSException *e) { + + } +} + +/** + * Dealloc + */ +- (void)dealloc +{ + [csvDataArray release], csvDataArray = nil; + [csvDataResult release], csvDataResult = nil; + [csvFieldSeparatorString release], csvFieldSeparatorString = nil; + [csvEnclosingCharacterString release], csvEnclosingCharacterString = nil; + [csvEscapeString release], csvEscapeString = nil; + [csvLineEndingString release], csvLineEndingString = nil; + [csvNULLString release], csvNULLString = nil; + [csvTableColumnNumericStatus release], csvTableColumnNumericStatus = nil; - [pool release]; + [super dealloc]; } @end |