First commit
This commit is contained in:
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