summaryrefslogtreecommitdiff
path: root/test/test_citation.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-20 14:59:03 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-20 14:59:03 +0200
commit56e36188c6afc208b96b15b9a1d3d62bbbe2f705 (patch)
tree5d60c75005c36ab9595a70ef06f261ed9b7efed4 /test/test_citation.ml
parenta4eb9ed3cd93cbac493a873991dd5817906aad56 (diff)
downloadcolitur-56e36188c6afc208b96b15b9a1d3d62bbbe2f705.tar.gz
colitur-56e36188c6afc208b96b15b9a1d3d62bbbe2f705.zip
feat(citation): parse citations into structure
The parsed form is a book and a LIST of chapter-parts: the data cites across chapters and lists disjoint verse ranges within one. Two rules the shipped data forces and that are not obvious: a semicolon-separated part may inherit the previous chapter rather than restate it, and a chapter may be separated from its verses by a comma.
Diffstat (limited to 'test/test_citation.ml')
-rw-r--r--test/test_citation.ml54
1 files changed, 54 insertions, 0 deletions
diff --git a/test/test_citation.ml b/test/test_citation.ml
index 7bc8ac7..75fffe8 100644
--- a/test/test_citation.ml
+++ b/test/test_citation.ml
@@ -146,3 +146,57 @@ let suite =
test_tokens_has_no_duplicate_spelling;
Alcotest.test_case "every data file token resolves" `Quick
test_every_data_file_token_resolves ] )
+
+module P = Colitur_citation.Parse
+
+(* Render a parse back to a debug string so a test can assert shape
+ compactly: "book|chapter:v-v,v-v|chapter:v". *)
+let show (t : P.t) =
+ let range (r : P.verse_range) =
+ match r.P.last with
+ | None -> string_of_int r.P.first
+ | Some l -> Printf.sprintf "%d-%d" r.P.first l
+ in
+ let part (p : P.part) =
+ Printf.sprintf "%d:%s" p.P.chapter
+ (String.concat "," (List.map range p.P.verses))
+ in
+ B.to_string t.P.book ^ "|" ^ String.concat "|" (List.map part t.P.parts)
+
+let parses input expected () =
+ match P.parse input with
+ | Error e -> Alcotest.failf "%s did not parse: %s" input e
+ | Ok t -> Alcotest.(check s) input expected (show t)
+
+let test_rejects_unknown_book () =
+ Alcotest.(check bool) "error" true (Result.is_error (P.parse "Nonesuch 1:1"))
+
+let test_rejects_garbage () =
+ List.iter
+ (fun bad ->
+ Alcotest.(check bool) bad true (Result.is_error (P.parse bad)))
+ [ ""; "Luke"; "Luke :"; "Luke 1:"; "Luke abc:1" ]
+
+let parse_suite =
+ [ ("simple", `Quick, parses "1 Cor 11:20-32" "corinthians_1|11:20-32");
+ ("trailing period", `Quick, parses "1 John 3:13-18." "john_1|3:13-18");
+ ("single verse", `Quick, parses "Luke 2:21" "luke|2:21");
+ ("dotted spelling", `Quick, parses "Isa. 1:16-19" "isaiah|1:16-19");
+ ("verse list", `Quick, parses "Acts 10:34, 42-48" "acts|10:34,42-48");
+ ("new chapter", `Quick, parses "1 Cor. 9:24-27; 10:1-5"
+ "corinthians_1|9:24-27|10:1-5");
+ (* Rule 1: the second part names no chapter, so it inherits chapter 2. *)
+ ("inherited chapter", `Quick, parses "Joel 2:23-24; 26-27" "joel|2:23-24|2:26-27");
+ (* Rule 2: comma separates chapter from verses in the second part. *)
+ ("comma chapter", `Quick, parses "Mark 14:32-72; 15, 1-46"
+ "mark|14:32-72|15:1-46");
+ ("four ranges", `Quick, parses "Dan 13:1-9, 15-17, 19-30, 33-62."
+ "daniel|13:1-9,15-17,19-30,33-62");
+ ("mixed", `Quick, parses "Num 20:1, 3; 6-13." "numbers|20:1,3|20:6-13");
+ ("trailing semicolon", `Quick, parses "1 Cor 1:18-25; 1:30;"
+ "corinthians_1|1:18-25|1:30");
+ ("four parts", `Quick, parses "Eccli 24:5; 14:7; 14:9-11; 24:30-31"
+ "ecclesiasticus|24:5|14:7|14:9-11|24:30-31");
+ ("modern name, vulgate id", `Quick, parses "Rev 12:1" "apocalypse|12:1");
+ ("unknown book", `Quick, test_rejects_unknown_book);
+ ("garbage", `Quick, test_rejects_garbage) ]