Files
cathcal/main.go
2026-02-11 00:13:43 +01:00

418 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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] <anno> | <anno_inizio-anno_fine>")
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
}