from typing import Dict, Any, Optional from datetime import datetime from i18n import t class StatsFormatter: @staticmethod def _format_rating_change(rating_change: int) -> str: """Format rating change with colored circles""" if rating_change > 0: return f"🟢 +{rating_change}" elif rating_change < 0: return f"🔴 {rating_change}" else: return f"0" @staticmethod def format_stats_response(data: Dict[str, Any], username: str, period: str, lang: str = 'en') -> str: """Format statistics response according to the template""" if not data or data.get('data') is None: message = data.get('message', t('no_data', lang)) if data else t('no_data', lang) return f"📭 {message}" # Extract data from API response api_data = data.get('data', {}) tasks = api_data.get('tasks', {}) games = api_data.get('games', {}) # Format date range date_range = StatsFormatter._get_date_range(period, lang) # Format tasks section task_text = "" if tasks and tasks.get('total', 0) > 0: total_tasks = tasks.get('total', 0) solved = tasks.get('solved', 0) unsolved = tasks.get('unsolved', 0) task_text = t('puzzles_section', lang, total=total_tasks, solved=solved, unsolved=unsolved) # Format games section games_text = "" if games: for game_type, game_data in games.items(): if not game_data or game_data.get('games_played', 0) == 0: continue # Get game type emoji emoji = StatsFormatter._get_game_type_emoji(game_type) games_count = game_data.get('games_played', 0) rating_change = game_data.get('rating_change', 0) rating = game_data.get('final_rating', 0) wins = game_data.get('wins', 0) losses = game_data.get('losses', 0) draws = game_data.get('draws', 0) # Format rating change rating_change_str = StatsFormatter._format_rating_change(rating_change) # Get game type name (capitalize first letter) game_type_name = game_type.title() games_text += t('games_section', lang, emoji=emoji, game_type=game_type_name, games_count=games_count, rating_change=rating_change_str, rating=rating, wins=wins, losses=losses, draws=draws ) # Combine all parts result = t('stats_title', lang, username=username, date_range=date_range) result += task_text result += games_text.rstrip() return result @staticmethod def _get_date_range(period: str, lang: str = 'en') -> str: """Get date range string for the period""" from datetime import datetime, timedelta today = datetime.now() if period == "today": return today.strftime("%d.%m.%Y") elif period == "yesterday": yesterday = today - timedelta(days=1) return yesterday.strftime("%d.%m.%Y") elif period == "week": week_ago = today - timedelta(days=7) return f"{week_ago.strftime('%d.%m.%Y')}–{today.strftime('%d.%m.%Y')}" else: return today.strftime("%d.%m.%Y") @staticmethod def _get_game_type_emoji(game_type: str) -> str: """Get emoji for game type""" emoji_map = { 'bullet': '⚡️', 'blitz': '🔥', 'rapid': '🐇', 'classical': '♟️', 'correspondence': '📮' } return emoji_map.get(game_type.lower(), '🎯') @staticmethod def format_period_notification(username: str, games_data: Optional[Dict], puzzles_data: Optional[Dict], period_minutes: int, lang: str = 'en') -> str: """Format notification for periodic checks""" from datetime import datetime # Format period text if period_minutes == 1: period_text = t('period_1_minute', lang) elif period_minutes in [2, 3, 4]: period_text = t('period_2_3_4_minutes', lang, period=period_minutes) else: period_text = t('period_minutes_text', lang, period=period_minutes) result = t('period_notification_title', lang, username=username, period_text=period_text) # Format puzzles first (if available and there's actual activity) if puzzles_data and puzzles_data.get('data'): puzzles_info = puzzles_data['data'] total_puzzles = puzzles_info.get('total_attempts', 0) solved = puzzles_info.get('solved', 0) failed = puzzles_info.get('failed', 0) # Only show tasks section if there's actual activity (not all zeros) if total_puzzles > 0 or solved > 0 or failed > 0: result += t('period_puzzles_section', lang, total=total_puzzles, solved=solved, failed=failed) # Format games if games_data and games_data.get('data'): games_info = games_data['data'] total_games = games_info.get('total', {}).get('games_played', 0) # Show details for each game type if there were games if total_games > 0: for game_type, game_data in games_info.items(): if game_type != 'total' and game_data and game_data.get('games_played', 0) > 0: emoji = StatsFormatter._get_game_type_emoji(game_type) games_count = game_data.get('games_played', 0) rating_change = game_data.get('rating_change', 0) rating = game_data.get('rating', 0) wins = game_data.get('wins', 0) losses = game_data.get('losses', 0) draws = game_data.get('draws', 0) rating_change_str = StatsFormatter._format_rating_change(rating_change) game_type_name = game_type.title() result += t('period_games_section', lang, emoji=emoji, game_type=game_type_name, games_count=games_count, rating_change=rating_change_str, rating=rating, wins=wins, losses=losses, draws=draws ) # If no activity if not (games_data and games_data.get('data')) and not (puzzles_data and puzzles_data.get('data')): result += t('no_activity', lang) return result.rstrip() @staticmethod def format_last_year_or_1000(data: Dict[str, Any], username: str, lang: str = 'en') -> str: """ Format response for last year or last 1000 games. Expects GamesOfPeriodResponse-like payload. """ if not data: return "📭 No data" games_count = data.get('games_count', 0) period_start = data.get('period_start') period_end = data.get('period_end') stats = (data.get('data') or {}) # Title and subheader if games_count >= 1000: header = f"📈 {username}: last 1000 rated games" # Use period_start as earliest known bound (server does not expose earliest game timestamp) if isinstance(period_start, int): earliest = datetime.fromtimestamp(period_start).strftime("%d.%m.%Y") header += f"\nНачало этих 1000 партий: {earliest}" else: header = f"📈 {username}: last year (rated), games: {games_count}" if isinstance(period_start, int) and isinstance(period_end, int): start_str = datetime.fromtimestamp(period_start).strftime("%d.%m.%Y") end_str = datetime.fromtimestamp(period_end).strftime("%d.%m.%Y") header += f"\nПериод: {start_str}–{end_str}" # Body per mode lines = [] for mode in ["bullet", "blitz", "rapid", "classical", "correspondence"]: mode_stats = stats.get(mode) if not mode_stats: continue games_played = mode_stats.get('games_played', 0) if games_played == 0: continue emoji = StatsFormatter._get_game_type_emoji(mode) wins = mode_stats.get('wins', 0) losses = mode_stats.get('losses', 0) draws = mode_stats.get('draws', 0) rating_change = mode_stats.get('rating_change', 0) rating_change_str = StatsFormatter._format_rating_change(rating_change) rating = mode_stats.get('rating') rating_str = rating if rating is not None else "—" lines.append( f"{emoji} {mode.title()}: {games_played} Δ {rating_change_str} R {rating_str} (+{wins} -{losses} ={draws})" ) # Total total = stats.get('total') or {} total_line = "" if total: tg = total.get('games_played', 0) tw = total.get('wins', 0) tl = total.get('losses', 0) td = total.get('draws', 0) trc = total.get('rating_change', 0) trc_str = StatsFormatter._format_rating_change(trc) tr = total.get('rating') tr_str = tr if tr is not None else "—" total_line = f"\nИтого: {tg} Δ {trc_str} R {tr_str} (+{tw} -{tl} ={td})" body = "\n".join(lines) + total_line return f"{header}\n{body}".rstrip()