2021-03-29 23:44:28 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package csv
import (
"bytes"
2021-04-20 01:25:08 +03:00
stdcsv "encoding/csv"
2021-03-29 23:44:28 +03:00
"errors"
2021-04-20 01:25:08 +03:00
"io"
2021-03-29 23:44:28 +03:00
"regexp"
"strings"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
)
var quoteRegexp = regexp . MustCompile ( ` ["'][\s\S]+?["'] ` )
// CreateReader creates a csv.Reader with the given delimiter.
2021-04-20 01:25:08 +03:00
func CreateReader ( input io . Reader , delimiter rune ) * stdcsv . Reader {
rd := stdcsv . NewReader ( input )
2021-03-29 23:44:28 +03:00
rd . Comma = delimiter
2021-10-27 00:46:56 +03:00
if delimiter != '\t' && delimiter != ' ' {
// TrimLeadingSpace can't be true when delimiter is a tab or a space as the value for a column might be empty,
// thus would change `\t\t` to just `\t` or ` ` (two spaces) to just ` ` (single space)
rd . TrimLeadingSpace = true
}
2021-03-29 23:44:28 +03:00
return rd
}
// CreateReaderAndGuessDelimiter tries to guess the field delimiter from the content and creates a csv.Reader.
2021-10-25 01:42:32 +03:00
// Reads at most 10k bytes.
2021-04-20 01:25:08 +03:00
func CreateReaderAndGuessDelimiter ( rd io . Reader ) ( * stdcsv . Reader , error ) {
var data = make ( [ ] byte , 1e4 )
2021-10-25 00:12:43 +03:00
size , err := util . ReadAtMost ( rd , data )
2021-04-20 01:25:08 +03:00
if err != nil {
return nil , err
}
2021-10-25 01:42:32 +03:00
return CreateReader (
io . MultiReader ( bytes . NewReader ( data [ : size ] ) , rd ) ,
guessDelimiter ( data [ : size ] ) ,
) , nil
2021-03-29 23:44:28 +03:00
}
// guessDelimiter scores the input CSV data against delimiters, and returns the best match.
func guessDelimiter ( data [ ] byte ) rune {
maxLines := 10
2021-10-25 01:42:32 +03:00
text := quoteRegexp . ReplaceAllLiteralString ( string ( data ) , "" )
2021-03-29 23:44:28 +03:00
lines := strings . SplitN ( text , "\n" , maxLines + 1 )
lines = lines [ : util . Min ( maxLines , len ( lines ) ) ]
delimiters := [ ] rune { ',' , ';' , '\t' , '|' , '@' }
bestDelim := delimiters [ 0 ]
bestScore := 0.0
for _ , delim := range delimiters {
score := scoreDelimiter ( lines , delim )
if score > bestScore {
bestScore = score
bestDelim = delim
}
}
return bestDelim
}
// scoreDelimiter uses a count & regularity metric to evaluate a delimiter against lines of CSV.
func scoreDelimiter ( lines [ ] string , delim rune ) float64 {
countTotal := 0
countLineMax := 0
linesNotEqual := 0
for _ , line := range lines {
if len ( line ) == 0 {
continue
}
countLine := strings . Count ( line , string ( delim ) )
countTotal += countLine
if countLine != countLineMax {
if countLineMax != 0 {
linesNotEqual ++
}
countLineMax = util . Max ( countLine , countLineMax )
}
}
return float64 ( countTotal ) * ( 1 - float64 ( linesNotEqual ) / float64 ( len ( lines ) ) )
}
// FormatError converts csv errors into readable messages.
func FormatError ( err error , locale translation . Locale ) ( string , error ) {
2021-08-05 19:56:11 +03:00
var perr * stdcsv . ParseError
2021-03-29 23:44:28 +03:00
if errors . As ( err , & perr ) {
2021-08-05 19:56:11 +03:00
if perr . Err == stdcsv . ErrFieldCount {
2021-03-29 23:44:28 +03:00
return locale . Tr ( "repo.error.csv.invalid_field_count" , perr . Line ) , nil
}
return locale . Tr ( "repo.error.csv.unexpected" , perr . Line , perr . Column ) , nil
}
return "" , err
}