1# csv - comma separated values, the way RFC 4180 writes them.2#3# Parse turns the text into a list of records, each one a list of fields, and4# Format writes that back out:5#6# csv = import("encoding/csv")7# os = import("os")8#9# rows = csv.Parse(os.ReadFileString("cities.csv"), null)10# println(rows[0])11# os.WriteFile("out.csv", csv.Format(rows, null))12#13# A field may be quoted with ", and a quote inside a quoted field is written14# twice. Quoted fields may hold the separator and newlines. Both \n and \r\n15# end a record. The separator is a single character, "," when it is null.16#17# ponytail: the whole text at once, no reader over a stream. A file that18# doesn't fit in memory needs one, and that is when to write it.1920# sep returns the separator to use, and complains when it isn't one character.21sepOf = fn(sep) {22 if sep == null {23 return ","24 }2526 sep = string(sep)27 if len(sep) != 1 {28 return error("csv: the separator is one character, got \"{sep}\"")29 }30 if sep == "\"" || sep == "\r" || sep == "\n" {31 return error("csv: {sep} cannot be a separator")32 }33 return sep34}3536# Parse reads text and returns its records. A record is a list of strings, and37# every one of them is returned as it was written, without a guess at what it38# means. An unterminated quoted field is an error.39Parse = fn(text, sep) {40 if failed(sep = sepOf(sep)) {41 return sep42 }4344 text = string(text)45 rows = []46 fields = []47 field = ""48 quoted = false # inside a "..." field49 started = false # anything at all seen in this record5051 i = 052 for i < len(text) {53 c = text[i]5455 if quoted {56 if c != "\"" {57 field = field + c58 ++i59 continue60 }6162 # A doubled quote is one quote, a single one ends the field.63 if i + 1 < len(text) && text[i + 1] == "\"" {64 field = field + "\""65 i = i + 266 continue67 }68 quoted = false69 ++i70 continue71 }7273 if c == "\"" && field == "" {74 quoted = true75 started = true76 ++i77 continue78 }7980 if c == sep {81 fields = append(fields, field)82 field = ""83 started = true84 ++i85 continue86 }8788 if c == "\n" || c == "\r" {89 if c == "\r" && i + 1 < len(text) && text[i + 1] == "\n" {90 ++i91 }92 ++i9394 # A blank line between records is skipped, the way Go does it.95 if started || len(fields) > 0 || field != "" {96 rows = append(rows, append(fields, field))97 }98 fields = []99 field = ""100 started = false101 continue102 }103104 field = field + c105 started = true106 ++i107 }108109 if quoted {110 return error("csv: the file ends inside a quoted field")111 }112 if started || len(fields) > 0 || field != "" {113 rows = append(rows, append(fields, field))114 }115 return rows116}117118# needsQuotes reports whether a field has to be written quoted.119needsQuotes = fn(field, sep) {120 if field == "" {121 return false122 }123 if field[0] == " " || field[len(field) - 1] == " " {124 return true125 }126127 for i = 0; i < len(field); ++i {128 c = field[i]129 if c == sep || c == "\"" || c == "\n" || c == "\r" {130 return true131 }132 }133 return false134}135136# FormatField returns one field written the way a CSV file wants it, quoted137# only when it has to be.138FormatField = fn(field, sep) {139 if failed(sep = sepOf(sep)) {140 return sep141 }142143 field = string(field)144 if !needsQuotes(field, sep) {145 return field146 }147148 out = "\""149 for i = 0; i < len(field); ++i {150 if field[i] == "\"" {151 out = out + "\"\""152 continue153 }154 out = out + field[i]155 }156 return out + "\""157}158159# Format writes the records as CSV text, one line each, the last one ended by160# a newline as well.161Format = fn(rows, sep) {162 if failed(sep = sepOf(sep)) {163 return sep164 }165166 out = ""167 for r = 0; r < len(rows); ++r {168 row = rows[r]169 for f = 0; f < len(row); ++f {170 if f > 0 {171 out = out + sep172 }173 out = out + FormatField(row[f], sep)174 }175 out = out + "\n"176 }177 return out178}