1use super::coref::{CorefChain, CorefDocument, Mention, MentionType};
28use super::loader::DatasetId;
29use anno::{Error, Result};
30use serde::{Deserialize, Serialize};
31use std::collections::HashMap;
32use std::fs;
33use std::path::PathBuf;
34
35pub fn parse_corefud_conllu(content: &str) -> Result<Vec<CorefDocument>> {
58 #[derive(Debug, Clone)]
59 struct TokenSpan {
60 is_empty_node: bool,
61 entity_value: Option<String>,
62 }
63
64 #[derive(Debug, Clone)]
65 struct OpenMention {
66 start_char: usize,
67 entity_type: Option<String>,
68 is_empty_node: bool,
69 }
70
71 #[derive(Debug, Clone)]
72 enum EntityMark {
73 Open {
74 cluster: String,
75 entity_type: Option<String>,
76 self_close: bool,
77 },
78 Close {
79 cluster: String,
80 },
81 }
82
83 fn parse_misc_entity(misc: &str) -> Option<String> {
84 if misc.trim().is_empty() || misc.trim() == "_" {
85 return None;
86 }
87 for part in misc.split('|') {
88 if let Some(rest) = part.strip_prefix("Entity=") {
89 if !rest.is_empty() && rest != "_" {
90 return Some(rest.to_string());
91 }
92 }
93 }
94 None
95 }
96
97 fn parse_space_after_no(misc: &str) -> bool {
98 if misc.trim().is_empty() || misc.trim() == "_" {
99 return false;
100 }
101 misc.split('|').any(|p| p == "SpaceAfter=No")
102 }
103
104 fn split_cluster_and_type(open_descriptor: &str) -> (String, Option<String>) {
105 let mut parts = open_descriptor.splitn(3, '-');
110 let raw_cluster = parts.next().unwrap_or("").to_string();
111 let etype = parts.next().map(|s| s.to_string());
112
113 let cluster = if let Some((base, _rest)) = raw_cluster.split_once('[') {
115 base.to_string()
116 } else {
117 raw_cluster
118 };
119 (cluster, etype)
120 }
121
122 fn split_cluster(close_descriptor: &str) -> String {
123 if let Some((base, _)) = close_descriptor.split_once('[') {
126 base.to_string()
127 } else {
128 close_descriptor.to_string()
129 }
130 }
131
132 fn parse_entity_marks(entity_value: &str) -> Vec<EntityMark> {
133 let mut marks = Vec::new();
142 let mut i = 0usize;
143 let bytes = entity_value.as_bytes();
144
145 while i < bytes.len() {
146 match bytes[i] as char {
147 '(' => {
148 i += 1;
149 let start = i;
150 while i < bytes.len() && (bytes[i] as char) != ')' && (bytes[i] as char) != '('
152 {
153 i += 1;
154 }
155 if i < bytes.len() && (bytes[i] as char) == ')' {
156 let descriptor = entity_value[start..i].to_string();
158 let (cluster, etype) = split_cluster_and_type(&descriptor);
159 marks.push(EntityMark::Open {
160 cluster,
161 entity_type: etype,
162 self_close: true,
163 });
164 i += 1;
165 } else {
166 let descriptor = entity_value[start..i].to_string();
169 let (cluster, etype) = split_cluster_and_type(&descriptor);
170 marks.push(EntityMark::Open {
171 cluster,
172 entity_type: etype,
173 self_close: false,
174 });
175 if i >= bytes.len() {
176 break;
177 }
178 }
179 }
180 'e' => {
181 let start = i;
183 while i < bytes.len() && (bytes[i] as char) != ')' {
184 if (bytes[i] as char) == '(' {
185 break;
186 }
187 i += 1;
188 }
189 if i < bytes.len() && (bytes[i] as char) == ')' {
190 let raw = entity_value[start..i].to_string();
191 marks.push(EntityMark::Close {
192 cluster: split_cluster(&raw),
193 });
194 i += 1;
195 } else {
196 break;
197 }
198 }
199 _ => i += 1,
200 }
201 }
202
203 marks
204 }
205
206 fn extract_span_text(text: &str, start: usize, end: usize) -> String {
207 if end <= start {
208 return String::new();
209 }
210 text.chars().skip(start).take(end - start).collect()
211 }
212
213 let mut docs: Vec<CorefDocument> = Vec::new();
215 let mut doc_idx: usize = 0;
216 let mut current_doc_id: Option<String> = None;
217 let mut text = String::new();
218 let mut text_char_len: usize = 0;
219
220 let mut tokens: Vec<TokenSpan> = Vec::new();
221 let mut clusters: HashMap<String, Vec<Mention>> = HashMap::new();
222 let mut open: HashMap<String, Vec<OpenMention>> = HashMap::new();
223
224 let mut prev_space_after_no = false;
225
226 let flush_doc = |docs: &mut Vec<CorefDocument>,
227 doc_idx: &mut usize,
228 current_doc_id: &mut Option<String>,
229 text: &mut String,
230 clusters: &mut HashMap<String, Vec<Mention>>,
231 open: &mut HashMap<String, Vec<OpenMention>>|
232 -> Result<()> {
233 if text.is_empty() && clusters.is_empty() {
234 *current_doc_id = None;
235 open.clear();
236 return Ok(());
237 }
238
239 if open.values().any(|stk| !stk.is_empty()) {
240 return Err(Error::InvalidInput(
241 "CorefUD parse error: document ended with unclosed Entity brackets".to_string(),
242 ));
243 }
244
245 let mut coref_chains: Vec<CorefChain> = Vec::new();
247 for (cluster_id, mut mentions) in std::mem::take(clusters).into_iter() {
248 for m in &mut mentions {
250 if m.mention_type == Some(MentionType::Zero) {
251 m.text = String::new();
252 } else {
253 m.text = extract_span_text(text, m.start, m.end);
254 }
255 }
256
257 let numeric_id = cluster_id.strip_prefix('e').and_then(|rest| {
259 rest.chars()
260 .take_while(|c| c.is_ascii_digit())
261 .collect::<String>()
262 .parse::<u64>()
263 .ok()
264 });
265
266 if let Some(cid) = numeric_id {
267 coref_chains.push(CorefChain::with_id(mentions, cid));
268 } else {
269 coref_chains.push(CorefChain::new(mentions));
270 }
271 }
272
273 if coref_chains.is_empty() {
274 return Err(Error::InvalidInput(
275 "CorefUD CoNLL-U contains no coreference chains".to_string(),
276 ));
277 }
278
279 let doc_id = current_doc_id
280 .clone()
281 .unwrap_or_else(|| format!("corefud_doc_{}", *doc_idx));
282 *doc_idx += 1;
283
284 docs.push(CorefDocument::with_id(
285 std::mem::take(text),
286 doc_id,
287 coref_chains,
288 ));
289
290 *current_doc_id = None;
292 open.clear();
293 Ok(())
294 };
295
296 for raw_line in content.lines() {
297 let line = raw_line.trim_end();
298
299 if line.starts_with("# newdoc") {
301 flush_doc(
302 &mut docs,
303 &mut doc_idx,
304 &mut current_doc_id,
305 &mut text,
306 &mut clusters,
307 &mut open,
308 )?;
309 tokens.clear();
310 text_char_len = 0;
311 prev_space_after_no = false;
312
313 if let Some(pos) = line.find("id") {
315 let maybe = line[pos..].split('=').nth(1).map(|s| s.trim());
316 if let Some(id) = maybe {
317 if !id.is_empty() {
318 current_doc_id = Some(id.to_string());
319 }
320 }
321 }
322 continue;
323 }
324
325 if line.starts_with('#') {
327 continue;
328 }
329
330 if line.trim().is_empty() {
332 continue;
333 }
334
335 let fields: Vec<&str> = line.split('\t').collect();
336 if fields.len() < 2 {
337 continue;
338 }
339
340 let id_field = fields[0];
341 if id_field.contains('-') {
343 continue;
344 }
345
346 let is_empty_node = id_field.contains('.');
347 let form = fields.get(1).copied().unwrap_or("_");
348 let misc = fields.get(9).copied().unwrap_or("_");
349 let entity_value = parse_misc_entity(misc);
350 let space_after_no = parse_space_after_no(misc);
351
352 let (char_start, char_end) = if is_empty_node {
353 (text_char_len, text_char_len)
354 } else {
355 if !text.is_empty() && !prev_space_after_no {
356 text.push(' ');
357 text_char_len += 1;
358 }
359 let start = text_char_len;
360 text.push_str(form);
361 text_char_len += form.chars().count();
362 (start, text_char_len)
363 };
364
365 if !is_empty_node {
366 prev_space_after_no = space_after_no;
367 }
368
369 let token_idx = tokens.len();
370 tokens.push(TokenSpan {
371 is_empty_node,
372 entity_value,
373 });
374
375 if let Some(ref ev) = tokens[token_idx].entity_value {
377 let marks = parse_entity_marks(ev);
378 for mark in marks {
379 match mark {
380 EntityMark::Open {
381 cluster,
382 entity_type,
383 self_close,
384 } => {
385 if self_close {
386 let mut m = Mention::new("", char_start, char_end);
387 if tokens[token_idx].is_empty_node {
388 m.mention_type = Some(MentionType::Zero);
389 }
390 if let Some(et) = entity_type {
391 m.entity_type = Some(et);
392 }
393 clusters.entry(cluster).or_default().push(m);
394 } else {
395 open.entry(cluster).or_default().push(OpenMention {
396 start_char: char_start,
397 entity_type,
398 is_empty_node: tokens[token_idx].is_empty_node,
399 });
400 }
401 }
402 EntityMark::Close { cluster } => {
403 let Some(stack) = open.get_mut(&cluster) else {
404 return Err(Error::InvalidInput(format!(
405 "CorefUD parse error: closing Entity for {} with no open mention",
406 cluster
407 )));
408 };
409 let Some(opened) = stack.pop() else {
410 return Err(Error::InvalidInput(format!(
411 "CorefUD parse error: closing Entity for {} with empty stack",
412 cluster
413 )));
414 };
415
416 let mut m = Mention::new("", opened.start_char, char_end);
417 if opened.is_empty_node
418 && tokens[token_idx].is_empty_node
419 && opened.start_char == char_end
420 {
421 m.mention_type = Some(MentionType::Zero);
422 }
423 if let Some(et) = opened.entity_type {
424 m.entity_type = Some(et);
425 }
426 clusters.entry(cluster).or_default().push(m);
427 }
428 }
429 }
430 }
431 }
432
433 flush_doc(
435 &mut docs,
436 &mut doc_idx,
437 &mut current_doc_id,
438 &mut text,
439 &mut clusters,
440 &mut open,
441 )?;
442
443 if docs.is_empty() {
444 return Err(Error::InvalidInput(
445 "CorefUD CoNLL-U contains no documents".to_string(),
446 ));
447 }
448
449 Ok(docs)
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct GapExample {
459 pub id: String,
461 pub text: String,
463 pub pronoun: String,
465 pub pronoun_offset: usize,
467 pub name_a: String,
469 pub offset_a: usize,
471 pub coref_a: bool,
473 pub name_b: String,
475 pub offset_b: usize,
477 pub coref_b: bool,
479 pub url: Option<String>,
481}
482
483impl GapExample {
484 #[must_use]
488 pub fn to_coref_document(&self) -> CorefDocument {
489 let mut chains = Vec::new();
490
491 let pronoun_mention = Mention::with_type(
493 &self.pronoun,
494 self.pronoun_offset,
495 self.pronoun_offset + self.pronoun.len(),
496 MentionType::Pronominal,
497 );
498
499 let mention_a = Mention::with_type(
501 &self.name_a,
502 self.offset_a,
503 self.offset_a + self.name_a.len(),
504 MentionType::Proper,
505 );
506
507 let mention_b = Mention::with_type(
508 &self.name_b,
509 self.offset_b,
510 self.offset_b + self.name_b.len(),
511 MentionType::Proper,
512 );
513
514 if self.coref_a {
516 chains.push(CorefChain::new(vec![mention_a, pronoun_mention.clone()]));
518 chains.push(CorefChain::singleton(mention_b));
519 } else if self.coref_b {
520 chains.push(CorefChain::singleton(mention_a));
522 chains.push(CorefChain::new(vec![mention_b, pronoun_mention.clone()]));
523 } else {
524 chains.push(CorefChain::singleton(mention_a));
526 chains.push(CorefChain::singleton(mention_b));
527 chains.push(CorefChain::singleton(pronoun_mention));
528 }
529
530 CorefDocument::with_id(&self.text, &self.id, chains)
531 }
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct PreCoDocument {
541 pub id: String,
543 pub sentences: Vec<Vec<String>>,
545 pub mentions: Vec<(usize, usize, usize, usize)>,
547}
548
549impl PreCoDocument {
550 #[must_use]
552 pub fn to_coref_document(&self) -> CorefDocument {
553 let mut text = String::new();
555 let mut sentence_offsets: Vec<usize> = Vec::new();
556 let mut token_offsets: Vec<Vec<(usize, usize)>> = Vec::new();
557
558 for sentence in &self.sentences {
559 sentence_offsets.push(text.len());
560 let mut sent_offsets = Vec::new();
561
562 for (i, token) in sentence.iter().enumerate() {
563 if i > 0 {
564 text.push(' ');
565 }
566 let start = text.len();
567 text.push_str(token);
568 let end = text.len();
569 sent_offsets.push((start, end));
570 }
571 text.push(' ');
572 token_offsets.push(sent_offsets);
573 }
574
575 let mut clusters: HashMap<usize, Vec<Mention>> = HashMap::new();
577
578 for &(sent_idx, start_tok, end_tok, cluster_id) in &self.mentions {
579 if sent_idx >= token_offsets.len() {
580 continue;
581 }
582 let sent_tokens = &token_offsets[sent_idx];
583 if start_tok >= sent_tokens.len() || end_tok > sent_tokens.len() {
584 continue;
585 }
586
587 let byte_start = sent_tokens[start_tok].0;
589 let byte_end = sent_tokens[end_tok.saturating_sub(1).max(start_tok)].1;
590 let mention_text = text[byte_start..byte_end].to_string();
591
592 let char_start = text[..byte_start].chars().count();
594 let char_end = char_start + mention_text.chars().count();
595
596 let mention = Mention::new(mention_text, char_start, char_end);
597 clusters.entry(cluster_id).or_default().push(mention);
598 }
599
600 let chains: Vec<CorefChain> = clusters
602 .into_iter()
603 .map(|(id, mentions)| CorefChain::with_id(mentions, id as u64))
604 .collect();
605
606 CorefDocument::with_id(text, &self.id, chains)
607 }
608}
609
610pub struct CorefLoader {
619 inner: super::loader::DatasetLoader,
620}
621
622impl CorefLoader {
623 pub fn new() -> Result<Self> {
625 Ok(Self {
626 inner: super::loader::DatasetLoader::new()?,
627 })
628 }
629
630 pub fn with_cache_dir(cache_dir: impl Into<PathBuf>) -> Result<Self> {
632 Ok(Self {
633 inner: super::loader::DatasetLoader::with_cache_dir(cache_dir)?,
634 })
635 }
636
637 pub fn load_gap(&self) -> Result<Vec<CorefDocument>> {
639 self.inner.load_coref(DatasetId::GAP)
640 }
641
642 pub fn load_gap_examples(&self) -> Result<Vec<GapExample>> {
644 let gap = super::loader::LoadableDatasetId::try_from(DatasetId::GAP)?;
645 let cache_path = self.inner.cache_path(gap);
646
647 if !cache_path.exists() {
648 return Err(Error::InvalidInput(format!(
649 "GAP dataset not cached at {:?}",
650 cache_path
651 )));
652 }
653
654 let content = fs::read_to_string(&cache_path)
655 .map_err(|e| Error::InvalidInput(format!("Failed to read {:?}: {}", cache_path, e)))?;
656
657 parse_gap_tsv(&content)
658 }
659
660 pub fn load_preco(&self) -> Result<Vec<CorefDocument>> {
662 self.inner.load_coref(DatasetId::PreCo)
663 }
664
665 pub fn load_corefud_from_path(
670 &self,
671 path: impl AsRef<std::path::Path>,
672 ) -> Result<Vec<CorefDocument>> {
673 let path = path.as_ref();
674 let content = fs::read_to_string(path)
675 .map_err(|e| Error::InvalidInput(format!("Failed to read {:?}: {}", path, e)))?;
676 parse_corefud_conllu(&content)
677 }
678
679 #[must_use]
681 pub fn is_cached(&self, id: DatasetId) -> bool {
682 match super::loader::LoadableDatasetId::try_from(id) {
683 Ok(loadable) => self.inner.is_cached(loadable),
684 Err(_) => false,
685 }
686 }
687
688 #[must_use]
690 pub fn dataset_loader(&self) -> &super::loader::DatasetLoader {
691 &self.inner
692 }
693}
694
695impl Default for CorefLoader {
696 fn default() -> Self {
697 Self::new().expect("Failed to create CorefLoader")
698 }
699}
700
701pub fn parse_gap_tsv(content: &str) -> Result<Vec<GapExample>> {
711 let mut examples = Vec::new();
712 let mut first_line = true;
713
714 for line in content.lines() {
715 if first_line {
717 first_line = false;
718 continue;
719 }
720
721 let parts: Vec<&str> = line.split('\t').collect();
722 if parts.len() < 10 {
723 continue;
724 }
725
726 let id = parts[0].to_string();
727 let text = parts[1].to_string();
728 let pronoun = parts[2].to_string();
729 let pronoun_offset: usize = parts[3].parse().unwrap_or(0);
730 let name_a = parts[4].to_string();
731 let offset_a: usize = parts[5].parse().unwrap_or(0);
732 let coref_a = parts[6].to_lowercase() == "true";
733 let name_b = parts[7].to_string();
734 let offset_b: usize = parts[8].parse().unwrap_or(0);
735 let coref_b = parts[9].to_lowercase() == "true";
736 let url = parts.get(10).map(|s| s.to_string());
737
738 examples.push(GapExample {
739 id,
740 text,
741 pronoun,
742 pronoun_offset,
743 name_a,
744 offset_a,
745 coref_a,
746 name_b,
747 offset_b,
748 coref_b,
749 url,
750 });
751 }
752
753 Ok(examples)
754}
755
756pub fn parse_preco_json(content: &str) -> Result<Vec<PreCoDocument>> {
759 let parsed: serde_json::Value = serde_json::from_str(content)
760 .map_err(|e| Error::InvalidInput(format!("Invalid PreCo JSON: {}", e)))?;
761
762 let mut docs = Vec::new();
763
764 if let Some(doc_array) = parsed.as_array() {
765 for (idx, doc) in doc_array.iter().enumerate() {
766 let sentences: Vec<Vec<String>> = doc
768 .get("sentences")
769 .and_then(|s| s.as_array())
770 .map(|arr| {
771 arr.iter()
772 .filter_map(|sent| {
773 sent.as_array().map(|tokens| {
774 tokens
775 .iter()
776 .filter_map(|t| t.as_str().map(String::from))
777 .collect()
778 })
779 })
780 .collect()
781 })
782 .unwrap_or_default();
783
784 let mentions: Vec<(usize, usize, usize, usize)> =
786 doc.get("mention_clusters")
787 .and_then(|m| m.as_array())
788 .map(|clusters| {
789 clusters
790 .iter()
791 .enumerate()
792 .flat_map(|(cluster_id, cluster)| {
793 cluster.as_array().into_iter().flatten().filter_map(
794 move |mention| {
795 let arr = mention.as_array()?;
796 if arr.len() >= 3 {
797 Some((
798 arr[0].as_u64()? as usize,
799 arr[1].as_u64()? as usize,
800 arr[2].as_u64()? as usize,
801 cluster_id,
802 ))
803 } else {
804 None
805 }
806 },
807 )
808 })
809 .collect()
810 })
811 .unwrap_or_default();
812
813 let id = doc
814 .get("id")
815 .and_then(|i| i.as_str())
816 .unwrap_or(&format!("doc_{}", idx))
817 .to_string();
818
819 docs.push(PreCoDocument {
820 id,
821 sentences,
822 mentions,
823 });
824 }
825 }
826
827 if docs.is_empty() {
828 return Err(Error::InvalidInput(
829 "PreCo JSON contains no valid documents".to_string(),
830 ));
831 }
832
833 Ok(docs)
834}
835
836#[must_use]
844pub fn synthetic_coref_dataset(num_docs: usize) -> Vec<CorefDocument> {
845 let templates = [
846 (
848 "John Smith went to the store. He bought some milk.",
849 vec![
850 ("John Smith", 0, 10, 0),
851 ("He", 35, 37, 0),
852 ],
853 ),
854 (
856 "Mary called Bob. She asked him about the meeting.",
857 vec![
858 ("Mary", 0, 4, 0),
859 ("She", 17, 20, 0),
860 ("Bob", 12, 15, 1),
861 ("him", 27, 30, 1),
862 ],
863 ),
864 (
866 "The CEO announced the merger. She said the company would benefit. The executive was confident.",
867 vec![
868 ("The CEO", 0, 7, 0),
869 ("She", 30, 33, 0),
870 ("The executive", 68, 81, 0),
871 ],
872 ),
873 (
875 "Apple released a new iPhone. The tech giant's device sold well.",
876 vec![
877 ("Apple", 0, 5, 0),
878 ("The tech giant", 29, 43, 0),
879 ("iPhone", 21, 27, 1),
880 ("device", 46, 52, 1),
881 ],
882 ),
883 (
885 "The weather was nice. Sarah went for a walk in the park.",
886 vec![
887 ("The weather", 0, 11, 0),
888 ("Sarah", 22, 27, 1),
889 ("the park", 47, 55, 2),
890 ],
891 ),
892 ];
893
894 let mut docs = Vec::new();
895
896 for i in 0..num_docs {
897 let (text, mentions) = &templates[i % templates.len()];
898
899 let mut clusters: HashMap<usize, Vec<Mention>> = HashMap::new();
901 for &(mention_text, start, end, cluster_id) in mentions {
902 let mention = Mention::new(mention_text, start, end);
903 clusters.entry(cluster_id).or_default().push(mention);
904 }
905
906 let chains: Vec<CorefChain> = clusters
907 .into_iter()
908 .map(|(id, mentions)| CorefChain::with_id(mentions, id as u64))
909 .collect();
910
911 docs.push(CorefDocument::with_id(
912 *text,
913 format!("synthetic_{}", i),
914 chains,
915 ));
916 }
917
918 docs
919}
920
921#[must_use]
923pub fn domain_specific_coref_dataset(domain: &str) -> Vec<CorefDocument> {
924 match domain {
925 "biomedical" => biomedical_coref_examples(),
926 "legal" => legal_coref_examples(),
927 "news" => news_coref_examples(),
928 _ => synthetic_coref_dataset(5),
929 }
930}
931
932fn biomedical_coref_examples() -> Vec<CorefDocument> {
934 vec![
935 CorefDocument::with_id(
936 "BRCA1 is a tumor suppressor gene. It plays a role in DNA repair. The gene is frequently mutated in breast cancer.",
937 "bio_1",
938 vec![CorefChain::new(vec![
939 Mention::new("BRCA1", 0, 5),
940 Mention::new("It", 34, 36),
941 Mention::new("The gene", 62, 70),
942 ])],
943 ),
944 CorefDocument::with_id(
945 "The patient presented with chest pain. She was diagnosed with myocardial infarction. The woman received immediate treatment.",
946 "bio_2",
947 vec![
948 CorefChain::new(vec![
949 Mention::new("The patient", 0, 11),
950 Mention::new("She", 39, 42),
951 Mention::new("The woman", 85, 94),
952 ]),
953 CorefChain::singleton(Mention::new("myocardial infarction", 62, 83)),
954 ],
955 ),
956 CorefDocument::with_id(
957 "Aspirin inhibits COX-1 and COX-2. The drug reduces inflammation. It is commonly used for pain relief.",
958 "bio_3",
959 vec![
960 CorefChain::new(vec![
961 Mention::new("Aspirin", 0, 7),
962 Mention::new("The drug", 35, 43),
963 Mention::new("It", 65, 67),
964 ]),
965 CorefChain::singleton(Mention::new("COX-1", 17, 22)),
966 CorefChain::singleton(Mention::new("COX-2", 27, 32)),
967 ],
968 ),
969 ]
970}
971
972fn legal_coref_examples() -> Vec<CorefDocument> {
974 vec![
975 CorefDocument::with_id(
976 "The defendant entered into a contract with the plaintiff. He failed to deliver the goods. The accused claimed force majeure.",
977 "legal_1",
978 vec![
979 CorefChain::new(vec![
980 Mention::new("The defendant", 0, 13),
981 Mention::new("He", 58, 60),
982 Mention::new("The accused", 89, 100),
983 ]),
984 CorefChain::singleton(Mention::new("the plaintiff", 43, 56)),
985 ],
986 ),
987 CorefDocument::with_id(
988 "Article 5 of the Treaty governs this matter. It states that parties must negotiate in good faith. The provision has been interpreted broadly.",
989 "legal_2",
990 vec![CorefChain::new(vec![
991 Mention::new("Article 5 of the Treaty", 0, 23),
992 Mention::new("It", 45, 47),
993 Mention::new("The provision", 99, 112),
994 ])],
995 ),
996 ]
997}
998
999fn news_coref_examples() -> Vec<CorefDocument> {
1001 vec![
1002 CorefDocument::with_id(
1003 "President Biden met with Chancellor Scholz. The American leader discussed trade. He emphasized cooperation. Biden later held a press conference.",
1004 "news_1",
1005 vec![
1006 CorefChain::new(vec![
1007 Mention::new("President Biden", 0, 14),
1008 Mention::new("The American leader", 44, 63),
1009 Mention::new("He", 81, 83),
1010 Mention::new("Biden", 107, 112),
1011 ]),
1012 CorefChain::singleton(Mention::new("Chancellor Scholz", 25, 42)),
1013 ],
1014 ),
1015 CorefDocument::with_id(
1016 "Nvidia announced record quarterly earnings. The chipmaker exceeded expectations. Its stock rose 5% in after-hours trading.",
1017 "news_2",
1018 vec![
1019 CorefChain::new(vec![
1020 Mention::new("Nvidia", 0, 6),
1021 Mention::new("The chipmaker", 44, 57),
1022 Mention::new("Its", 80, 83),
1023 ]),
1024 ],
1025 ),
1026 CorefDocument::with_id(
1027 "The hurricane made landfall in Florida. It caused widespread damage. The storm was Category 4. Authorities ordered evacuations before it arrived.",
1028 "news_3",
1029 vec![
1030 CorefChain::new(vec![
1031 Mention::new("The hurricane", 0, 13),
1032 Mention::new("It", 40, 42),
1033 Mention::new("The storm", 68, 77),
1034 Mention::new("it", 133, 135),
1035 ]),
1036 ],
1037 ),
1038 ]
1039}
1040
1041#[must_use]
1045pub fn adversarial_coref_examples() -> Vec<(CorefDocument, CorefDocument, &'static str)> {
1046 vec![
1047 (
1049 CorefDocument::new(
1050 "John saw Mary. He waved.",
1051 vec![
1052 CorefChain::new(vec![Mention::new("John", 0, 4), Mention::new("He", 15, 17)]),
1053 CorefChain::singleton(Mention::new("Mary", 9, 13)),
1054 ],
1055 ),
1056 CorefDocument::new(
1057 "John saw Mary. He waved.",
1058 vec![CorefChain::new(vec![
1059 Mention::new("John", 0, 4),
1060 Mention::new("Mary", 9, 13),
1061 Mention::new("He", 15, 17),
1062 ])],
1063 ),
1064 "over-clustering",
1065 ),
1066 (
1068 CorefDocument::new(
1069 "Barack Obama gave a speech. The president was eloquent. Obama smiled.",
1070 vec![CorefChain::new(vec![
1071 Mention::new("Barack Obama", 0, 12),
1072 Mention::new("The president", 28, 41),
1073 Mention::new("Obama", 56, 61),
1074 ])],
1075 ),
1076 CorefDocument::new(
1077 "Barack Obama gave a speech. The president was eloquent. Obama smiled.",
1078 vec![
1079 CorefChain::new(vec![
1080 Mention::new("Barack Obama", 0, 12),
1081 Mention::new("Obama", 56, 61),
1082 ]),
1083 CorefChain::singleton(Mention::new("The president", 28, 41)),
1084 ],
1085 ),
1086 "under-clustering",
1087 ),
1088 (
1090 CorefDocument::new(
1091 "The dog ran. It was fast. The animal stopped.",
1092 vec![CorefChain::new(vec![
1093 Mention::new("The dog", 0, 7),
1094 Mention::new("It", 13, 15),
1095 Mention::new("The animal", 26, 36),
1096 ])],
1097 ),
1098 CorefDocument::new(
1099 "The dog ran. It was fast. The animal stopped.",
1100 vec![CorefChain::new(vec![
1101 Mention::new("The dog", 0, 7),
1102 Mention::new("It", 13, 15),
1103 ])], ),
1105 "missed-mention",
1106 ),
1107 (
1109 CorefDocument::new(
1110 "A B C",
1111 vec![
1112 CorefChain::singleton(Mention::new("A", 0, 1)),
1113 CorefChain::singleton(Mention::new("B", 2, 3)),
1114 CorefChain::singleton(Mention::new("C", 4, 5)),
1115 ],
1116 ),
1117 CorefDocument::new(
1118 "A B C",
1119 vec![CorefChain::new(vec![
1120 Mention::new("A", 0, 1),
1121 Mention::new("B", 2, 3),
1122 Mention::new("C", 4, 5),
1123 ])],
1124 ),
1125 "singletons-vs-one-cluster",
1126 ),
1127 ]
1128}
1129
1130pub fn parse_bookcoref_json(content: &str) -> Result<Vec<CorefDocument>> {
1157 let mut documents = Vec::new();
1158
1159 if content.trim().starts_with('[') {
1161 let parsed: Vec<serde_json::Value> = serde_json::from_str(content).map_err(|e| {
1163 Error::InvalidInput(format!("Failed to parse BookCoref JSON array: {}", e))
1164 })?;
1165 for item in parsed {
1166 if let Some(doc) = parse_bookcoref_item(&item)? {
1167 documents.push(doc);
1168 }
1169 }
1170 } else {
1171 for line in content.lines() {
1173 let line = line.trim();
1174 if line.is_empty() {
1175 continue;
1176 }
1177
1178 let item: serde_json::Value = serde_json::from_str(line).map_err(|e| {
1179 Error::InvalidInput(format!("Failed to parse BookCoref JSONL: {}", e))
1180 })?;
1181
1182 if let Some(doc) = parse_bookcoref_item(&item)? {
1183 documents.push(doc);
1184 }
1185 }
1186 }
1187
1188 if documents.is_empty() {
1189 return Err(Error::InvalidInput(
1190 "BookCoref content contains no valid documents".to_string(),
1191 ));
1192 }
1193
1194 Ok(documents)
1195}
1196
1197fn parse_bookcoref_item(item: &serde_json::Value) -> Result<Option<CorefDocument>> {
1199 let sentences = match item.get("sentences").and_then(|v| v.as_array()) {
1201 Some(s) => s,
1202 None => return Ok(None),
1203 };
1204
1205 let clusters = match item.get("clusters").and_then(|v| v.as_array()) {
1207 Some(c) => c,
1208 None => return Ok(None),
1209 };
1210
1211 let mut tokens: Vec<String> = Vec::new();
1213 for sentence in sentences {
1214 if let Some(sent_tokens) = sentence.as_array() {
1215 for token in sent_tokens {
1216 if let Some(t) = token.as_str() {
1217 tokens.push(t.to_string());
1218 }
1219 }
1220 }
1221 }
1222
1223 if tokens.is_empty() {
1224 return Ok(None);
1225 }
1226
1227 let mut text = String::new();
1230 let mut token_char_starts: Vec<usize> = Vec::new();
1231 let mut token_char_ends: Vec<usize> = Vec::new();
1232
1233 for (i, token) in tokens.iter().enumerate() {
1234 if i > 0 {
1235 text.push(' ');
1236 }
1237 let start = text.chars().count();
1238 text.push_str(token);
1239 let end = text.chars().count();
1240 token_char_starts.push(start);
1241 token_char_ends.push(end);
1242 }
1243
1244 let mut coref_chains = Vec::new();
1246 for cluster in clusters {
1247 if let Some(spans) = cluster.as_array() {
1248 let mut mentions = Vec::new();
1249 for span in spans {
1250 if let Some(span_arr) = span.as_array() {
1251 if span_arr.len() >= 2 {
1252 let start_tok = span_arr[0].as_u64().unwrap_or(0) as usize;
1253 let end_tok = span_arr[1].as_u64().unwrap_or(0) as usize;
1254
1255 if start_tok < token_char_starts.len() && end_tok < token_char_ends.len() {
1257 let char_start = token_char_starts[start_tok];
1258 let char_end = token_char_ends[end_tok];
1259
1260 let mention_text: String = text
1262 .chars()
1263 .skip(char_start)
1264 .take(char_end - char_start)
1265 .collect();
1266
1267 mentions.push(Mention::new(&mention_text, char_start, char_end));
1268 }
1269 }
1270 }
1271 }
1272
1273 if !mentions.is_empty() {
1274 coref_chains.push(CorefChain::new(mentions));
1275 }
1276 }
1277 }
1278
1279 Ok(Some(CorefDocument::new(&text, coref_chains)))
1280}
1281
1282pub fn parse_ecb_plus_coref(content: &str) -> Result<Vec<CorefDocument>> {
1291 if content.as_bytes().starts_with(b"PK\x03\x04") {
1293 return parse_ecb_plus_zip(content.as_bytes());
1294 }
1295 parse_ecb_plus_sentence_index(content)
1296}
1297
1298pub fn parse_ecb_plus_zip(data: &[u8]) -> Result<Vec<CorefDocument>> {
1306 use std::io::Cursor;
1307
1308 let cursor = Cursor::new(data);
1309 let mut archive = zip::ZipArchive::new(cursor)
1310 .map_err(|e| Error::InvalidInput(format!("Failed to open ECB+ zip: {}", e)))?;
1311
1312 let mut all_docs: Vec<CorefDocument> = Vec::new();
1313
1314 let file_names: Vec<String> = (0..archive.len())
1316 .filter_map(|i| {
1317 let f = archive.by_index(i).ok()?;
1318 let name = f.name().to_string();
1319 if name.ends_with(".xml") {
1320 Some(name)
1321 } else {
1322 None
1323 }
1324 })
1325 .collect();
1326
1327 for name in &file_names {
1328 let mut file = archive
1329 .by_name(name)
1330 .map_err(|e| Error::InvalidInput(format!("Failed to read {} from zip: {}", name, e)))?;
1331
1332 let mut xml_content = String::new();
1333 std::io::Read::read_to_string(&mut file, &mut xml_content)
1334 .map_err(|e| Error::InvalidInput(format!("Failed to read XML {}: {}", name, e)))?;
1335
1336 let parts: Vec<&str> = name.split('/').collect();
1338 let (topic, doc_name) = if parts.len() >= 3 {
1339 (
1340 parts[parts.len() - 2].to_string(),
1341 parts[parts.len() - 1].trim_end_matches(".xml").to_string(),
1342 )
1343 } else if let Some(fname) = name.split('/').next_back() {
1344 let base = fname.trim_end_matches(".xml");
1345 if let Some((t, _)) = base.split_once('_') {
1347 (t.to_string(), base.to_string())
1348 } else {
1349 ("unknown".to_string(), base.to_string())
1350 }
1351 } else {
1352 continue;
1353 };
1354
1355 match parse_ecb_plus_xml(&xml_content, &topic, &doc_name) {
1356 Ok(doc) => all_docs.push(doc),
1357 Err(e) => {
1358 log::debug!("Skipping {}: {}", name, e);
1359 }
1360 }
1361 }
1362
1363 if all_docs.is_empty() {
1364 return Err(Error::InvalidInput(
1365 "ECB+ zip contains no parseable XML documents".to_string(),
1366 ));
1367 }
1368
1369 all_docs.sort_by(|a, b| a.doc_id.cmp(&b.doc_id));
1371 Ok(all_docs)
1372}
1373
1374fn parse_ecb_plus_xml(xml: &str, topic: &str, doc_name: &str) -> Result<CorefDocument> {
1395 use quick_xml::events::Event;
1396 use quick_xml::Reader;
1397 use std::collections::{BTreeMap, BTreeSet};
1398
1399 let mut tokens: BTreeMap<u32, (u32, u32, String)> = BTreeMap::new();
1401 let mut mentions: HashMap<u32, Vec<u32>> = HashMap::new();
1403 let mut clusters: HashMap<String, BTreeSet<u32>> = HashMap::new();
1405
1406 let mut reader = Reader::from_str(xml);
1407 reader.config_mut().trim_text(true);
1408
1409 let mut in_token = false;
1410 let mut cur_t_id = 0u32;
1411 let mut cur_sentence = 0u32;
1412 let mut cur_number = 0u32;
1413 let mut current_token_text = String::new();
1414
1415 let mut in_markable = false;
1416 let mut cur_m_id = 0u32;
1417 let mut current_mention_tokens: Vec<u32> = Vec::new();
1418
1419 let mut in_coref = false;
1420 let mut cur_coref_note = String::new();
1421 let mut current_coref_mentions: Vec<u32> = Vec::new();
1422
1423 fn get_attr(e: &quick_xml::events::BytesStart<'_>, name: &str) -> Option<String> {
1424 e.attributes().flatten().find_map(|a| {
1425 if a.key.as_ref() == name.as_bytes() {
1426 Some(String::from_utf8_lossy(&a.value).to_string())
1427 } else {
1428 None
1429 }
1430 })
1431 }
1432
1433 fn is_markable_tag(name: &[u8]) -> bool {
1434 name.starts_with(b"ACTION_")
1435 || name.starts_with(b"HUMAN_")
1436 || name.starts_with(b"LOC_")
1437 || name.starts_with(b"TIME_")
1438 || name.starts_with(b"NEG_")
1439 }
1440
1441 #[allow(clippy::too_many_arguments)]
1442 fn handle_start(
1443 e: &quick_xml::events::BytesStart<'_>,
1444 in_token: &mut bool,
1445 cur_t_id: &mut u32,
1446 cur_sentence: &mut u32,
1447 cur_number: &mut u32,
1448 current_token_text: &mut String,
1449 in_markable: &mut bool,
1450 cur_m_id: &mut u32,
1451 current_mention_tokens: &mut Vec<u32>,
1452 in_coref: &mut bool,
1453 cur_coref_note: &mut String,
1454 current_coref_mentions: &mut Vec<u32>,
1455 ) {
1456 let tag = e.name();
1457 let tag_bytes = tag.as_ref();
1458
1459 if tag_bytes == b"token" {
1460 *in_token = true;
1461 *cur_t_id = get_attr(e, "t_id")
1462 .and_then(|v| v.parse().ok())
1463 .unwrap_or(0);
1464 *cur_sentence = get_attr(e, "sentence")
1465 .and_then(|v| v.parse().ok())
1466 .unwrap_or(0);
1467 *cur_number = get_attr(e, "number")
1468 .and_then(|v| v.parse().ok())
1469 .unwrap_or(0);
1470 current_token_text.clear();
1471 } else if tag_bytes == b"token_anchor" {
1472 if let Some(tid) = get_attr(e, "t_id").and_then(|v| v.parse::<u32>().ok()) {
1473 current_mention_tokens.push(tid);
1474 }
1475 } else if tag_bytes == b"source" || tag_bytes == b"target" {
1476 if let Some(mid) = get_attr(e, "m_id").and_then(|v| v.parse::<u32>().ok()) {
1477 current_coref_mentions.push(mid);
1478 }
1479 } else if tag_bytes == b"CROSS_DOC_COREF" {
1480 *in_coref = true;
1481 *cur_coref_note = get_attr(e, "note").unwrap_or_default();
1482 current_coref_mentions.clear();
1483 } else if is_markable_tag(tag_bytes) {
1484 *in_markable = true;
1485 *cur_m_id = get_attr(e, "m_id")
1486 .and_then(|v| v.parse().ok())
1487 .unwrap_or(0);
1488 current_mention_tokens.clear();
1489 }
1490 }
1491
1492 let mut buf = Vec::new();
1493 loop {
1494 match reader.read_event_into(&mut buf) {
1495 Ok(Event::Start(ref e)) => {
1496 handle_start(
1497 e,
1498 &mut in_token,
1499 &mut cur_t_id,
1500 &mut cur_sentence,
1501 &mut cur_number,
1502 &mut current_token_text,
1503 &mut in_markable,
1504 &mut cur_m_id,
1505 &mut current_mention_tokens,
1506 &mut in_coref,
1507 &mut cur_coref_note,
1508 &mut current_coref_mentions,
1509 );
1510 }
1511 Ok(Event::Empty(ref e)) => {
1512 handle_start(
1514 e,
1515 &mut in_token,
1516 &mut cur_t_id,
1517 &mut cur_sentence,
1518 &mut cur_number,
1519 &mut current_token_text,
1520 &mut in_markable,
1521 &mut cur_m_id,
1522 &mut current_mention_tokens,
1523 &mut in_coref,
1524 &mut cur_coref_note,
1525 &mut current_coref_mentions,
1526 );
1527 let name = e.name();
1529 let tag_bytes = name.as_ref();
1530 if is_markable_tag(tag_bytes) && !current_mention_tokens.is_empty() {
1531 mentions.insert(cur_m_id, current_mention_tokens.clone());
1532 current_mention_tokens.clear();
1533 in_markable = false;
1534 }
1535 }
1537 Ok(Event::Text(ref e)) if in_token => {
1538 current_token_text.push_str(&e.unescape().unwrap_or_default());
1539 }
1540 Ok(Event::End(ref e)) => {
1541 let name = e.name();
1542 let tag_bytes = name.as_ref();
1543 if tag_bytes == b"token" && in_token {
1544 tokens.insert(
1545 cur_t_id,
1546 (cur_sentence, cur_number, current_token_text.clone()),
1547 );
1548 in_token = false;
1549 } else if tag_bytes == b"CROSS_DOC_COREF" && in_coref {
1550 if !cur_coref_note.is_empty() {
1551 let entry = clusters.entry(cur_coref_note.clone()).or_default();
1552 for mid in ¤t_coref_mentions {
1553 entry.insert(*mid);
1554 }
1555 }
1556 current_coref_mentions.clear();
1557 in_coref = false;
1558 } else if is_markable_tag(tag_bytes) && in_markable {
1559 if !current_mention_tokens.is_empty() {
1560 mentions.insert(cur_m_id, current_mention_tokens.clone());
1561 }
1562 current_mention_tokens.clear();
1563 in_markable = false;
1564 }
1565 }
1566 Ok(Event::Eof) => break,
1567 Err(e) => {
1568 return Err(Error::InvalidInput(format!(
1569 "XML parse error in {}: {}",
1570 doc_name, e
1571 )));
1572 }
1573 _ => {}
1574 }
1575 buf.clear();
1576 }
1577
1578 if tokens.is_empty() {
1579 return Err(Error::InvalidInput(format!(
1580 "ECB+ XML {} contains no tokens",
1581 doc_name
1582 )));
1583 }
1584
1585 let mut text = String::new();
1587 let mut token_char_starts: BTreeMap<u32, usize> = BTreeMap::new();
1588 let mut token_char_ends: BTreeMap<u32, usize> = BTreeMap::new();
1589
1590 for (&t_id, (_sent, _num, tok_text)) in &tokens {
1591 if !text.is_empty() {
1592 text.push(' ');
1593 }
1594 let start = text.chars().count();
1595 text.push_str(tok_text);
1596 let end = text.chars().count();
1597 token_char_starts.insert(t_id, start);
1598 token_char_ends.insert(t_id, end);
1599 }
1600
1601 let mut coref_chains: Vec<CorefChain> = Vec::new();
1603
1604 let mut cluster_keys: Vec<_> = clusters.keys().cloned().collect();
1605 cluster_keys.sort();
1606
1607 for cluster_key in cluster_keys {
1608 let m_ids = &clusters[&cluster_key];
1609 let mut chain_mentions: Vec<Mention> = Vec::new();
1610
1611 for &m_id in m_ids {
1612 if let Some(t_ids) = mentions.get(&m_id) {
1613 if t_ids.is_empty() {
1614 continue;
1615 }
1616 let start = t_ids
1618 .iter()
1619 .filter_map(|tid| token_char_starts.get(tid))
1620 .min()
1621 .copied();
1622 let end = t_ids
1623 .iter()
1624 .filter_map(|tid| token_char_ends.get(tid))
1625 .max()
1626 .copied();
1627
1628 if let (Some(s), Some(e)) = (start, end) {
1629 let mention_text: String = text.chars().skip(s).take(e - s).collect();
1630 chain_mentions.push(Mention {
1631 text: mention_text,
1632 start: s,
1633 end: e,
1634 head_start: None,
1635 head_end: None,
1636 entity_type: Some("EVENT".to_string()),
1637 mention_type: Some(MentionType::Proper),
1638 });
1639 }
1640 }
1641 }
1642
1643 if !chain_mentions.is_empty() {
1644 let cid = cluster_key.parse::<u64>().unwrap_or(0);
1645 coref_chains.push(CorefChain::with_id(chain_mentions, cid));
1646 }
1647 }
1648
1649 let doc_id = format!("{}_{}", topic, doc_name);
1650 Ok(CorefDocument::with_id(&text, doc_id, coref_chains))
1651}
1652
1653fn parse_ecb_plus_sentence_index(content: &str) -> Result<Vec<CorefDocument>> {
1664 let mut docs: HashMap<(String, String), ecb_plus_acc::DocAcc> = HashMap::new();
1666
1667 let mut lines = content.lines();
1668
1669 if let Some(header) = lines.next() {
1671 let lower = header.to_lowercase();
1673 if !lower.contains("token") && !lower.contains("topic") {
1674 parse_ecb_plus_line(header, &mut docs);
1677 }
1678 }
1679
1680 for line in lines {
1681 parse_ecb_plus_line(line, &mut docs);
1682 }
1683
1684 if docs.is_empty() {
1685 return Err(Error::InvalidInput(
1686 "ECB+ CSV contains no valid token rows".to_string(),
1687 ));
1688 }
1689
1690 let mut result: Vec<CorefDocument> = Vec::new();
1692
1693 let mut doc_keys: Vec<_> = docs.keys().cloned().collect();
1695 doc_keys.sort();
1696
1697 for key in doc_keys {
1698 let acc = docs.remove(&key).unwrap();
1699 let (topic, file) = &key;
1700
1701 let mut text = String::new();
1703 let mut token_char_offsets: HashMap<(u32, u32), (usize, usize)> = HashMap::new();
1704
1705 for (&(sent, tok), token_text) in &acc.tokens {
1706 let start = text.chars().count();
1707 text.push_str(token_text);
1708 let end = text.chars().count();
1709 token_char_offsets.insert((sent, tok), (start, end));
1710 text.push(' ');
1711 }
1712
1713 let mut coref_chains: Vec<CorefChain> = Vec::new();
1715 let mut chain_ids: Vec<_> = acc.chains.keys().cloned().collect();
1716 chain_ids.sort();
1717
1718 for chain_id in chain_ids {
1719 let token_positions = &acc.chains[&chain_id];
1720 let mentions: Vec<Mention> = token_positions
1721 .iter()
1722 .filter_map(|pos| {
1723 let (start, end) = token_char_offsets.get(pos)?;
1724 let token_text = acc.tokens.get(pos)?;
1725 Some(Mention {
1726 text: token_text.clone(),
1727 start: *start,
1728 end: *end,
1729 head_start: None,
1730 head_end: None,
1731 entity_type: Some("EVENT".to_string()),
1732 mention_type: Some(MentionType::Proper),
1733 })
1734 })
1735 .collect();
1736
1737 if !mentions.is_empty() {
1738 coref_chains.push(CorefChain::new(mentions));
1739 }
1740 }
1741
1742 let doc = CorefDocument::with_id(&text, format!("{}_{}", topic, file), coref_chains);
1744 result.push(doc);
1745 }
1746
1747 Ok(result)
1748}
1749
1750fn parse_ecb_plus_line(line: &str, docs: &mut HashMap<(String, String), ecb_plus_acc::DocAcc>) {
1752 let line = line.trim();
1753 if line.is_empty() {
1754 return;
1755 }
1756
1757 let parts: Vec<&str> = line.split(',').collect();
1759 if parts.len() < 5 {
1760 return;
1761 }
1762
1763 let topic = parts[0].trim().to_string();
1764 let file = parts[1].trim().to_string();
1765 let sent_num: u32 = match parts[2].trim().parse() {
1766 Ok(n) => n,
1767 Err(_) => return, };
1769 let tok_num: u32 = match parts[3].trim().parse() {
1770 Ok(n) => n,
1771 Err(_) => return,
1772 };
1773 let token = parts[4].trim().to_string();
1774
1775 let chain_id = parts
1778 .get(7)
1779 .or_else(|| parts.get(6))
1780 .map(|s| s.trim())
1781 .filter(|s| !s.is_empty() && *s != "-" && *s != "_")
1782 .map(|s| s.to_string());
1783
1784 let acc = docs.entry((topic, file)).or_default();
1785 acc.tokens.insert((sent_num, tok_num), token);
1786 if let Some(cid) = chain_id {
1787 acc.chains.entry(cid).or_default().push((sent_num, tok_num));
1788 }
1789}
1790
1791mod ecb_plus_acc {
1793 use std::collections::{BTreeMap, HashMap};
1794
1795 #[derive(Default)]
1796 pub struct DocAcc {
1797 pub tokens: BTreeMap<(u32, u32), String>,
1798 pub chains: HashMap<String, Vec<(u32, u32)>>,
1799 }
1800}
1801
1802#[cfg(test)]
1807mod tests {
1808 use super::*;
1809
1810 #[test]
1811 fn test_gap_example_to_coref() {
1812 let example = GapExample {
1813 id: "test-1".to_string(),
1814 text: "John saw Mary. He waved.".to_string(),
1815 pronoun: "He".to_string(),
1816 pronoun_offset: 15,
1817 name_a: "John".to_string(),
1818 offset_a: 0,
1819 coref_a: true,
1820 name_b: "Mary".to_string(),
1821 offset_b: 9,
1822 coref_b: false,
1823 url: None,
1824 };
1825
1826 let doc = example.to_coref_document();
1827 assert_eq!(doc.mention_count(), 3);
1828 assert_eq!(doc.chain_count(), 2); }
1830
1831 #[test]
1832 fn test_gap_example_mention_types() {
1833 use crate::eval::coref::MentionType;
1834
1835 let example = GapExample {
1836 id: "test-2".to_string(),
1837 text: "Alice met Bob. She smiled.".to_string(),
1838 pronoun: "She".to_string(),
1839 pronoun_offset: 15,
1840 name_a: "Alice".to_string(),
1841 offset_a: 0,
1842 coref_a: true,
1843 name_b: "Bob".to_string(),
1844 offset_b: 10,
1845 coref_b: false,
1846 url: None,
1847 };
1848
1849 let doc = example.to_coref_document();
1850
1851 let all_mentions: Vec<_> = doc.chains.iter().flat_map(|c| &c.mentions).collect();
1853 assert_eq!(all_mentions.len(), 3);
1854
1855 let proper_count = all_mentions
1857 .iter()
1858 .filter(|m| m.mention_type == Some(MentionType::Proper))
1859 .count();
1860 assert_eq!(
1861 proper_count, 2,
1862 "Should have 2 proper noun mentions (Alice, Bob)"
1863 );
1864
1865 let pronominal_count = all_mentions
1867 .iter()
1868 .filter(|m| m.mention_type == Some(MentionType::Pronominal))
1869 .count();
1870 assert_eq!(
1871 pronominal_count, 1,
1872 "Should have 1 pronominal mention (She)"
1873 );
1874 }
1875
1876 #[test]
1877 fn test_synthetic_coref_dataset() {
1878 let docs = synthetic_coref_dataset(5);
1879 assert_eq!(docs.len(), 5);
1880
1881 for doc in &docs {
1882 assert!(!doc.text.is_empty());
1883 assert!(!doc.chains.is_empty());
1884 }
1885 }
1886
1887 #[test]
1888 fn test_adversarial_examples() {
1889 let examples = adversarial_coref_examples();
1890 assert!(!examples.is_empty());
1891
1892 for (gold, pred, name) in &examples {
1893 assert!(!gold.chains.is_empty(), "Gold chains empty for {}", name);
1894 assert!(!pred.chains.is_empty(), "Pred chains empty for {}", name);
1895 }
1896 }
1897
1898 #[test]
1899 fn test_gap_tsv_parsing() {
1900 let tsv = "ID\tText\tPronoun\tPronoun-offset\tA\tA-offset\tA-coref\tB\tB-offset\tB-coref\tURL\n\
1901 1\tJohn saw Mary. He waved.\tHe\t15\tJohn\t0\tTRUE\tMary\t9\tFALSE\thttps://example.com";
1902
1903 let examples = parse_gap_tsv(tsv).unwrap();
1904 assert_eq!(examples.len(), 1);
1905 assert_eq!(examples[0].id, "1");
1906 assert!(examples[0].coref_a);
1907 assert!(!examples[0].coref_b);
1908 }
1909
1910 #[test]
1911 fn test_bookcoref_json_parsing() {
1912 let json = r#"{"doc_key": "test_book_1", "gutenberg_key": "1", "sentences": [["Alice", "met", "Bob", "."], ["She", "waved", "."]], "clusters": [[[0, 0], [4, 4]], [[2, 2]]], "characters": [{"name": "Alice", "cluster": [[0, 0], [4, 4]]}]}"#;
1915
1916 let docs = parse_bookcoref_json(json).unwrap();
1917 assert_eq!(docs.len(), 1);
1918
1919 let doc = &docs[0];
1920 assert!(doc.text.contains("Alice"));
1922 assert!(doc.text.contains("She"));
1923
1924 assert_eq!(doc.chain_count(), 2);
1926
1927 let alice_cluster = doc
1929 .chains
1930 .iter()
1931 .find(|c| c.mentions.iter().any(|m| m.text == "Alice"));
1932 assert!(alice_cluster.is_some());
1933 assert_eq!(alice_cluster.unwrap().mentions.len(), 2);
1934
1935 let bob_cluster = doc
1937 .chains
1938 .iter()
1939 .find(|c| c.mentions.iter().any(|m| m.text == "Bob"));
1940 assert!(bob_cluster.is_some());
1941 assert_eq!(bob_cluster.unwrap().mentions.len(), 1);
1942 }
1943
1944 #[test]
1945 fn test_bookcoref_json_array_parsing() {
1946 let json_array = r#"[{"doc_key": "book1", "sentences": [["He", "ran", "."]], "clusters": [[[0, 0]]]}, {"doc_key": "book2", "sentences": [["She", "walked", "."]], "clusters": [[[0, 0]]]}]"#;
1948
1949 let docs = parse_bookcoref_json(json_array).unwrap();
1950 assert_eq!(docs.len(), 2);
1951 }
1952
1953 #[test]
1954 fn test_bookcoref_jsonl_parsing() {
1955 let jsonl = r#"{"doc_key": "book1", "sentences": [["He", "ran", "."]], "clusters": [[[0, 0]]]}
1957{"doc_key": "book2", "sentences": [["She", "walked", "."]], "clusters": [[[0, 0]]]}"#;
1958
1959 let docs = parse_bookcoref_json(jsonl).unwrap();
1960 assert_eq!(docs.len(), 2);
1961 }
1962
1963 #[test]
1964 fn test_corefud_conllu_parsing_multilingual_and_zero() {
1965 let conllu = r#"# newdoc id = doc_en
1975# sent_id = 1
19761 Marie _ PROPN _ _ 0 root _ Entity=(e1-person-1
19772 Curie _ PROPN _ _ 1 flat _ Entity=e1)
19783 met _ VERB _ _ 1 dep _ _
19794 Cher _ PROPN _ _ 3 obj _ Entity=(e2-person-1)
19805 . _ PUNCT _ _ 3 punct _ _
1981
1982# newdoc id = doc_multi
1983# sent_id = 1
19841 習近平 _ PROPN _ _ 0 root _ Entity=(e10-person-1)
19852 在 _ ADP _ _ 1 case _ _
19863 北京 _ PROPN _ _ 1 obl _ Entity=(e11-place-1)
19874 會見 _ VERB _ _ 1 dep _ _
19885 了 _ AUX _ _ 4 aux _ _
19896 普京 _ PROPN _ _ 4 obj _ Entity=(e12-person-1)
19907 。 _ PUNCT _ _ 4 punct _ _
19918.1 _ _ _ _ _ _ _ _ Entity=(e13-person-1)
1992
1993# newdoc id = doc_ar
1994# sent_id = 1
19951 محمد _ PROPN _ _ 0 root _ Entity=(e20-person-1
19962 بن _ PART _ _ 1 flat _ SpaceAfter=No
19973 سلمان _ PROPN _ _ 1 flat _ Entity=e20)
19984 . _ PUNCT _ _ 1 punct _ _
1999
2000# newdoc id = doc_ru
2001# sent_id = 1
20021 Путин _ PROPN _ _ 0 root _ Entity=(e30-person-1)
20032 встретился _ VERB _ _ 1 dep _ _
20043 с _ ADP _ _ 1 case _ _
20054 Си _ PROPN _ _ 1 obl _ Entity=(e31-person-1
20065 Цзиньпином _ PROPN _ _ 4 flat _ Entity=e31)
20076 . _ PUNCT _ _ 1 punct _ _
2008
2009# newdoc id = doc_hi
2010# sent_id = 1
20111 प्रधानमंत्री _ NOUN _ _ 0 root _ Entity=(e40-person-1
20122 शर्मा _ PROPN _ _ 1 flat _ Entity=e40)
20133 दिल्ली _ PROPN _ _ 1 obl _ Entity=(e41-place-1)
20144 में _ ADP _ _ 3 case _ _
20155 थे _ AUX _ _ 1 cop _ _
20166 । _ PUNCT _ _ 1 punct _ _
2017"#;
2018
2019 let docs = parse_corefud_conllu(conllu).unwrap();
2020 assert_eq!(docs.len(), 5);
2021
2022 let doc_en = docs
2024 .iter()
2025 .find(|d| d.doc_id.as_deref() == Some("doc_en"))
2026 .unwrap();
2027 let char_len = doc_en.text.chars().count();
2028 for m in doc_en.all_mentions() {
2029 assert!(m.start <= m.end);
2030 assert!(m.end <= char_len);
2031 }
2032
2033 let doc_multi = docs
2035 .iter()
2036 .find(|d| d.doc_id.as_deref() == Some("doc_multi"))
2037 .unwrap();
2038 let zeros: Vec<_> = doc_multi
2039 .all_mentions()
2040 .into_iter()
2041 .filter(|m| m.mention_type == Some(MentionType::Zero) || (m.start == m.end))
2042 .collect();
2043 assert!(
2044 !zeros.is_empty(),
2045 "Expected at least one zero/empty mention in doc_multi"
2046 );
2047
2048 let doc_ar = docs
2050 .iter()
2051 .find(|d| d.doc_id.as_deref() == Some("doc_ar"))
2052 .unwrap();
2053 assert!(
2054 doc_ar.text.contains("محمد بنسلمان") || doc_ar.text.contains("محمد بن سلمان"),
2055 "Arabic spacing should be Unicode-safe; got: {:?}",
2056 doc_ar.text
2057 );
2058 }
2059
2060 #[test]
2061 fn test_parse_ecb_plus_coref() {
2062 let csv = "\
2063Topic,File,Sentence Number,Token Number,Token,Lemma,Event Mention,Coreference Chain
20641,1ecb,0,0,The,the,,
20651,1ecb,0,1,earthquake,earthquake,ACT,1
20661,1ecb,0,2,struck,strike,ACT,2
20671,1ecb,0,3,at,at,,
20681,1ecb,0,4,dawn,dawn,,
20691,1ecb,1,0,A,a,,
20701,1ecb,1,1,tremor,tremor,ACT,2
20711,1ecb,1,2,was,be,,
20721,1ecb,1,3,felt,feel,ACT,
20731,2ecb,0,0,The,the,,
20741,2ecb,0,1,quake,quake,ACT,1
20751,2ecb,0,2,damaged,damage,ACT,3
20761,2ecb,0,3,buildings,building,,
2077";
2078 let docs = parse_ecb_plus_coref(csv).unwrap();
2079
2080 assert_eq!(docs.len(), 2);
2082
2083 let doc1 = docs
2084 .iter()
2085 .find(|d| d.doc_id.as_deref() == Some("1_1ecb"))
2086 .unwrap();
2087 let doc2 = docs
2088 .iter()
2089 .find(|d| d.doc_id.as_deref() == Some("1_2ecb"))
2090 .unwrap();
2091
2092 assert_eq!(doc1.chains.len(), 2);
2094 assert_eq!(doc2.chains.len(), 2);
2096
2097 let chain1 = doc1
2099 .chains
2100 .iter()
2101 .find(|c| c.mentions.iter().any(|m| m.text == "earthquake"));
2102 assert!(chain1.is_some());
2103
2104 let chain2 = doc1
2106 .chains
2107 .iter()
2108 .find(|c| c.mentions.iter().any(|m| m.text == "struck"));
2109 assert!(chain2.is_some());
2110 assert!(chain2.unwrap().mentions.iter().any(|m| m.text == "tremor"));
2111
2112 let chain1_doc2 = doc2
2114 .chains
2115 .iter()
2116 .find(|c| c.mentions.iter().any(|m| m.text == "quake"));
2117 assert!(chain1_doc2.is_some());
2118 }
2119
2120 #[test]
2121 fn test_parse_ecb_plus_xml() {
2122 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
2123<Document doc_name="1_1ecb" doc_id="1_1ecb.xml.xml">
2124 <token t_id="1" sentence="0" number="0">The</token>
2125 <token t_id="2" sentence="0" number="1">earthquake</token>
2126 <token t_id="3" sentence="0" number="2">struck</token>
2127 <token t_id="4" sentence="0" number="3">at</token>
2128 <token t_id="5" sentence="0" number="4">dawn</token>
2129 <token t_id="6" sentence="1" number="0">A</token>
2130 <token t_id="7" sentence="1" number="1">tremor</token>
2131 <token t_id="8" sentence="1" number="2">was</token>
2132 <token t_id="9" sentence="1" number="3">felt</token>
2133 <Markables>
2134 <ACTION_OCCURRENCE m_id="30">
2135 <token_anchor t_id="2"/>
2136 </ACTION_OCCURRENCE>
2137 <ACTION_OCCURRENCE m_id="31">
2138 <token_anchor t_id="3"/>
2139 </ACTION_OCCURRENCE>
2140 <ACTION_OCCURRENCE m_id="32">
2141 <token_anchor t_id="7"/>
2142 </ACTION_OCCURRENCE>
2143 </Markables>
2144 <Relations>
2145 <CROSS_DOC_COREF r_id="1" note="30001">
2146 <source m_id="30"/>
2147 <target m_id="32"/>
2148 </CROSS_DOC_COREF>
2149 <CROSS_DOC_COREF r_id="2" note="30002">
2150 <source m_id="31"/>
2151 </CROSS_DOC_COREF>
2152 </Relations>
2153</Document>"#;
2154
2155 let doc = parse_ecb_plus_xml(xml, "1", "1_1ecb").unwrap();
2156 assert_eq!(doc.doc_id.as_deref(), Some("1_1_1ecb"));
2157 assert!(doc.text.contains("earthquake"));
2158 assert!(doc.text.contains("tremor"));
2159
2160 let cluster_30001 = doc.chains.iter().find(|c| {
2162 c.mentions.iter().any(|m| m.text == "earthquake")
2163 && c.mentions.iter().any(|m| m.text == "tremor")
2164 });
2165 assert!(
2166 cluster_30001.is_some(),
2167 "Expected cross-doc cluster linking earthquake and tremor"
2168 );
2169
2170 let cluster_30002 = doc
2172 .chains
2173 .iter()
2174 .find(|c| c.mentions.iter().any(|m| m.text == "struck"));
2175 assert!(cluster_30002.is_some(), "Expected cluster for struck");
2176 }
2177}