From 343a675a5a779cfce5a3dec9a3ad4a6ffb22de75 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 23 Jul 2026 12:55:24 +0200 Subject: bible: Polish citation -> English ref (with psalm versification) --- internal/bible/convert.go | 127 +++++++++++++++++++++++++++++++++++++++++ internal/bible/convert_test.go | 19 ++++++ 2 files changed, 146 insertions(+) create mode 100644 internal/bible/convert.go create mode 100644 internal/bible/convert_test.go (limited to 'internal') diff --git a/internal/bible/convert.go b/internal/bible/convert.go new file mode 100644 index 0000000..6181dbf --- /dev/null +++ b/internal/bible/convert.go @@ -0,0 +1,127 @@ +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`) + 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 +} diff --git a/internal/bible/convert_test.go b/internal/bible/convert_test.go new file mode 100644 index 0000000..6f16c89 --- /dev/null +++ b/internal/bible/convert_test.go @@ -0,0 +1,19 @@ +package bible + +import "testing" + +func TestToEnglishRef(t *testing.T) { + cases := []struct{ in, system, want string }{ + {"Mt 7, 1-5", "vulgate", "Mat 7:1-5"}, + {"J 20, 1. 11-18", "vulgate", "John 20:1,11-18"}, + {"por. J 20, 11", "vulgate", "John 20:11"}, + {"Ps 63 (62), 2. 3-4. 5-6. 8-9 (R.: por. 2ab)", "vulgate", "Psalms 62:2,3-4,5-6,8-9"}, + {"Ps 63 (62), 2. 3-4. 5-6. 8-9 (R.: por. 2ab)", "drb", "Psalms 63:1,2-3,4-5,7-8"}, + } + for _, c := range cases { + got, err := ToEnglishRef(c.in, c.system) + if err != nil || got != c.want { + t.Errorf("ToEnglishRef(%q,%q)=%q,%v want %q", c.in, c.system, got, err, c.want) + } + } +} -- cgit v1.3