package meteo import ( "encoding/csv" "fmt" "io" "pkg" "strconv" "strings" "time" ) func (mt *MeteoData) validate() error { if mt.MaxTemp > 60 { return ErrMaxTempOutOfRange } if mt.MinTemp < -20 { return ErrMinTempOutOfRange } if mt.Rainfall < 0 || mt.Rainfall > 80 { return ErrRainfallOutOfRange } if mt.Cloudiness < 0 || mt.Cloudiness > 100 { return ErrCloudinessOutOfRange } return nil } type FileIngest interface { Parse(io io.Reader) ([]MeteoData, []RejectedMeteoData, error) } type CSV struct{} var _ FileIngest = (*CSV)(nil) func (c *CSV) Parse(r io.Reader) ([]MeteoData, []RejectedMeteoData, error) { reader := csv.NewReader(r) reader.Comma = ';' reader.TrimLeadingSpace = true header, err := reader.Read() if err != nil { return nil, nil, fmt.Errorf("%w: %v", ErrReadingCSVHeader, err) } if len(header) == 1 { return nil, nil, fmt.Errorf("%w: invalid separator detected, expected semicolon (;)", ErrCannotParseFile) } for i := range header { header[i] = strings.TrimSpace(header[i]) } var meteoDataList []MeteoData var rejectedDataList []RejectedMeteoData for { row, err := reader.Read() if err == io.EOF { break } if err != nil { return nil, nil, fmt.Errorf("%w: %v", ErrReadingCSVRow, err) } if len(row) == 0 || (len(row) == 1 && row[0] == "") { continue } rowValue := strings.Join(row, ";") record := make(pkg.H) for i, value := range row { if i < len(header) { record[header[i]] = value } } meteoData, err := normalize(record) if err != nil { rejectedDataList = append(rejectedDataList, RejectedMeteoData{ RowValue: rowValue, Reason: err.Error(), }) continue } if err := meteoData.validate(); err != nil { rejectedDataList = append(rejectedDataList, RejectedMeteoData{ RowValue: rowValue, Reason: err.Error(), }) continue } meteoDataList = append(meteoDataList, *meteoData) } return meteoDataList, rejectedDataList, nil } func normalize(record pkg.H) (*MeteoData, error) { meteoData := &MeteoData{} var err error meteoData.Timestamp, err = parseDate(record, "Fecha", ErrMissingOrInvalidDate) if err != nil { return nil, err } meteoData.Location, err = parseString(record, "Ciudad", ErrMissingOrInvalidLocation) if err != nil { return nil, err } meteoData.MaxTemp, err = parseFloatField(record, "Temperatura Máxima (C)", ErrMissingOrInvalidMaxTemp) if err != nil { return nil, err } meteoData.MinTemp, err = parseFloatField(record, "Temperatura Mínima (C)", ErrMissingOrInvalidMinTemp) if err != nil { return nil, err } meteoData.Rainfall, err = parseFloatField(record, "Precipitación (mm)", ErrMissingOrInvalidRainfall) if err != nil { return nil, err } meteoData.Cloudiness, err = parseIntField(record, "Nubosidad (%)", ErrMissingOrInvalidCloudiness) if err != nil { return nil, err } return meteoData, nil } func parseDate(record pkg.H, key string, errMissing error) (time.Time, error) { if str, ok := record[key].(string); ok && str != "" { t, err := time.Parse("2006/01/02", str) if err != nil { return time.Time{}, errMissing } return t, nil } return time.Time{}, errMissing } func parseString(record pkg.H, key string, errMissing error) (string, error) { if str, ok := record[key].(string); ok && str != "" { return str, nil } return "", errMissing } func parseFloatField(record pkg.H, key string, errMissing error) (float32, error) { if str, ok := record[key].(string); ok && str != "" { str = strings.Replace(str, ",", ".", 1) f, err := strconv.ParseFloat(str, 32) if err != nil { return 0, errMissing } return float32(f), nil } return 0, errMissing } func parseIntField(record pkg.H, key string, errMissing error) (int, error) { if str, ok := record[key].(string); ok && str != "" { str = strings.TrimSpace(str) i, err := strconv.Atoi(str) if err != nil { return 0, errMissing } return i, nil } return 0, errMissing }