package main import ( "encoding/csv" "flag" "fmt" "math" "os" "sort" "strconv" "strings" "time" ) type Trip struct { StartTime time.Time EndTime time.Time StartAddr string EndAddr string KMStart float64 KMEnd float64 Distance float64 License string BusinessCost float64 Type string } type MonthStats struct { TotalKM float64 Trips int Longest float64 Shortest float64 TotalDuration time.Duration LongestGap time.Duration OdoAnomalies int AvgSpeed float64 AvgTripDuration time.Duration FuelCost float64 } func main() { fuelPrice := flag.Float64("fuelprice", 0, "Fuel price per liter (EUR)") fuelEfficiency := flag.Float64("efficiency", 0, "Fuel efficiency (km per liter)") lPer100km := flag.Float64("lper100km", 0, "Fuel consumption (liters per 100km)") flag.Parse() if len(flag.Args()) < 1 { fmt.Println("Usage: go run main.go -fuelprice [-efficiency | -lper100km ] ") flag.PrintDefaults() return } // Convert l/100km to km/l if provided finalEfficiency := *fuelEfficiency if *lPer100km > 0 { finalEfficiency = 100.0 / *lPer100km } file, err := os.Open(flag.Arg(0)) if err != nil { panic(err) } defer file.Close() reader := csv.NewReader(file) reader.Comma = ',' records, err := reader.ReadAll() if err != nil { panic(err) } dutchMonths := map[string]string{ "januari": "January", "februari": "February", "maart": "March", "april": "April", "mei": "May", "juni": "June", "juli": "July", "augustus": "August", "september": "September", "oktober": "October", "november": "November", "december": "December", } tripsByMonth := make(map[string][]Trip) startAddrCount := make(map[string]int) endAddrCount := make(map[string]int) fuelEnabled := *fuelPrice > 0 && finalEfficiency > 0 // Parse CSV for _, record := range records[1:] { if len(record) < 13 { continue } // Parse start time startTime, err := parseDutchTime(record[1], dutchMonths) if err != nil { continue } // Parse end time endTime, err := parseDutchTime(record[2], dutchMonths) if err != nil { continue } // Parse distance data kmStart, _ := strconv.ParseFloat(strings.ReplaceAll(record[5], ",", ""), 64) kmEnd, _ := strconv.ParseFloat(strings.ReplaceAll(record[6], ",", ""), 64) distance, _ := strconv.ParseFloat(strings.ReplaceAll(record[7], ",", ""), 64) trip := Trip{ StartTime: startTime, EndTime: endTime, StartAddr: record[3], EndAddr: record[4], KMStart: kmStart, KMEnd: kmEnd, Distance: distance, License: record[8], BusinessCost: parseFloat(record[11]), Type: strings.TrimSpace(record[12]), } monthKey := fmt.Sprintf("%d-%02d", startTime.Year(), startTime.Month()) tripsByMonth[monthKey] = append(tripsByMonth[monthKey], trip) startAddrCount[trip.StartAddr]++ endAddrCount[trip.EndAddr]++ } // Calculate stats months := sortedKeys(tripsByMonth) statsByMonth := calculateStats(tripsByMonth, fuelEnabled, *fuelPrice, finalEfficiency) // Print results printMainTable(statsByMonth, months, fuelEnabled, tripsByMonth) printTopAddresses(startAddrCount, endAddrCount) } func parseDutchTime(datetime string, monthMap map[string]string) (time.Time, error) { parts := strings.Split(datetime, " ") if len(parts) < 4 { return time.Time{}, fmt.Errorf("invalid time format") } engMonth, ok := monthMap[strings.ToLower(parts[1])] if !ok { return time.Time{}, fmt.Errorf("unknown month") } timeStr := fmt.Sprintf("%s %s %s %s", parts[0], engMonth, parts[2], parts[3]) return time.Parse("2 January 2006 15:04", timeStr) } func calculateStats(tripsByMonth map[string][]Trip, fuelEnabled bool, fuelPrice, fuelEfficiency float64) map[string]MonthStats { stats := make(map[string]MonthStats) for month, trips := range tripsByMonth { var s MonthStats var prevEnd time.Time var longestGap time.Duration sumSpeed := 0.0 speedCount := 0 sort.Slice(trips, func(i, j int) bool { return trips[i].StartTime.Before(trips[j].StartTime) }) for i, t := range trips { s.TotalKM += t.Distance s.Trips++ duration := t.EndTime.Sub(t.StartTime) s.TotalDuration += duration if duration.Hours() > 0 { sumSpeed += t.Distance / duration.Hours() speedCount++ } if t.Distance > s.Longest { s.Longest = t.Distance } if t.Distance < s.Shortest || s.Shortest == 0 { s.Shortest = t.Distance } if i > 0 { gap := t.StartTime.Sub(prevEnd) if gap > longestGap { longestGap = gap } if math.Abs(trips[i-1].KMEnd-t.KMStart) > 0.01 { s.OdoAnomalies++ } } prevEnd = t.EndTime } s.LongestGap = longestGap if speedCount > 0 { s.AvgSpeed = sumSpeed / float64(speedCount) } else { s.AvgSpeed = 0 } if s.Trips > 0 { s.AvgTripDuration = time.Duration(int64(s.TotalDuration) / int64(s.Trips)) } if fuelEnabled { s.FuelCost = (s.TotalKM / fuelEfficiency) * fuelPrice } stats[month] = s } return stats } func printMainTable(stats map[string]MonthStats, months []string, fuelEnabled bool, tripsByMonth map[string][]Trip) { fmt.Println("\n=== Monthly Driving Overview ===") headers := []string{"Month", "Total", "Trips", "AvgKM", "Longest", "Shortest", "DriveTime", "AvgTripDur", "OdoErr", "AvgSpeed"} format := "%-10s | %-16s | %-7s | %-14s | %-24s | %-26s | %-18s | %-18s | %-10s | %-18s" if fuelEnabled { headers = append(headers, "Fuel Cost (EUR)") format += " | %-18s" } fmt.Printf(format+"\n", toInterfaceSlice(headers)...) // print header fmt.Println(strings.Repeat("-", 180)) for _, month := range months { s := stats[month] trips := tripsByMonth[month] // Find longest and shortest trip durations var longestDur, shortestDur time.Duration var longestDist, shortestDist float64 if len(trips) > 0 { for i, t := range trips { dur := t.EndTime.Sub(t.StartTime) if t.Distance > longestDist || i == 0 { longestDist = t.Distance longestDur = dur } if t.Distance < shortestDist || i == 0 { shortestDist = t.Distance shortestDur = dur } } } row := []interface{}{ month, fmt.Sprintf("%.2f Km", s.TotalKM), fmt.Sprintf("%d", s.Trips), fmt.Sprintf("%.2f Km", safeDiv(s.TotalKM, float64(s.Trips))), fmt.Sprintf("%.2f Km (%s)", longestDist, fmtDuration(longestDur)), fmt.Sprintf("%.2f Km (%s)", shortestDist, fmtDuration(shortestDur)), fmtDuration(s.TotalDuration), fmtDuration(s.AvgTripDuration), fmt.Sprintf("%d", s.OdoAnomalies), fmt.Sprintf("%.2f Km/h", s.AvgSpeed), } if fuelEnabled { row = append(row, fmt.Sprintf("%.2f EUR", s.FuelCost)) } fmt.Printf(format+"\n", row...) } } func toInterfaceSlice(strs []string) []interface{} { res := make([]interface{}, len(strs)) for i, v := range strs { res[i] = v } return res } func printTopAddresses(start, end map[string]int) { fmt.Println("\n=== Frequent Locations ===") fmt.Println("Top 3 Start Addresses:") printTopN(start, 3) fmt.Println("\nTop 3 End Addresses:") printTopN(end, 3) } // Helper functions (safeDiv, fmtDuration, printTopN) remain unchanged from previous version // [Include the helper functions from previous script here] func safeDiv(a, b float64) float64 { if b == 0 { return 0 } return a / b } func fmtDuration(d time.Duration) string { h := int(d.Hours()) m := int(d.Minutes()) % 60 return fmt.Sprintf("%02dh%02dm", h, m) } func printTopN(counter map[string]int, n int) { type kv struct { Key string Value int } var sorted []kv for k, v := range counter { sorted = append(sorted, kv{k, v}) } sort.Slice(sorted, func(i, j int) bool { return sorted[i].Value > sorted[j].Value }) for i := 0; i < n && i < len(sorted); i++ { fmt.Printf("%d. %s (%d)\n", i+1, sorted[i].Key, sorted[i].Value) } } func sortedKeys(m map[string][]Trip) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys } func parseFloat(s string) float64 { f, _ := strconv.ParseFloat(strings.ReplaceAll(s, ",", ""), 64) return f }