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 }