418 lines
11 KiB
Go
418 lines
11 KiB
Go
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
|
||
}
|