Skip to content

Instantly share code, notes, and snippets.

@c4software
Last active November 16, 2024 22:21
Show Gist options
  • Save c4software/a02f9eb0fb9875a17fd70797c75b7a36 to your computer and use it in GitHub Desktop.
Save c4software/a02f9eb0fb9875a17fd70797c75b7a36 to your computer and use it in GitHub Desktop.
ICS Sum calculator

ICS Calendar Time Analyzer

Un script Python pour analyser les fichiers de calendrier ICS et calculer le temps passé sur des événements spécifiques, avec support des événements récurrents.

🌟 Fonctionnalités

  • Analyse des fichiers ICS depuis une URL
  • Calcul du temps passé par mois et par année
  • Conversion automatique des heures en jours (base : 8h/jour)
  • Support des événements récurrents :
    • Quotidiens (FREQ=DAILY)
    • Hebdomadaires (FREQ=WEEKLY)
    • Mensuels (FREQ=MONTHLY)
    • Annuels (FREQ=YEARLY)
  • Filtrage des événements par mot-clé
  • Affichage en français
  • Gestion des fuseaux horaires

📋 Prérequis

  • Python 3.6 ou supérieur
  • Pas de dépendances externes requises (utilise uniquement la bibliothèque standard Python)

💻 Utilisation

Syntaxe de base

python ics_analyzer.py <url_du_calendrier_ics> [filtre_evenement]

Paramètres

  • url_du_calendrier_ics : URL du fichier ICS à analyser
  • filtre_evenement (optionnel) : Mot-clé pour filtrer les événements

Exemples

  1. Analyser tous les événements d'un calendrier :
python ics_analyzer.py https://example.com/calendar.ics
  1. Analyser uniquement les événements contenant "Réunion" :
python ics_analyzer.py https://example.com/calendar.ics Réunion

📊 Format de sortie

Temps passé par année et par mois :
======================================================================

Année 2024
----------------------------------------------------------------------
Janvier    :   12.5 heures        (1.6 jours)
Février    :   15.0 heures        (1.9 jours)
Mars       :   10.5 heures        (1.3 jours)
----------------------------------------------------------------------
Total Année :   38.0 heures        (4.8 jours)

🔧 Fonctionnement technique

Gestion des événements récurrents

Le script prend en charge les règles de récurrence (RRULE) suivantes :

  • Paramètre FREQ (DAILY, WEEKLY, MONTHLY, YEARLY)
  • Paramètre COUNT (nombre maximum d'occurrences)
  • Paramètre UNTIL (date de fin de récurrence)
  • Paramètre INTERVAL (intervalle entre les occurrences)

Calcul du temps

  • Le temps est d'abord calculé en heures
  • La conversion en jours utilise une base de 8 heures par jour
  • Les événements sur plusieurs jours sont correctement comptabilisés

⚠️ Limitations connues

  1. Les règles de récurrence complexes (ex: BYDAY avec plusieurs valeurs) ne sont pas entièrement supportées
  2. Les exceptions aux événements récurrents (EXDATE) ne sont pas prises en compte
  3. Les fuseaux horaires complexes peuvent causer des imprécisions mineures
from urllib.request import urlopen
from datetime import datetime, timedelta, date
import sys
from collections import defaultdict
import re
def parse_datetime(dt_str):
"""Parse datetime string from ICS format"""
dt_str = dt_str.split('Z')[0].split('T')
if len(dt_str) == 1: # Date only
return datetime.strptime(dt_str[0], '%Y%m%d')
else: # DateTime
return datetime.strptime(dt_str[0] + dt_str[1], '%Y%m%d%H%M%S')
def parse_rrule(rrule_str, start_date, end_date):
"""
Parse recurring rule and return list of dates
Basic implementation for common recurrence patterns
"""
dates = []
if not rrule_str:
return dates
rrule_parts = dict(part.split('=') for part in rrule_str.split(';') if '=' in part)
freq = rrule_parts.get('FREQ', '')
count = int(rrule_parts.get('COUNT', '0'))
until_str = rrule_parts.get('UNTIL', '')
interval = int(rrule_parts.get('INTERVAL', '1'))
if until_str:
try:
until = parse_datetime(until_str)
except ValueError:
until = None
else:
until = None
current = start_date
occurrences = 0
while True:
# Stop conditions
if count and occurrences >= count:
break
if until and current > until:
break
if current > end_date:
break
dates.append(current)
occurrences += 1
# Calculate next occurrence based on frequency
if freq == 'DAILY':
current = current + timedelta(days=interval)
elif freq == 'WEEKLY':
current = current + timedelta(weeks=interval)
elif freq == 'MONTHLY':
# Simple month addition, might need refinement for edge cases
year = current.year + ((current.month - 1 + interval) // 12)
month = ((current.month - 1 + interval) % 12) + 1
try:
current = current.replace(year=year, month=month)
except ValueError:
# Handle edge case for months with different number of days
current = (current.replace(day=1) + timedelta(days=32)).replace(day=1)
elif freq == 'YEARLY':
try:
current = current.replace(year=current.year + interval)
except ValueError:
# Handle February 29 edge case
current = current.replace(year=current.year + interval, day=28)
else:
break
return dates
def get_event_duration(start, end):
"""Calculate duration between two datetime objects in hours"""
return (end - start).total_seconds() / 3600
def parse_ics_from_url(url, event_summary_filter=None):
"""Parse ICS file from URL and calculate duration of events"""
yearly_monthly_hours = defaultdict(lambda: defaultdict(float))
try:
with urlopen(url) as response:
content = response.read().decode('utf-8').splitlines()
in_event = False
event_data = {}
all_events = []
for line in content:
line = line.strip()
if line == 'BEGIN:VEVENT':
in_event = True
event_data = {}
elif line == 'END:VEVENT':
if in_event and 'DTSTART' in event_data and 'DTEND' in event_data:
all_events.append(event_data.copy())
in_event = False
elif in_event and ':' in line:
key, value = line.split(':', 1)
# Handle properties with parameters
if ';' in key:
key = key.split(';')[0]
event_data[key] = value
# Find date range for all events
all_dates = []
for event in all_events:
try:
all_dates.append(parse_datetime(event['DTSTART']))
all_dates.append(parse_datetime(event['DTEND']))
except (KeyError, ValueError):
continue
if not all_dates:
return yearly_monthly_hours
min_date = min(all_dates)
max_date = max(all_dates)
# Process each event
for event in all_events:
try:
summary = event.get('SUMMARY', '').lower()
if event_summary_filter and event_summary_filter.lower() not in summary:
continue
start = parse_datetime(event['DTSTART'])
end = parse_datetime(event['DTEND'])
duration = get_event_duration(start, end)
if(duration > 8):
# print("La durée excède 8h, on coupe")
duration = 8
# Handle recurring events
if 'RRULE' in event:
occurrences = parse_rrule(event['RRULE'], start, max_date)
for occurrence_start in occurrences:
# Calculate corresponding end time
occurrence_end = occurrence_start + (end - start)
year = occurrence_start.year
month = occurrence_start.month
yearly_monthly_hours[year][month] += duration
else:
# Single event
year = start.year
month = start.month
yearly_monthly_hours[year][month] += duration
except (KeyError, ValueError) as e:
print(f"Erreur lors du traitement d'un événement : {e}")
continue
except Exception as e:
print(f"Erreur lors de la récupération ou l'analyse du fichier ICS : {e}")
sys.exit(1)
return yearly_monthly_hours
def hours_to_days_str(hours):
"""Convert hours to days and return formatted string"""
days = hours / 8
if days == 1:
return f"(1 jour)"
elif days.is_integer():
return f"({int(days)} jours)"
return f"({days:.1f} jours)"
def display_yearly_monthly_hours(yearly_monthly_hours):
"""Display hours grouped by year and month in French"""
if not yearly_monthly_hours:
print("Aucun événement trouvé")
return
month_names = {
1: "Janvier", 2: "Février", 3: "Mars", 4: "Avril",
5: "Mai", 6: "Juin", 7: "Juillet", 8: "Août",
9: "Septembre", 10: "Octobre", 11: "Novembre", 12: "Décembre"
}
print("\nTemps passé par année et par mois :")
print("=" * 70)
for year in sorted(yearly_monthly_hours.keys()):
year_total = 0
print(f"\nAnnée {year}")
print("-" * 70)
for month in sorted(yearly_monthly_hours[year].keys()):
hours = yearly_monthly_hours[year][month]
year_total += hours
days_str = hours_to_days_str(hours)
print(f"{month_names[month]:10} : {hours:6.1f} heures {days_str:>15}")
print("-" * 70)
year_days_str = hours_to_days_str(year_total)
print(f"Total Année : {year_total:6.1f} heures {year_days_str:>15}")
def main():
if len(sys.argv) < 2:
print("Usage: python script.py <url_ics> [filtre_evenement]")
sys.exit(1)
ics_url = sys.argv[1]
event_filter = sys.argv[2] if len(sys.argv) > 2 else None
if event_filter:
print(f"Filtrage des événements contenant : {event_filter}")
yearly_monthly_hours = parse_ics_from_url(ics_url, event_filter)
display_yearly_monthly_hours(yearly_monthly_hours)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment