aboutsummaryrefslogtreecommitdiff
path: root/internal/caldata/caldata.go
blob: 14b3985b22f006589954da498cde8a056184a28e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// Package caldata embeds and parses lectio's owned universal General Roman
// Calendar data into a calendar.Layer. It imports only internal/ini and
// internal/calendar (types).
package caldata

import (
	_ "embed"
	"fmt"
	"os"
	"path/filepath"

	"github.com/lukaszkasprzak/lectio/internal/calendar"
	"github.com/lukaszkasprzak/lectio/internal/ini"
)

//go:embed roman-calendar.ini
var romanCalendar []byte

//go:embed tridentine-calendar.ini
var tridentineCalendar []byte

// Universal parses the embedded Ordinary Form universal calendar. It panics on a
// malformed embedded file (a build-time bug caught by tests).
func Universal() calendar.Layer {
	l, err := ParseLayer(romanCalendar)
	if err != nil {
		panic(fmt.Sprintf("caldata: embedded roman-calendar.ini invalid: %v", err))
	}
	return l
}

// Tridentine parses the embedded Extraordinary Form (1962) universal calendar.
func Tridentine() calendar.Layer {
	l, err := ParseLayer(tridentineCalendar)
	if err != nil {
		panic(fmt.Sprintf("caldata: embedded tridentine-calendar.ini invalid: %v", err))
	}
	return l
}

// Base returns the universal base layer for a form: the 1962 calendar for
// "old" (EF), the General Roman Calendar otherwise (OF).
func Base(form string) calendar.Layer {
	if form == "old" {
		return Tridentine()
	}
	return Universal()
}

// LoadLayer reads and parses a user calendar layer file. id (the filename stem)
// is used as the layer's ID when the file's [layer] header omits one.
func LoadLayer(path, id string) (calendar.Layer, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return calendar.Layer{}, err
	}
	l, err := ParseLayer(data)
	if err != nil {
		return calendar.Layer{}, fmt.Errorf("%s: %w", path, err)
	}
	if l.ID == "" {
		l.ID = id
	}
	return l, nil
}

// Stack returns the ordered layer stack for the engine: the form's universal
// base first, then each user layer named in use (matched to "<dir>/<id>.ini"),
// in order. A layer that fails to load is skipped and reported in the error
// slice so a single bad file never breaks calendar computation.
func Stack(form, dir string, use []string) ([]calendar.Layer, []error) {
	layers := []calendar.Layer{Base(form)}
	var errs []error
	for _, id := range use {
		l, err := LoadLayer(filepath.Join(dir, id+".ini"), id)
		if err != nil {
			errs = append(errs, err)
			continue
		}
		layers = append(layers, l)
	}
	return layers, errs
}

// ParseLayer parses INI bytes (a [layer] header + [slug]/[slug/variant]
// celebration sections) into a calendar.Layer.
func ParseLayer(data []byte) (calendar.Layer, error) {
	secs, err := ini.Parse(data)
	if err != nil {
		return calendar.Layer{}, err
	}
	layer := calendar.Layer{Cels: map[string]calendar.RawCelebration{}}
	for _, s := range secs {
		fields := map[string]string{}
		for _, p := range s.Pairs {
			fields[p.Key] = p.Val
		}
		switch {
		case s.Name == "" && len(fields) == 0:
			continue
		case s.Name == "layer":
			layer.ID, layer.Name, layer.Type = fields["id"], fields["name"], fields["type"]
		default:
			base, variant, isVariant := cutVariant(s.Name)
			rc := layer.Cels[base]
			if rc.Fields == nil {
				rc = calendar.RawCelebration{Fields: map[string]string{}, Variants: map[string]map[string]string{}}
			}
			if isVariant {
				rc.Variants[variant] = fields
			} else {
				for k, v := range fields {
					rc.Fields[k] = v
				}
			}
			layer.Cels[base] = rc
		}
	}
	return layer, nil
}

func cutVariant(name string) (base, variant string, ok bool) {
	for i := 0; i < len(name); i++ {
		if name[i] == '/' {
			return name[:i], name[i+1:], true
		}
	}
	return name, "", false
}