package series import ( "encoding/csv" "fmt" "os" "slices" "strconv" "strings" ) type CSVData struct { Columns []string Rows [][]string index map[string]int } func ReadCSV(path string) (*CSVData, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() records, err := csv.NewReader(file).ReadAll() if err != nil { return nil, err } if len(records) == 0 { return &CSVData{Columns: []string{}, Rows: [][]string{}, index: map[string]int{}}, nil } data := &CSVData{ Columns: records[0], Rows: records[1:], index: make(map[string]int, len(records[0])), } for i, column := range data.Columns { data.index[strings.ToLower(strings.TrimSpace(column))] = i } return data, nil } func (d *CSVData) Index(name string) (int, error) { idx, ok := d.index[strings.ToLower(name)] if !ok { return -1, fmt.Errorf("missing column %q", name) } return idx, nil } func (d *CSVData) NumericColumns(names ...string) ([][]float64, error) { indices := make([]int, len(names)) for i, name := range names { idx, err := d.Index(name) if err != nil { return nil, err } indices[i] = idx } values := make([][]float64, len(names)) for i := range values { values[i] = make([]float64, 0, len(d.Rows)) } maxIndex := MaxInt(indices...) for _, row := range d.Rows { if len(row) <= maxIndex { continue } parsed := make([]float64, len(indices)) valid := true for i, idx := range indices { value, err := ParseFloat(row[idx]) if err != nil { valid = false break } parsed[i] = value } if !valid { continue } for i, value := range parsed { values[i] = append(values[i], value) } } return values, nil } func ParseFloat(value string) (float64, error) { return strconv.ParseFloat(strings.TrimSpace(value), 64) } func MaxInt(values ...int) int { max := 0 for _, value := range values { if value > max { max = value } } return max } func MaxLastHalf(values []float64) float64 { if len(values) == 0 { return 0 } return slices.Max(values[len(values)/2:]) }