aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/citation/book.ml74
-rw-r--r--lib/citation/book.mli49
-rw-r--r--lib/citation/dune2
-rw-r--r--test/dune2
-rw-r--r--test/test_citation.ml43
-rw-r--r--test/test_colitur.ml1
6 files changed, 170 insertions, 1 deletions
diff --git a/lib/citation/book.ml b/lib/citation/book.ml
new file mode 100644
index 0000000..a2f9678
--- /dev/null
+++ b/lib/citation/book.ml
@@ -0,0 +1,74 @@
+(* SPDX-License-Identifier: AGPL-3.0-or-later *)
+
+type id = string
+
+let to_string t = t
+
+(* Every book the shipped EF lectionary cites, with every spelling it uses.
+ The dotted/undotted pairs are inherited from lectio -- see book.mli. *)
+let table =
+ [ ("genesis", [ "Gen" ]);
+ ("exodus", [ "Ex" ]);
+ ("leviticus", [ "Lev" ]);
+ ("numbers", [ "Num" ]);
+ ("kings_3", [ "3 Kings"; "3 Kgs." ]);
+ ("kings_4", [ "4 Kings" ]);
+ ("esdras_2", [ "2 Esd." ]);
+ ("esther", [ "Esther" ]);
+ ("ecclesiasticus", [ "Ecclus" ]);
+ ("isaiah", [ "Isa"; "Isa." ]);
+ ("jeremiah", [ "Jer" ]);
+ ("ezekiel", [ "Ezech" ]);
+ ("daniel", [ "Dan" ]);
+ ("osee", [ "Osee" ]);
+ ("joel", [ "Joel" ]);
+ ("jonas", [ "Jonas" ]);
+ ("matthew", [ "Matt"; "Matt." ]);
+ ("mark", [ "Mark" ]);
+ ("luke", [ "Luke" ]);
+ ("john", [ "John" ]);
+ ("acts", [ "Acts" ]);
+ ("romans", [ "Rom" ]);
+ ("corinthians_1", [ "1 Cor"; "1 Cor." ]);
+ ("corinthians_2", [ "2 Cor." ]);
+ ("galatians", [ "Gal" ]);
+ ("ephesians", [ "Eph"; "Eph." ]);
+ ("philippians", [ "Phil" ]);
+ ("colossians", [ "Col" ]);
+ ("thessalonians_1", [ "1 Thess"; "1 Thess." ]);
+ ("thessalonians_2", [ "2 Thess" ]);
+ ("titus", [ "Titus" ]);
+ ("hebrews", [ "Heb" ]);
+ ("james", [ "Jas" ]);
+ ("peter_1", [ "1 Pet"; "1 Pet." ]);
+ ("john_1", [ "1 John" ]) ]
+
+(* Targets a tradition can map ONTO that the Vulgate data never cites
+ directly. Present so [tradition_of_fields] can validate both sides. *)
+let tradition_targets =
+ [ "kings_1"; "kings_2"; "nehemiah"; "sirach"; "hosea"; "jonah" ]
+
+let all = List.map fst table @ tradition_targets
+
+let tokens =
+ List.concat_map (fun (id, sp) -> List.map (fun s -> (s, id)) sp) table
+
+let of_token s =
+ let s = String.trim s in
+ List.assoc_opt s tokens
+
+type tradition = (string * string) list
+
+let vulgate = []
+
+let known id = List.mem id all
+
+let tradition_of_fields fields =
+ List.filter (fun (a, b) -> known a && known b) fields
+
+let unknown_fields fields =
+ List.filter_map
+ (fun (a, b) -> if known a && known b then None else Some a)
+ fields
+
+let map tr id = match List.assoc_opt id tr with Some x -> x | None -> id
diff --git a/lib/citation/book.mli b/lib/citation/book.mli
new file mode 100644
index 0000000..dd04e0d
--- /dev/null
+++ b/lib/citation/book.mli
@@ -0,0 +1,49 @@
+(* SPDX-License-Identifier: AGPL-3.0-or-later *)
+
+(** Bible books: one canonical id per book, and the tradition that decides
+ which book an id denotes.
+
+ Ids follow the VULGATE structure ([kings_3], [esdras_2],
+ [ecclesiasticus]), because the Vulgate is the default tradition and the
+ shipped 1962 data is Vulgate throughout. An id is internal -- it is never
+ shown to a reader, exactly as a slug is never shown. *)
+
+type id
+
+val to_string : id -> string
+
+(** Resolve one spelling as it appears in the data. Returns [None] for
+ anything not in {!tokens}.
+
+ SEVEN books arrive in two spellings ([Isa]/[Isa.], [3 Kgs.]/[3 Kings],
+ and five more). That inconsistency is INHERITED from lectio's own ini,
+ which is itself generated from missalemeum/Divinum Officium -- it is not
+ a colitur transcription error, and the data is deliberately left
+ untouched. Both spellings resolve here instead. *)
+val of_token : string -> id option
+
+(** Every id this build knows, for coverage checks. *)
+val all : id list
+
+(** Every accepted spelling paired with its id. *)
+val tokens : (string * id) list
+
+(** A numbering tradition: which book an id denotes. Separate from NAMING
+ (what a book is called), which lives in a language file's [\[bible\]]
+ section, because naming varies by language and this does not -- "modern
+ numbering" is the same decision in Latin, Polish and English. *)
+type tradition
+
+(** The identity tradition: the Vulgate, as the 1962 Missal prints it. The
+ default; colitur never silently renumbers. *)
+val vulgate : tradition
+
+(** Build a tradition from an ini section's fields. An entry naming an
+ unknown id on either side is IGNORED, not fatal: a traditions file
+ written for a newer colitur must still work on an older one. Use
+ {!unknown_fields} to report them. *)
+val tradition_of_fields : (string * string) list -> tradition
+
+val unknown_fields : (string * string) list -> string list
+
+val map : tradition -> id -> id
diff --git a/lib/citation/dune b/lib/citation/dune
new file mode 100644
index 0000000..e6cec3c
--- /dev/null
+++ b/lib/citation/dune
@@ -0,0 +1,2 @@
+(library
+ (name colitur_citation))
diff --git a/test/dune b/test/dune
index 84c5ec7..a90b715 100644
--- a/test/dune
+++ b/test/dune
@@ -1,6 +1,6 @@
(test
(name test_colitur)
- (libraries colitur_kernel colitur_naming colitur_render rite_ef alcotest qcheck qcheck-alcotest sexplib)
+ (libraries colitur_kernel colitur_naming colitur_render colitur_citation rite_ef alcotest qcheck qcheck-alcotest sexplib)
(deps
../data/ef/sanctoral.sexp
../data/ef/adjustments.sexp
diff --git a/test/test_citation.ml b/test/test_citation.ml
new file mode 100644
index 0000000..0694d8b
--- /dev/null
+++ b/test/test_citation.ml
@@ -0,0 +1,43 @@
+module B = Colitur_citation.Book
+
+let s = Alcotest.string
+
+let test_both_spellings_are_one_book () =
+ (* The seven inherited duplicate spellings must collapse. This is the
+ whole reason the parser exists rather than a regex. *)
+ let same a b =
+ match B.of_token a, B.of_token b with
+ | Some x, Some y ->
+ Alcotest.(check s) (a ^ " = " ^ b) (B.to_string x) (B.to_string y)
+ | _ -> Alcotest.failf "%s or %s did not resolve" a b
+ in
+ same "Isa" "Isa.";
+ same "Matt" "Matt.";
+ same "1 Cor" "1 Cor.";
+ same "1 Pet" "1 Pet.";
+ same "1 Thess" "1 Thess.";
+ same "Eph" "Eph.";
+ same "3 Kgs." "3 Kings"
+
+let test_unknown_token_is_none () =
+ Alcotest.(check bool) "not a book" true (B.of_token "Nonesuch" = None)
+
+let test_vulgate_is_identity () =
+ match B.of_token "3 Kings" with
+ | None -> Alcotest.fail "3 Kings did not resolve"
+ | Some k ->
+ Alcotest.(check s) "unmapped" "kings_3" (B.to_string (B.map B.vulgate k))
+
+let test_modern_renumbers () =
+ let modern = B.tradition_of_fields [ ("kings_3", "kings_1") ] in
+ match B.of_token "3 Kings" with
+ | None -> Alcotest.fail "3 Kings did not resolve"
+ | Some k ->
+ Alcotest.(check s) "renumbered" "kings_1" (B.to_string (B.map modern k))
+
+let suite =
+ ( "book",
+ [ Alcotest.test_case "both spellings one book" `Quick test_both_spellings_are_one_book;
+ Alcotest.test_case "unknown token" `Quick test_unknown_token_is_none;
+ Alcotest.test_case "vulgate identity" `Quick test_vulgate_is_identity;
+ Alcotest.test_case "modern renumbers" `Quick test_modern_renumbers ] )
diff --git a/test/test_colitur.ml b/test/test_colitur.ml
index 1bdd220..46a31aa 100644
--- a/test/test_colitur.ml
+++ b/test/test_colitur.ml
@@ -4,6 +4,7 @@ let () =
[ Test_date.suite; Test_computus.suite; Test_colour.suite; Test_slug.suite; Test_names.suite;
Test_lang.suite;
Test_lang_coverage.suite;
+ Test_citation.suite;
Test_config.suite;
Test_overlay.suite; Test_overlay_ini.suite; Test_temporal_ef.suite; Test_validate.suite; Test_precedence.suite;
Test_calendar.suite; Test_precedence_ef.suite; Test_sanctoral_ef.suite; Test_rite_ef.suite;