use std::collections::HashMap;
use thiserror::Error;
pub mod cohesion;
pub mod config;
pub mod markers;
pub mod results;
pub mod rhetorical;
use cohesion::{CohesionAnalysisError, CohesionAnalyzer};
use config::{DiscourseCoherenceConfig, DiscourseCoherenceError};
use markers::{DiscourseMarkerAnalyzer, DiscourseMarkerError};
use results::{DetailedDiscourseMetrics, DiscourseCoherenceResult};
use rhetorical::{RhetoricalAnalysisError, RhetoricalStructureAnalyzer};
#[derive(Debug, Error)]
pub enum ModularDiscourseError {
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Text processing error: {0}")]
TextProcessingError(String),
#[error("Marker analysis failed: {0}")]
MarkerAnalysisError(#[from] DiscourseMarkerError),
#[error("Rhetorical analysis failed: {0}")]
RhetoricalAnalysisError(#[from] RhetoricalAnalysisError),
#[error("Cohesion analysis failed: {0}")]
CohesionAnalysisError(#[from] CohesionAnalysisError),
#[error("Analysis integration error: {0}")]
IntegrationError(String),
}
pub struct DiscourseCoherenceAnalyzer {
config: DiscourseCoherenceConfig,
marker_analyzer: DiscourseMarkerAnalyzer,
rhetorical_analyzer: RhetoricalStructureAnalyzer,
cohesion_analyzer: CohesionAnalyzer,
}
impl DiscourseCoherenceAnalyzer {
pub fn new() -> Self {
Self::with_config(DiscourseCoherenceConfig::default())
}
pub fn with_config(config: DiscourseCoherenceConfig) -> Self {
let marker_analyzer = DiscourseMarkerAnalyzer::new(config.markers.clone());
let rhetorical_analyzer = RhetoricalStructureAnalyzer::new(config.rhetorical.clone());
let cohesion_analyzer = CohesionAnalyzer::new(config.cohesion.clone());
Self {
config,
marker_analyzer,
rhetorical_analyzer,
cohesion_analyzer,
}
}
pub fn analyze_discourse_coherence(
&self,
text: &str,
) -> Result<DiscourseCoherenceResult, ModularDiscourseError> {
if text.trim().is_empty() {
return Err(ModularDiscourseError::TextProcessingError(
"Empty text provided".to_string(),
));
}
let sentences = self.split_into_sentences(text)?;
let discourse_markers = self.marker_analyzer.analyze_markers(&sentences)?;
let marker_coherence_score = self.calculate_marker_coherence(&discourse_markers);
let rhetorical_analysis = self
.rhetorical_analyzer
.analyze_rhetorical_structure(&sentences, &discourse_markers)?;
let rhetorical_structure_score = rhetorical_analysis.relation_distribution_score;
let rhetorical_relations: HashMap<String, usize> = rhetorical_analysis
.relations
.iter()
.map(|(rel_type, &count)| (format!("{:?}", rel_type), count))
.collect();
let cohesion_analysis = self.cohesion_analyzer.analyze_cohesion(&sentences)?;
let cohesion_score = cohesion_analysis.overall_cohesion_score;
let transition_scores = self.extract_transition_scores(&cohesion_analysis);
let overall_coherence_score = self.calculate_overall_coherence_score(
marker_coherence_score,
rhetorical_structure_score,
cohesion_score,
);
let detailed_metrics = if self.config.general.detailed_metrics {
Some(self.build_detailed_metrics(
&discourse_markers,
&rhetorical_analysis,
&cohesion_analysis,
)?)
} else {
None
};
Ok(DiscourseCoherenceResult {
overall_coherence_score,
marker_coherence_score,
rhetorical_structure_score,
cohesion_score,
transition_scores,
discourse_markers,
rhetorical_relations,
detailed_metrics,
})
}
fn split_into_sentences(&self, text: &str) -> Result<Vec<String>, ModularDiscourseError> {
let sentences: Vec<String> = text
.split(|c| c == '.' || c == '!' || c == '?')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && s.len() >= self.config.general.min_sentence_length)
.collect();
if sentences.is_empty() {
return Err(ModularDiscourseError::TextProcessingError(
"No valid sentences found".to_string(),
));
}
Ok(sentences)
}
fn calculate_marker_coherence(&self, markers: &[results::DiscourseMarker]) -> f64 {
if markers.is_empty() {
return 0.3; }
let total_strength: f64 = markers.iter().map(|m| m.rhetorical_strength).sum();
let average_confidence: f64 =
markers.iter().map(|m| m.confidence).sum::<f64>() / markers.len() as f64;
let strength_score = (total_strength / markers.len() as f64).min(1.0);
(strength_score * 0.6 + average_confidence * 0.4).min(1.0)
}
fn extract_transition_scores(&self, cohesion_analysis: &results::CohesionAnalysis) -> Vec<f64> {
let base_score = cohesion_analysis.overall_cohesion_score;
let reference_score = cohesion_analysis.reference_cohesion.resolution_success_rate;
let conjunctive_score = cohesion_analysis.conjunctive_cohesion.logical_flow_score;
let transition_count = (cohesion_analysis.cohesive_devices.len() / 2).max(1);
vec![
(base_score * 0.4 + reference_score * 0.3 + conjunctive_score * 0.3).min(1.0);
transition_count
]
}
fn calculate_overall_coherence_score(
&self,
marker_score: f64,
rhetorical_score: f64,
cohesion_score: f64,
) -> f64 {
let marker_weight = 0.3;
let rhetorical_weight = 0.4;
let cohesion_weight = 0.3;
(marker_score * marker_weight
+ rhetorical_score * rhetorical_weight
+ cohesion_score * cohesion_weight)
.min(1.0)
}
fn build_detailed_metrics(
&self,
discourse_markers: &[results::DiscourseMarker],
rhetorical_analysis: &results::RhetoricalStructureAnalysis,
cohesion_analysis: &results::CohesionAnalysis,
) -> Result<DetailedDiscourseMetrics, ModularDiscourseError> {
let marker_analysis = Some(self.marker_analyzer.generate_analysis(discourse_markers));
let rhetorical_analysis = Some(rhetorical_analysis.clone());
let cohesion_analysis = Some(cohesion_analysis.clone());
let transition_analysis = None; let information_structure = None; let advanced_analysis = None;
Ok(DetailedDiscourseMetrics {
marker_analysis,
rhetorical_analysis,
cohesion_analysis,
transition_analysis,
information_structure,
advanced_analysis,
})
}
pub fn get_configuration(&self) -> &DiscourseCoherenceConfig {
&self.config
}
pub fn validate_input(&self, text: &str) -> Result<(), ModularDiscourseError> {
if text.trim().is_empty() {
return Err(ModularDiscourseError::TextProcessingError(
"Empty text provided".to_string(),
));
}
let word_count = text.split_whitespace().count();
if word_count < self.config.general.min_sentence_length {
return Err(ModularDiscourseError::TextProcessingError(format!(
"Text too short: {} words",
word_count
)));
}
Ok(())
}
pub fn analyze_markers_only(
&self,
text: &str,
) -> Result<Vec<results::DiscourseMarker>, ModularDiscourseError> {
let sentences = self.split_into_sentences(text)?;
Ok(self.marker_analyzer.analyze_markers(&sentences)?)
}
pub fn analyze_rhetorical_structure_only(
&self,
text: &str,
) -> Result<results::RhetoricalStructureAnalysis, ModularDiscourseError> {
let sentences = self.split_into_sentences(text)?;
let markers = self.marker_analyzer.analyze_markers(&sentences)?;
Ok(self
.rhetorical_analyzer
.analyze_rhetorical_structure(&sentences, &markers)?)
}
pub fn analyze_cohesion_only(
&self,
text: &str,
) -> Result<results::CohesionAnalysis, ModularDiscourseError> {
let sentences = self.split_into_sentences(text)?;
Ok(self.cohesion_analyzer.analyze_cohesion(&sentences)?)
}
}
impl Default for DiscourseCoherenceAnalyzer {
fn default() -> Self {
Self::new()
}
}
pub use config::{
AdvancedAnalysisConfig, CohesionAnalysisConfig, CohesiveDeviceType, DiscourseMarkerConfig,
DiscourseMarkerType, InformationStructureConfig, RhetoricalRelationType,
RhetoricalStructureConfig, TransitionAnalysisConfig, TransitionQuality,
};
pub use results::{
CohesionAnalysis, CohesiveDevice, ConjunctiveCohesionMetrics, ContextAnalysis, DiscourseMarker,
DiscourseNode, DiscourseTree, LexicalCohesionMetrics, ReferenceCohesionMetrics,
RhetoricalStructureAnalysis, SyntacticPosition, TemporalCoherenceMetrics,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discourse_coherence_analyzer_creation() {
let analyzer = DiscourseCoherenceAnalyzer::new();
assert!(!format!("{:?}", analyzer.get_configuration()).is_empty());
}
#[test]
fn test_basic_discourse_coherence_analysis() {
let analyzer = DiscourseCoherenceAnalyzer::new();
let text = "This is a test sentence. However, this is another sentence. Therefore, we can conclude something.";
let result = analyzer.analyze_discourse_coherence(text);
assert!(result.is_ok());
let analysis = result.expect("operation should succeed");
assert!(analysis.overall_coherence_score >= 0.0);
assert!(analysis.overall_coherence_score <= 1.0);
}
#[test]
fn test_individual_component_analysis() {
let analyzer = DiscourseCoherenceAnalyzer::new();
let text = "First, we consider the problem. Then, we propose a solution. Finally, we evaluate the results.";
let markers_result = analyzer.analyze_markers_only(text);
assert!(markers_result.is_ok());
let rhetorical_result = analyzer.analyze_rhetorical_structure_only(text);
assert!(rhetorical_result.is_ok());
let cohesion_result = analyzer.analyze_cohesion_only(text);
assert!(cohesion_result.is_ok());
}
#[test]
fn test_empty_text_handling() {
let analyzer = DiscourseCoherenceAnalyzer::new();
let result = analyzer.analyze_discourse_coherence("");
assert!(result.is_err());
}
#[test]
fn test_configuration_variants() {
let minimal_config = DiscourseCoherenceConfig::minimal();
let minimal_analyzer = DiscourseCoherenceAnalyzer::with_config(minimal_config);
let comprehensive_config = DiscourseCoherenceConfig::comprehensive();
let comprehensive_analyzer = DiscourseCoherenceAnalyzer::with_config(comprehensive_config);
let text = "This is a test. However, it is simple.";
let minimal_result = minimal_analyzer.analyze_discourse_coherence(text);
let comprehensive_result = comprehensive_analyzer.analyze_discourse_coherence(text);
assert!(minimal_result.is_ok());
assert!(comprehensive_result.is_ok());
}
#[test]
fn test_input_validation() {
let analyzer = DiscourseCoherenceAnalyzer::new();
assert!(analyzer.validate_input("").is_err());
assert!(analyzer.validate_input("a").is_err()); assert!(analyzer
.validate_input("This is a proper sentence.")
.is_ok());
}
}