diff options
Diffstat (limited to 'internal/bible')
| -rw-r--r-- | internal/bible/booktable.go | 16 | ||||
| -rw-r--r-- | internal/bible/testdata/corpora/badbook.tsv | 1 | ||||
| -rw-r--r-- | internal/bible/testdata/corpora/badsystem.ini | 4 | ||||
| -rw-r--r-- | internal/bible/testdata/corpora/badsystem.tsv | 1 | ||||
| -rw-r--r-- | internal/bible/testdata/corpora/gap.tsv | 3 | ||||
| -rw-r--r-- | internal/bible/testdata/corpora/good.ini | 4 | ||||
| -rw-r--r-- | internal/bible/testdata/corpora/good.tsv | 4 | ||||
| -rw-r--r-- | internal/bible/testdata/corpora/nosidecar.tsv | 2 | ||||
| -rw-r--r-- | internal/bible/validate.go | 93 | ||||
| -rw-r--r-- | internal/bible/validate_test.go | 24 |
10 files changed, 152 insertions, 0 deletions
diff --git a/internal/bible/booktable.go b/internal/bible/booktable.go index ffa3907..360f526 100644 --- a/internal/bible/booktable.go +++ b/internal/bible/booktable.go @@ -92,6 +92,22 @@ func mustReadEmbedded() []byte { // /settings editor shows when the user has no override yet). func DefaultBooksINI() []byte { return mustReadEmbedded() } +// CanonicalBooks returns the 73 canonical English book keys -- the exact +// vocabulary a corpus .tsv's Book column must use -- read from the embedded +// books.ini [en] section. It reuses the existing embedded parse (parseBooks +// + mustReadEmbedded); the book list is never re-embedded or hardcoded here. +func CanonicalBooks() map[string]bool { + out := map[string]bool{} + def, err := parseBooks(mustReadEmbedded()) + if err != nil { + return out // our bug, not the caller's; an empty set flags everything + } + for canon := range def["en"] { + out[canon] = true + } + return out +} + func parseBooks(data []byte) (map[string]map[string][]string, error) { secs, err := ini.Parse(data) if err != nil { diff --git a/internal/bible/testdata/corpora/badbook.tsv b/internal/bible/testdata/corpora/badbook.tsv new file mode 100644 index 0000000..55d5571 --- /dev/null +++ b/internal/bible/testdata/corpora/badbook.tsv @@ -0,0 +1 @@ +Genessis Gen 1 1 1 In the beginning God created heaven and earth. diff --git a/internal/bible/testdata/corpora/badsystem.ini b/internal/bible/testdata/corpora/badsystem.ini new file mode 100644 index 0000000..aca54c3 --- /dev/null +++ b/internal/bible/testdata/corpora/badsystem.ini @@ -0,0 +1,4 @@ +; test fixture: invalid psalm_system +lang = en +name = Bad System Test +psalm_system = klingon diff --git a/internal/bible/testdata/corpora/badsystem.tsv b/internal/bible/testdata/corpora/badsystem.tsv new file mode 100644 index 0000000..fd051c3 --- /dev/null +++ b/internal/bible/testdata/corpora/badsystem.tsv @@ -0,0 +1 @@ +Genesis Gen 1 1 1 In the beginning God created heaven and earth. diff --git a/internal/bible/testdata/corpora/gap.tsv b/internal/bible/testdata/corpora/gap.tsv new file mode 100644 index 0000000..f0d67bf --- /dev/null +++ b/internal/bible/testdata/corpora/gap.tsv @@ -0,0 +1,3 @@ +Genesis Gen 1 1 1 In the beginning God created heaven and earth. +Genesis Gen 1 1 2 And the earth was void and empty, and darkness was upon the face of the deep. +Genesis Gen 1 1 4 And God said: Be light made. And light was made. diff --git a/internal/bible/testdata/corpora/good.ini b/internal/bible/testdata/corpora/good.ini new file mode 100644 index 0000000..6169a64 --- /dev/null +++ b/internal/bible/testdata/corpora/good.ini @@ -0,0 +1,4 @@ +; test fixture: valid sidecar +lang = en +name = Good Test Corpus +psalm_system = vulgate diff --git a/internal/bible/testdata/corpora/good.tsv b/internal/bible/testdata/corpora/good.tsv new file mode 100644 index 0000000..7cb9015 --- /dev/null +++ b/internal/bible/testdata/corpora/good.tsv @@ -0,0 +1,4 @@ +Genesis Gen 1 1 1 In the beginning God created heaven and earth. +Genesis Gen 1 1 2 And the earth was void and empty, and darkness was upon the face of the deep. +Exodus Exod 2 1 1 Now there went out a man of the house of Levi. +Exodus Exod 2 1 2 And he took a wife of his own kindred. diff --git a/internal/bible/testdata/corpora/nosidecar.tsv b/internal/bible/testdata/corpora/nosidecar.tsv new file mode 100644 index 0000000..bba582a --- /dev/null +++ b/internal/bible/testdata/corpora/nosidecar.tsv @@ -0,0 +1,2 @@ +Genesis Gen 1 1 1 In the beginning God created heaven and earth. +Genesis Gen 1 1 2 And the earth was void and empty, and darkness was upon the face of the deep. diff --git a/internal/bible/validate.go b/internal/bible/validate.go new file mode 100644 index 0000000..27ec519 --- /dev/null +++ b/internal/bible/validate.go @@ -0,0 +1,93 @@ +package bible + +import ( + "fmt" + "sort" +) + +// CorpusReport is the outcome of validating one corpus's sidecar + text (see +// CheckCorpus). Errors mean the corpus is unusable/malformed and fail +// --corpus-check (exit 1); Warnings flag coverage gaps against the reference +// Vulgate ("vul") and never fail the check -- a corpus may legitimately be +// incomplete (the built-in wuj is) and still be a valid drop-in. +type CorpusReport struct { + Code string + Errors []string + Warnings []string +} + +// OK reports whether the corpus is usable: no errors. Warnings never affect it. +func (r CorpusReport) OK() bool { return len(r.Errors) == 0 } + +var validPsalmSystems = map[string]bool{"vulgate": true, "hebrew": true, "drb": true} + +// CheckCorpus validates code's sidecar and text and reports coverage gaps vs +// "vul" as warnings. +// +// Sidecar: lang, name and psalm_system are required (psalm_system must be +// vulgate/hebrew/drb); sigla and autoselect are optional -- their absence, or +// autoselect=false, is never flagged (the built-in wuj sets autoselect=false +// and must pass clean). +// +// Text: every Book column value must be one of CanonicalBooks' 73 keys +// (error). A chapter missing entirely versus vul, a verse-number gap within a +// chapter, or a repeated verse number within a chapter, is a warning only -- +// real source texts legitimately do this (e.g. the LXX's lettered doublet +// verses in 3 Kingdoms, or a scanned translation's occasional merged verse), +// and it must never turn a corpus that otherwise loads fine into a failure. +func CheckCorpus(code string) CorpusReport { + r := CorpusReport{Code: code} + + m, ok := Meta(code) + if !ok || m.Lang == "" || m.Name == "" || m.PsalmSystem == "" { + r.Errors = append(r.Errors, "missing or incomplete sidecar (need lang, name, psalm_system)") + } + if m.PsalmSystem != "" && !validPsalmSystems[m.PsalmSystem] { + r.Errors = append(r.Errors, fmt.Sprintf("invalid psalm_system %q (want vulgate|hebrew|drb)", m.PsalmSystem)) + } + + c := load(code) + if len(c.books) == 0 { + r.Errors = append(r.Errors, "no verses parsed (empty or malformed .tsv)") + sort.Strings(r.Errors) + return r + } + + canon := CanonicalBooks() + for book := range c.books { + if !canon[book] { + r.Errors = append(r.Errors, "unknown book name: "+book) + } + } + + ref := load("vul") + for book, chaps := range c.books { + for ch, verses := range chaps { + seen := map[int]bool{} + maxV := 0 + for _, v := range verses { + if seen[v.Verse] { + r.Warnings = append(r.Warnings, fmt.Sprintf("%s %d: duplicate verse %d", book, ch, v.Verse)) + } + seen[v.Verse] = true + if v.Verse > maxV { + maxV = v.Verse + } + } + if maxV > len(seen) { + r.Warnings = append(r.Warnings, fmt.Sprintf("%s %d: verse gap (have %d verse(s), highest numbered %d)", book, ch, len(seen), maxV)) + } + } + if refChaps, ok := ref.books[book]; ok { + for ch := range refChaps { + if _, have := chaps[ch]; !have { + r.Warnings = append(r.Warnings, fmt.Sprintf("%s: missing chapter %d (present in vul)", book, ch)) + } + } + } + } + + sort.Strings(r.Errors) + sort.Strings(r.Warnings) + return r +} diff --git a/internal/bible/validate_test.go b/internal/bible/validate_test.go new file mode 100644 index 0000000..9cfec26 --- /dev/null +++ b/internal/bible/validate_test.go @@ -0,0 +1,24 @@ +package bible + +import "testing" + +func TestCheckCorpus(t *testing.T) { + SetUserCorporaDir("testdata/corpora") + t.Cleanup(func() { SetUserCorporaDir("") }) + + if r := CheckCorpus("good"); !r.OK() { + t.Fatalf("good corpus flagged: %v", r.Errors) + } + if r := CheckCorpus("badbook"); r.OK() { + t.Fatal("unknown book not caught") + } + if r := CheckCorpus("gap"); len(r.Warnings) == 0 { + t.Fatal("verse gap not warned") + } + if r := CheckCorpus("nosidecar"); r.OK() { + t.Fatal("missing sidecar not caught") + } + if r := CheckCorpus("badsystem"); r.OK() { + t.Fatal("bad psalm_system not caught") + } +} |
