aboutsummaryrefslogtreecommitdiff
path: root/internal/bible/convert.go
blob: e7d59445fb0e873bcd83ebcfd11211ee8b0a1a7d (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
package bible

import (
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"github.com/lukaszkasprzak/lectio/internal/psalter"
)

// polishToEn maps Polish Biblical abbreviations to the book name accepted by
// the kjv-family tools. Gospels use the short forms (Mat/Mark/Luke/John);
// other books use full names that the tools resolve by unique prefix.
// Ported verbatim from ewangelia.py POLISH_TO_EN.
var polishToEn = map[string]string{
	// Old Testament
	"Rdz": "Genesis", "Wj": "Exodus", "Kpł": "Leviticus", "Lb": "Numbers",
	"Pwt": "Deuteronomy", "Joz": "Joshua", "Sdz": "Judges", "Rt": "Ruth",
	"1 Sm": "1 Samuel", "2 Sm": "2 Samuel", "1 Krl": "1 Kings", "2 Krl": "2 Kings",
	"1 Krn": "1 Chronicles", "2 Krn": "2 Chronicles", "Ezd": "Ezra",
	"Ne": "Nehemiah", "Tb": "Tobit", "Jdt": "Judith", "Est": "Esther",
	"1 Mch": "1 Maccabees", "2 Mch": "2 Maccabees", "Hi": "Job", "Ps": "Psalms",
	"Prz": "Proverbs", "Koh": "Ecclesiastes", "Pnp": "Song of Solomon",
	"Mdr": "Wisdom", "Syr": "Sirach", "Iz": "Isaiah", "Jr": "Jeremiah",
	"Lm": "Lamentations", "Ba": "Baruch", "Ez": "Ezekiel", "Dn": "Daniel",
	"Oz": "Hosea", "Jl": "Joel", "Am": "Amos", "Ab": "Obadiah", "Jon": "Jonah",
	"Mi": "Micah", "Na": "Nahum", "Ha": "Habakkuk", "So": "Zephaniah",
	"Ag": "Haggai", "Za": "Zechariah", "Ml": "Malachi",
	// New Testament
	"Mt": "Mat", "Mk": "Mark", "Łk": "Luke", "J": "John", "Dz": "Acts",
	"Rz": "Romans", "1 Kor": "1 Corinthians", "2 Kor": "2 Corinthians",
	"Ga": "Galatians", "Ef": "Ephesians", "Flp": "Philippians",
	"Kol": "Colossians", "1 Tes": "1 Thessalonians", "2 Tes": "2 Thessalonians",
	"1 Tm": "1 Timothy", "2 Tm": "2 Timothy", "Tt": "Titus", "Flm": "Philemon",
	"Hbr": "Hebrews", "Jk": "James", "1 P": "1 Peter", "2 P": "2 Peter",
	"1 J": "1 John", "2 J": "2 John", "3 J": "3 John", "Jud": "Jude",
	"Ap": "Revelation",
}

var (
	porRe         = regexp.MustCompile(`(?i)^\s*por\.\s*`)
	refrainRe     = regexp.MustCompile(`\s*\(R\.:.*$`)
	wsRe          = regexp.MustCompile(`\s+`)
	iRe           = regexp.MustCompile(`\s+i\s+`)
	dashRe        = regexp.MustCompile(`\s*[-–—]\s*`)
	verseLetterRe = regexp.MustCompile(`(\d)[a-c]\b`)
	bookDotRe     = regexp.MustCompile(`(\p{L})\.`) // abbreviation dot after a letter ("Cor." -> "Cor")
	psalmRestRe   = regexp.MustCompile(`(\d+)(?:\s*\((\d+)\))?(.*)$`)
	digitsRe      = regexp.MustCompile(`\d+`)
)

// polishBookKeys holds the POLISH_TO_EN keys sorted longest-first, matching
// the Python `sorted(POLISH_TO_EN, key=len, reverse=True)` traversal order.
var polishBookKeys = sortedPolishBookKeys()

func sortedPolishBookKeys() []string {
	keys := make([]string, 0, len(polishToEn))
	for k := range polishToEn {
		keys = append(keys, k)
	}
	sort.SliceStable(keys, func(i, j int) bool { return len(keys[i]) > len(keys[j]) })
	return keys
}

// psalmRef maps the psalm citation body ("63 (62), 2. 3-4. ...") to the target Psalter.
func psalmRef(rest, system string) string {
	m := psalmRestRe.FindStringSubmatch(rest)
	if m == nil {
		return rest
	}
	heb, _ := strconv.Atoi(m[1])
	tail := m[3]
	if system == "vulgate" {
		ch := m[1]
		if m[2] != "" {
			ch = m[2]
		}
		return ch + tail
	}
	if system == "drb" {
		tail = digitsRe.ReplaceAllStringFunc(tail, func(s string) string {
			n, _ := strconv.Atoi(s)
			return strconv.Itoa(psalter.DrbVerse(heb, n))
		})
	}
	return strconv.Itoa(heb) + tail
}

// ToEnglishRef converts a Polish citation to kjv-tool style: "Mt 7, 1-5" -> "Mat 7:1-5".
//
// Handles the extra apparatus of non-gospel readings: a leading "por." (compare)
// marker, a trailing "(R.: ...)" responsorial refrain, and psalm versification.
// system selects the target Psalter: "vulgate" (vul/grb/wuj) uses the citation's
// Vulgate chapter (62 of "63 (62)"); "drb" uses the Hebrew chapter (63) with the
// DRB title-fold verse shift; other values use the Hebrew chapter unshifted.
func ToEnglishRef(plRef, system string) (string, error) {
	plRef = porRe.ReplaceAllString(plRef, "")     // 'compare' marker
	plRef = refrainRe.ReplaceAllString(plRef, "") // responsorial refrain
	plRef = wsRe.ReplaceAllString(strings.TrimSpace(plRef), " ")

	var book string
	found := false
	for _, k := range polishBookKeys {
		if plRef == k || strings.HasPrefix(plRef, k+" ") {
			book = k
			found = true
			break
		}
	}
	if !found {
		return "", fmt.Errorf("unknown book in reference: %q", plRef)
	}

	rest := strings.TrimSpace(plRef[len(book):])
	if polishToEn[book] == "Psalms" {
		rest = psalmRef(rest, system)
	}
	rest = strings.ReplaceAll(rest, ", ", ":")        // chapter/verse separator
	rest = strings.ReplaceAll(rest, ". ", ",")        // disjoint verse groups
	rest = iRe.ReplaceAllString(rest, ",")            // Polish 'and'
	rest = dashRe.ReplaceAllString(rest, "-")         // normalise ranges
	rest = verseLetterRe.ReplaceAllString(rest, "$1") // drop verse-part letters (15a -> 15)
	rest = strings.ReplaceAll(rest, " ", "")

	return strings.TrimSpace(fmt.Sprintf("%s %s", polishToEn[book], rest)), nil
}