First commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
calendario_*
|
||||||
417
main.go
Normal file
417
main.go
Normal file
@@ -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] <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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user