diff options
Diffstat (limited to 'encoding/csv.ts')
| -rw-r--r-- | encoding/csv.ts | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/encoding/csv.ts b/encoding/csv.ts new file mode 100644 index 000000000..3d50180cc --- /dev/null +++ b/encoding/csv.ts @@ -0,0 +1,151 @@ +// Ported from Go: +// https://github.com/golang/go/blob/go1.12.5/src/encoding/csv/ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +import { BufReader, BufState } from "../io/bufio.ts"; +import { TextProtoReader } from "../textproto/mod.ts"; + +const INVALID_RUNE = ["\r", "\n", '"']; + +export class ParseError extends Error { + StartLine: number; + Line: number; + constructor(start: number, line: number, message: string) { + super(message); + this.StartLine = start; + this.Line = line; + } +} + +export interface ParseOptions { + comma: string; + comment?: string; + trimLeadingSpace: boolean; + lazyQuotes?: boolean; + fieldsPerRecord?: number; +} + +function chkOptions(opt: ParseOptions): Error | null { + if ( + INVALID_RUNE.includes(opt.comma) || + INVALID_RUNE.includes(opt.comment) || + opt.comma === opt.comment + ) { + return Error("Invalid Delimiter"); + } + return null; +} + +export async function read( + Startline: number, + reader: BufReader, + opt: ParseOptions = { comma: ",", comment: "#", trimLeadingSpace: false } +): Promise<[string[], BufState]> { + const tp = new TextProtoReader(reader); + let err: BufState; + let line: string; + let result: string[] = []; + let lineIndex = Startline; + + [line, err] = await tp.readLine(); + + // Normalize \r\n to \n on all input lines. + if ( + line.length >= 2 && + line[line.length - 2] === "\r" && + line[line.length - 1] === "\n" + ) { + line = line.substring(0, line.length - 2); + line = line + "\n"; + } + + const trimmedLine = line.trimLeft(); + if (trimmedLine.length === 0) { + return [[], err]; + } + + // line starting with comment character is ignored + if (opt.comment && trimmedLine[0] === opt.comment) { + return [result, err]; + } + + result = line.split(opt.comma); + + let quoteError = false; + result = result.map( + (r): string => { + if (opt.trimLeadingSpace) { + r = r.trimLeft(); + } + if (r[0] === '"' && r[r.length - 1] === '"') { + r = r.substring(1, r.length - 1); + } else if (r[0] === '"') { + r = r.substring(1, r.length); + } + + if (!opt.lazyQuotes) { + if (r[0] !== '"' && r.indexOf('"') !== -1) { + quoteError = true; + } + } + return r; + } + ); + if (quoteError) { + return [ + [], + new ParseError(Startline, lineIndex, 'bare " in non-quoted-field') + ]; + } + return [result, err]; +} + +export async function readAll( + reader: BufReader, + opt: ParseOptions = { + comma: ",", + trimLeadingSpace: false, + lazyQuotes: false + } +): Promise<[string[][], BufState]> { + const result: string[][] = []; + let _nbFields: number; + let err: BufState; + let lineResult: string[]; + let first = true; + let lineIndex = 0; + err = chkOptions(opt); + if (err) return [result, err]; + + for (;;) { + [lineResult, err] = await read(lineIndex, reader, opt); + if (err) break; + lineIndex++; + // If fieldsPerRecord is 0, Read sets it to + // the number of fields in the first record + if (first) { + first = false; + if (opt.fieldsPerRecord !== undefined) { + if (opt.fieldsPerRecord === 0) { + _nbFields = lineResult.length; + } else { + _nbFields = opt.fieldsPerRecord; + } + } + } + + if (lineResult.length > 0) { + if (_nbFields && _nbFields !== lineResult.length) { + return [ + null, + new ParseError(lineIndex, lineIndex, "wrong number of fields") + ]; + } + result.push(lineResult); + } + } + if (err !== "EOF") { + return [result, err]; + } + return [result, null]; +} |
