From 7ea6b3eb9f985c25ad4749afc2bef33b9039fd37 Mon Sep 17 00:00:00 2001 From: Mattia Mascarello Date: Wed, 11 Feb 2026 00:13:43 +0100 Subject: [PATCH] First commit --- .gitignore | 1 + main.go | 417 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 418 insertions(+) create mode 100644 .gitignore create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..442f90d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +calendario_* \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..87bdcdd --- /dev/null +++ b/main.go @@ -0,0 +1,417 @@ +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 +}