diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 15:07:12 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 15:07:12 +0200 |
| commit | 310ac404f25b412c5c1ead12247b4eb36e9f0025 (patch) | |
| tree | 6e1dcc5e1f85a483cef7044d341b7c940c1690a8 /internal/bible/ofref.go | |
| parent | 44472bbe31475a3c672ae003d928d7caffb4b50a (diff) | |
| parent | c5abf87fa7198c8f245f407774e2326e4b6c9b9c (diff) | |
| download | lectio-310ac404f25b412c5c1ead12247b4eb36e9f0025.tar.gz lectio-310ac404f25b412c5c1ead12247b4eb36e9f0025.zip | |
Merge branch 'engine-precedence-fixes': OF/EF calendar+readings flawlessness pass
Per-day validation vs litcal (OF) and missalemeum (EF) 2005-2050, all defects
fixed and re-validated to zero: OF calendar 0 real errors forward, OF readings
0 unresolvable, EF 0 reading gaps + resumed Sundays correct.
Diffstat (limited to 'internal/bible/ofref.go')
| -rw-r--r-- | internal/bible/ofref.go | 88 |
1 files changed, 87 insertions, 1 deletions
diff --git a/internal/bible/ofref.go b/internal/bible/ofref.go index 8a72342..ffb2376 100644 --- a/internal/bible/ofref.go +++ b/internal/bible/ofref.go @@ -13,22 +13,57 @@ var ( // verse tail: "2 Samuel 6:12b-15,17-19" -> ("2 Samuel", "6", "12b-15,17-19"). // The book is non-greedy so a leading ordinal ("2 Samuel") stays with it. ofCiteRe = regexp.MustCompile(`^(.*?)\s+(\d+):(.+)$`) + // ofBookVerseRe matches a citation with no chapter ("2 John 4-9", + // "Jude 17,20b-25") -> book + verse tail; used to supply chapter 1 for + // single-chapter books. The tail allows sub-verse letters, dropped later. + ofBookVerseRe = regexp.MustCompile(`^(.*?)\s+(\d[\d,\-a-z]*)$`) // ofVerseLetterRe drops sub-verse part letters so lookups resolve at whole // verses: "12b" -> "12", "3ab" -> "3", "3cd" -> "3". ofVerseLetterRe = regexp.MustCompile(`(\d)[a-z]+`) + // ofRangeRe matches a verse range so an abbreviated end can be expanded: + // "127-28" -> "127-128", "129-30" -> "129-130" (the end borrows the start's + // leading digits, the standard citation shorthand). + ofRangeRe = regexp.MustCompile(`(\d+)-(\d+)`) ) +// singleChapterBook lists the one-chapter books the lectionary cites without a +// chapter ("2 John 4-9"); OFRef supplies "1:" so bible.Lookup resolves them. +var singleChapterBook = map[string]bool{ + "Obadiah": true, "Philemon": true, "2 John": true, "3 John": true, "Jude": true, +} + // normalizeOFVerses rewrites a citation's verse tail into the shape // bible.Lookup parses: cross-chapter semicolons become commas, ranges use a // plain hyphen, sub-verse letters are dropped, and spaces are removed. func normalizeOFVerses(tail string) string { tail = strings.ReplaceAll(tail, ";", ",") + tail = strings.ReplaceAll(tail, "+", ",") // responsorial "6+8" join = verses 6 and 8 tail = dashRe.ReplaceAllString(tail, "-") tail = ofVerseLetterRe.ReplaceAllString(tail, "$1") tail = strings.ReplaceAll(tail, " ", "") + tail = expandAbbrevRanges(tail) return tail } +// expandAbbrevRanges expands a range whose end omits the start's leading digits +// ("127-28" -> "127-128"): when the numeric end is below the start and shorter, +// the end borrows the start's high-order digits. +func expandAbbrevRanges(tail string) string { + return ofRangeRe.ReplaceAllStringFunc(tail, func(s string) string { + m := ofRangeRe.FindStringSubmatch(s) + a, b := m[1], m[2] + ai, _ := strconv.Atoi(a) + bi, _ := strconv.Atoi(b) + if bi < ai && len(b) < len(a) { + full := a[:len(a)-len(b)] + b + if fi, err := strconv.Atoi(full); err == nil && fi > ai { + return a + "-" + full + } + } + return s + }) +} + // OFRef turns an English-canonical Ordinary Form citation (lectio's authored // form) into a bible.Lookup-ready reference for the target corpus's Psalter. // @@ -47,6 +82,12 @@ func normalizeOFVerses(tail string) string { // A citation OFRef cannot parse (no "chapter:verse") is returned unchanged for // bible.Lookup to accept or reject. func OFRef(citation, system string) string { + // A single-chapter book cited without a chapter ("2 John 4-9") gets "1:". + if !strings.Contains(citation, ":") { + if bv := ofBookVerseRe.FindStringSubmatch(citation); bv != nil && singleChapterBook[bv[1]] { + citation = bv[1] + " 1:" + bv[2] + } + } m := ofCiteRe.FindStringSubmatch(citation) if m == nil { return citation @@ -58,7 +99,7 @@ func OFRef(citation, system string) string { if err == nil { switch system { case "vulgate": - chap = strconv.Itoa(psalter.HebrewToVulgateChapter(heb)) + return "Psalms " + psalmVulgate(heb, verses) case "drb": verses = digitsRe.ReplaceAllStringFunc(verses, func(s string) string { n, _ := strconv.Atoi(s) @@ -69,3 +110,48 @@ func OFRef(citation, system string) string { } return book + " " + chap + ":" + verses } + +// psalmVulgate renders a Masoretic psalm's verse tail ("12-13,14-15,19-20") in +// Vulgate numbering, converting every verse (see psalter.HebrewToVulgate). The +// split psalms (116, 147) change chapter mid-psalm, so a group emits its own +// "chapter:" prefix whenever it lands in a different Vulgate chapter than the +// group before it -- a form bible.Lookup resolves as separate chapter groups. +func psalmVulgate(heb int, verses string) string { + var b strings.Builder + cur := -1 + for i, group := range strings.Split(verses, ",") { + if group == "" { + continue + } + if i > 0 && b.Len() > 0 { + b.WriteByte(',') + } + lo, hi := group, "" + if k := strings.IndexByte(group, '-'); k >= 0 { + lo, hi = group[:k], group[k+1:] + } + n, err := strconv.Atoi(lo) + if err != nil { + b.WriteString(group) // non-numeric group: pass through + continue + } + c, v := psalter.HebrewToVulgate(heb, n) + if c != cur { + b.WriteString(strconv.Itoa(c)) + b.WriteByte(':') + cur = c + } + b.WriteString(strconv.Itoa(v)) + if hi != "" { + if n2, err := strconv.Atoi(hi); err == nil { + _, v2 := psalter.HebrewToVulgate(heb, n2) + b.WriteByte('-') + b.WriteString(strconv.Itoa(v2)) + } else { + b.WriteByte('-') + b.WriteString(hi) + } + } + } + return b.String() +} |
