├── .github └── workflows │ └── build.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── csv ├── column_scanner.go ├── column_scanner_test.go ├── example_test.go ├── options.go ├── scanner.go ├── scanner_test.go ├── struct_scanner.go ├── struct_scanner_test.go └── writer.go ├── fields ├── example_test.go ├── scanner.go └── scanner_test.go ├── fixedwidth ├── example_test.go ├── scanner.go └── scanner_test.go ├── go.mod ├── go.sum └── scanners.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-go@v4 14 | with: 15 | go-version: 'stable' 16 | - uses: actions/cache@v4 17 | with: 18 | path: ~/go/pkg/mod 19 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 20 | restore-keys: | 21 | ${{ runner.os }}-go- 22 | 23 | - run: make build 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /coverage.* 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: go 4 | 5 | go: 6 | - 1.x 7 | 8 | env: 9 | - GO111MODULE=on 10 | 11 | script: 12 | - make build 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | 17 | git: 18 | depth: 1 19 | 20 | cache: 21 | directories: 22 | - $HOME/.cache/go-build 23 | - $HOME/gopath/pkg/mod 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | In general, the code posted to the [Smarty github organization](https://github.com/smarty) is created to solve specific problems at Smartythat are ancillary to our core products in the address verification industry and may or may not be useful to other organizations or developers. Our reason for posting said code isn't necessarily to solicit feedback or contributions from the community but more as a showcase of some of the approaches to solving problems we have adopted. 4 | 5 | Having stated that, we do consider issues raised by other githubbers as well as contributions submitted via pull requests. When submitting such a pull request, please follow these guidelines: 6 | 7 | - _Look before you leap:_ If the changes you plan to make are significant, it's in everyone's best interest for you to discuss them with a Smartyteam member prior to opening a pull request. 8 | - _License and ownership:_ If modifying the `LICENSE.md` file, limit your changes to fixing typographical mistakes. Do NOT modify the actual terms in the license or the copyright by **Smarty, LLC**. Code submitted to Smartyprojects becomes property of Smartyand must be compatible with the associated license. 9 | - _Testing:_ If the code you are submitting resides in packages/modules covered by automated tests, be sure to add passing tests that cover your changes and assert expected behavior and state. Submit the additional test cases as part of your change set. 10 | - _Style:_ Match your approach to **naming** and **formatting** with the surrounding code. Basically, the code you submit shouldn't stand out. 11 | - "Naming" refers to such constructs as variables, methods, functions, classes, structs, interfaces, packages, modules, directories, files, etc... 12 | - "Formatting" refers to such constructs as whitespace, horizontal line length, vertical function length, vertical file length, indentation, curly braces, etc... 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Smarty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | NOTE: Various optional and subordinate components carry their own licensing 24 | requirements and restrictions. Use of those components is subject to the terms 25 | and conditions outlined in the respective license of each component. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | test: fmt 4 | go test -timeout=1s -race -covermode=atomic -count=1 ./... 5 | 6 | fmt: 7 | go fmt ./... 8 | 9 | compile: 10 | go build ./... 11 | 12 | build: test compile 13 | 14 | .PHONY: test fmt compile build 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### SMARTY DISCLAIMER: Subject to the terms of the associated license agreement, this software is freely available for your use. This software is FREE, AS IN PUPPIES, and is a gift. Enjoy your new responsibility. This means that while we may consider enhancement requests, we may or may not choose to entertain requests at our sole and absolute discretion. 2 | 3 | [![Build Status](https://travis-ci.org/smarty/scanners.svg?branch=master)](https://travis-ci.org/smarty/scanners) 4 | [![Code Coverage](https://codecov.io/gh/smarty/scanners/branch/master/graph/badge.svg)](https://codecov.io/gh/smarty/scanners) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/smarty/scanners)](https://goreportcard.com/report/github.com/smarty/scanners) 6 | [![GoDoc](https://pkg.go.dev/badge/github.com/smarty/scanners)](https://pkg.go.dev/github.com/smarty/scanners) 7 | -------------------------------------------------------------------------------- /csv/column_scanner.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | ) 8 | 9 | // ColumnScanner uses a [Scanner] to provide access to the fields of 10 | // CSV-encoded data by column name. The scanner assumes the first 11 | // record in the data to be the header with column names. If 12 | // duplicate names exist in the header, the last column for the 13 | // duplicate name will be used. 14 | type ColumnScanner struct { 15 | *Scanner 16 | headerRecord []string 17 | columnIndex map[string]int 18 | toUpperHeader bool 19 | } 20 | 21 | // ColumnOpt is a func type received by NewColumnScanner. 22 | // Each one allows configuration of the column scanner. 23 | type ColumnOpt func(*ColumnScanner) 24 | 25 | // ColumnOpts (a singleton instance) provides access to built-in 26 | // functional options. 27 | var ColumnOpts columnOpts 28 | 29 | type columnOpts struct{} 30 | 31 | // ToUpperHeader calls [strings.ToUpper] on each of the header column names. 32 | func (columnOpts) ToUpperHeader() ColumnOpt { 33 | return func(s *ColumnScanner) { 34 | s.toUpperHeader = true 35 | } 36 | } 37 | 38 | // Header sets the column names used to reference the data. 39 | // If this ColumnOpt is given then the ColumnScanner assumes the 40 | // first record in the data is *not* a header. 41 | func (columnOpts) Header(header []string) ColumnOpt { 42 | return func(s *ColumnScanner) { 43 | s.headerRecord = header 44 | } 45 | } 46 | 47 | // NewColumnScanner returns a ColumnScanner that reads from scanner, 48 | // configured with the provided options, and assumes the first record 49 | // to be the header. It calls Scan once to read the header; subsequent 50 | // calls to Scan will return the remaining records. 51 | func NewColumnScanner(scanner *Scanner, options ...ColumnOpt) (*ColumnScanner, error) { 52 | cs := &ColumnScanner{ 53 | Scanner: scanner, 54 | columnIndex: make(map[string]int), 55 | } 56 | cs.configure(options) 57 | if err := cs.readHeader(); err != nil { 58 | return nil, err 59 | } 60 | return cs, nil 61 | } 62 | func (this *ColumnScanner) configure(options []ColumnOpt) { 63 | for _, configure := range options { 64 | configure(this) 65 | } 66 | } 67 | 68 | func (this *ColumnScanner) readHeader() error { 69 | if len(this.headerRecord) < 1 { 70 | if !this.Scanner.Scan() { 71 | return this.Scanner.Error() 72 | } 73 | this.headerRecord = this.Scanner.Record() 74 | } 75 | for i := range this.headerRecord { 76 | if this.toUpperHeader { 77 | this.headerRecord[i] = strings.ToUpper(this.headerRecord[i]) 78 | } 79 | this.columnIndex[this.headerRecord[i]] = i 80 | } 81 | return nil 82 | } 83 | 84 | // Header returns the header record. 85 | func (this *ColumnScanner) Header() []string { 86 | return this.headerRecord 87 | } 88 | 89 | // ColumnErr returns the value for column name of the most recent 90 | // record generated by a call to Scan as a string. It returns an 91 | // error if column was not present in the header record. 92 | func (this *ColumnScanner) ColumnErr(column string) (string, error) { 93 | index, ok := this.columnIndex[column] 94 | if !ok { 95 | return "", fmt.Errorf("Column [%s] not present in header record: %#v\n", column, this.headerRecord) 96 | } 97 | return this.Record()[index], nil 98 | } 99 | 100 | // Column wraps [ColumnScanner.ColumnErr], but panics if the 101 | // name was not present in the header record. 102 | func (this *ColumnScanner) Column(column string) string { 103 | value, err := this.ColumnErr(column) 104 | if err != nil { 105 | log.Panic(err) 106 | } 107 | return value 108 | } 109 | -------------------------------------------------------------------------------- /csv/column_scanner_test.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/smarty/assertions/should" 8 | "github.com/smarty/gunit" 9 | ) 10 | 11 | func TestColumnScannerFixture(t *testing.T) { 12 | gunit.Run(new(ColumnScannerFixture), t) 13 | } 14 | 15 | type ColumnScannerFixture struct { 16 | *gunit.Fixture 17 | 18 | scanner *ColumnScanner 19 | err error 20 | } 21 | 22 | func (this *ColumnScannerFixture) Setup() { 23 | this.scanner, this.err = NewColumnScanner(NewScanner(reader(csvCanon))) 24 | this.So(this.err, should.BeNil) 25 | this.So(this.scanner.Header(), should.Resemble, []string{"first_name", "last_name", "username"}) 26 | } 27 | 28 | func ScanAllUsers(scanner *ColumnScanner) []User { 29 | users := []User{} 30 | for scanner.Scan() { 31 | users = append(users, User{ 32 | FirstName: scanner.Column(scanner.Header()[0]), 33 | LastName: scanner.Column(scanner.Header()[1]), 34 | Username: scanner.Column(scanner.Header()[2]), 35 | }) 36 | } 37 | return users 38 | } 39 | 40 | func (this *ColumnScannerFixture) TestReadColumns() { 41 | users := ScanAllUsers(this.scanner) 42 | 43 | this.So(this.scanner.Error(), should.BeNil) 44 | this.So(users, should.Resemble, []User{ 45 | {FirstName: "Rob", LastName: "Pike", Username: "rob"}, 46 | {FirstName: "Ken", LastName: "Thompson", Username: "ken"}, 47 | {FirstName: "Robert", LastName: "Griesemer", Username: "gri"}, 48 | }) 49 | } 50 | 51 | func (this *ColumnScannerFixture) TestCannotReadHeader() { 52 | scanner, err := NewColumnScanner(NewScanner(new(ErrorReader))) 53 | this.So(scanner, should.BeNil) 54 | this.So(err, should.NotBeNil) 55 | } 56 | 57 | func (this *ColumnScannerFixture) TestColumnNotFound_Error() { 58 | this.scanner.Scan() 59 | value, err := this.scanner.ColumnErr("nope") 60 | this.So(value, should.BeBlank) 61 | this.So(err, should.NotBeNil) 62 | } 63 | 64 | func (this *ColumnScannerFixture) TestColumnNotFound_Panic() { 65 | this.scanner.Scan() 66 | this.So(func() { this.scanner.Column("nope") }, should.Panic) 67 | } 68 | 69 | // TestDuplicateColumnNames confirms that duplicated/repeated 70 | // column names results in the last repeated column being 71 | // added to the map and used to retrieve values for that name. 72 | func (this *ColumnScannerFixture) TestDuplicateColumnNames() { 73 | scanner, err := NewColumnScanner(NewScanner(reader([]string{ 74 | "Col1,Col2,Col2", 75 | "foo,bar,baz", 76 | }))) 77 | this.So(err, should.BeNil) 78 | this.So(scanner.Header(), should.Resemble, []string{"Col1", "Col2", "Col2"}) 79 | scanner.Scan() 80 | this.So(scanner.Column("Col2"), should.Equal, "baz") 81 | } 82 | 83 | func (this *ColumnScannerFixture) TestColumnOpt_ToUpperHeader() { 84 | data := append([]string{"first_name,LAST_NAME,uSeRNaMe"}, csvCanon[1:]...) 85 | scanner, err := NewColumnScanner( 86 | NewScanner(reader(data)), 87 | ColumnOpts.ToUpperHeader()) 88 | this.So(err, should.BeNil) 89 | 90 | users := ScanAllUsers(scanner) 91 | this.So(this.scanner.Error(), should.BeNil) 92 | this.So(users, should.Resemble, []User{ 93 | {FirstName: "Rob", LastName: "Pike", Username: "rob"}, 94 | {FirstName: "Ken", LastName: "Thompson", Username: "ken"}, 95 | {FirstName: "Robert", LastName: "Griesemer", Username: "gri"}, 96 | }) 97 | } 98 | 99 | func (this *ColumnScannerFixture) TestColumnOpt_Header() { 100 | header := []string{"first_name", "last_name", "username"} 101 | scanner, err := NewColumnScanner( 102 | NewScanner(reader(csvCanon[1:])), 103 | ColumnOpts.Header(header), 104 | ) 105 | this.So(err, should.BeNil) 106 | 107 | users := ScanAllUsers(scanner) 108 | this.So(this.scanner.Error(), should.BeNil) 109 | this.So(users, should.Resemble, []User{ 110 | {FirstName: "Rob", LastName: "Pike", Username: "rob"}, 111 | {FirstName: "Ken", LastName: "Thompson", Username: "ken"}, 112 | {FirstName: "Robert", LastName: "Griesemer", Username: "gri"}, 113 | }) 114 | } 115 | 116 | func (this *ColumnScannerFixture) TestCustomUpperHeader() { 117 | header := []string{"FIRST", "last", "uSEr"} 118 | scanner, err := NewColumnScanner( 119 | NewScanner(reader(csvCanon), Options.SkipHeaderRecord()), 120 | ColumnOpts.Header(header), 121 | ColumnOpts.ToUpperHeader(), 122 | ) 123 | this.So(err, should.BeNil) 124 | 125 | users := ScanAllUsers(scanner) 126 | this.So(this.scanner.Error(), should.BeNil) 127 | this.So(users, should.Resemble, []User{ 128 | {FirstName: "Rob", LastName: "Pike", Username: "rob"}, 129 | {FirstName: "Ken", LastName: "Thompson", Username: "ken"}, 130 | {FirstName: "Robert", LastName: "Griesemer", Username: "gri"}, 131 | }) 132 | } 133 | 134 | type User struct { 135 | FirstName string 136 | LastName string 137 | Username string 138 | } 139 | 140 | type ErrorReader struct{} 141 | 142 | func (this *ErrorReader) Read(p []byte) (n int, err error) { 143 | return 0, errors.New("ERROR") 144 | } 145 | -------------------------------------------------------------------------------- /csv/example_test.go: -------------------------------------------------------------------------------- 1 | package csv_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/smarty/scanners/v3/csv" 9 | ) 10 | 11 | func ExampleScanner() { 12 | in := strings.Join([]string{ 13 | `first_name,last_name,username`, 14 | `"Rob","Pike",rob`, 15 | `Ken,Thompson,ken`, 16 | `"Robert","Griesemer","gri"`, 17 | }, "\n") 18 | scanner := csv.NewScanner(strings.NewReader(in)) 19 | 20 | for scanner.Scan() { 21 | fmt.Println(scanner.Record()) 22 | } 23 | 24 | if err := scanner.Error(); err != nil { 25 | log.Panic(err) 26 | } 27 | 28 | // Output: 29 | // [first_name last_name username] 30 | // [Rob Pike rob] 31 | // [Ken Thompson ken] 32 | // [Robert Griesemer gri] 33 | } 34 | 35 | // This example shows how csv.Scanner can be configured to handle other 36 | // types of CSV files. 37 | func ExampleScanner_options() { 38 | in := strings.Join([]string{ 39 | `first_name;last_name;username`, 40 | `"Rob";"Pike";rob`, 41 | `# lines beginning with a # character are ignored`, 42 | `Ken;Thompson;ken`, 43 | `"Robert";"Griesemer";"gri"`, 44 | }, "\n") 45 | 46 | scanner := csv.NewScanner(strings.NewReader(in), csv.Options.Comma(';'), csv.Options.Comment('#')) 47 | 48 | for scanner.Scan() { 49 | fmt.Println(scanner.Record()) 50 | } 51 | 52 | if err := scanner.Error(); err != nil { 53 | log.Panic(err) 54 | } 55 | 56 | // Output: 57 | // [first_name last_name username] 58 | // [Rob Pike rob] 59 | // [Ken Thompson ken] 60 | // [Robert Griesemer gri] 61 | } 62 | 63 | // A ColumnScanner maps field values in each row to column 64 | // names. The column name is taken from the first row, which 65 | // is assumed to be the header row. 66 | func ExampleColumnScanner() { 67 | in := strings.Join([]string{ 68 | `first_name,last_name,username`, 69 | `"Rob","Pike",rob`, 70 | `Ken,Thompson,ken`, 71 | `"Robert","Griesemer","gri"`, 72 | }, "\n") 73 | scanner, _ := csv.NewColumnScanner(csv.NewScanner(strings.NewReader(in))) 74 | 75 | for scanner.Scan() { 76 | fmt.Println(scanner.Column("last_name"), scanner.Column("first_name")) 77 | } 78 | 79 | if err := scanner.Error(); err != nil { 80 | log.Panic(err) 81 | } 82 | 83 | // Output: 84 | // Pike Rob 85 | // Thompson Ken 86 | // Griesemer Robert 87 | } 88 | 89 | func ExampleStructScanner() { 90 | type person struct { 91 | Firstname string `csv:"first_name"` 92 | Lastname string `csv:"last_name"` 93 | Username string `csv:"username"` 94 | } 95 | 96 | in := strings.Join([]string{ 97 | `first_name,last_name,username`, 98 | `"Rob","Pike",rob`, 99 | `Ken,Thompson,ken`, 100 | `"Robert","Griesemer","gri"`, 101 | }, "\n") 102 | 103 | scanner, _ := csv.NewStructScanner(strings.NewReader(in)) 104 | 105 | for scanner.Scan() { 106 | var p person 107 | scanner.Populate(&p) 108 | fmt.Printf("%+v\n", p) 109 | } 110 | 111 | if err := scanner.Error(); err != nil { 112 | log.Panic(err) 113 | } 114 | 115 | // Output: 116 | // {Firstname:Rob Lastname:Pike Username:rob} 117 | // {Firstname:Ken Lastname:Thompson Username:ken} 118 | // {Firstname:Robert Lastname:Griesemer Username:gri} 119 | } 120 | -------------------------------------------------------------------------------- /csv/options.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | // Option is a func type received by NewScanner. 4 | // Each one allows configuration of the scanner and/or its internal *csv.Reader. 5 | type Option func(*Scanner) 6 | 7 | // Options (a singleton instance) provides access to built-in functional options. 8 | var Options options 9 | 10 | type options struct{} 11 | 12 | // ContinueOnError controls scanner behavior in error scenarios. 13 | // If true is passed, continue scanning until io.EOF is reached. 14 | // If false is passed (default), any error encountered during scanning 15 | // will result in the next call to Scan returning false and 16 | // the Scanner may be considered dead. Check [Scanner.Error] for the exact error 17 | // (before the next call to [Scanner.Scan]). 18 | // 19 | // See the error variables starting at [csv.ErrBareQuote], and the 20 | // [csv.ParseError] type, for more information regarding possible 21 | // error values. 22 | func (options) ContinueOnError(continue_ bool) Option { 23 | return func(s *Scanner) { 24 | s.continueOnError = continue_ 25 | } 26 | } 27 | func (options) Comma(comma rune) Option { 28 | return func(s *Scanner) { 29 | s.reader.Comma = comma 30 | } 31 | } 32 | func (options) Comment(comment rune) Option { 33 | return func(s *Scanner) { 34 | s.reader.Comment = comment 35 | } 36 | } 37 | func (options) FieldsPerRecord(fields int) Option { 38 | return func(s *Scanner) { 39 | s.reader.FieldsPerRecord = fields 40 | } 41 | } 42 | func (options) LazyQuotes(lazy bool) Option { 43 | return func(s *Scanner) { 44 | s.reader.LazyQuotes = lazy 45 | } 46 | } 47 | func (options) ReuseRecord(reuse bool) Option { 48 | return func(s *Scanner) { 49 | s.reader.ReuseRecord = reuse 50 | } 51 | } 52 | func (options) TrimLeadingSpace(trim bool) Option { 53 | return func(s *Scanner) { 54 | s.reader.TrimLeadingSpace = trim 55 | } 56 | } 57 | 58 | // SkipHeaderRecord skips the first record of the reader, regardless of its 59 | // contents. 60 | func (options) SkipHeaderRecord() Option { 61 | return func(s *Scanner) { 62 | s.Scan() 63 | } 64 | } 65 | func (options) SkipRecords(count int) Option { 66 | return func(s *Scanner) { 67 | for x := 0; x < count; x++ { 68 | s.Scan() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /csv/scanner.go: -------------------------------------------------------------------------------- 1 | // Package csv scans CSV files, provides easy access to individual 2 | // columns, and can also read field values into a struct (analogous 3 | // to unmarshaling JSON or XML). 4 | // 5 | // It thinly wraps the standard library's [csv.Reader] and exposes 6 | // most of its configuration "knobs" and behavior. Knowledge of 7 | // the csv.Reader will help in configuring and running these 8 | // scanners. 9 | // 10 | // Advance the scanners with the Scan method and check errors with 11 | // the Error method (unlike fields and fixedwidth, which use Err). 12 | package csv 13 | 14 | import ( 15 | "encoding/csv" 16 | "io" 17 | ) 18 | 19 | // Scanner provides access to the fields of CSV-encoded data. 20 | // 21 | // All configurations of the underlying *csv.Reader are available 22 | // through an [Option]. 23 | type Scanner struct { 24 | reader *csv.Reader 25 | record []string 26 | err error 27 | 28 | continueOnError bool 29 | } 30 | 31 | // NewScanner returns a Scanner that reads from reader, configured 32 | // with the provided options. 33 | func NewScanner(reader io.Reader, options ...Option) *Scanner { 34 | return new(Scanner).initialize(reader).configure(options) 35 | } 36 | func (this *Scanner) initialize(reader io.Reader) *Scanner { 37 | this.reader = csv.NewReader(reader) 38 | return this 39 | } 40 | func (this *Scanner) configure(options []Option) *Scanner { 41 | for _, configure := range options { 42 | configure(this) 43 | } 44 | return this 45 | } 46 | 47 | // Scan advances the Scanner to the next record, which will then be available 48 | // through the [Scanner.Record] method. It returns false when the scan stops, 49 | // either by reaching the end of the input or an error. After Scan returns 50 | // false, the [Scanner.Error] method will return any error that occurred 51 | // during scanning, except that if it was io.EOF, Error will return nil. 52 | func (this *Scanner) Scan() bool { 53 | if this.eof() { 54 | return false 55 | } 56 | this.record, this.err = this.reader.Read() 57 | return !this.eof() 58 | } 59 | 60 | func (this *Scanner) eof() bool { 61 | if this.err == io.EOF { 62 | return true 63 | } 64 | if this.err == nil { 65 | return false 66 | } 67 | return !this.continueOnError 68 | } 69 | 70 | // Record returns the most recent record generated by a call to Scan as a 71 | // []string. 72 | // 73 | // See the [ReuseRecord] Option and follow the link to the standard library 74 | // for details on the strategy for reusing the underlying array. 75 | func (this *Scanner) Record() []string { 76 | return this.record 77 | } 78 | 79 | // Error returns the last non-nil error produced by Scan (if there was one). 80 | // It will never return io.EOF. This method may be called at any point 81 | // during or after scanning but the underlying err will be reset by each call 82 | // to Scan. 83 | func (this *Scanner) Error() error { 84 | if this.err == io.EOF { 85 | return nil 86 | } 87 | return this.err 88 | } 89 | -------------------------------------------------------------------------------- /csv/scanner_test.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "encoding/csv" 5 | "io" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/smarty/assertions/should" 10 | "github.com/smarty/gunit" 11 | ) 12 | 13 | func TestScanAllFixture(t *testing.T) { 14 | gunit.Run(new(ScanAllFixture), t) 15 | } 16 | 17 | type ScanAllFixture struct { 18 | *gunit.Fixture 19 | } 20 | 21 | func (this *ScanAllFixture) scanAll(inputs []string, options ...Option) (scanned []Record) { 22 | scanner := NewScanner(reader(inputs), options...) 23 | line := 1 24 | for ; scanner.Scan(); line++ { 25 | scanned = append(scanned, Record{ 26 | line: line, 27 | record: scanner.Record(), 28 | err: scanner.Error(), 29 | }) 30 | } 31 | if err := scanner.Error(); err != nil { 32 | scanned = append(scanned, Record{ 33 | line: line, 34 | err: err, 35 | }) 36 | } 37 | return scanned 38 | } 39 | 40 | func (this *ScanAllFixture) TestCanonical() { 41 | scanned := this.scanAll(csvCanon, Options.Comma(','), Options.FieldsPerRecord(3)) 42 | this.So(scanned, should.Resemble, expectedScannedOutput) 43 | } 44 | 45 | func (this *ScanAllFixture) TestCanonicalWithOptions() { 46 | scanned := this.scanAll(csvCanonRequiringConfigOptions, Options.Comma(';'), Options.Comment('#')) 47 | this.So(scanned, should.Resemble, expectedScannedOutput) 48 | } 49 | 50 | func (this *ScanAllFixture) TestOptions() { 51 | scanner := NewScanner(nil, Options.ReuseRecord(true), Options.TrimLeadingSpace(true), Options.LazyQuotes(true)) 52 | this.So(scanner.reader.ReuseRecord, should.BeTrue) 53 | this.So(scanner.reader.LazyQuotes, should.BeTrue) 54 | this.So(scanner.reader.TrimLeadingSpace, should.BeTrue) 55 | } 56 | 57 | func (this *ScanAllFixture) TestInconsistentFieldCounts_ContinueOnError() { 58 | scanned := this.scanAll(csvCanonInconsistentFieldCounts, Options.ContinueOnError(true)) 59 | this.So(scanned, should.Resemble, []Record{ 60 | {line: 1, record: []string{"1", "2", "3"}, err: nil}, 61 | {line: 2, record: []string{"1", "2", "3", "4"}, err: &csv.ParseError{StartLine: 2, Line: 2, Column: 1, Err: csv.ErrFieldCount}}, 62 | {line: 3, record: []string{"1", "2", "3"}, err: nil}, 63 | }) 64 | } 65 | 66 | func (this *ScanAllFixture) TestInconsistentFieldCounts_HaltOnError() { 67 | scanned := this.scanAll(csvCanonInconsistentFieldCounts) 68 | this.So(scanned, should.Resemble, []Record{ 69 | {line: 1, record: []string{"1", "2", "3"}, err: nil}, 70 | {line: 2, record: nil, err: &csv.ParseError{StartLine: 2, Line: 2, Column: 1, Err: csv.ErrFieldCount}}, 71 | }) 72 | } 73 | 74 | func (this *ScanAllFixture) TestCallsToScanAfterEOFReturnFalse() { 75 | scanner := NewScanner(strings.NewReader("1,2,3"), Options.Comma(',')) 76 | 77 | this.So(scanner.Scan(), should.BeTrue) 78 | this.So(scanner.Record(), should.Resemble, []string{"1", "2", "3"}) 79 | this.So(scanner.Error(), should.BeNil) 80 | 81 | for x := 0; x < 100; x++ { 82 | this.So(scanner.Scan(), should.BeFalse) 83 | this.So(scanner.Record(), should.BeNil) 84 | this.So(scanner.Error(), should.BeNil) 85 | } 86 | } 87 | 88 | func (this *ScanAllFixture) TestSkipHeader() { 89 | scanned := this.scanAll(csvCanon, Options.Comma(','), Options.SkipHeaderRecord()) 90 | this.So(scanned, should.Resemble, []Record{ 91 | {line: 1, record: []string{"Rob", "Pike", "rob"}}, 92 | {line: 2, record: []string{"Ken", "Thompson", "ken"}}, 93 | {line: 3, record: []string{"Robert", "Griesemer", "gri"}}, 94 | }) 95 | } 96 | 97 | func (this *ScanAllFixture) TestRecords() { 98 | scanned := this.scanAll(csvCanon, Options.Comma(','), Options.SkipRecords(3)) 99 | this.So(scanned, should.Resemble, []Record{ 100 | {line: 1, record: []string{"Robert", "Griesemer", "gri"}}, 101 | }) 102 | } 103 | 104 | func reader(lines []string) io.Reader { 105 | return strings.NewReader(strings.Join(lines, "\n")) 106 | } 107 | 108 | var ( // https://golang.org/pkg/encoding/csv/#example_Reader 109 | csvCanon = []string{ 110 | "first_name,last_name,username", 111 | `"Rob","Pike",rob`, 112 | `Ken,Thompson,ken`, 113 | `"Robert","Griesemer","gri"`, 114 | } 115 | csvCanonRequiringConfigOptions = []string{ 116 | `first_name;last_name;username`, 117 | `"Rob";"Pike";rob`, 118 | `# lines beginning with a # character are ignored`, 119 | `Ken;Thompson;ken`, 120 | `"Robert";"Griesemer";"gri"`, 121 | } 122 | csvCanonInconsistentFieldCounts = []string{ 123 | `1,2,3`, 124 | `1,2,3,4`, 125 | `1,2,3`, 126 | } 127 | expectedScannedOutput = []Record{ 128 | {1, []string{"first_name", "last_name", "username"}, nil}, 129 | {2, []string{"Rob", "Pike", "rob"}, nil}, 130 | {3, []string{"Ken", "Thompson", "ken"}, nil}, 131 | {4, []string{"Robert", "Griesemer", "gri"}, nil}, 132 | } 133 | ) 134 | 135 | type Record struct { 136 | line int 137 | record []string 138 | err error 139 | } 140 | -------------------------------------------------------------------------------- /csv/struct_scanner.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "reflect" 7 | ) 8 | 9 | // StructScanner provides access to the fields of CSV-encoded 10 | // data through a struct's fields. 11 | // 12 | // Like unmarshaling with the standard JSON or XML decoders, the 13 | // fields of the struct must be exported and tagged with a `"csv:"` 14 | // prefix. 15 | // 16 | // All configurations of the underlying *csv.Reader are available 17 | // through an [Option]. 18 | type StructScanner struct { 19 | *ColumnScanner 20 | } 21 | 22 | // NewStructScanner returns a StructScanner that reads from reader, 23 | // configured with the provided options. 24 | func NewStructScanner(reader io.Reader, options ...Option) (*StructScanner, error) { 25 | inner, err := NewColumnScanner(NewScanner(reader, options...)) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &StructScanner{ColumnScanner: inner}, nil 30 | } 31 | 32 | // Populate gets the most recent record generated by a call to Scan 33 | // and stores the values for tagged fields in the value pointed to 34 | // by v. 35 | func (this *StructScanner) Populate(v interface{}) error { 36 | type_ := reflect.TypeOf(v) 37 | if type_.Kind() != reflect.Ptr { 38 | return fmt.Errorf("Provided value must be reflect.Ptr. You provided [%v] ([%v]).", v, type_.Kind()) 39 | } 40 | 41 | value := reflect.ValueOf(v) 42 | if value.IsNil() { 43 | return fmt.Errorf("The provided value was nil. Please provide a non-nil pointer.") 44 | } 45 | 46 | this.populate(type_.Elem(), value.Elem()) 47 | return nil 48 | } 49 | 50 | func (this *StructScanner) populate(type_ reflect.Type, value reflect.Value) { 51 | for x := 0; x < type_.NumField(); x++ { 52 | column := type_.Field(x).Tag.Get("csv") 53 | 54 | _, found := this.columnIndex[column] 55 | if !found { 56 | continue 57 | } 58 | 59 | field := value.Field(x) 60 | if field.Kind() != reflect.String { 61 | continue // Future: return err? 62 | } else if !field.CanSet() { 63 | continue // Future: return err? 64 | } 65 | 66 | field.SetString(this.Column(column)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /csv/struct_scanner_test.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/smarty/assertions/should" 7 | "github.com/smarty/gunit" 8 | ) 9 | 10 | func TestStructScannerFixture(t *testing.T) { 11 | gunit.Run(new(StructScannerFixture), t) 12 | } 13 | 14 | type StructScannerFixture struct { 15 | *gunit.Fixture 16 | scanner *StructScanner 17 | err error 18 | users []TaggedUser 19 | } 20 | 21 | func (this *StructScannerFixture) Setup() { 22 | this.scanner, this.err = NewStructScanner(reader(csvCanon)) 23 | this.So(this.err, should.BeNil) 24 | } 25 | 26 | func (this *StructScannerFixture) ScanAll() { 27 | for this.scanner.Scan() { 28 | var user TaggedUser 29 | this.scanner.Populate(&user) 30 | this.users = append(this.users, user) 31 | } 32 | } 33 | 34 | func (this *StructScannerFixture) Test() { 35 | this.ScanAll() 36 | 37 | this.So(this.scanner.Error(), should.BeNil) 38 | this.So(this.users, should.Resemble, []TaggedUser{ 39 | {FirstName: "Rob", LastName: "Pike", Username: "rob"}, 40 | {FirstName: "Ken", LastName: "Thompson", Username: "ken"}, 41 | {FirstName: "Robert", LastName: "Griesemer", Username: "gri"}, 42 | }) 43 | } 44 | 45 | type TaggedUser struct { 46 | FirstName string `csv:"first_name"` 47 | LastName string `csv:"last_name"` 48 | Username string `csv:"username"` 49 | } 50 | 51 | func (this *StructScannerFixture) TestCannotReadHeader() { 52 | scanner, err := NewStructScanner(new(ErrorReader)) 53 | this.So(scanner, should.BeNil) 54 | this.So(err, should.NotBeNil) 55 | } 56 | 57 | func (this *StructScannerFixture) TestScanIntoLessCompatibleType() { 58 | this.scanner.Scan() 59 | 60 | var nonPointer User 61 | this.So(this.scanner.Populate(nonPointer), should.NotBeNil) 62 | 63 | var nilPointer *User 64 | this.So(this.scanner.Populate(nilPointer), should.NotBeNil) 65 | } 66 | -------------------------------------------------------------------------------- /csv/writer.go: -------------------------------------------------------------------------------- 1 | package csv 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // Writer wraps a csv.Writer. 10 | type Writer struct{ *csv.Writer } 11 | 12 | // NewWriter accepts a target io.Writer and an optional comma rune 13 | // and builds a Writer with an internal csv.Writer. 14 | func NewWriter(w io.Writer, comma ...rune) *Writer { 15 | writer := csv.NewWriter(w) 16 | if len(comma) > 0 { 17 | writer.Comma = comma[0] 18 | } 19 | return &Writer{Writer: writer} 20 | } 21 | 22 | // WriteFields accepts zero or more interface{} values and converts 23 | // them to strings using fmt.Sprint and writes them as a single record 24 | // to the underlying csv.Writer. Make sure you are comfortable with 25 | // whatever the default format is for each field value you provide. 26 | func (this *Writer) WriteFields(fields ...interface{}) error { 27 | record := make([]string, len(fields)) 28 | for f, field := range fields { 29 | record[f] = fmt.Sprint(field) 30 | } 31 | return this.Write(record) 32 | } 33 | 34 | // WriteFormattedFields accepts a format string for 0 or more fields which 35 | // will be passed to fmt.Sprintf before being written as a single record 36 | // to the underlying csv.Writer. 37 | func (this *Writer) WriteFormattedFields(format string, fields ...interface{}) error { 38 | record := make([]string, len(fields)) 39 | for f, field := range fields { 40 | record[f] = fmt.Sprintf(format, field) 41 | } 42 | return this.Write(record) 43 | } 44 | 45 | // WriteStringers accepts zero or more fmt.Stinger values and converts 46 | // them to strings by calling their String() method and writes them as 47 | // a single record to the underlying csv.Writer. 48 | func (this *Writer) WriteStringers(fields ...fmt.Stringer) error { 49 | record := make([]string, len(fields)) 50 | for f, field := range fields { 51 | record[f] = field.String() 52 | } 53 | return this.Write(record) 54 | } 55 | 56 | // WriteStrings accepts zero or more string values and writes them as 57 | // a single record to the underlying csv.Writer. 58 | // IMHO, it's how csv.Writer.Write should have been defined. 59 | func (this *Writer) WriteStrings(fields ...string) error { 60 | return this.Writer.Write(fields) 61 | } 62 | 63 | // WriteStream accepts a chan []string and ranges over it, passing 64 | // each []string as a record to the underlying csv.Writer. Like 65 | // it's counterpart (csv.Writer.WriteAll) it calls Flush() if 66 | // all records are written without error. It is assumed that 67 | // the channel is or will be closed by the caller or a separate 68 | // goroutine, otherwise this call will block indefinitely. 69 | func (this *Writer) WriteStream(records chan []string) error { 70 | for record := range records { 71 | err := this.Write(record) 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | this.Flush() 77 | return this.Error() 78 | } 79 | -------------------------------------------------------------------------------- /fields/example_test.go: -------------------------------------------------------------------------------- 1 | package fields_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/smarty/scanners/v3/fields" 9 | ) 10 | 11 | // Justification of fields should not affect the scanned values. 12 | func ExampleScanner() { 13 | in := strings.Join([]string{ 14 | " a\t 1 foo i ", 15 | " b\t 10 bar ii ", 16 | " c\t100 bazzle iii", 17 | }, "\n") 18 | 19 | scanner := fields.NewScanner(strings.NewReader(in)) 20 | 21 | for scanner.Scan() { 22 | fmt.Println(scanner.Fields()) 23 | } 24 | 25 | if err := scanner.Err(); err != nil { 26 | log.Panic(err) 27 | } 28 | 29 | // Output: 30 | // [a 1 foo i] 31 | // [b 10 bar ii] 32 | // [c 100 bazzle iii] 33 | } 34 | -------------------------------------------------------------------------------- /fields/scanner.go: -------------------------------------------------------------------------------- 1 | // Package fields scans fields, splitting on whitespace—fields 2 | // themselves cannot contain whitespace. 3 | // 4 | // Advance the scanner with the Scan method and check errors with 5 | // the Err method, both from the underlying bufio.Scanner. 6 | package fields 7 | 8 | import ( 9 | "bufio" 10 | "io" 11 | "strings" 12 | ) 13 | 14 | // Scanner provides access to the whitespace-separated fields of 15 | // data. Field values cannot contain any whitespace. 16 | // 17 | // For a file that follows the encoding scheme of a so-called TSV, 18 | // use [github.com/smarty/scanners/csv.Scanner] and configure it 19 | // for tabs with [github.com/smarty/scanners/csv.Comma]. 20 | type Scanner struct { 21 | *bufio.Scanner 22 | } 23 | 24 | // NewScanner returns a fields scanner. 25 | func NewScanner(reader io.Reader) *Scanner { 26 | return &Scanner{Scanner: bufio.NewScanner(reader)} 27 | } 28 | 29 | // Fields returns the most recent fields generated by a call to Scan as a 30 | // []string. 31 | func (this *Scanner) Fields() []string { 32 | return strings.Fields(this.Text()) 33 | } 34 | -------------------------------------------------------------------------------- /fields/scanner_test.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/smarty/assertions" 9 | "github.com/smarty/assertions/should" 10 | ) 11 | 12 | func TestFieldsScanner(t *testing.T) { 13 | assert := assertions.New(t) 14 | 15 | reader := new(bytes.Buffer) 16 | for x := 0; x < 100; x++ { 17 | fmt.Fprintf(reader, "%d %d %d\n", x, x, x) 18 | } 19 | 20 | scanner := NewScanner(reader) 21 | x := 0 22 | for ; scanner.Scan(); x++ { 23 | X := fmt.Sprint(x) 24 | assert.So(scanner.Fields(), should.Resemble, []string{X, X, X}) 25 | } 26 | 27 | assert.So(x, should.Equal, 100) 28 | assert.So(scanner.Err(), should.BeNil) 29 | assert.So(scanner.Scan(), should.BeFalse) 30 | assert.So(scanner.Text(), should.BeEmpty) 31 | assert.So(scanner.Fields(), should.BeEmpty) 32 | } 33 | -------------------------------------------------------------------------------- /fixedwidth/example_test.go: -------------------------------------------------------------------------------- 1 | package fixedwidth_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | fw "github.com/smarty/scanners/v3/fixedwidth" 9 | ) 10 | 11 | func ExampleScanner() { 12 | in := strings.Join([]string{ 13 | "name username", 14 | "Rob Pike rob ", 15 | "Ken Thompson ken ", 16 | "Robert Griesemer gri ", 17 | }, "\n") 18 | 19 | scanner := fw.NewScanner(strings.NewReader(in)) 20 | 21 | for scanner.Scan() { 22 | var ( 23 | name = scanner.Field(fw.Field(0, 16)) 24 | username = scanner.Field(fw.Field(17, 8)) 25 | ) 26 | 27 | fmt.Printf("* % s* %s *\n", name, username) 28 | } 29 | 30 | if err := scanner.Err(); err != nil { 31 | log.Panic(err) 32 | } 33 | 34 | // Output: 35 | // * name * username * 36 | // * Rob Pike * rob * 37 | // * Ken Thompson * ken * 38 | // * Robert Griesemer* gri * 39 | } 40 | 41 | var ( 42 | namef fw.Substring = func(x string) string { return x[0:16] } 43 | usernamef fw.Substring = func(x string) string { return x[17:25] } 44 | ) 45 | 46 | // Define custom [Substring] functions with particular index 47 | // ranges. 48 | func ExampleScanner_substring() { 49 | in := strings.Join([]string{ 50 | "name username", 51 | "Rob Pike rob ", 52 | "Ken Thompson ken ", 53 | "Robert Griesemer gri ", 54 | }, "\n") 55 | 56 | scanner := fw.NewScanner(strings.NewReader(in)) 57 | 58 | for scanner.Scan() { 59 | fmt.Printf("* % s* %s *\n", scanner.Field(namef), scanner.Field(usernamef)) 60 | } 61 | 62 | if err := scanner.Err(); err != nil { 63 | log.Panic(err) 64 | } 65 | 66 | // Output: 67 | // * name * username * 68 | // * Rob Pike * rob * 69 | // * Ken Thompson * ken * 70 | // * Robert Griesemer* gri * 71 | } 72 | -------------------------------------------------------------------------------- /fixedwidth/scanner.go: -------------------------------------------------------------------------------- 1 | // Package fixedwidth scans fixed-width files and provides easy 2 | // access to individual columns. 3 | // 4 | // Advance the scanner with the Scan method and check errors with 5 | // the Err method, both from the underlying bufio.Scanner. 6 | package fixedwidth 7 | 8 | import ( 9 | "bufio" 10 | "io" 11 | ) 12 | 13 | type Substring func(line string) (field string) 14 | 15 | func Field(index, width int) Substring { 16 | return func(line string) string { 17 | return line[index : index+width] 18 | } 19 | } 20 | 21 | // A Scanner reads records from a fixed-width-encode file. 22 | type Scanner struct { 23 | *bufio.Scanner 24 | } 25 | 26 | // NewScanner returns a Scanner that reads from reader. 27 | func NewScanner(reader io.Reader) *Scanner { 28 | return &Scanner{Scanner: bufio.NewScanner(reader)} 29 | } 30 | 31 | // Field returns the specified Substring from the most recent 32 | // record generated by a call to Scanner.Scan as a string. 33 | func (this *Scanner) Field(field Substring) string { 34 | return field(this.Text()) 35 | } 36 | 37 | // Fields returns the specified Substrings from the most recent 38 | // record generated by a call to Scanner.Scan as a []string. 39 | func (this *Scanner) Fields(fields ...Substring) (values []string) { 40 | for _, field := range fields { 41 | values = append(values, this.Field(field)) 42 | } 43 | return values 44 | } 45 | -------------------------------------------------------------------------------- /fixedwidth/scanner_test.go: -------------------------------------------------------------------------------- 1 | package fixedwidth 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/smarty/assertions/should" 9 | "github.com/smarty/gunit" 10 | ) 11 | 12 | func TestScannerFixture(t *testing.T) { 13 | gunit.Run(new(ScannerFixture), t) 14 | } 15 | 16 | type ScannerFixture struct { 17 | *gunit.Fixture 18 | } 19 | 20 | func (this *ScannerFixture) TestScanner() { 21 | scanner := NewScanner(bytes.NewBufferString(strings.Repeat("122333444455555\n", 10))) 22 | records := 0 23 | for ; scanner.Scan(); records++ { 24 | this.So(scanner.Field(Field(0, 1)), should.Equal, "1") 25 | this.So(scanner.Field(Field(1, 2)), should.Equal, "22") 26 | this.So(scanner.Field(Field(3, 3)), should.Equal, "333") 27 | this.So(scanner.Field(Field(6, 4)), should.Equal, "4444") 28 | this.So(scanner.Field(Field(10, 5)), should.Equal, "55555") 29 | } 30 | this.So(records, should.Equal, 10) 31 | this.So(scanner.Err(), should.BeNil) 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smarty/scanners/v3 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/smarty/assertions v1.15.1 7 | github.com/smarty/gunit v1.5.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/smarty/assertions v1.15.1 h1:812oFiXI+G55vxsFf+8bIZ1ux30qtkdqzKbEFwyX3Tk= 2 | github.com/smarty/assertions v1.15.1/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= 3 | github.com/smarty/gunit v1.5.0 h1:OmG6a/rgi7qCjlQis6VjXbvx/WqZ8I6xSlbfN4YB5MY= 4 | github.com/smarty/gunit v1.5.0/go.mod h1:uAeNibUD292KZRcg5OTy7lb6WR5++UC0BQOzNuiRzpU= 5 | -------------------------------------------------------------------------------- /scanners.go: -------------------------------------------------------------------------------- 1 | // Package scanners provides scanners for text files that encode 2 | // data as CSV, space-delimited fields, or fixed-width columns. 3 | // 4 | // All three scanners either emulate or wrap a bufio.Scanner, 5 | // and incorporate the bufio.Scanner style of defining a scan-loop, 6 | // looping, and then checking for errors after the scan-loop has 7 | // completed: 8 | // 9 | // scanner := SomeNewScanner() 10 | // 11 | // for scanner.Scan() { 12 | // scanner.GetSomeValues() 13 | // } 14 | // 15 | // if err := scanner.Err(); err != nil { 16 | // log.Fatal(err) 17 | // } 18 | package scanners 19 | --------------------------------------------------------------------------------