Skip to content
Browse files

Added ability to specify a delimiter when reading and writing

  • Loading branch information...
1 parent 5d221a8 commit 9e3acd05f5acd662ef4bf47694c712f1cf459d9b @davedelong committed
Showing with 189 additions and 20 deletions.
  1. +4 −0 CHCSVParser.h
  2. +39 −5 CHCSVParser.m
  3. +4 −0 CHCSVParser.xcodeproj/project.pbxproj
  4. +9 −0 CHCSVWriter.h
  5. +79 −5 CHCSVWriter.m
  6. +5 −1 NSArray+CHCSVAdditions.h
  7. +27 −7 NSArray+CHCSVAdditions.m
  8. +12 −2 main.m
  9. +10 −0 test.tsv
View
4 CHCSVParser.h
@@ -35,6 +35,9 @@
NSString * csvFile;
NSStringEncoding fileEncoding;
+ BOOL hasStarted;
+ NSString * delimiter;
+
NSMutableData * currentChunk;
NSMutableString * currentChunkString;
NSUInteger stringIndex;
@@ -53,6 +56,7 @@
@property (assign) __weak id<CHCSVParserDelegate> parserDelegate;
@property (readonly) NSError * error;
@property (readonly) NSString * csvFile;
+@property (nonatomic, copy) NSString *delimiter;
- (id) initWithContentsOfCSVFile:(NSString *)aCSVFile encoding:(NSStringEncoding)encoding error:(NSError **)anError;
- (id) initWithContentsOfCSVFile:(NSString *)aCSVFile usedEncoding:(NSStringEncoding *)usedEncoding error:(NSError **)anError;
View
44 CHCSVParser.m
@@ -26,7 +26,6 @@ of this software and associated documentation files (the "Software"), to deal
#import "CHCSVParser.h"
#define CHUNK_SIZE 32
#define STRING_QUOTE @"\""
-#define STRING_COMMA @","
#define STRING_BACKSLASH @"\\"
enum {
@@ -83,7 +82,7 @@ - (void) finishCurrentLine;
#define SETSTATE(_s) if (state != CHCSVParserStateCancelled) { state = _s; }
@implementation CHCSVParser
-@synthesize parserDelegate, currentChunk, error, csvFile;
+@synthesize parserDelegate, currentChunk, error, csvFile, delimiter;
- (id) initWithContentsOfCSVFile:(NSString *)aCSVFile encoding:(NSStringEncoding)encoding error:(NSError **)anError {
self = [super init];
@@ -110,6 +109,8 @@ - (id) initWithContentsOfCSVFile:(NSString *)aCSVFile encoding:(NSStringEncoding
currentChunkString = [[NSMutableString alloc] init];
stringIndex = 0;
+ [self setDelimiter:@","];
+
SETSTATE(CHCSVParserStateInsideFile)
}
return self;
@@ -124,6 +125,8 @@ - (id) initWithContentsOfCSVFile:(NSString *)aCSVFile usedEncoding:(NSStringEnco
fileEncoding = [self textEncodingForData:chunk offset:&seekOffset];
[csvFileHandle seekToFileOffset:seekOffset];
+ [self setDelimiter:@","];
+
if (usedEncoding) {
*usedEncoding = fileEncoding;
}
@@ -148,6 +151,8 @@ - (id) initWithCSVString:(NSString *)csvString encoding:(NSStringEncoding)encodi
doneReadingFile = YES;
stringIndex = 0;
+ [self setDelimiter:@","];
+
SETSTATE(CHCSVParserStateInsideFile)
}
return self;
@@ -160,6 +165,7 @@ - (void) dealloc {
[currentChunk release];
[currentChunkString release];
[error release];
+ [delimiter release];
[super dealloc];
}
@@ -217,6 +223,32 @@ - (NSStringEncoding) textEncodingForData:(NSData *)chunkToSniff offset:(NSUInteg
return encoding;
}
+- (void) setDelimiter:(NSString *)newDelimiter {
+ if (hasStarted) {
+ [NSException raise:NSInvalidArgumentException format:@"You cannot set a delimiter after parsing has started"];
+ return;
+ }
+
+ // the delimiter cannot be
+ BOOL shouldThrow = ([newDelimiter length] != 1);
+ if ([[NSCharacterSet newlineCharacterSet] characterIsMember:[newDelimiter characterAtIndex:0]]) {
+ shouldThrow = YES;
+ }
+ if ([newDelimiter hasPrefix:@"#"]) { shouldThrow = YES; }
+ if ([newDelimiter hasPrefix:@"\""]) { shouldThrow = YES; }
+ if ([newDelimiter hasPrefix:@"\\"]) { shouldThrow = YES; }
+
+ if (shouldThrow) {
+ [NSException raise:NSInvalidArgumentException format:@"%@ cannot be used as a delimiter", newDelimiter];
+ return;
+ }
+
+ if (newDelimiter != delimiter) {
+ [delimiter release];
+ delimiter = [newDelimiter copy];
+ }
+}
+
#pragma mark Parsing methods
- (void) readNextChunk {
@@ -277,6 +309,7 @@ - (NSString *) nextCharacter {
}
- (void) parse {
+ hasStarted = YES;
[[self parserDelegate] parser:self didStartDocument:[self csvFile]];
[self runParseLoop];
@@ -286,6 +319,7 @@ - (void) parse {
} else {
[[self parserDelegate] parser:self didEndDocument:[self csvFile]];
}
+ hasStarted = NO;
}
- (void) runParseLoop {
@@ -357,7 +391,7 @@ - (void) processComposedCharacter:(NSString *)currentCharacter previousCharacter
balancedQuotes = !balancedQuotes;
}
}
- } else if ([currentCharacter isEqual:STRING_COMMA]) {
+ } else if ([currentCharacter isEqual:delimiter]) {
if (state == CHCSVParserStateInsideLine) {
[self beginCurrentField];
[self finishCurrentField];
@@ -424,8 +458,8 @@ - (void) finishCurrentField {
if ([currentField hasPrefix:STRING_QUOTE] && [currentField hasSuffix:STRING_QUOTE]) {
[currentField trimString_csv:STRING_QUOTE];
}
- if ([currentField hasPrefix:STRING_COMMA]) {
- [currentField replaceCharactersInRange:NSMakeRange(0, [STRING_COMMA length]) withString:@""];
+ if ([currentField hasPrefix:delimiter]) {
+ [currentField replaceCharactersInRange:NSMakeRange(0, [delimiter length]) withString:@""];
}
[currentField trimCharactersInSet_csv:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
View
4 CHCSVParser.xcodeproj/project.pbxproj
@@ -12,6 +12,7 @@
5516BCBB12578EA90025F235 /* NSString+CHCSVAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5516BCB412578CFC0025F235 /* NSString+CHCSVAdditions.m */; };
5516BCBC12578EAD0025F235 /* CHCSVSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 5516BCB812578D750025F235 /* CHCSVSupport.m */; };
551981D61203715400FBE033 /* CHCSVParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 551981D51203715400FBE033 /* CHCSVParser.m */; };
+ 5538B52D1344F0A1004930DD /* test.tsv in Resources */ = {isa = PBXBuildFile; fileRef = 5538B52C1344F0A1004930DD /* test.tsv */; };
557FCEB61203F938009FCDBA /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 557FCEB51203F938009FCDBA /* CoreServices.framework */; };
557FD0431204A45D009FCDBA /* NSArray+CHCSVAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 557FD0421204A45D009FCDBA /* NSArray+CHCSVAdditions.m */; };
557FD0581204A71C009FCDBA /* CHCSVParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 551981D51203715400FBE033 /* CHCSVParser.m */; };
@@ -50,6 +51,7 @@
551981D41203715400FBE033 /* CHCSVParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHCSVParser.h; sourceTree = "<group>"; };
551981D51203715400FBE033 /* CHCSVParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CHCSVParser.m; sourceTree = "<group>"; };
551981EE1203800300FBE033 /* Test.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Test.csv; sourceTree = "<group>"; };
+ 5538B52C1344F0A1004930DD /* test.tsv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = test.tsv; sourceTree = "<group>"; };
557FCEB51203F938009FCDBA /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
557FD0411204A45D009FCDBA /* NSArray+CHCSVAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+CHCSVAdditions.h"; sourceTree = "<group>"; };
557FD0421204A45D009FCDBA /* NSArray+CHCSVAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+CHCSVAdditions.m"; sourceTree = "<group>"; };
@@ -103,6 +105,7 @@
08FB7796FE84155DC02AAC07 /* main.m */,
5516BCBA12578DA90025F235 /* CHCSVParser */,
551981EE1203800300FBE033 /* Test.csv */,
+ 5538B52C1344F0A1004930DD /* test.tsv */,
557FD05A1204A72B009FCDBA /* UnitTests.h */,
557FD05B1204A72B009FCDBA /* UnitTests.m */,
);
@@ -222,6 +225,7 @@
buildActionMask = 2147483647;
files = (
557FD05E1204A736009FCDBA /* Test.csv in Resources */,
+ 5538B52D1344F0A1004930DD /* test.tsv in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
View
9 CHCSVWriter.h
@@ -31,9 +31,11 @@
NSString * handleFile;
NSFileHandle * outputHandle;
BOOL atomically;
+ BOOL hasStarted;
NSUInteger currentField;
NSStringEncoding encoding;
+ NSString *delimiter;
NSCharacterSet * illegalCharacters;
@@ -41,12 +43,19 @@
}
@property (nonatomic) NSStringEncoding encoding;
+@property (nonatomic, copy) NSString *delimiter;
- (id) initWithCSVFile:(NSString *)outputFile atomic:(BOOL)atomicWrite;
- (NSError *) error;
- (void) writeField:(id)field;
+- (void) writeFields:(id)field, ... NS_REQUIRES_NIL_TERMINATION;
+
- (void) writeLine;
+- (void) writeLineOfFields:(id)field, ... NS_REQUIRES_NIL_TERMINATION;
+- (void) writeLineWithFields:(NSArray *)fields;
+
+- (void) writeCommentLine:(id)comment;
- (void) closeFile;
View
84 CHCSVWriter.m
@@ -27,7 +27,7 @@ of this software and associated documentation files (the "Software"), to deal
@implementation CHCSVWriter
-@synthesize encoding;
+@synthesize encoding, delimiter;
- (id) initWithCSVFile:(NSString *)outputFile atomic:(BOOL)atomicWrite {
if (self = [super init]) {
@@ -49,10 +49,9 @@ - (id) initWithCSVFile:(NSString *)outputFile atomic:(BOOL)atomicWrite {
outputHandle = [[NSFileHandle fileHandleForWritingAtPath:handleFile] retain];
encoding = 0;
+ hasStarted = NO;
- NSMutableCharacterSet * bad = [NSMutableCharacterSet newlineCharacterSet];
- [bad addCharactersInString:@",\"\\"];
- illegalCharacters = [bad retain];
+ [self setDelimiter:@","];
}
return self;
}
@@ -60,6 +59,7 @@ - (id) initWithCSVFile:(NSString *)outputFile atomic:(BOOL)atomicWrite {
- (void) dealloc {
[self closeFile];
[destinationFile release];
+ [delimiter release];
[handleFile release];
[outputHandle release];
[illegalCharacters release];
@@ -70,14 +70,47 @@ - (NSError *) error {
return error;
}
+- (void) setDelimiter:(NSString *)newDelimiter {
+ if (hasStarted) {
+ [NSException raise:NSInvalidArgumentException format:@"You cannot set a delimiter after writing has started"];
+ return;
+ }
+
+ // the delimiter cannot be
+ BOOL shouldThrow = ([newDelimiter length] != 1);
+ if ([[NSCharacterSet newlineCharacterSet] characterIsMember:[newDelimiter characterAtIndex:0]]) {
+ shouldThrow = YES;
+ }
+ if ([newDelimiter hasPrefix:@"#"]) { shouldThrow = YES; }
+ if ([newDelimiter hasPrefix:@"\""]) { shouldThrow = YES; }
+ if ([newDelimiter hasPrefix:@"\\"]) { shouldThrow = YES; }
+
+ if (shouldThrow) {
+ [NSException raise:NSInvalidArgumentException format:@"%@ cannot be used as a delimiter", newDelimiter];
+ return;
+ }
+
+ if (newDelimiter != delimiter) {
+ [delimiter release];
+ delimiter = [newDelimiter copy];
+
+ [illegalCharacters release];
+ NSMutableCharacterSet * bad = [NSMutableCharacterSet newlineCharacterSet];
+ [bad addCharactersInString:@"\"\\"];
+ [bad addCharactersInString:delimiter];
+ illegalCharacters = [bad copy];
+ }
+}
+
- (void) writeField:(id)field {
+ hasStarted = YES;
NSMutableString * write = [[field description] mutableCopy];
if (encoding == 0) {
encoding = [write fastestEncoding];
}
if (currentField > 0) {
- [outputHandle writeData:[@"," dataUsingEncoding:encoding]];
+ [outputHandle writeData:[delimiter dataUsingEncoding:encoding]];
}
if ([write rangeOfCharacterFromSet:illegalCharacters].location != NSNotFound || [write hasPrefix:@"#"]) {
@@ -92,6 +125,20 @@ - (void) writeField:(id)field {
currentField++;
}
+- (void) writeFields:(id)field, ... {
+ if (field == nil) { return; }
+
+ [self writeField:field];
+
+ va_list args;
+ va_start(args, field);
+ id next = nil;
+ while ((next = va_arg(args, id))) {
+ [self writeField:next];
+ }
+ va_end(args);
+}
+
- (void) writeLine {
if (encoding == 0) {
encoding = NSUTF8StringEncoding;
@@ -100,6 +147,33 @@ - (void) writeLine {
currentField = 0;
}
+- (void) writeLineOfFields:(id)field, ... {
+ if (field == nil) { return; }
+ NSMutableArray *fields = [NSMutableArray arrayWithObject:field];
+ va_list args;
+ va_start(args, field);
+ id next = nil;
+ while ((next = va_arg(args, id))) {
+ [fields addObject:next];
+ }
+ [self writeLineWithFields:fields];
+ va_end(args);
+}
+
+- (void) writeLineWithFields:(NSArray *)fields {
+ for (id field in fields) {
+ [self writeField:field];
+ }
+ [self writeLine];
+}
+
+- (void) writeCommentLine:(id)comment {
+ if (currentField > 0) { [self writeLine]; }
+ [outputHandle writeData:[@"#" dataUsingEncoding:encoding]];
+ [outputHandle writeData:[comment dataUsingEncoding:encoding]];
+ [self writeLine];
+}
+
- (void) closeFile {
if (outputHandle) {
[outputHandle closeFile];
View
6 NSArray+CHCSVAdditions.h
@@ -30,13 +30,17 @@
+ (id) arrayWithContentsOfCSVFile:(NSString *)csvFile encoding:(NSStringEncoding)encoding error:(NSError **)error;
- (id) initWithContentsOfCSVFile:(NSString *)csvFile encoding:(NSStringEncoding)encoding error:(NSError **)error;
+- (id) initWithContentsOfCSVFile:(NSString *)csvFile encoding:(NSStringEncoding)encoding delimiter:(NSString *)delimiter error:(NSError **)error;
+ (id) arrayWithContentsOfCSVFile:(NSString *)csvFile usedEncoding:(NSStringEncoding *)usedEncoding error:(NSError **)error;
- (id) initWithContentsOfCSVFile:(NSString *)csvFile usedEncoding:(NSStringEncoding *)usedEncoding error:(NSError **)error;
+- (id) initWithContentsOfCSVFile:(NSString *)csvFile usedEncoding:(NSStringEncoding *)usedEncoding delimiter:(NSString *)delimiter error:(NSError **)error;
+ (id) arrayWithContentsOfCSVString:(NSString *)csvString encoding:(NSStringEncoding)encoding error:(NSError **)error;
- (id) initWithContentsOfCSVString:(NSString *)csvString encoding:(NSStringEncoding)encoding error:(NSError **)error;
+- (id) initWithContentsOfCSVString:(NSString *)csvString encoding:(NSStringEncoding)encoding delimiter:(NSString *)delimiter error:(NSError **)error;
-- (BOOL) writeToCSVFile:(NSString *)csvFile atomically:(BOOL)atomically;
+- (BOOL) writeToCSVFile:(NSString *)csvFile atomically:(BOOL)atomically error:(NSError **)error;
+- (BOOL) writeToCSVFile:(NSString *)csvFile withDelimiter:(NSString *)delimiter atomically:(BOOL)atomically error:(NSError **)error;
@end
View
34 NSArray+CHCSVAdditions.m
@@ -35,11 +35,15 @@ + (id) arrayWithContentsOfCSVFile:(NSString *)csvFile encoding:(NSStringEncoding
}
- (id) initWithContentsOfCSVFile:(NSString *)csvFile encoding:(NSStringEncoding)encoding error:(NSError **)error {
+ return [self initWithContentsOfCSVFile:csvFile encoding:encoding delimiter:@"," error:error];
+}
+
+- (id) initWithContentsOfCSVFile:(NSString *)csvFile encoding:(NSStringEncoding)encoding delimiter:(NSString *)delimiter error:(NSError **)error {
NSString * rawCSV = [NSString stringWithContentsOfFile:csvFile encoding:encoding error:error];
if ((error && *error) || rawCSV == nil) {
return [self init];
}
- return [self initWithContentsOfCSVString:rawCSV encoding:encoding error:error];
+ return [self initWithContentsOfCSVString:rawCSV encoding:encoding delimiter:delimiter error:error];
}
+ (id) arrayWithContentsOfCSVFile:(NSString *)csvFile usedEncoding:(NSStringEncoding *)usedEncoding error:(NSError **)error {
@@ -47,16 +51,21 @@ + (id) arrayWithContentsOfCSVFile:(NSString *)csvFile usedEncoding:(NSStringEnco
}
- (id) initWithContentsOfCSVFile:(NSString *)csvFile usedEncoding:(NSStringEncoding *)usedEncoding error:(NSError **)error {
+ return [self initWithContentsOfCSVFile:csvFile usedEncoding:usedEncoding delimiter:@"," error:error];
+}
+
+- (id) initWithContentsOfCSVFile:(NSString *)csvFile usedEncoding:(NSStringEncoding *)usedEncoding delimiter:(NSString *)delimiter error:(NSError **)error {
NSString * rawCSV = [NSString stringWithContentsOfFile:csvFile usedEncoding:usedEncoding error:error];
if ((error && *error) || rawCSV == nil) {
if (error) { *error = nil; }
rawCSV = [NSString stringWithContentsOfFile:csvFile encoding:NSMacOSRomanStringEncoding error:error];
+ if (usedEncoding) { *usedEncoding = NSMacOSRomanStringEncoding; }
}
if ((error && *error) || rawCSV == nil) {
return [self init];
}
- return [self initWithContentsOfCSVString:rawCSV encoding:(usedEncoding ? *usedEncoding : NSMacOSRomanStringEncoding) error:error];
+ return [self initWithContentsOfCSVString:rawCSV encoding:(usedEncoding ? *usedEncoding : NSMacOSRomanStringEncoding) delimiter:delimiter error:error];
}
+ (id) arrayWithContentsOfCSVString:(NSString *)csvString encoding:(NSStringEncoding)encoding error:(NSError **)error {
@@ -64,7 +73,13 @@ + (id) arrayWithContentsOfCSVString:(NSString *)csvString encoding:(NSStringEnco
}
- (id) initWithContentsOfCSVString:(NSString *)csvString encoding:(NSStringEncoding)encoding error:(NSError **)error {
+ return [self initWithContentsOfCSVString:csvString encoding:encoding delimiter:@"," error:error];
+}
+
+- (id) initWithContentsOfCSVString:(NSString *)csvString encoding:(NSStringEncoding)encoding delimiter:(NSString *)delimiter error:(NSError **)error {
CHCSVParser * parser = [[CHCSVParser alloc] initWithCSVString:csvString encoding:encoding error:error];
+ [parser setDelimiter:delimiter];
+
if (error && *error) {
[parser release];
return [self init];
@@ -89,7 +104,11 @@ - (id) initWithContentsOfCSVString:(NSString *)csvString encoding:(NSStringEncod
return [self initWithArray:lines];
}
-- (BOOL) writeToCSVFile:(NSString *)csvFile atomically:(BOOL)atomically {
+- (BOOL) writeToCSVFile:(NSString *)csvFile atomically:(BOOL)atomically error:(NSError **)error {
+ return [self writeToCSVFile:csvFile withDelimiter:@"," atomically:atomically error:error];
+}
+
+- (BOOL) writeToCSVFile:(NSString *)csvFile withDelimiter:(NSString *)delimiter atomically:(BOOL)atomically error:(NSError **)error {
//first, verify that this is (at least) an NSArray of NSArrays:
for (id object in self) {
if ([object isKindOfClass:[NSArray class]] == NO) { return NO; }
@@ -98,14 +117,15 @@ - (BOOL) writeToCSVFile:(NSString *)csvFile atomically:(BOOL)atomically {
BOOL ok = YES;
CHCSVWriter * writer = [[CHCSVWriter alloc] initWithCSVFile:csvFile atomic:atomically];
+ [writer setDelimiter:delimiter];
for (NSArray * row in self) {
- for (id field in row) {
- [writer writeField:[field description]];
- }
- [writer writeLine];
+ [writer writeLineWithFields:row];
}
ok = ([writer error] == nil);
+ if (!ok && error) {
+ *error = [[[writer error] retain] autorelease];
+ }
[writer closeFile];
[writer release];
View
14 main.m
@@ -29,9 +29,10 @@ - (void) parser:(CHCSVParser *)parser didFailWithError:(NSError *)error {
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
- NSString * file = @"/Users/dave/Downloads/test2.csv";
+ NSString * file = @"/Users/dave/Developer/Open Source/Git Projects/CHCSVParser/test.tsv";
NSStringEncoding encoding = 0;
CHCSVParser * p = [[CHCSVParser alloc] initWithContentsOfCSVFile:file usedEncoding:&encoding error:nil];
+ [p setDelimiter:@"\t"];
NSLog(@"encoding: %@", CFStringGetNameOfEncoding(CFStringConvertNSStringEncodingToEncoding(encoding)));
@@ -44,7 +45,7 @@ int main (int argc, const char * argv[]) {
[p release];
NSError * error = nil;
- NSArray * rows = [NSArray arrayWithContentsOfCSVFile:file usedEncoding:&encoding error:&error];
+ NSArray * rows = [[NSArray alloc] initWithContentsOfCSVFile:file usedEncoding:&encoding delimiter:@"\t" error:&error];
if ([rows count] == 0) {
NSLog(@"error: %@", error);
error = nil;
@@ -52,7 +53,16 @@ int main (int argc, const char * argv[]) {
}
NSLog(@"error: %@", error);
NSLog(@"%@", rows);
+
+ CHCSVWriter *w = [[CHCSVWriter alloc] initWithCSVFile:[NSTemporaryDirectory() stringByAppendingPathComponent:@"test.tsv"] atomic:NO];
+ [w setDelimiter:@"\t"];
+ for (NSArray *row in rows) {
+ [w writeLineWithFields:row];
+ }
+ [w closeFile];
+ [w release];
+ [rows release];
[pool drain];
return 0;
}
View
10 test.tsv
@@ -0,0 +1,10 @@
+1,a 1,b 1,c 1,d
+2,a 2,b 2,c 2,d
+3,a 3,b 3,c 3,d
+4,a 4,b 4,c 4,d
+5,a 5,b 5,c 5,d
+6,a 6,b 6,c 6,d
+7,a 7,b 7,c 7,d
+8,a 8,b 8,c 8,d
+9,a 9,b 9,c 9,d
+10,a 10,b 10,c 10,d

0 comments on commit 9e3acd0

Please sign in to comment.
Something went wrong with that request. Please try again.