Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bible/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def lookup_reference(self, reference, language='en', translation=None):
m = range_re.match(verse_range)
last_chapter, last_verse = m.group(3), m.group(4)
if books.is_chapterless(book):
first_chapter, first_verse = 1, m.group(1)
# "Jude 11-25" and "Jude 1:11-25" both mean verses 11-25.
first_chapter, first_verse = 1, m.group(2) or m.group(1)
last_chapter = None
elif m.group(2):
first_chapter, first_verse = m.group(1), m.group(2)
elif previous_chapter:
Expand Down
2 changes: 2 additions & 0 deletions bible/tests/tests_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def test_reference(self):
("Mark 15.22, 25, 33-41", 11),
# single chapter book
("Jude 1-10", 10),
("Jude 1.11-25", 15),
("3 John 1:5-14", 10),
("1 John 2.7-17", 11),
("Gen 17.1-2, 4, 5-7, 8, 9-10, 11-12, 14", 12),
# discontinuous chapters
Expand Down
3,095 changes: 3,095 additions & 0 deletions calendarium/tests/data/antiochian-readings.tsv

Large diffs are not rendered by default.

204 changes: 204 additions & 0 deletions calendarium/tests/test_antiochian_readings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import itertools
from datetime import date
from pathlib import Path

from django.test import TestCase

from bible.models import Verse
from ingest_antiochian import parse_reading_citation
from .. import liturgics
from ..datetools import Tradition

GOLDEN = Path(__file__).resolve().parent / 'data/antiochian-readings.tsv'


async def characterize(entries):
"""Classify each antiochian.org (date, title) against the Greek readings.

One line per entry: date, title, status, and for a boundary mismatch the
overlapping orthocal readings. Status is:

exact a Greek reading that day covers the identical verses
boundary a Greek reading overlaps but covers different verses
different no Greek reading overlaps (a genuinely different reading)
unresolved the title does not parse or resolve to any verses

Verse sets are compared rather than reference strings, so notation such
as "5:22-26; 6:1-2" versus "5.22-6.2" cannot register as a difference.
tools/greek/antiochian_characterize.py regenerates the golden file.
"""

verses = {}

async def verse_set(reference):
if reference not in verses:
verses[reference] = frozenset({
(v.book, v.chapter, v.verse)
async for v in Verse.objects.lookup_reference(reference)
})
return verses[reference]

lines = []
for day_iso, group in itertools.groupby(entries, key=lambda e: e[0]):
day_date = date.fromisoformat(day_iso)
day = liturgics.Day(day_date.year, day_date.month, day_date.day,
tradition=Tradition.Greek)
await day.ainitialize()
displays = [r.pericope.sdisplay for r in await day.aget_readings()]
served = [(d, await verse_set(d)) for d in displays]

for _, title in group:
overlapping = '-'
try:
wanted = await verse_set(parse_reading_citation(title))
except ValueError:
wanted = frozenset()
if not wanted:
status = 'unresolved'
elif any(found == wanted for _, found in served):
status = 'exact'
elif overlaps := [d for d, found in served if found & wanted]:
status = 'boundary'
overlapping = ' | '.join(overlaps)
else:
status = 'different'
lines.append(f'{day_iso}\t{title}\t{status}\t{overlapping}')
return lines


class TestAntiochianReadingCitations(TestCase):
def test_title_case_and_jude_preamble(self):
self.assertEqual(
parse_reading_citation("ST. JUDE'S FIRST UNIVERSAL LETTER 1:1-25"),
'Jude 1:1-25',
)

def test_colon_before_new_chapter(self):
self.assertEqual(
parse_reading_citation('MARK 13:31-37: 14:1-2'),
'Mark 13:31-37; 14:1-2',
)

def test_bare_range_after_semicolon_continues_chapter(self):
self.assertEqual(
parse_reading_citation('MATTHEW 10:32-33; 37-38; 19:27-30'),
'Matthew 10:32-33, 37-38; 19:27-30',
)

def test_preamble_variants(self):
for title, expected in (
("ST. JAMES' UNIVERSAL LETTER 2:1-13", 'James 2:1-13'),
('ST. PAUL TO THE HEBREWS 11:9-10, 32-40', 'Hebrews 11:9-10, 32-40'),
('ST. PAUL EPISTLE TO THE HEBREWS. 7:26-8:3', 'Hebrews 7:26-8:3'),
("ST. PAUL'S SECOND LETTER TO ST. TIMOTHY 3:10-15", '2 Timothy 3:10-15'),
('ST. JOHN. 10:9-16', 'John 10:9-16'),
):
with self.subTest(title=title):
self.assertEqual(parse_reading_citation(title), expected)

def test_preserves_discontinuous_ranges(self):
self.assertEqual(
parse_reading_citation(
"ST. PAUL'S FIRST LETTER TO THE CORINTHIANS 10:28-33; 11:1-8"
),
'1 Corinthians 10:28-33; 11:1-8',
)


class TestExactGreekReadingBoundaries(TestCase):
fixtures = ['calendarium.json', 'commemorations.json']

# Every field reported in the 2026 Antiochian/GOA comparison.
CASES = (
(2, 9, 'Epistle', '1 John 2.18-29; 3.1-8'),
(2, 16, 'Epistle', '3 John 1.1-15'),
(3, 1, 'Epistle', 'Heb 11.24-26, 32-40'),
(4, 10, 'Gospel', 'Matt 27.62-66'),
(4, 15, 'Epistle', 'Acts 2.22-38'),
(5, 7, 'Epistle', 'Acts 26.1, 12-20'),
(5, 8, 'Gospel', 'John 19.25-28, 21.24-25'),
(5, 10, 'Epistle', 'Acts 11.19-30'),
(5, 14, 'Epistle', 'Acts 14.20-28; 15.1-4'),
(5, 15, 'Epistle', 'Acts 15.5-12'),
(5, 18, 'Epistle', 'Acts 17.1-9'),
(5, 18, 'Gospel', 'John 11.47-54'),
(5, 21, 'Epistle', 'Acts 26.1, 12-20'),
(5, 29, 'Epistle', 'Acts 27.1-44; 28.1'),
(5, 30, 'Gospel', 'John 21.14-25'),
(6, 2, 'Gospel', 'Matt 4.23-25; 5.1-13'),
(6, 5, 'Epistle', 'Rom 2.14-28'),
(6, 11, 'Epistle', 'Acts 11.19-30'),
(6, 13, 'Epistle', 'Rom 3.19-24'),
(6, 15, 'Epistle', 'Rom 7.1-14'),
(6, 19, 'Epistle', 'Jude 1.1-25'),
(6, 24, 'Gospel', 'Luke 1.1-25, 57-68, 76, 80'),
(7, 6, 'Gospel', 'Matt 13.10-23, 43'),
(7, 23, 'Epistle', '1 Cor 10.28-33; 11.1-8'),
(7, 24, 'Epistle', '1 Cor 11.8-23'),
(7, 25, 'Epistle', 'Gal 4.22-27'),
(8, 13, 'Epistle', '2 Cor 4.1-12'),
(8, 22, 'Epistle', '1 Cor 1.26-31; 2.1-5'),
(8, 26, 'Gospel', 'Mark 3.20-27'),
(8, 29, 'Epistle', 'Acts 13.25-33'),
(9, 3, 'Epistle', 'Gal 1.1-3, 20-24; 2.1-5'),
(9, 6, 'Gospel', 'Matt 22.2-14'),
(9, 14, 'Gospel', 'John 19.6-11, 13-20, 25-28, 30'),
(9, 19, 'Epistle', '1 Cor 1.26-31; 2.1-5'),
(9, 23, 'Epistle', 'Gal 4.22-27'),
(10, 12, 'Epistle', 'Phil 2.12-15'),
(10, 18, 'Epistle', 'Col 4.5-11, 14-18'),
(10, 22, 'Epistle', 'Col 1.24-29, 2.1'),
(11, 22, 'Epistle', 'Eph 4.1-7'),
(12, 9, 'Epistle', 'Gal 4.22-27'),
(12, 10, 'Epistle', 'Titus 1.5-14'),
(12, 22, 'Epistle', 'Heb 9.8-23'),
(12, 28, 'Epistle', 'Heb 11.17-31'),
(12, 30, 'Gospel', 'Mark 10.17-27'),
(12, 31, 'Gospel', 'Mark 10.24-32'),
)

async def test_all_reported_fields_are_exact(self):
for month, day, source, expected in self.CASES:
with self.subTest(date=date(2026, month, day), source=source):
pday = liturgics.Day(
2026, month, day, tradition=Tradition.Greek
)
await pday.ainitialize()
readings = await pday.aget_readings()
actual = {
reading.pericope.sdisplay
for reading in readings
if reading.source == source
}
self.assertIn(expected, actual)


class TestAntiochianReadingCharacterization(TestCase):
"""Pins how every harvested antiochian.org reading compares with orthocal.

The hand-written cases above encode the reported 2026 defect. This covers
every year in data/antiochian_raw instead, so a change that fixes one year
while breaking another shows up as a readable diff naming the date. Lines
marked `boundary` are known, unresolved differences, not accepted ones.

data/ is excluded from the Docker image, so the titles are carried in the
golden file itself. Regenerate deliberately, never to make a red test green:

docker compose run --rm local python tools/greek/antiochian_characterize.py --write
"""

fixtures = ['calendarium.json', 'commemorations.json']

async def test_antiochian_comparison_is_unchanged(self):
expected = GOLDEN.read_text().splitlines()
entries = [line.split('\t')[:2] for line in expected]
actual = await characterize(entries)

differences = [(e, a) for e, a in zip(expected, actual) if e != a]
if differences:
report = '\n'.join(
f' expected: {e}\n actual: {a}' for e, a in differences[:10])
self.fail(
f'{len(differences)} of {len(expected)} readings changed. '
f'First few:\n{report}'
)
20 changes: 9 additions & 11 deletions calendarium/tests/test_liturgics.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,16 +227,15 @@ def test_shared_anchors_agree_between_traditions(self):

async def test_common_reading_is_shared_by_both_traditions(self):
"""With no tradition-specific override, both traditions should see the
same 'common' Reading row -- this is the day-to-day case today, since
no overlay rows exist yet."""
same 'common' Reading row."""

for tradition in (Tradition.Slavic, Tradition.Greek):
with self.subTest(tradition):
day = liturgics.Day(2026, 9, 14, tradition=tradition)
day = liturgics.Day(2026, 1, 1, tradition=tradition)
await day.ainitialize()
readings = await day.aget_readings()
displays = {r.pericope.sdisplay for r in readings}
self.assertIn('John 19.6-11, 13-20, 25-28, 30-35', displays)
self.assertIn('Col 2.8-12', displays)

async def test_tradition_specific_row_overrides_common_row(self):
"""A tradition-tagged row should shadow the 'common' row for the same slot."""
Expand Down Expand Up @@ -1484,12 +1483,11 @@ class TestGreekPreTriodionWeekdayCycle(TestCase):
fixtures = ['calendarium.json', 'commemorations.json']

# Luke-section week -> weekday (Mon=0) -> Gospel, in this project's own
# `Pericope.sdisplay` notation. Week 14 Friday is `Mark 10.23-32` where
# goarch.org prints `Mark 10:24-32` -- one of the known one-verse
# citation-boundary variants, not a discrepancy.
# `Pericope.sdisplay` notation. Week 14 Friday follows goarch.org's and
# antiochian.org's `Mark 10:24-32` in the Greek tradition.
LUKE_SECTION_TAIL = {
14: {0: 'Mark 9.42-10.1', 1: 'Mark 10.2-12', 2: 'Mark 10.11-16',
3: 'Mark 10.17-27', 4: 'Mark 10.23-32', 5: 'Luke 16.10-15'},
3: 'Mark 10.17-27', 4: 'Mark 10.24-32', 5: 'Luke 16.10-15'},
15: {0: 'Mark 10.46-52', 1: 'Mark 11.11-23', 2: 'Mark 11.22-26',
3: 'Mark 11.27-33', 4: 'Mark 12.1-12', 5: 'Luke 17.3-10'},
16: {0: 'Mark 12.13-17', 1: 'Mark 12.18-27', 2: 'Mark 12.28-37',
Expand Down Expand Up @@ -1667,7 +1665,7 @@ class TestGreekMenaionReadings(TestCase):
CASES = [
(4, 25, 'Gospel', 'Luke 10.16-21', 'Mark 6.7-13'), # Mark the Apostle
(4, 30, 'Gospel', 'Luke 9.1-6', 'Luke 5.1-11'), # James the Apostle
(5, 7, 'Epistle', 'Acts 26.1-5, 12-20', ABSENT), # Appearance of the Cross
(5, 7, 'Epistle', 'Acts 26.1, 12-20', ABSENT), # Appearance of the Cross
(7, 5, 'Epistle', 'Gal 5.22-6.2', SHARED), # Athanasius of Athos
(7, 5, 'Gospel', 'Matt 11.27-30', 'Luke 6.17-23'), # Athanasius of Athos
(7, 13, 'Epistle', 'Heb 2.2-10', ABSENT), # Synaxis of Gabriel
Expand Down Expand Up @@ -1737,7 +1735,7 @@ async def labelled_gospels(self, year, month, day):

async def test_both_are_shown_and_labelled_when_the_ordos_disagree(self):
cases = [
(2021, 1, 19, 'Matt 22.1-14', 'Matt 19.16-26'),
(2021, 1, 19, 'Matt 22.2-14', 'Matt 19.16-26'),
(2021, 1, 26, 'Matt 22.35-46', 'Mark 11.11-23'),
(2024, 1, 19, 'Matt 9.1-8', 'Matt 19.16-26'),
(2026, 1, 24, 'Luke 18.35-43', 'Mark 5.24-34'),
Expand All @@ -1755,7 +1753,7 @@ async def test_labels_have_a_short_and_a_full_form(self):
# full one.
self.assertEqual(
await self.labelled_gospels(2021, 1, 19),
[('Matt 22.1-14', 'GOA', 'Greek Archdiocese'),
[('Matt 22.2-14', 'GOA', 'Greek Archdiocese'),
('Matt 19.16-26', 'Antiochian', 'Antiochian Archdiocese')])

async def test_ordinary_readings_have_no_short_form(self):
Expand Down
2 changes: 1 addition & 1 deletion calendarium/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,4 @@ def test_greek_ordo_gospel_is_served(self):
# The annual-ordo overlay is the substantive change this PR makes to
# what a reader actually sees on these dates.
response = self.client.get('/readings/greek/gregorian/2021/1/19/')
self.assertContains(response, 'Matthew 22.1-14')
self.assertContains(response, 'Matthew 22.2-14')
19 changes: 19 additions & 0 deletions docs/greek-lectionary.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and are marked as such.
Last measured 2026-08-27: against goarch.org over calendar 2026, the app is
**correct on 333 of 336 days (99.1%)**.

Re-measured 2026-09-18 by verse set after the exact-boundary work; see the
addendum under "Accuracy, measured".

---

# Part I — Current state
Expand Down Expand Up @@ -166,6 +169,22 @@ Three of those are the shared unsolved items above; the other three are dates
where the two jurisdictions genuinely differ, and on the annual-ordo ones both
readings are now shown side by side rather than only GOA's.

**Addendum, 2026-09-18.** The figures above compared only a citation's
opening chapter and verse, so a reading that starts together with goarch.org's
but ends elsewhere (`Acts 15:5-34` against `Acts 15:5-12`) counted as correct.
Comparing the resolved verse sets instead found 45 such fields in 2026 alone.
`tools/greek/load_exact_boundaries.py` now carries the exact citations where
goarch.org and antiochian.org agree, and `load_shared_menaion.py` gained the
weekday saints' readings that 2026 hid because their dates fall on Sundays.
Measured by verse set against goarch.org: 2024 **660 of 669 fields**, 2025
**661 of 665**, 2026 **669 of 669**. Of the three items in the table above,
Holy Friday now serves the daily Gospel both sources print (`Matthew 27:62-66`)
and the two December days are carried as `OrdoReading` rows for 2026; the
general December rule is still unmodelled. Against antiochian.org, 2026 differs
on 3 days of 365 (Jan 24, Feb 6, Jun 14), each a jurisdictional choice rather
than a boundary. `calendarium/tests/data/antiochian-readings.tsv` pins the
comparison for every harvested year.

There is no separate Antiochian tradition: one was built, measured and removed
-- see Part III.

Expand Down
Loading