├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config.go ├── config_test.go ├── csv_test.go ├── csvutil.go ├── file.go ├── file_test.go ├── reader.go ├── reader_test.go ├── row.go ├── writer.go └── writer_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.[568qv] 2 | [568qv].out 3 | _test 4 | _testmain.go 5 | _obj 6 | synopsis 7 | *.test 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Bryan Matsuo 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | csvutil - Your CSV pocket-knife (golang) 2 | 3 | #WARNING 4 | 5 | **I would advise against using this package. It was a language learning 6 | exercise from a time before "encoding/csv" existed. If encoding/csv doesn't fit your needs I would advise finding a different maintained package or writing a new one** 7 | 8 | Version 9 | ======= 10 | 11 | This is csvutil version 1.1_2 12 | 13 | Synopsis 14 | ======== 15 | 16 | The csvutil package can be used to read CSV data from any io.Reader and, 17 | write CSV data to any io.Writer. It can automatically generate CSV rows 18 | from slices containing native Go types, other slices/arrays of native go 19 | types, or flat structs. It can also convert CSV rows and assign values to 20 | memory referenced by a slice of pointers. 21 | 22 | ```go 23 | package main 24 | import "os" 25 | import "github.com/bmatsuo/csvutil" 26 | 27 | type Person struct { 28 | Name string 29 | Height float64 30 | Weight float64 31 | } 32 | 33 | func main() { 34 | writer := csvutil.NewWriter(os.Stdout, nil) 35 | errdo := csvutil.DoFile(os.Args[1], func(r csvutil.Row) bool { 36 | if r.HasError() { 37 | panic(r.Error) 38 | } 39 | var person Person 40 | if _, errc := r.Format(&person); errc != nil { 41 | panic("Row is not a Person") 42 | } 43 | bmi := person.Weight / (person.Height * person.Height) 44 | writer.WriteRow(csvutil.FormatRow(person, bmi).Fields...) 45 | return true 46 | }) 47 | if errdo != nil { 48 | panic(errdo) 49 | } 50 | writer.Flush() 51 | } 52 | ``` 53 | 54 | Given a CSV file 'in.csv' with contents 55 | 56 | ``` 57 | alice,1.4,50 58 | bob,2,80 59 | chris,1.6,67 60 | ``` 61 | 62 | When the above program is called and given the path 'in.csv', the following 63 | is printed to standard output. 64 | 65 | ``` 66 | alice,1.4,50,25.510204081632658 67 | bob,2,80,20 68 | chris,1.6,67,26.171874999999996 69 | ``` 70 | 71 | About 72 | ===== 73 | 74 | The csvutil package is written to make interacting with CSV data as easy 75 | as possible. Efficiency of its underlying functions and methods are 76 | slightly less important factors in its design. However, that being said, 77 | **it is important**. And, csvutil should be capable of handling both 78 | extremely large and fairly small streams of CSV data through input and 79 | output quite well in terms of speed and memory footprint. It should do 80 | this while not making your code bend over backwards (more than necessary). 81 | As libraries should never make you do. 82 | 83 | Features 84 | ======== 85 | 86 | * Slurping/spewing CSV data. That is, reading/writing whole files or data 87 | streams at once. 88 | 89 | * Iteration through individual rows of a CSV data stream for a smaller 90 | memory footprint. 91 | 92 | * Writing of individual writing fields/rows (along with batch writing). 93 | 94 | * Automated CSV row serialization and deserialization (formatting) for flat 95 | data structures and types. 96 | 97 | Todo 98 | ==== 99 | 100 | * Enhance and clean the formatting API and allow better formatting of data. 101 | 102 | Install 103 | ======= 104 | 105 | The easiest installation of csvutil is done through goinstall. 106 | 107 | goinstall github.com/bmatsuo/csvutil 108 | 109 | Documentation 110 | ============= 111 | 112 | The best way to read the current csvutil documentation is using 113 | godoc. 114 | 115 | godoc github.com/bmatsuo/csvutil 116 | 117 | Or better yet, you can run a godoc http server. 118 | 119 | godoc -http=":6060" 120 | 121 | Then go to the url http://localhost:6060/pkg/github.com/bmatsuo/csvutil/ 122 | 123 | Copyright & License 124 | =================== 125 | 126 | Copyright (c) 2011, Bryan Matsuo. 127 | All rights reserved. 128 | 129 | Use of this source code is governed by a BSD-style license that can be 130 | found in the LICENSE file. 131 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | /* 4 | * Filename: config.go 5 | * Package: csvutil 6 | * Author: Bryan Matsuo 7 | * Created: Tue Jul 12 01:56:03 PDT 2011 8 | * Description: Define the configuration type for Readers and Writers. 9 | */ 10 | import () 11 | 12 | // A configuration structure that can be shared between a Reader and Writer. 13 | type Config struct { 14 | // General configuration 15 | // Field seperator 16 | Sep rune 17 | // Trim leading/trailing whitespace in fields. 18 | Trim bool 19 | // Characters to trim from fields. 20 | Cutset string 21 | // Prefix for comment lines. 22 | CommentPrefix string 23 | 24 | // Reader specific config 25 | // Are comments allowed in the input. 26 | Comments bool 27 | // Comments can appear in the body (Comments must be true). 28 | CommentsInBody bool 29 | } 30 | 31 | // The default configuration is used for Readers and Writers when none is 32 | // given. 33 | var ( 34 | DefaultConfig = &Config{ 35 | Sep: ',', Trim: false, Cutset: " \t", CommentPrefix: "#", 36 | Comments: false, CommentsInBody: false} 37 | ) 38 | 39 | // Return a freshly allocated Config that is initialized to DefaultConfig. 40 | func NewConfig() *Config { 41 | var c = new(Config) 42 | *c = *DefaultConfig 43 | return c 44 | } 45 | 46 | func (c *Config) LooksLikeComment(line string) bool { 47 | return line[:len(c.CommentPrefix)] == c.CommentPrefix 48 | } 49 | 50 | func (c *Config) IsSep(rune rune) bool { 51 | return rune == c.Sep 52 | } 53 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | /* 4 | * Filename: config_test.go 5 | * Author: Bryan Matsuo 6 | * Created: Tue Jul 12 01:56:03 PDT 2011 7 | * Description: 8 | * Usage: gotest 9 | */ 10 | import ( 11 | "testing" 12 | "unicode/utf8" 13 | ) 14 | 15 | // Some rediculously dumb tests of Config methods, which are very simple. 16 | func TestConfig(T *testing.T) { 17 | var config = NewConfig() 18 | 19 | // Test comment detection. 20 | config.CommentPrefix = "//" 21 | if !config.LooksLikeComment("// This should be a comment.\n") { 22 | T.Error("Did not correctly identify a // comment") 23 | } 24 | if config.LooksLikeComment("/ This, is not, a comment\n") { 25 | T.Error("Incorrectly labeled something a // comment") 26 | } 27 | 28 | // Test seperator detection. 29 | config.Sep = '\t' 30 | str := "\t" 31 | c, n := utf8.DecodeRuneInString(str) 32 | if c == utf8.RuneError && n == 1 { 33 | T.Errorf("Could not decode rune in string %q", str) 34 | } 35 | if !config.IsSep(c) { 36 | T.Error("Did not correctly identify a \\t separator") 37 | } 38 | if config.IsSep(52) { 39 | T.Error("Incorrectly labelled 52 a \\t separator") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /csv_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 | // This file doesn't actually do any tests. 6 | // But it has helper functions/data for testing functions. 7 | package csvutil 8 | 9 | import ( 10 | "bytes" 11 | "strings" 12 | ) 13 | 14 | func StringReader(s string, c *Config) *Reader { 15 | sreader := strings.NewReader(s) 16 | return NewReader(sreader, c) 17 | } 18 | 19 | func BufferWriter(c *Config) (*Writer, *bytes.Buffer) { 20 | bwriter := bytes.NewBufferString("") 21 | return NewWriter(bwriter, c), bwriter 22 | } 23 | 24 | // TEST1 - Simple 3x3 matrix w/ comma separators and w/o excess whitespace. 25 | // This is a simple test of the readers ability to return something of 26 | // ideal input format in the proper internal form. the dimensions and 27 | // values of the resulting parsed matrix are verified against the [][]string 28 | // matrix which created the CSV data. 29 | var ( 30 | TestMatrix1 = [][]string{ 31 | []string{"field1", "field2", "field3"}, 32 | []string{"Ben Franklin", "3.704", "10"}, 33 | []string{"Tom Jefferson", "5.7", "15"}} 34 | ) 35 | 36 | func csvTestString1() string { 37 | var testfields [][]string = TestMatrix1 38 | var rows []string = make([]string, 4) 39 | rows[0] = strings.Join(testfields[0], ",") 40 | rows[1] = strings.Join(testfields[1], ",") 41 | rows[2] = strings.Join(testfields[2], ",") 42 | rows[3] = "" 43 | return strings.Join(rows, "\n") 44 | } 45 | func csvTestInstance1() ([][]string, string) { 46 | return TestMatrix1, csvTestString1() 47 | } 48 | 49 | // END TEST1 50 | 51 | // TEST2 - 3x3 matrix w/ tab separators, w/o excess whitespace. And with 52 | // leading '#' comments. 53 | var ( 54 | TestMatrix2 = [][]string{ 55 | []string{"field1", "field2", "field3"}, 56 | []string{"Ben Franklin", "3.704", "10"}, 57 | []string{"Tom Jefferson", "5.7", "15"}} 58 | TestMatrix2Comments = []string{ 59 | " This is a comment string", 60 | " This another comment string"} 61 | ) 62 | 63 | func csvTestString2() string { 64 | var testfields = TestMatrix2 65 | var comments = TestMatrix2Comments 66 | var rows = make([]string, 6) 67 | rows[0] = strings.Join([]string{"#", comments[0]}, "") 68 | rows[1] = strings.Join([]string{"#", comments[1]}, "") 69 | rows[2] = strings.Join(testfields[0], "\t") 70 | rows[3] = strings.Join(testfields[1], "\t") 71 | rows[4] = strings.Join(testfields[2], "\t") 72 | rows[5] = "" 73 | return strings.Join(rows, "\n") 74 | } 75 | func csvTestInstance2() ([][]string, string) { 76 | return TestMatrix2, csvTestString2() 77 | } 78 | 79 | // END TEST2 80 | -------------------------------------------------------------------------------- /csvutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 csvutil is not maintained. It provides formatted reading and writing of CSV data. 6 | package csvutil 7 | 8 | import () 9 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 csvutil 6 | 7 | /* 8 | * File: file.go 9 | * Author: Bryan Matsuo [bmatsuo@soe.ucsc.edu] 10 | * Created: Sun May 29 23:14:48 PDT 2011 11 | * 12 | * This file is part of csvutil. 13 | * 14 | * csvutil is free software: you can redistribute it and/or modify 15 | * it under the terms of the GNU Lesser Public License as published by 16 | * the Free Software Foundation, either version 3 of the License, or 17 | * (at your option) any later version. 18 | * 19 | * csvutil is distributed in the hope that it will be useful, 20 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | * GNU Lesser Public License for more details. 23 | * 24 | * You should have received a copy of the GNU Lesser Public License 25 | * along with csvutil. If not, see . 26 | */ 27 | import ( 28 | "io" 29 | "os" 30 | ) 31 | 32 | // Write a slice of rows (string slices) to an io.Writer object. 33 | func Write(w io.Writer, rows [][]string) (int, error) { 34 | var ( 35 | csvw = NewWriter(w, nil) 36 | nbytes, err = csvw.WriteRows(rows) 37 | ) 38 | if err != nil { 39 | return nbytes, err 40 | } 41 | return nbytes, csvw.Flush() 42 | } 43 | 44 | // Write CSV data to a named file. If the file does not exist, it is 45 | // created. If the file exists, it is truncated upon opening. Requires 46 | // that file permissions be specified. Recommended permissions are 0600, 47 | // 0622, and 0666 (6:rw, 4:w, 2:r). 48 | func WriteFile(filename string, perm os.FileMode, rows [][]string) (int, error) { 49 | var ( 50 | out *os.File 51 | nbytes int 52 | err error 53 | mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC 54 | ) 55 | if out, err = os.OpenFile(filename, mode, perm); err != nil { 56 | return nbytes, err 57 | } 58 | if nbytes, err = Write(out, rows); err != nil { 59 | return nbytes, err 60 | } 61 | return nbytes, out.Close() 62 | } 63 | 64 | // Read rows from an io.Reader until EOF is encountered. 65 | func Read(r io.Reader) ([][]string, error) { 66 | var csvr = NewReader(r, nil) 67 | return csvr.RemainingRows() 68 | } 69 | 70 | // Read a named CSV file into a new slice of new string slices. 71 | func ReadFile(filename string) ([][]string, error) { 72 | var ( 73 | in *os.File 74 | rows [][]string 75 | err error 76 | ) 77 | if in, err = os.Open(filename); err != nil { 78 | return rows, err 79 | } 80 | if rows, err = Read(in); err != nil { 81 | return rows, err 82 | } 83 | return rows, in.Close() 84 | } 85 | 86 | // Iteratively apply a function to Row objects read from an io.Reader. 87 | func Do(r io.Reader, f func(r Row) bool) { 88 | var csvr = NewReader(r, nil) 89 | csvr.Do(f) 90 | } 91 | 92 | // Iteratively apply a function to Row objects read from a named file. 93 | func DoFile(filename string, f func(r Row) bool) error { 94 | var in, err = os.Open(filename) 95 | if err != nil { 96 | return err 97 | } 98 | Do(in, f) 99 | return in.Close() 100 | } 101 | -------------------------------------------------------------------------------- /file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 | // See csv_test.go for more information about each test. 6 | package csvutil 7 | 8 | import ( 9 | "bytes" 10 | "io/ioutil" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | func cleanTestFile(f string, T *testing.T) { 16 | _, statErr := os.Stat(f) 17 | if os.IsNotExist(statErr) { 18 | return 19 | } 20 | if statErr != nil { 21 | T.Errorf("Error stat'ing the test file %s; %s\n", f, statErr.Error()) 22 | } 23 | rmErr := os.Remove(f) 24 | if rmErr != nil { 25 | T.Error("Error removing the test file %s; %s\n", f, rmErr.Error()) 26 | } 27 | } 28 | 29 | // TEST1 - Simple 3x3 matrix w/ comma separators and w/o excess whitespace. 30 | var ( 31 | TestIn string = "_test-csvutil-01-i.csv" 32 | TestOut string = "_test-csvutil-01-o.csv" 33 | TestPerm os.FileMode = 0622 34 | ) 35 | 36 | func TestWriteFile(T *testing.T) { 37 | var testFilename string = TestOut 38 | defer cleanTestFile(testFilename, T) 39 | mat, str := csvTestInstance1() 40 | nbytes, err := WriteFile(testFilename, TestPerm, mat) 41 | if err != nil { 42 | T.Error(err) 43 | } 44 | if nbytes == 0 { 45 | T.Error("Wrote 0 bytes.") 46 | return 47 | } 48 | T.Logf("Wrote %d bytes.\n", nbytes) 49 | var outputString []byte 50 | outputString, err = ioutil.ReadFile(testFilename) 51 | if err != nil { 52 | T.Errorf("Error reading the test output %s for verification", testFilename) 53 | } 54 | T.Logf("\nExpected:\n'%s'\nReceived:\n'%s'\n\n", outputString, str) 55 | if string(outputString) != str { 56 | T.Error("OUTPUT MISMATCH") 57 | } 58 | } 59 | 60 | func TestReadFile(T *testing.T) { 61 | var testFilename string = TestOut 62 | defer cleanTestFile(testFilename, T) 63 | 64 | mat, str := csvTestInstance1() 65 | err := ioutil.WriteFile(testFilename, bytes.NewBufferString(str).Bytes(), TestPerm) 66 | if err != nil { 67 | T.Error(err) 68 | } 69 | inputMat, csvErr := ReadFile(testFilename) 70 | if csvErr != nil { 71 | T.Errorf("CSV reading error: %s", err.Error()) 72 | } 73 | T.Logf("\nExpected;\n'%v'\n Received:\n'%v'\n\n", mat, inputMat) 74 | if len(inputMat) != len(mat) { 75 | T.Fatal("INPUT MISMATCH; number of rows") 76 | } 77 | for i := 0; i < len(mat); i++ { 78 | if len(mat[i]) != len(inputMat[i]) { 79 | T.Errorf("INPUT MISMATCH; row %d", i) 80 | } else { 81 | for j := 0; j < len(mat[i]); j++ { 82 | if mat[i][j] != inputMat[i][j] { 83 | T.Errorf("INPUT MISMATCH; %d, %d", i, j) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | // END TEST1 91 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 csvutil 6 | 7 | import ( 8 | "bufio" 9 | "io" 10 | "strings" 11 | ) 12 | 13 | // readerBufferMinimumSize is the smallest csvutil will allow the 14 | // Reader's internal "long-line buffer" to be allocated as. 15 | const readerBufferMinimumSize = 30 16 | 17 | // A reader object for CSV data utilizing the bufio package. 18 | type Reader struct { 19 | *Config 20 | r io.Reader // Base reader object. 21 | br *bufio.Reader // Buffering for efficiency and line reading. 22 | p []byte // A buffer for longer lines 23 | pi int // An index into the p buffer. 24 | lineNum int 25 | pastHeader bool 26 | } 27 | 28 | // Create a new reader object. 29 | func NewReader(r io.Reader, c *Config) *Reader { 30 | var csvr *Reader = new(Reader) 31 | if csvr.Config = c; c == nil { 32 | csvr.Config = NewConfig() 33 | } 34 | csvr.r = r 35 | csvr.br = bufio.NewReader(r) 36 | return csvr 37 | } 38 | 39 | // Create a new reader with a buffer of a specified size. 40 | func NewReaderSize(r io.Reader, c *Config, size int) *Reader { 41 | var csvr *Reader = new(Reader) 42 | if csvr.Config = c; c == nil { 43 | csvr.Config = NewConfig() 44 | } 45 | csvr.r = r 46 | br := bufio.NewReaderSize(r, size) 47 | csvr.br = br 48 | return csvr 49 | } 50 | 51 | func (csvr *Reader) readLine() (string, error) { 52 | var ( 53 | isPrefix = true 54 | piece []byte 55 | err error 56 | ) 57 | for isPrefix { 58 | piece, isPrefix, err = csvr.br.ReadLine() 59 | switch err { 60 | case nil: 61 | break 62 | case io.EOF: 63 | fallthrough 64 | default: 65 | return "", err 66 | } 67 | var ( 68 | readLen = len(piece) 69 | necLen = csvr.pi + readLen 70 | pLen = len(csvr.p) 71 | ) 72 | if pLen == 0 { 73 | if pLen = readerBufferMinimumSize; pLen < necLen { 74 | pLen = necLen 75 | } 76 | csvr.p = make([]byte, pLen) 77 | csvr.pi = 0 78 | } else if pLen < necLen { 79 | if pLen = 2 * pLen; pLen < necLen { 80 | pLen = necLen 81 | } 82 | var p = make([]byte, pLen) 83 | copy(p, csvr.p[:csvr.pi]) 84 | } 85 | csvr.pi += copy(csvr.p[csvr.pi:], piece) 86 | } 87 | var s = string(csvr.p[:csvr.pi]) 88 | for i := 0; i < csvr.pi; i++ { 89 | csvr.p[i] = 0 90 | } 91 | csvr.pi = 0 92 | return s, nil 93 | } 94 | 95 | // Returns the number of lines of input read by the Reader 96 | func (csvr *Reader) LineNum() int { 97 | return csvr.lineNum 98 | } 99 | 100 | // Attempt to read up to a new line, skipping any comment lines found in 101 | // the process. Return a Row object containing the fields read and any 102 | // error encountered. 103 | func (csvr *Reader) ReadRow() Row { 104 | var ( 105 | r Row 106 | line string 107 | ) 108 | // Read lines until a non-comment line is found. 109 | for true { 110 | if line, r.Error = csvr.readLine(); r.Error != nil { 111 | return r 112 | } 113 | csvr.lineNum++ 114 | if !csvr.Comments { 115 | break 116 | } else if !csvr.LooksLikeComment(line) { 117 | break 118 | } else if csvr.pastHeader && !csvr.CommentsInBody { 119 | break 120 | } 121 | } 122 | csvr.pastHeader = true 123 | 124 | // Break the line up into fields. 125 | r.Fields = strings.FieldsFunc(line, func(c rune) bool { return csvr.IsSep(c) }) 126 | 127 | // Trim any unwanted characters. 128 | if csvr.Trim { 129 | for i := 0; i < len(r.Fields); i++ { 130 | r.Fields[i] = strings.Trim(r.Fields[i], csvr.Cutset) 131 | } 132 | } 133 | return r 134 | } 135 | 136 | // Read rows into a preallocated buffer. Return the number of rows read, 137 | // and any error encountered. 138 | func (csvr *Reader) ReadRows(rbuf [][]string) (int, error) { 139 | var ( 140 | i int 141 | err error 142 | ) 143 | csvr.DoN(len(rbuf), func(r Row) bool { 144 | err = r.Error 145 | if r.Fields != nil { 146 | rbuf[i] = r.Fields 147 | i++ 148 | } 149 | return !r.HasError() 150 | }) 151 | return i, err 152 | } 153 | 154 | // Reads any remaining rows of CSV data in the underlying io.Reader. 155 | func (csvr *Reader) RemainingRows() (rows [][]string, err error) { 156 | return csvr.RemainingRowsSize(16) 157 | } 158 | 159 | // Like csvr.RemainingRows(), but allows specification of the initial 160 | // row buffer capacity to avoid unnecessary reallocations. 161 | func (csvr *Reader) RemainingRowsSize(size int) ([][]string, error) { 162 | var ( 163 | err error 164 | rbuf = make([][]string, 0, size) 165 | ) 166 | csvr.Do(func(r Row) bool { 167 | err = r.Error 168 | //log.Printf("Scanned %v", r) 169 | if r.Fields != nil { 170 | rbuf = append(rbuf, r.Fields) 171 | } 172 | return !r.HasError() 173 | }) 174 | return rbuf, err 175 | } 176 | 177 | // Iteratively read the remaining rows in the reader and call f on each 178 | // of them. If f returns false, no more rows will be read. 179 | func (csvr *Reader) Do(f func(Row) bool) { 180 | for r := csvr.ReadRow(); true; r = csvr.ReadRow() { 181 | if r.HasEOF() { 182 | //log.Printf("EOF") 183 | break 184 | } 185 | if !f(r) { 186 | //log.Printf("Break") 187 | break 188 | } 189 | } 190 | } 191 | 192 | // Process rows from the reader like Do, but stop after processing n of 193 | // them. If f returns false before n rows have been process, no more rows 194 | // will be processed. 195 | func (csvr *Reader) DoN(n int, f func(Row) bool) { 196 | var i int 197 | csvr.Do(func(r Row) bool { 198 | if i < n { 199 | return f(r) 200 | } 201 | return false 202 | }) 203 | } 204 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 csvutil 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestDo(T *testing.T) { 12 | var csvr *Reader = StringReader(csvTestString1(), nil) 13 | var rowlen = -1 14 | csvr.Do(func(r Row) bool { 15 | if r.HasError() { 16 | T.Errorf("Read error encountered: %v", r.Error) 17 | return false 18 | } 19 | if rowlen == -1 { 20 | rowlen = len(r.Fields) 21 | } else if rowlen != len(r.Fields) { 22 | T.Error("Row length error, non-rectangular.") 23 | } 24 | return true 25 | }) 26 | } 27 | 28 | // TEST1 - Simple 3x3 matrix w/ comma separators and w/o excess whitespace. 29 | func TestReadRow(T *testing.T) { 30 | T.Log("Beginning test\n") 31 | var csvr *Reader = StringReader(csvTestString1(), nil) 32 | var n int = -1 33 | var rows [][]string 34 | var headrow Row = csvr.ReadRow() 35 | n = len(headrow.Fields) 36 | if n != 3 { 37 | T.Errorf("Unexpected row size %d\n", n) 38 | } 39 | rows = make([][]string, n) // Expect a square matrix. 40 | rows[0] = headrow.Fields 41 | var i int = 1 42 | csvr.Do(func(row Row) bool { 43 | var k int = len(row.Fields) 44 | if k != n { 45 | T.Errorf("Unexpected row size %d (!= %d)\n", k, n) 46 | } 47 | var j int = 0 48 | for j = 0; j < k; j++ { 49 | var field string = row.Fields[j] 50 | if len(field) < 1 { 51 | T.Error("Unexpected non-empty string\n") 52 | } 53 | } 54 | rows[i] = row.Fields 55 | i++ 56 | return true 57 | }) 58 | var test_matrix [][]string = TestMatrix1 59 | var assert_val = func(i, j int) { 60 | if rows[i][j] != test_matrix[i][j] { 61 | T.Errorf("Unexpected value in (%d,%d), %s", i, j, rows[i][j]) 62 | } 63 | } 64 | for i := 0; i < n; i++ { 65 | for j := 0; j < n; j++ { 66 | assert_val(i, j) 67 | } 68 | } 69 | T.Log("Finished test\n") 70 | } 71 | 72 | // END TEST1 73 | 74 | func TestComments(T *testing.T) { 75 | // Create the test configuration. 76 | var config = NewConfig() 77 | config.Sep = '\t' 78 | config.Comments = true 79 | 80 | // Create a Reader for the test string and parse the rows. 81 | var ( 82 | reader = StringReader(csvTestString2(), config) 83 | rows, err = reader.RemainingRows() 84 | ) 85 | if err != nil { 86 | T.Error("Error:", err.Error()) 87 | } 88 | if len(rows) != len(TestMatrix2) { 89 | T.Error("Different number of rows in parsed and original", len(rows), len(TestMatrix2)) 90 | } 91 | for i, row := range rows { 92 | if i >= len(TestMatrix2) { 93 | break 94 | } 95 | for j, s := range row { 96 | if j >= len(TestMatrix2[i]) { 97 | T.Errorf("Row %d: different number of columns in parsed %d and original %d", 98 | i, len(row), len(TestMatrix2[i])) 99 | break 100 | } 101 | if s != TestMatrix2[i][j] { 102 | T.Errorf("'%s' != '%s' at (%d,%d)", s, TestMatrix2[i][j], i, j) 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /row.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 csvutil 6 | 7 | /* 8 | * File: row.go 9 | * Author: Bryan Matsuo [bmatsuo@soe.ucsc.edu] 10 | * Created: Wed Jun 1 16:48:20 PDT 2011 11 | * Description: Row related types and methods. 12 | */ 13 | import ( 14 | "errors" 15 | "io" 16 | //"fmt" 17 | //"log" 18 | "reflect" 19 | "strconv" 20 | ) 21 | 22 | // See strconv.Ftoa32() 23 | var ( 24 | FloatFmt byte = 'g' 25 | FloatPrec int = -1 26 | ) 27 | 28 | // A simple row structure for rows read by a csvutil.Reader that 29 | // encapsulates any read error enountered along with any data read 30 | // prior to encountering an error. 31 | type Row struct { 32 | Fields []string "CSV row field data" 33 | Error error "Error encountered reading" 34 | } 35 | 36 | // A wrapper for the test r.Error == os.EOF 37 | func (r Row) HasEOF() bool { 38 | return r.Error == io.EOF 39 | } 40 | 41 | // A wrapper for the test r.Error != nil 42 | func (r Row) HasError() bool { 43 | return r.Error != nil 44 | } 45 | 46 | var ( 47 | ErrorIndex = errors.New("Not enough fields to format") 48 | ErrorStruct = errors.New("Cannot format unreferenced structs") 49 | ErrorUnimplemented = errors.New("Unimplemented field type") 50 | ErrorFieldType = errors.New("Field type incompatible.") 51 | ErrorNonPointer = errors.New("Target is not a pointer.") 52 | ErrorCantSet = errors.New("Cannot set value.") 53 | ) 54 | 55 | func (r Row) formatReflectValue(i int, x reflect.Value) (int, error) { 56 | if i >= len(r.Fields) { 57 | return 0, ErrorIndex 58 | } 59 | if !x.CanSet() { 60 | return 0, ErrorCantSet 61 | } 62 | var ( 63 | assigned int 64 | errc error 65 | kind = x.Kind() 66 | ) 67 | switch kind { 68 | // Format pointers to standard types. 69 | case reflect.String: 70 | x.SetString(r.Fields[i]) 71 | assigned++ 72 | case reflect.Int: 73 | fallthrough 74 | case reflect.Int8: 75 | fallthrough 76 | case reflect.Int16: 77 | fallthrough 78 | case reflect.Int32: 79 | fallthrough 80 | case reflect.Int64: 81 | var vint int64 82 | vint, errc = strconv.ParseInt(r.Fields[i], 10, 64) 83 | if errc == nil { 84 | x.SetInt(vint) 85 | assigned++ 86 | } 87 | case reflect.Uint: 88 | fallthrough 89 | case reflect.Uint8: 90 | fallthrough 91 | case reflect.Uint16: 92 | fallthrough 93 | case reflect.Uint32: 94 | fallthrough 95 | case reflect.Uint64: 96 | var vuint uint64 97 | vuint, errc = strconv.ParseUint(r.Fields[i], 10, 64) 98 | if errc == nil { 99 | x.SetUint(vuint) 100 | assigned++ 101 | } 102 | case reflect.Float32: 103 | fallthrough 104 | case reflect.Float64: 105 | var vfloat float64 106 | vfloat, errc = strconv.ParseFloat(r.Fields[i], 64) 107 | if errc == nil { 108 | x.SetFloat(vfloat) 109 | assigned++ 110 | } 111 | case reflect.Complex64: 112 | fallthrough 113 | case reflect.Complex128: 114 | errc = ErrorUnimplemented 115 | case reflect.Bool: 116 | var vbool bool 117 | vbool, errc = strconv.ParseBool(r.Fields[i]) 118 | if errc == nil { 119 | x.SetBool(vbool) 120 | assigned++ 121 | } 122 | default: 123 | errc = ErrorFieldType 124 | } 125 | return assigned, errc 126 | } 127 | 128 | func (r Row) formatValue(i int, x interface{}) (int, error) { 129 | // TODO add complex types 130 | if i >= len(r.Fields) { 131 | return 0, ErrorIndex 132 | } 133 | var ( 134 | assigned = 0 135 | errc error 136 | n int 137 | value = reflect.ValueOf(x) 138 | ) 139 | if !value.IsValid() { 140 | return 0, ErrorFieldType 141 | } 142 | //var t = value.Type() 143 | var kind = value.Kind() 144 | switch kind { 145 | case reflect.Ptr: 146 | //log.Print("PtrType") 147 | break 148 | case reflect.Array: 149 | //log.Print("ArrayType") 150 | fallthrough 151 | case reflect.Slice: 152 | //log.Print("SliceType") 153 | n = value.Len() 154 | for j := 0; j < n; j++ { 155 | var vj = value.Index(j) 156 | rvasgn, rverr := r.formatReflectValue(i+j, vj) 157 | assigned += rvasgn 158 | if rverr != nil { 159 | return assigned, rverr 160 | } 161 | } 162 | return assigned, errc 163 | case reflect.Map: 164 | //log.Print("MapType") 165 | return 0, ErrorUnimplemented 166 | default: 167 | return 0, ErrorFieldType 168 | } 169 | var ( 170 | eVal = value.Elem() 171 | eType = eVal.Type() 172 | eKind = eType.Kind() 173 | ) 174 | switch eKind { 175 | // Format pointers to standard types. 176 | case reflect.Struct: 177 | switch kind { 178 | case reflect.Ptr: 179 | n = eVal.NumField() 180 | for j := 0; j < n; j++ { 181 | var vj = eVal.Field(j) 182 | rvasgn, rverr := r.formatReflectValue(i+j, vj) 183 | assigned += rvasgn 184 | if rverr != nil { 185 | return assigned, rverr 186 | } 187 | } 188 | default: 189 | errc = ErrorStruct 190 | } 191 | case reflect.Array: 192 | //log.Print("ArrayType") 193 | fallthrough 194 | case reflect.Slice: 195 | //log.Print("SliceType") 196 | n = eVal.Len() 197 | for j := 0; j < n; j++ { 198 | var vj = eVal.Index(j) 199 | rvasgn, rverr := r.formatReflectValue(i+j, vj) 200 | assigned += rvasgn 201 | if rverr != nil { 202 | return assigned, rverr 203 | } 204 | } 205 | return assigned, errc 206 | case reflect.Map: 207 | //log.Print("MapType") 208 | return 0, ErrorUnimplemented 209 | default: 210 | assigned, errc = r.formatReflectValue(i, eVal) 211 | } 212 | return assigned, errc 213 | } 214 | 215 | // Iteratively take values from the argument list and assigns to them 216 | // successive fields from the row object. Returns the number of row fields 217 | // assigned to arguments and any error that occurred. 218 | func (r Row) Format(x ...interface{}) (int, error) { 219 | var ( 220 | assigned int 221 | vasg int 222 | err error 223 | ) 224 | for _, elm := range x { 225 | vasg, err = r.formatValue(assigned, elm) 226 | assigned += vasg 227 | if err != nil { 228 | return assigned, err 229 | } 230 | } 231 | return assigned, err 232 | } 233 | 234 | func formatReflectValue(x reflect.Value) (string, error) { 235 | /* 236 | if !x.CanSet() { 237 | return "", ErrorCantSet 238 | } 239 | */ 240 | var ( 241 | errc error 242 | kind = x.Kind() 243 | //vintstr string 244 | ) 245 | switch kind { 246 | // Format pointers to standard types. 247 | case reflect.String: 248 | return x.Interface().(string), nil 249 | case reflect.Int: 250 | return strconv.Itoa(x.Interface().(int)), nil 251 | case reflect.Int8: 252 | return strconv.FormatInt(int64(x.Interface().(int8)), 10), nil 253 | case reflect.Int16: 254 | return strconv.FormatInt(int64(x.Interface().(int16)), 10), nil 255 | case reflect.Int32: 256 | return strconv.FormatInt(int64(x.Interface().(int32)), 10), nil 257 | case reflect.Int64: 258 | return strconv.FormatInt(x.Interface().(int64), 10), nil 259 | case reflect.Uint: 260 | return strconv.FormatUint(uint64(x.Interface().(uint)), 10), nil 261 | case reflect.Uint8: 262 | return strconv.FormatUint(uint64(x.Interface().(uint8)), 10), nil 263 | case reflect.Uint16: 264 | return strconv.FormatUint(uint64(x.Interface().(uint16)), 10), nil 265 | case reflect.Uint32: 266 | return strconv.FormatUint(uint64(x.Interface().(uint32)), 10), nil 267 | case reflect.Uint64: 268 | return strconv.FormatUint(x.Interface().(uint64), 10), nil 269 | case reflect.Float32: 270 | return strconv.FormatFloat(float64(x.Interface().(float32)), FloatFmt, FloatPrec, 32), nil 271 | case reflect.Float64: 272 | return strconv.FormatFloat(x.Interface().(float64), FloatFmt, FloatPrec, 64), nil 273 | case reflect.Complex64: 274 | fallthrough 275 | case reflect.Complex128: 276 | errc = ErrorUnimplemented 277 | case reflect.Bool: 278 | return strconv.FormatBool(x.Interface().(bool)), nil 279 | default: 280 | errc = ErrorFieldType 281 | } 282 | return "", errc 283 | } 284 | 285 | func formatValue(x interface{}) ([]string, error) { 286 | // TODO add complex types 287 | var ( 288 | formatted = make([]string, 0, 1) 289 | appendwhenok = func(s string, e error) error { 290 | if e == nil { 291 | formatted = append(formatted, s) 292 | } 293 | return e 294 | } 295 | errc error 296 | n int 297 | value = reflect.ValueOf(x) 298 | ) 299 | if !value.IsValid() { 300 | return formatted, ErrorFieldType 301 | } 302 | //var t = value.Type() 303 | var kind = value.Kind() 304 | switch kind { 305 | case reflect.Ptr: 306 | //log.Print("PtrType") 307 | break 308 | case reflect.Struct: 309 | n = value.NumField() 310 | for j := 0; j < n; j++ { 311 | var vj = value.Field(j) 312 | errc = appendwhenok(formatReflectValue(vj)) 313 | if errc != nil { 314 | break 315 | } 316 | } 317 | return formatted, errc 318 | case reflect.Array: 319 | //log.Print("ArrayType") 320 | fallthrough 321 | case reflect.Slice: 322 | //log.Print("SliceType") 323 | n = value.Len() 324 | for j := 0; j < n; j++ { 325 | var vj = value.Index(j) 326 | errc = appendwhenok(formatReflectValue(vj)) 327 | if errc != nil { 328 | break 329 | } 330 | } 331 | return formatted, errc 332 | case reflect.Map: 333 | //log.Print("MapType") 334 | return formatted, ErrorUnimplemented 335 | default: 336 | errc = appendwhenok(formatReflectValue(value)) 337 | return formatted, errc 338 | } 339 | var ( 340 | eVal = value.Elem() 341 | eType = eVal.Type() 342 | eKind = eType.Kind() 343 | ) 344 | switch eKind { 345 | // Format pointers to standard types. 346 | case reflect.Struct: 347 | switch kind { 348 | case reflect.Ptr: 349 | n = eVal.NumField() 350 | for j := 0; j < n; j++ { 351 | var vj = eVal.Field(j) 352 | errc = appendwhenok(formatReflectValue(vj)) 353 | if errc != nil { 354 | break 355 | } 356 | } 357 | return formatted, errc 358 | default: 359 | errc = ErrorStruct 360 | } 361 | case reflect.Array: 362 | //log.Print("ArrayType") 363 | fallthrough 364 | case reflect.Slice: 365 | //log.Print("SliceType") 366 | n = eVal.Len() 367 | for j := 0; j < n; j++ { 368 | var vj = eVal.Index(j) 369 | errc = appendwhenok(formatReflectValue(vj)) 370 | if errc != nil { 371 | break 372 | } 373 | } 374 | return formatted, errc 375 | case reflect.Map: 376 | //log.Print("MapType") 377 | return formatted, ErrorUnimplemented 378 | default: 379 | errc = appendwhenok(formatReflectValue(eVal)) 380 | } 381 | return nil, errc 382 | } 383 | 384 | // Iteratively take values from the argument list and formats them (or 385 | // their elements/fields) as a (list of) string(s). Returns a Row object 386 | // that contains the formatted arguments, as well as any error that 387 | // occured. 388 | func FormatRow(x ...interface{}) Row { 389 | var ( 390 | err error 391 | formatted = make([]string, 0, len(x)) 392 | appendwhenok = func(s []string, e error) error { 393 | if e == nil { 394 | formatted = append(formatted, s...) 395 | } 396 | return e 397 | } 398 | ) 399 | for _, elm := range x { 400 | err = appendwhenok(formatValue(elm)) 401 | if err != nil { 402 | break 403 | } 404 | } 405 | return Row{formatted, err} 406 | } 407 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 csvutil 6 | 7 | import ( 8 | "bufio" 9 | "bytes" 10 | "io" 11 | "unicode/utf8" 12 | //"strings" 13 | ) 14 | 15 | // A simple CSV file writer using the package bufio for effeciency. 16 | // But, because of this, the method Flush() must be called to ensure 17 | // data is written to any given io.Writer before it is closed. 18 | type Writer struct { 19 | *Config 20 | w io.Writer // Base writer object. 21 | bw *bufio.Writer // Buffering writer for efficiency. 22 | } 23 | 24 | // Create a new CSV writer with the default field seperator and a 25 | // buffer of a default size. 26 | func NewWriter(w io.Writer, c *Config) *Writer { 27 | csvw := new(Writer) 28 | if csvw.Config = c; c == nil { 29 | csvw.Config = NewConfig() 30 | } 31 | csvw.w = w 32 | csvw.bw = bufio.NewWriter(w) 33 | return csvw 34 | } 35 | 36 | // Create a new CSV writer using a buffer of at least n bytes. 37 | // 38 | // See bufio.NewWriterSize(io.Writer, int) (*bufio.NewWriter). 39 | func NewWriterSize(w io.Writer, c *Config, n int) (*Writer, error) { 40 | csvw := new(Writer) 41 | if csvw.Config = c; c == nil { 42 | csvw.Config = NewConfig() 43 | } 44 | csvw.w = w 45 | csvw.bw = bufio.NewWriterSize(w, n) 46 | return csvw, nil 47 | } 48 | 49 | // Write a slice of bytes to the data stream. No checking for containment 50 | // of the separator is done, so this file can be used to write multiple 51 | // fields if desired. 52 | func (csvw *Writer) write(p []byte) (int, error) { 53 | return csvw.bw.Write(p) 54 | } 55 | 56 | // Write a single field of CSV data. If the ln argument is true, a 57 | // trailing new line is printed after the field. Otherwise, when 58 | // the ln argument is false, a separator character is printed after 59 | // the field. 60 | func (csvw *Writer) writeField(field string, ln bool) (int, error) { 61 | // Contains some code modified from 62 | // $GOROOT/src/pkg/fmt/print.go: func (p *pp) fmtC(c int64) @ ~317,322 63 | var trail rune = csvw.Sep 64 | if ln { 65 | trail = '\n' 66 | } 67 | var ( 68 | fLen = len(field) 69 | bp = make([]byte, fLen+utf8.UTFMax) 70 | ) 71 | copy(bp, field) 72 | return csvw.write(bp[:fLen+utf8.EncodeRune(bp[fLen:], trail)]) 73 | } 74 | 75 | // Write a slice of field values with a trailing field seperator (no '\n'). 76 | // Returns any error incurred from writing. 77 | func (csvw *Writer) WriteFields(fields ...string) (int, error) { 78 | var ( 79 | n = len(fields) 80 | success int 81 | err error 82 | ) 83 | for i := 0; i < n; i++ { 84 | if nbytes, err := csvw.writeField(fields[i], false); err != nil { 85 | return success, err 86 | } else { 87 | success += nbytes 88 | } 89 | } 90 | return success, err 91 | } 92 | 93 | /* 94 | func (csvw *Writer) WriteFieldsln(fields...string) (int, os.Error) { 95 | var n int = len(fields) 96 | var success int = 0 97 | var err os.Error 98 | for i := 0; i < n; i++ { 99 | var onLastField bool = i == n-1 100 | nbytes, err := csvw.writeField(fields[i], onLastField) 101 | success += nbytes 102 | 103 | var trail int = csvw.Sep 104 | if onLastField { 105 | trail = '\n' 106 | } 107 | 108 | if nbytes < len(fields[i])+utf8.RuneLen(trail) { 109 | return success, err 110 | } 111 | } 112 | return success, err 113 | } 114 | */ 115 | 116 | // Write a slice of field values with a trailing new line '\n'. 117 | // Returns any error incurred from writing. 118 | func (csvw *Writer) WriteRow(fields ...string) (int, error) { 119 | var ( 120 | n = len(fields) 121 | success int 122 | ) 123 | for i := 0; i < n; i++ { 124 | var EORow = i == n-1 125 | if nbytes, err := csvw.writeField(fields[i], EORow); err != nil { 126 | return success, err 127 | } else { 128 | success += nbytes 129 | } 130 | } 131 | return success, nil 132 | } 133 | 134 | // Write a comment. Each comment string given will start on a new line. If 135 | // the string is contains multiple lines, comment prefixes will be 136 | // inserted at the beginning of each one. 137 | func (csvw *Writer) WriteComments(comments ...string) (int, error) { 138 | if len(comments) == 0 { 139 | return 0, nil 140 | } 141 | 142 | // Break the comments into lines (w/o trailing '\n' chars) 143 | var lines [][]byte 144 | for _, c := range comments { 145 | var cp = make([]byte, len(c)) 146 | copy(cp, c) 147 | lines = append(lines, bytes.Split(cp, []byte{'\n'})...) 148 | } 149 | 150 | // Count the total number of characters in the comments. 151 | var commentLen = len(lines) * (len(csvw.CommentPrefix) + 1) 152 | for _, cline := range lines { 153 | commentLen += len(cline) 154 | } 155 | 156 | // Allocate, fill, and write the comment byte slice 157 | var comment = make([]byte, commentLen) 158 | var ci int 159 | for _, cline := range lines { 160 | ci += copy(comment[ci:], csvw.CommentPrefix) 161 | ci += copy(comment[ci:], cline) 162 | ci += copy(comment[ci:], []byte{'\n'}) 163 | } 164 | return csvw.write(comment[:ci]) 165 | } 166 | 167 | // Flush any buffered data to the underlying io.Writer. 168 | func (csvw *Writer) Flush() error { 169 | return csvw.bw.Flush() 170 | } 171 | 172 | // Write multple CSV rows at once. 173 | func (csvw *Writer) WriteRows(rows [][]string) (int, error) { 174 | var success int 175 | for i := 0; i < len(rows); i++ { 176 | if nbytes, err := csvw.WriteRow(rows[i]...); err != nil { 177 | return success, err 178 | } else { 179 | success += nbytes 180 | } 181 | } 182 | return success, nil 183 | } 184 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011, Bryan Matsuo. 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 | // See csv_test.go for more information about each test. 6 | package csvutil 7 | 8 | import ( 9 | "testing" 10 | ) 11 | 12 | // TEST1 - Simple 3x3 matrix w/ comma separators and w/o excess whitespace. 13 | func TestWriteRow(T *testing.T) { 14 | var csvw, buff = BufferWriter(nil) 15 | var csvMatrix = TestMatrix1 16 | var n int = len(csvMatrix) 17 | var length = 0 18 | for i := 0; i < n; i++ { 19 | nbytes, err := csvw.WriteRow(csvMatrix[i]...) 20 | if err != nil { 21 | T.Errorf("Write error: %s\n", err.Error()) 22 | } 23 | errFlush := csvw.Flush() 24 | if errFlush != nil { 25 | T.Logf("Wrote %d bytes on row %d\n", nbytes, i) 26 | } 27 | length += nbytes 28 | } 29 | flushErr := csvw.Flush() 30 | if flushErr != nil { 31 | T.Errorf("Error flushing output; %v\n", flushErr) 32 | } 33 | var output string = buff.String() 34 | if len(output) == 0 { 35 | T.Error("Read 0 bytes\n") 36 | } else { 37 | T.Logf("Read %d bytes from the buffer.", len(output)) 38 | } 39 | var csvStr string = csvTestString1() 40 | if output != csvStr { 41 | T.Errorf("Unexpected output.\n\nExpected:\n'%s'\nReceived:\n'%s'\n\n", 42 | csvStr, output) 43 | } 44 | } 45 | 46 | // END TEST1 47 | 48 | func TestWriterComments(T *testing.T) { 49 | var config = NewConfig() 50 | config.Sep = '\t' 51 | var ( 52 | matrix = TestMatrix2 53 | comments = TestMatrix2Comments 54 | verification = csvTestString2() 55 | writer, buff = BufferWriter(config) 56 | ) 57 | writer.WriteComments(comments...) 58 | writer.WriteRows(matrix) 59 | writer.Flush() 60 | var output = buff.String() 61 | if output != verification { 62 | T.Errorf("Error writing comments\n\n'%s'\n'%s'", verification, output) 63 | } 64 | } 65 | --------------------------------------------------------------------------------