Parser limpa prefixo R$ e símbolos de moeda antes de parsear o valor. Flags SkipNegative e AllExpenses para CSV de fatura de cartão. UI: checkboxes "Fatura de cartão" e "Ignorar valores negativos". Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
118 lines
2.4 KiB
Go
118 lines
2.4 KiB
Go
package parser
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"financeiro-carvalho/internal/model"
|
|
)
|
|
|
|
func ParseCSV(r io.Reader, m model.CSVMapping) ([]model.ImportRow, []string) {
|
|
if m.DateFormat == "" {
|
|
m.DateFormat = "02/01/2006"
|
|
}
|
|
if m.DecimalSeparator == "" {
|
|
m.DecimalSeparator = ","
|
|
}
|
|
fieldSep := ','
|
|
if m.FieldSeparator == ";" {
|
|
fieldSep = ';'
|
|
}
|
|
|
|
reader := csv.NewReader(r)
|
|
reader.Comma = fieldSep
|
|
reader.LazyQuotes = true
|
|
reader.TrimLeadingSpace = true
|
|
|
|
var rows []model.ImportRow
|
|
var errs []string
|
|
lineNum := 0
|
|
|
|
for {
|
|
record, err := reader.Read()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
lineNum++
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("linha %d: %v", lineNum, err))
|
|
continue
|
|
}
|
|
if lineNum == 1 && m.HasHeader {
|
|
continue
|
|
}
|
|
|
|
maxCol := max3(m.DateColumn, m.AmountColumn, m.DescriptionColumn)
|
|
if len(record) <= maxCol {
|
|
errs = append(errs, fmt.Sprintf("linha %d: colunas insuficientes (%d)", lineNum, len(record)))
|
|
continue
|
|
}
|
|
|
|
dateStr := strings.TrimSpace(record[m.DateColumn])
|
|
amtStr := strings.TrimSpace(record[m.AmountColumn])
|
|
desc := cleanDescription(record[m.DescriptionColumn])
|
|
|
|
// Skip blank rows (e.g. empty line after header in some bank exports)
|
|
if dateStr == "" && amtStr == "" && desc == "" {
|
|
continue
|
|
}
|
|
|
|
t, err := time.Parse(m.DateFormat, dateStr)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("linha %d: data inválida %q", lineNum, dateStr))
|
|
continue
|
|
}
|
|
|
|
// Strip currency symbols (R$, $, €, etc.) keeping digits, comma, dot, minus
|
|
cleaned := strings.Map(func(r rune) rune {
|
|
if r >= '0' && r <= '9' || r == ',' || r == '.' || r == '-' {
|
|
return r
|
|
}
|
|
return -1
|
|
}, amtStr)
|
|
cleaned = strings.TrimSpace(cleaned)
|
|
|
|
if m.DecimalSeparator == "," {
|
|
cleaned = strings.ReplaceAll(cleaned, ".", "")
|
|
cleaned = strings.ReplaceAll(cleaned, ",", ".")
|
|
}
|
|
amt, err := strconv.ParseFloat(cleaned, 64)
|
|
if err != nil || cleaned == "" {
|
|
errs = append(errs, fmt.Sprintf("linha %d: valor inválido %q", lineNum, amtStr))
|
|
continue
|
|
}
|
|
|
|
if m.SkipNegative && amt < 0 {
|
|
continue
|
|
}
|
|
|
|
txType := "expense"
|
|
if !m.AllExpenses && amt > 0 {
|
|
txType = "income"
|
|
}
|
|
|
|
rows = append(rows, model.ImportRow{
|
|
Date: t.Format("2006-01-02"),
|
|
Amount: math.Abs(amt),
|
|
Description: desc,
|
|
Type: txType,
|
|
})
|
|
}
|
|
return rows, errs
|
|
}
|
|
|
|
func max3(a, b, c int) int {
|
|
if b > a {
|
|
a = b
|
|
}
|
|
if c > a {
|
|
a = c
|
|
}
|
|
return a
|
|
}
|