aboutsummaryrefslogtreecommitdiff
path: root/internal/caldata/caldata.go
blob: 9272fbb48b426e436156821769a51463ce0e7c91 (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
// 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

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

// 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 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(dir string, use []string) ([]calendar.Layer, []error) {
	layers := []calendar.Layer{Universal()}
	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
}