package main import ( "fmt" "os" "sort" "strconv" "strings" "time" ) // Event represents a liturgical event with its date and title type Event struct { Date time.Time Title string } // Config holds the program configuration type Config struct { Years []int Merge bool } func main() { if len(os.Args) == 1 { printHelp() return } config, err := parseConfig(os.Args[1:]) if err != nil { fmt.Printf("Errore: %v\n", err) printHelp() return } if err := generateCalendars(config); err != nil { fmt.Printf("Errore: %v\n", err) os.Exit(1) } } func printHelp() { fmt.Println("Uso: go run main.go [--merge] | ") fmt.Println("\nEsempi:") fmt.Println(" go run main.go 2026") fmt.Println(" go run main.go 2024-2027") fmt.Println(" go run main.go --merge 2024-2027") } // parseConfig parses command-line arguments into a Config func parseConfig(args []string) (*Config, error) { config := &Config{} var yearArg string for _, arg := range args { switch { case arg == "--merge": config.Merge = true case strings.HasPrefix(arg, "-"): return nil, fmt.Errorf("opzione non valida: %s", arg) case yearArg != "": return nil, fmt.Errorf("troppi argomenti") default: yearArg = arg } } if yearArg == "" { return nil, fmt.Errorf("argomento mancante") } years, err := parseYears(yearArg) if err != nil { return nil, err } config.Years = years return config, nil } // parseYears parses a year argument (single year or range) func parseYears(arg string) ([]int, error) { if !strings.Contains(arg, "-") { year, err := strconv.Atoi(strings.TrimSpace(arg)) if err != nil || year <= 0 { return nil, fmt.Errorf("anno non valido: %s", arg) } return []int{year}, nil } parts := strings.Split(arg, "-") if len(parts) != 2 { return nil, fmt.Errorf("range non valido: %s", arg) } start, err := strconv.Atoi(strings.TrimSpace(parts[0])) if err != nil || start <= 0 { return nil, fmt.Errorf("anno iniziale non valido: %s", parts[0]) } end, err := strconv.Atoi(strings.TrimSpace(parts[1])) if err != nil || end <= 0 { return nil, fmt.Errorf("anno finale non valido: %s", parts[1]) } if end < start { return nil, fmt.Errorf("l'anno finale deve essere >= anno iniziale") } years := make([]int, 0, end-start+1) for y := start; y <= end; y++ { years = append(years, y) } return years, nil } // generateCalendars generates and writes calendar files based on config func generateCalendars(config *Config) error { loc := getLocation() if config.Merge { return generateMergedCalendar(config.Years, loc) } for _, year := range config.Years { if err := generateSingleCalendar(year, loc); err != nil { return err } } return nil } // generateMergedCalendar creates a single merged calendar file func generateMergedCalendar(years []int, loc *time.Location) error { var allEvents []Event for _, year := range years { allEvents = append(allEvents, generateYearEvents(year, loc)...) } sort.Slice(allEvents, func(i, j int) bool { return allEvents[i].Date.Before(allEvents[j].Date) }) filename := buildFilename(years) if err := writeICS(filename, allEvents); err != nil { return err } fmt.Printf("Creato: %s\n", filename) return nil } // generateSingleCalendar creates a calendar file for a single year func generateSingleCalendar(year int, loc *time.Location) error { events := generateYearEvents(year, loc) filename := fmt.Sprintf("calendario_liturgico_%d.ics", year) if err := writeICS(filename, events); err != nil { return err } fmt.Printf("Creato: %s\n", filename) return nil } // buildFilename creates a filename based on the year range func buildFilename(years []int) string { if len(years) == 0 { return "calendario_liturgico.ics" } if len(years) == 1 { return fmt.Sprintf("calendario_liturgico_%d.ics", years[0]) } return fmt.Sprintf("calendario_liturgico_%d-%d.ics", years[0], years[len(years)-1]) } // generateYearEvents generates all liturgical events for a given year func generateYearEvents(year int, loc *time.Location) []Event { cal := newLiturgicalCalendar(year, loc) events := []Event{} // Add fixed solemnities events = append(events, cal.fixedSolemnities()...) // Add movable solemnities events = append(events, cal.movableSolemnities()...) // Add liturgical seasons events = append(events, cal.liturgicalSeasons()...) // Add all Sundays events = append(events, cal.allSundays()...) return events } // LiturgicalCalendar holds key dates for a liturgical year type LiturgicalCalendar struct { Year int Location *time.Location Easter time.Time Ash time.Time Advent time.Time Christmas time.Time Epiphany time.Time Baptism time.Time } // newLiturgicalCalendar creates a new calendar with calculated dates func newLiturgicalCalendar(year int, loc *time.Location) *LiturgicalCalendar { easter := calculateEaster(year, loc) epiphany := time.Date(year, 1, 6, 0, 0, 0, 0, loc) return &LiturgicalCalendar{ Year: year, Location: loc, Easter: easter, Ash: easter.AddDate(0, 0, -46), Advent: calculateFirstAdvent(year, loc), Christmas: time.Date(year, 12, 25, 0, 0, 0, 0, loc), Epiphany: epiphany, Baptism: nextSunday(epiphany), } } // fixedSolemnities returns all fixed-date solemnities func (c *LiturgicalCalendar) fixedSolemnities() []Event { return []Event{ {time.Date(c.Year, 1, 1, 0, 0, 0, 0, c.Location), "Maria SS. Madre di Dio"}, {c.Epiphany, "Epifania del Signore"}, {time.Date(c.Year, 3, 19, 0, 0, 0, 0, c.Location), "San Giuseppe"}, {time.Date(c.Year, 6, 29, 0, 0, 0, 0, c.Location), "Santi Pietro e Paolo"}, {time.Date(c.Year, 8, 15, 0, 0, 0, 0, c.Location), "Assunzione di Maria"}, {time.Date(c.Year, 11, 1, 0, 0, 0, 0, c.Location), "Ognissanti"}, {time.Date(c.Year, 12, 8, 0, 0, 0, 0, c.Location), "Immacolata Concezione"}, {c.Christmas, "Natale del Signore"}, } } // movableSolemnities returns all date-variable solemnities func (c *LiturgicalCalendar) movableSolemnities() []Event { return []Event{ {c.Easter, "Pasqua di Risurrezione"}, {c.Easter.AddDate(0, 0, 49), "Pentecoste"}, {c.Easter.AddDate(0, 0, 40), "Ascensione del Signore"}, {c.Easter.AddDate(0, 0, 63), "Corpus Domini"}, {c.Easter.AddDate(0, 0, -7), "Domenica delle Palme"}, {c.Ash, "Mercoledì delle Ceneri"}, } } // liturgicalSeasons returns liturgical season markers func (c *LiturgicalCalendar) liturgicalSeasons() []Event { return []Event{ {c.Advent, "I Domenica di Avvento – Inizio Avvento"}, {c.Ash, "Inizio Quaresima"}, {c.Easter, "Inizio Tempo di Pasqua"}, {c.Baptism, "Battesimo del Signore – Fine Tempo di Natale"}, } } // allSundays generates all numbered Sundays for the year func (c *LiturgicalCalendar) allSundays() []Event { events := []Event{} used := c.buildUsedDatesMap() // Ordinary Time (first part) ordinaryCount := c.addOrdinaryTimePart1(&events, used) // Lent c.addLent(&events, used) // Easter c.addEasterSeason(&events, used) // Ordinary Time (second part) c.addOrdinaryTimePart2(&events, used, ordinaryCount) // Advent c.addAdvent(&events, used) return events } // buildUsedDatesMap creates a map of already-used dates func (c *LiturgicalCalendar) buildUsedDatesMap() map[string]bool { used := make(map[string]bool) for _, events := range [][]Event{ c.fixedSolemnities(), c.movableSolemnities(), c.liturgicalSeasons(), } { for _, e := range events { used[e.Date.Format("20060102")] = true } } return used } // addOrdinaryTimePart1 adds first part of Ordinary Time (returns week count) func (c *LiturgicalCalendar) addOrdinaryTimePart1(events *[]Event, used map[string]bool) int { start := nextSunday(c.Baptism.AddDate(0, 0, 1)) return c.addSundayRange(events, used, start, c.Ash, "%dª Domenica del Tempo Ordinario", 1) } // addLent adds Lenten Sundays func (c *LiturgicalCalendar) addLent(events *[]Event, used map[string]bool) { start := c.Ash.AddDate(0, 0, 4) // First Sunday of Lent c.addSundayRange(events, used, start, c.Easter, "%dª Domenica di Quaresima", 1) } // addEasterSeason adds Easter season Sundays func (c *LiturgicalCalendar) addEasterSeason(events *[]Event, used map[string]bool) { start := c.Easter.AddDate(0, 0, 7) // Second Sunday of Easter end := c.Easter.AddDate(0, 0, 49) // Pentecost c.addSundayRange(events, used, start, end, "%dª Domenica di Pasqua", 2) } // addOrdinaryTimePart2 adds second part of Ordinary Time func (c *LiturgicalCalendar) addOrdinaryTimePart2(events *[]Event, used map[string]bool, startCount int) { start := c.Easter.AddDate(0, 0, 56) // Week after Pentecost c.addSundayRange(events, used, start, c.Advent, "%dª Domenica del Tempo Ordinario", startCount) } // addAdvent adds Advent Sundays func (c *LiturgicalCalendar) addAdvent(events *[]Event, used map[string]bool) { c.addSundayRange(events, used, c.Advent, c.Christmas, "%dª Domenica di Avvento", 1) } // addSundayRange adds a range of Sundays with a numbering pattern func (c *LiturgicalCalendar) addSundayRange(events *[]Event, used map[string]bool, start, end time.Time, format string, startNum int) int { count := startNum for d := start; d.Before(end); d = d.AddDate(0, 0, 7) { key := d.Format("20060102") if !used[key] { *events = append(*events, Event{d, fmt.Sprintf(format, count)}) used[key] = true } count++ } return count } // writeICS writes events to an ICS file func writeICS(filename string, events []Event) error { f, err := os.Create(filename) if err != nil { return fmt.Errorf("impossibile creare file: %w", err) } defer f.Close() fmt.Fprintln(f, "BEGIN:VCALENDAR") fmt.Fprintln(f, "VERSION:2.0") fmt.Fprintln(f, "PRODID:-//Calendario Liturgico Rito Romano//IT") for _, e := range events { fmt.Fprintln(f, "BEGIN:VEVENT") fmt.Fprintf(f, "DTSTART;VALUE=DATE:%s\n", e.Date.Format("20060102")) fmt.Fprintf(f, "DTEND;VALUE=DATE:%s\n", e.Date.AddDate(0, 0, 1).Format("20060102")) fmt.Fprintf(f, "SUMMARY:%s\n", e.Title) fmt.Fprintln(f, "END:VEVENT") } fmt.Fprintln(f, "END:VCALENDAR") return nil } // calculateEaster computes Easter date using Meeus/Jones/Butcher algorithm func calculateEaster(year int, loc *time.Location) time.Time { a := year % 19 b := year / 100 c := year % 100 d := b / 4 e := b % 4 f := (b + 8) / 25 g := (b - f + 1) / 3 h := (19*a + b - d - g + 15) % 30 i := c / 4 k := c % 4 l := (32 + 2*e + 2*i - h - k) % 7 m := (a + 11*h + 22*l) / 451 month := (h + l - 7*m + 114) / 31 day := ((h + l - 7*m + 114) % 31) + 1 return time.Date(year, time.Month(month), day, 0, 0, 0, 0, loc) } // calculateFirstAdvent finds the first Sunday of Advent func calculateFirstAdvent(year int, loc *time.Location) time.Time { christmas := time.Date(year, 12, 25, 0, 0, 0, 0, loc) // Find Sunday on or before Christmas sundayBeforeChristmas := christmas for sundayBeforeChristmas.Weekday() != time.Sunday { sundayBeforeChristmas = sundayBeforeChristmas.AddDate(0, 0, -1) } // Go back 4 weeks return sundayBeforeChristmas.AddDate(0, 0, -28) } // nextSunday returns the next Sunday after the given date func nextSunday(d time.Time) time.Time { d = d.AddDate(0, 0, 1) for d.Weekday() != time.Sunday { d = d.AddDate(0, 0, 1) } return d } // getLocation returns Europe/Rome timezone or local time func getLocation() *time.Location { loc, err := time.LoadLocation("Europe/Rome") if err != nil { return time.Local } return loc }