246 lines
8.2 KiB
Python
Executable File
246 lines
8.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
|
|
# Constants for fitness levels
|
|
FITNESS_LEVELS = {
|
|
(0, 9.0): "Fitness level below pace for long games",
|
|
(9.1, 10.0): "Fitness level for amateur player",
|
|
(10.1, 11.0): "Fitness level for above average amateur player",
|
|
(11.1, 12.0): "Fitness level for elite player",
|
|
(12.1, 13.0): "Very high fitness level"
|
|
}
|
|
|
|
|
|
class ValidationError(Exception):
|
|
pass
|
|
|
|
|
|
class PlayerFitnessData:
|
|
def __init__(self, date, player, level, laps, speed, lap_time):
|
|
self.date = date
|
|
self.player = player
|
|
self.level = level
|
|
self.laps = laps
|
|
self.speed = speed
|
|
self.lap_time = lap_time
|
|
|
|
def __repr__(self):
|
|
return f"PlayerFitnessData({self.player}, {self.date})"
|
|
|
|
|
|
class PlayerFitnessAnalyzer:
|
|
def __init__(self, data_file):
|
|
self.data_file = data_file
|
|
self.player_data = defaultdict(list)
|
|
self.errors = []
|
|
|
|
def validate_data(self, date, player, level, laps, speed, lap_time):
|
|
# Validate date format (DD Mon)
|
|
if not isinstance(date, str) or len(date.split()) != 2:
|
|
raise ValidationError(f"Invalid date format: {date}. Expected 'DD Mon'")
|
|
|
|
if not isinstance(player, str) or not player.strip():
|
|
raise ValidationError("Player name cannot be empty")
|
|
|
|
try:
|
|
level = float(level)
|
|
if level < 0 or level > 13:
|
|
raise ValidationError(f"Level {level} out of range (0-13)")
|
|
except ValueError:
|
|
raise ValidationError(f"Level must be numeric: {level}")
|
|
|
|
try:
|
|
laps = int(laps)
|
|
if laps <= 0:
|
|
raise ValidationError(f"Laps must be positive: {laps}")
|
|
except ValueError:
|
|
raise ValidationError(f"Laps must be integer: {laps}")
|
|
|
|
try:
|
|
speed = float(speed)
|
|
if speed <= 0:
|
|
raise ValidationError(f"Speed must be positive: {speed}")
|
|
except ValueError:
|
|
raise ValidationError(f"Speed must be numeric: {speed}")
|
|
|
|
try:
|
|
lap_time = float(lap_time)
|
|
if lap_time <= 0:
|
|
raise ValidationError(f"Lap time must be positive: {lap_time}")
|
|
except ValueError:
|
|
raise ValidationError(f"Lap time must be numeric: {lap_time}")
|
|
|
|
return level, laps, speed, lap_time
|
|
|
|
def load_data(self):
|
|
"""
|
|
Load and parse player data from file.
|
|
|
|
Expected format (space/tab separated):
|
|
Date Player Level Laps Speed Lap Time(s)
|
|
11 Jan Harry Keane 9 11 12.5 5.76
|
|
...
|
|
"""
|
|
if not os.path.exists(self.data_file):
|
|
raise FileNotFoundError(f"Data file not found: {self.data_file}")
|
|
|
|
try:
|
|
with open(self.data_file, 'r') as f:
|
|
lines = f.readlines()
|
|
except IOError as e:
|
|
raise IOError(f"Cannot read file {self.data_file}: {e}")
|
|
|
|
if not lines:
|
|
raise ValidationError("Data file is empty")
|
|
|
|
# Skip header line if present
|
|
start_line = 0
|
|
if lines[0].strip().lower().startswith('date'):
|
|
start_line = 1
|
|
|
|
for line_num, line in enumerate(lines[start_line:], start=start_line + 1):
|
|
line = line.strip()
|
|
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
|
|
try:
|
|
parts = line.split()
|
|
|
|
if len(parts) < 8:
|
|
self.errors.append(f"Line {line_num}: Not enough fields: {line}")
|
|
continue
|
|
|
|
date = f"{parts[0]} {parts[1]}"
|
|
player = f"{parts[2]} {parts[3]}"
|
|
level_str = parts[4]
|
|
laps_str = parts[5]
|
|
speed_str = parts[6]
|
|
lap_time_str = parts[7]
|
|
|
|
level, laps, speed, lap_time = self.validate_data(
|
|
date, player, level_str, laps_str, speed_str, lap_time_str
|
|
)
|
|
|
|
data = PlayerFitnessData(date, player, level, laps, speed, lap_time)
|
|
self.player_data[player].append(data)
|
|
|
|
except ValidationError as e:
|
|
self.errors.append(f"Line {line_num}: {str(e)}")
|
|
except (IndexError, ValueError) as e:
|
|
self.errors.append(f"Line {line_num}: Parse error: {str(e)}")
|
|
|
|
def get_fitness_level_description(self, average_level):
|
|
for (min_val, max_val), description in FITNESS_LEVELS.items():
|
|
if min_val <= average_level <= max_val:
|
|
return description
|
|
return "Unknown fitness level"
|
|
|
|
def analyze_player(self, player_name, weeks=4):
|
|
if player_name not in self.player_data:
|
|
return None
|
|
|
|
data_list = self.player_data[player_name]
|
|
|
|
if len(data_list) < weeks:
|
|
recent_data = data_list
|
|
weeks_used = len(data_list)
|
|
else:
|
|
recent_data = data_list[-weeks:]
|
|
weeks_used = weeks
|
|
|
|
if not recent_data:
|
|
return None
|
|
|
|
avg_level = sum(d.level for d in recent_data) / len(recent_data)
|
|
avg_laps = sum(d.laps for d in recent_data) / len(recent_data)
|
|
avg_speed = sum(d.speed for d in recent_data) / len(recent_data)
|
|
avg_lap_time = sum(d.lap_time for d in recent_data) / len(recent_data)
|
|
|
|
return {
|
|
'player': player_name,
|
|
'weeks_analyzed': weeks_used,
|
|
'average_level': round(avg_level, 2),
|
|
'average_laps': round(avg_laps, 2),
|
|
'average_speed': round(avg_speed, 2),
|
|
'average_lap_time': round(avg_lap_time, 2),
|
|
'fitness_description': self.get_fitness_level_description(avg_level)
|
|
}
|
|
|
|
def generate_report(self):
|
|
if not self.player_data:
|
|
print("No player data loaded.")
|
|
return
|
|
|
|
print("\n" + "=" * 80)
|
|
print("PENDLETOWN FS - MONTHLY FITNESS REPORT")
|
|
print("=" * 80)
|
|
|
|
for player_name in sorted(self.player_data.keys()):
|
|
summary = self.analyze_player(player_name)
|
|
|
|
if summary:
|
|
print(f"\nPlayer: {summary['player']}")
|
|
print("-" * 80)
|
|
print(f" Weeks Analyzed: {summary['weeks_analyzed']}")
|
|
print(f" Average Level: {summary['average_level']:.2f}")
|
|
print(f" Average Laps: {summary['average_laps']:.2f}")
|
|
print(f" Average Speed: {summary['average_speed']:.2f} km/h")
|
|
print(f" Average Lap Time: {summary['average_lap_time']:.2f} seconds")
|
|
print(f"\n Fitness Level: {summary['fitness_description']}")
|
|
|
|
print("\n" + "=" * 80 + "\n")
|
|
|
|
def print_errors(self):
|
|
if self.errors:
|
|
print("\n" + "!" * 80)
|
|
print("DATA VALIDATION ERRORS:")
|
|
print("!" * 80)
|
|
for error in self.errors:
|
|
print(f" {error}")
|
|
print("!" * 80 + "\n")
|
|
return True
|
|
return False
|
|
|
|
|
|
def main():
|
|
data_file = "player_data.txt"
|
|
|
|
if len(sys.argv) > 1:
|
|
data_file = sys.argv[1]
|
|
|
|
print(f"\nLoading player data from: {data_file}")
|
|
|
|
try:
|
|
analyzer = PlayerFitnessAnalyzer(data_file)
|
|
analyzer.load_data()
|
|
|
|
has_errors = analyzer.print_errors()
|
|
|
|
if analyzer.player_data:
|
|
analyzer.generate_report()
|
|
if has_errors:
|
|
print("Note: Some lines had errors but valid data was processed.\n")
|
|
else:
|
|
print("No valid data to process.")
|
|
sys.exit(1)
|
|
|
|
except FileNotFoundError as e:
|
|
print(f"ERROR: {e}")
|
|
sys.exit(1)
|
|
except IOError as e:
|
|
print(f"ERROR: {e}")
|
|
sys.exit(1)
|
|
except ValidationError as e:
|
|
print(f"ERROR: {e}")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"UNEXPECTED ERROR: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |