329 lines
8.0 KiB
Go
329 lines
8.0 KiB
Go
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 <price> [-efficiency <km/l> | -lper100km <l/100km>] <filename.csv>")
|
|
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
|
|
}
|