mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-26 12:20:10 +01:00
128 lines
2.5 KiB
Go
128 lines
2.5 KiB
Go
|
package csvimport
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// scanner is csv scanner
|
||
|
type scanner struct {
|
||
|
// The line value read after the call to NextLine()
|
||
|
Line string
|
||
|
|
||
|
// The column value read after the call to NextColumn()
|
||
|
Column string
|
||
|
|
||
|
// Error may be set only on NextColumn call.
|
||
|
// It is cleared on NextLine call.
|
||
|
Error error
|
||
|
|
||
|
s string
|
||
|
}
|
||
|
|
||
|
// Init initializes sc with s
|
||
|
func (sc *scanner) Init(s string) {
|
||
|
sc.Line = ""
|
||
|
sc.Column = ""
|
||
|
sc.Error = nil
|
||
|
sc.s = s
|
||
|
}
|
||
|
|
||
|
// NextLine advances csv scanner to the next line and sets cs.Line to it.
|
||
|
//
|
||
|
// It clears sc.Error.
|
||
|
//
|
||
|
// false is returned if no more lines left in sc.s
|
||
|
func (sc *scanner) NextLine() bool {
|
||
|
s := sc.s
|
||
|
sc.Error = nil
|
||
|
for len(s) > 0 {
|
||
|
n := strings.IndexByte(s, '\n')
|
||
|
var line string
|
||
|
if n >= 0 {
|
||
|
line = trimTrailingSpace(s[:n])
|
||
|
s = s[n+1:]
|
||
|
} else {
|
||
|
line = trimTrailingSpace(s)
|
||
|
s = ""
|
||
|
}
|
||
|
sc.Line = line
|
||
|
sc.s = s
|
||
|
if len(line) > 0 {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// NextColumn advances sc.Line to the next Column and sets sc.Column to it.
|
||
|
//
|
||
|
// false is returned if no more columns left in sc.Line or if any error occurs.
|
||
|
// sc.Error is set to error in the case of error.
|
||
|
func (sc *scanner) NextColumn() bool {
|
||
|
s := sc.Line
|
||
|
if len(s) == 0 {
|
||
|
return false
|
||
|
}
|
||
|
if sc.Error != nil {
|
||
|
return false
|
||
|
}
|
||
|
if s[0] == '"' {
|
||
|
sc.Column, sc.Line, sc.Error = readQuotedField(s)
|
||
|
return sc.Error == nil
|
||
|
}
|
||
|
n := strings.IndexByte(s, ',')
|
||
|
if n >= 0 {
|
||
|
sc.Column = s[:n]
|
||
|
sc.Line = s[n+1:]
|
||
|
} else {
|
||
|
sc.Column = s
|
||
|
sc.Line = ""
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func trimTrailingSpace(s string) string {
|
||
|
if len(s) > 0 && s[len(s)-1] == '\r' {
|
||
|
return s[:len(s)-1]
|
||
|
}
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
func readQuotedField(s string) (string, string, error) {
|
||
|
sOrig := s
|
||
|
if len(s) == 0 || s[0] != '"' {
|
||
|
return "", sOrig, fmt.Errorf("missing opening quote for %q", sOrig)
|
||
|
}
|
||
|
s = s[1:]
|
||
|
hasEscapedQuote := false
|
||
|
for {
|
||
|
n := strings.IndexByte(s, '"')
|
||
|
if n < 0 {
|
||
|
return "", sOrig, fmt.Errorf("missing closing quote for %q", sOrig)
|
||
|
}
|
||
|
s = s[n+1:]
|
||
|
if len(s) == 0 {
|
||
|
// The end of string found
|
||
|
return unquote(sOrig[1:len(sOrig)-1], hasEscapedQuote), "", nil
|
||
|
}
|
||
|
if s[0] == '"' {
|
||
|
// Take into account escaped quote
|
||
|
s = s[1:]
|
||
|
hasEscapedQuote = true
|
||
|
continue
|
||
|
}
|
||
|
if s[0] != ',' {
|
||
|
return "", sOrig, fmt.Errorf("missing comma after quoted field in %q", sOrig)
|
||
|
}
|
||
|
return unquote(sOrig[1:len(sOrig)-len(s)-1], hasEscapedQuote), s[1:], nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func unquote(s string, hasEscapedQuote bool) string {
|
||
|
if !hasEscapedQuote {
|
||
|
return s
|
||
|
}
|
||
|
return strings.ReplaceAll(s, `""`, `"`)
|
||
|
}
|