feat: enhance flitsmeister utility with improved CSV parsing and statistics calculation
This commit is contained in:
parent
f8d426ce1a
commit
30569c4019
@ -2,7 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
@ -10,13 +12,51 @@ import (
|
||||
"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() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: flitsmeister <filename.csv>")
|
||||
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
|
||||
}
|
||||
|
||||
file, err := os.Open(os.Args[1])
|
||||
// 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)
|
||||
}
|
||||
@ -29,7 +69,6 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Dutch to English month translation
|
||||
dutchMonths := map[string]string{
|
||||
"januari": "January", "februari": "February", "maart": "March",
|
||||
"april": "April", "mei": "May", "juni": "June", "juli": "July",
|
||||
@ -37,91 +76,253 @@ func main() {
|
||||
"november": "November", "december": "December",
|
||||
}
|
||||
|
||||
monthlyTotals := make(map[string]float64)
|
||||
monthlyCounts := make(map[string]int)
|
||||
monthlyLongest := make(map[string]float64)
|
||||
monthlyShortest := make(map[string]float64)
|
||||
tripsByMonth := make(map[string][]Trip)
|
||||
startAddrCount := make(map[string]int)
|
||||
endAddrCount := make(map[string]int)
|
||||
fuelEnabled := *fuelPrice > 0 && finalEfficiency > 0
|
||||
|
||||
// Skip header row
|
||||
// Parse CSV
|
||||
for _, record := range records[1:] {
|
||||
if len(record) < 8 {
|
||||
if len(record) < 13 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse date with Dutch month names
|
||||
dateParts := strings.Split(record[1], " ")
|
||||
if len(dateParts) < 4 {
|
||||
// Parse start time
|
||||
startTime, err := parseDutchTime(record[1], dutchMonths)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Translate Dutch month to English
|
||||
dutchMonth := strings.ToLower(dateParts[1])
|
||||
engMonth, ok := dutchMonths[dutchMonth]
|
||||
// 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 {
|
||||
fmt.Printf("Unknown month: %s\n", dutchMonth)
|
||||
continue
|
||||
return time.Time{}, fmt.Errorf("unknown month")
|
||||
}
|
||||
|
||||
// Rebuild date string with English month
|
||||
dateStr := fmt.Sprintf("%s %s %s %s",
|
||||
dateParts[0], engMonth, dateParts[2], dateParts[3])
|
||||
|
||||
// Parse date (single-digit days allowed)
|
||||
tripDate, err := time.Parse("2 January 2006 15:04", dateStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing date: %v\n", err)
|
||||
continue
|
||||
timeStr := fmt.Sprintf("%s %s %s %s", parts[0], engMonth, parts[2], parts[3])
|
||||
return time.Parse("2 January 2006 15:04", timeStr)
|
||||
}
|
||||
|
||||
// Create month key (format: YYYY-MM)
|
||||
monthKey := fmt.Sprintf("%d-%02d", tripDate.Year(), tripDate.Month())
|
||||
func calculateStats(tripsByMonth map[string][]Trip, fuelEnabled bool, fuelPrice, fuelEfficiency float64) map[string]MonthStats {
|
||||
stats := make(map[string]MonthStats)
|
||||
|
||||
// Parse distance
|
||||
distance, err := strconv.ParseFloat(record[7], 64)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing distance: %v\n", err)
|
||||
continue
|
||||
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++
|
||||
}
|
||||
|
||||
monthlyTotals[monthKey] += distance
|
||||
monthlyCounts[monthKey]++
|
||||
if t.Distance > s.Longest {
|
||||
s.Longest = t.Distance
|
||||
}
|
||||
if t.Distance < s.Shortest || s.Shortest == 0 {
|
||||
s.Shortest = t.Distance
|
||||
}
|
||||
|
||||
// Track longest and shortest trip
|
||||
if _, exists := monthlyLongest[monthKey]; !exists {
|
||||
monthlyLongest[monthKey] = distance
|
||||
monthlyShortest[monthKey] = 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 {
|
||||
if distance > monthlyLongest[monthKey] {
|
||||
monthlyLongest[monthKey] = distance
|
||||
}
|
||||
if distance < monthlyShortest[monthKey] {
|
||||
monthlyShortest[monthKey] = distance
|
||||
}
|
||||
s.AvgSpeed = 0
|
||||
}
|
||||
if s.Trips > 0 {
|
||||
s.AvgTripDuration = time.Duration(int64(s.TotalDuration) / int64(s.Trips))
|
||||
}
|
||||
|
||||
// Sort months chronologically
|
||||
var months []string
|
||||
for k := range monthlyTotals {
|
||||
months = append(months, k)
|
||||
if fuelEnabled {
|
||||
s.FuelCost = (s.TotalKM / fuelEfficiency) * fuelPrice
|
||||
}
|
||||
sort.Strings(months)
|
||||
|
||||
// Print table header
|
||||
fmt.Printf("%-10s | %-12s | %-7s | %-12s | %-12s | %-12s\n", "Month", "Total KM", "Trips", "Avg KM/Trip", "Longest", "Shortest")
|
||||
fmt.Println(strings.Repeat("-", 77))
|
||||
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))
|
||||
|
||||
// Print stats per month
|
||||
for _, month := range months {
|
||||
total := monthlyTotals[month]
|
||||
trips := monthlyCounts[month]
|
||||
avg := 0.0
|
||||
if trips > 0 {
|
||||
avg = total / float64(trips)
|
||||
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
|
||||
}
|
||||
longest := monthlyLongest[month]
|
||||
shortest := monthlyShortest[month]
|
||||
fmt.Printf("%-10s | %-12.2f | %-7d | %-12.2f | %-12.2f | %-12.2f\n",
|
||||
month, total, trips, avg, longest, shortest)
|
||||
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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user