Source file src/encoding/csv/writer.go

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package csv
     6  
     7  import (
     8  	"bufio"
     9  	"io"
    10  	"strings"
    11  	"unicode"
    12  	"unicode/utf8"
    13  )
    14  
    15  // A Writer writes records using CSV encoding.
    16  //
    17  // As returned by NewWriter, a Writer writes records terminated by a
    18  // newline and uses ',' as the field delimiter. The exported fields can be
    19  // changed to customize the details before the first call to Write or WriteAll.
    20  //
    21  // Comma is the field delimiter.
    22  //
    23  // If UseCRLF is true, the Writer ends each output line with \r\n instead of \n.
    24  //
    25  // The writes of individual records are buffered.
    26  // After all data has been written, the client should call the
    27  // Flush method to guarantee all data has been forwarded to
    28  // the underlying io.Writer.  Any errors that occurred should
    29  // be checked by calling the Error method.
    30  type Writer struct {
    31  	Comma   rune // Field delimiter (set to ',' by NewWriter)
    32  	UseCRLF bool // True to use \r\n as the line terminator
    33  	w       *bufio.Writer
    34  }
    35  
    36  // NewWriter returns a new Writer that writes to w.
    37  func NewWriter(w io.Writer) *Writer {
    38  	return &Writer{
    39  		Comma: ',',
    40  		w:     bufio.NewWriter(w),
    41  	}
    42  }
    43  
    44  // Write writes a single CSV record to w along with any necessary quoting.
    45  // A record is a slice of strings with each string being one field.
    46  // Writes are buffered, so Flush must eventually be called to ensure
    47  // that the record is written to the underlying io.Writer.
    48  func (w *Writer) Write(record []string) error {
    49  	if !validDelim(w.Comma) {
    50  		return errInvalidDelim
    51  	}
    52  
    53  	for n, field := range record {
    54  		if n > 0 {
    55  			if _, err := w.w.WriteRune(w.Comma); err != nil {
    56  				return err
    57  			}
    58  		}
    59  
    60  		// If we don't have to have a quoted field then just
    61  		// write out the field and continue to the next field.
    62  		if !w.fieldNeedsQuotes(field) {
    63  			if _, err := w.w.WriteString(field); err != nil {
    64  				return err
    65  			}
    66  			continue
    67  		}
    68  
    69  		if err := w.w.WriteByte('"'); err != nil {
    70  			return err
    71  		}
    72  		for len(field) > 0 {
    73  			// Search for special characters.
    74  			i := strings.IndexAny(field, "\"\r\n")
    75  			if i < 0 {
    76  				i = len(field)
    77  			}
    78  
    79  			// Copy verbatim everything before the special character.
    80  			if _, err := w.w.WriteString(field[:i]); err != nil {
    81  				return err
    82  			}
    83  			field = field[i:]
    84  
    85  			// Encode the special character.
    86  			if len(field) > 0 {
    87  				var err error
    88  				switch field[0] {
    89  				case '"':
    90  					_, err = w.w.WriteString(`""`)
    91  				case '\r':
    92  					if !w.UseCRLF {
    93  						err = w.w.WriteByte('\r')
    94  					}
    95  				case '\n':
    96  					if w.UseCRLF {
    97  						_, err = w.w.WriteString("\r\n")
    98  					} else {
    99  						err = w.w.WriteByte('\n')
   100  					}
   101  				}
   102  				field = field[1:]
   103  				if err != nil {
   104  					return err
   105  				}
   106  			}
   107  		}
   108  		if err := w.w.WriteByte('"'); err != nil {
   109  			return err
   110  		}
   111  	}
   112  	var err error
   113  	if w.UseCRLF {
   114  		_, err = w.w.WriteString("\r\n")
   115  	} else {
   116  		err = w.w.WriteByte('\n')
   117  	}
   118  	return err
   119  }
   120  
   121  // Flush writes any buffered data to the underlying io.Writer.
   122  // To check if an error occurred during the Flush, call Error.
   123  func (w *Writer) Flush() {
   124  	w.w.Flush()
   125  }
   126  
   127  // Error reports any error that has occurred during a previous Write or Flush.
   128  func (w *Writer) Error() error {
   129  	_, err := w.w.Write(nil)
   130  	return err
   131  }
   132  
   133  // WriteAll writes multiple CSV records to w using Write and then calls Flush,
   134  // returning any error from the Flush.
   135  func (w *Writer) WriteAll(records [][]string) error {
   136  	for _, record := range records {
   137  		err := w.Write(record)
   138  		if err != nil {
   139  			return err
   140  		}
   141  	}
   142  	return w.w.Flush()
   143  }
   144  
   145  // fieldNeedsQuotes reports whether our field must be enclosed in quotes.
   146  // Fields with a Comma, fields with a quote or newline, and
   147  // fields which start with a space must be enclosed in quotes.
   148  // We used to quote empty strings, but we do not anymore (as of Go 1.4).
   149  // The two representations should be equivalent, but Postgres distinguishes
   150  // quoted vs non-quoted empty string during database imports, and it has
   151  // an option to force the quoted behavior for non-quoted CSV but it has
   152  // no option to force the non-quoted behavior for quoted CSV, making
   153  // CSV with quoted empty strings strictly less useful.
   154  // Not quoting the empty string also makes this package match the behavior
   155  // of Microsoft Excel and Google Drive.
   156  // For Postgres, quote the data terminating string `\.`.
   157  func (w *Writer) fieldNeedsQuotes(field string) bool {
   158  	if field == "" {
   159  		return false
   160  	}
   161  
   162  	if field == `\.` {
   163  		return true
   164  	}
   165  
   166  	if w.Comma < utf8.RuneSelf {
   167  		for i := 0; i < len(field); i++ {
   168  			c := field[i]
   169  			if c == '\n' || c == '\r' || c == '"' || c == byte(w.Comma) {
   170  				return true
   171  			}
   172  		}
   173  	} else {
   174  		if strings.ContainsRune(field, w.Comma) || strings.ContainsAny(field, "\"\r\n") {
   175  			return true
   176  		}
   177  	}
   178  
   179  	r1, _ := utf8.DecodeRuneInString(field)
   180  	return unicode.IsSpace(r1)
   181  }
   182  

View as plain text