1#![warn(missing_docs)]
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7use crate::change_set::ChangeSet;
8use crate::types::ChangeType;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ChangeStats {
13 pub total_change_sets: usize,
15 pub change_sets_by_type: HashMap<ChangeType, usize>,
17 pub change_sets_by_package: HashMap<String, usize>,
19 pub breaking_changes: usize,
21 pub features: usize,
23 pub bug_fixes: usize,
25 pub other_changes: usize,
27 pub avg_changes_per_package: f64,
29 pub most_common_change_type: Option<ChangeType>,
31 pub most_affected_package: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ChangeTrendPoint {
38 pub timestamp: DateTime<Utc>,
40 pub change_set_count: usize,
42 pub change_sets_by_type: HashMap<ChangeType, usize>,
44 pub packages_affected: usize,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ChangeTrend {
51 pub time_period: String,
53 pub data_points: Vec<ChangeTrendPoint>,
55 pub overall_stats: ChangeStats,
57 pub change_rate: f64,
59 pub trend_direction: String,
61 pub most_active_period: Option<DateTime<Utc>>,
63}
64
65impl ChangeStats {
66 pub fn from_change_sets(change_sets: &[ChangeSet]) -> Self {
68 let total_change_sets = change_sets.len();
69 let mut change_sets_by_type = HashMap::new();
70 let mut change_sets_by_package = HashMap::new();
71 let mut breaking_changes = 0;
72 let mut features = 0;
73 let mut bug_fixes = 0;
74 let mut other_changes = 0;
75
76 for change_set in change_sets {
78 *change_sets_by_type.entry(change_set.r#type.clone()).or_insert(0) += 1;
80
81 for package in &change_set.packages {
83 *change_sets_by_package.entry(package.clone()).or_insert(0) += 1;
84 }
85
86 match change_set.r#type {
88 ChangeType::Breaking => breaking_changes += 1,
89 ChangeType::Feature => features += 1,
90 ChangeType::Fix => bug_fixes += 1,
91 _ => other_changes += 1,
92 }
93 }
94
95 let avg_changes_per_package = if change_sets_by_package.is_empty() { 0.0 } else { change_sets.iter().map(|cs| cs.packages.len()).sum::<usize>() as f64 / change_sets_by_package.len() as f64 };
97
98 let most_common_change_type = change_sets_by_type.iter().max_by(|a, b| a.1.cmp(b.1)).map(|(ctype, _)| ctype.clone());
100
101 let most_affected_package = change_sets_by_package.iter().max_by(|a, b| a.1.cmp(b.1)).map(|(pkg, _)| pkg.clone());
103
104 Self { total_change_sets, change_sets_by_type, change_sets_by_package, breaking_changes, features, bug_fixes, other_changes, avg_changes_per_package, most_common_change_type, most_affected_package }
105 }
106
107 pub fn generate_summary(&self) -> String {
109 let mut summary = String::new();
110
111 summary.push_str(&format!("Change Statistics Summary:\n"));
112 summary.push_str(&format!("- Total Change Sets: {}\n", self.total_change_sets));
113 summary.push_str(&format!("- Breaking Changes: {}\n", self.breaking_changes));
114 summary.push_str(&format!("- Features: {}\n", self.features));
115 summary.push_str(&format!("- Bug Fixes: {}\n", self.bug_fixes));
116 summary.push_str(&format!("- Other Changes: {}\n", self.other_changes));
117 summary.push_str(&format!("- Average Changes per Package: {:.2}\n", self.avg_changes_per_package));
118
119 if let Some(ctype) = &self.most_common_change_type {
120 summary.push_str(&format!("- Most Common Change Type: {}\n", ctype.as_str()));
121 }
122
123 if let Some(pkg) = &self.most_affected_package {
124 summary.push_str(&format!("- Most Affected Package: {}\n", pkg));
125 }
126
127 summary.push_str("\nChanges by Type:\n");
128 for (ctype, count) in &self.change_sets_by_type {
129 summary.push_str(&format!("- {}: {}\n", ctype.as_str(), count));
130 }
131
132 summary.push_str("\nChanges by Package:\n");
133 for (pkg, count) in &self.change_sets_by_package {
134 summary.push_str(&format!("- {}: {}\n", pkg, count));
135 }
136
137 summary
138 }
139}
140
141impl ChangeTrend {
142 pub fn from_change_sets(change_sets: &[ChangeSet], time_period: &str) -> Self {
144 let stats = ChangeStats::from_change_sets(change_sets);
147
148 let data_point = ChangeTrendPoint { timestamp: chrono::Utc::now(), change_set_count: change_sets.len(), change_sets_by_type: stats.change_sets_by_type.clone(), packages_affected: stats.change_sets_by_package.len() };
150
151 let change_rate = change_sets.len() as f64 / 30.0;
154
155 let trend_direction = if change_rate > 1.0 {
157 "positive"
158 }
159 else if change_rate < 0.5 {
160 "negative"
161 }
162 else {
163 "stable"
164 };
165
166 Self { time_period: time_period.to_string(), data_points: vec![data_point], overall_stats: stats, change_rate, trend_direction: trend_direction.to_string(), most_active_period: Some(chrono::Utc::now()) }
167 }
168
169 pub fn generate_summary(&self) -> String {
171 let mut summary = String::new();
172
173 summary.push_str(&format!("Change Trend Analysis ({}):\n", self.time_period));
174 summary.push_str(&format!("- Change Rate: {:.2} changes per day\n", self.change_rate));
175 summary.push_str(&format!("- Trend Direction: {}\n", self.trend_direction));
176
177 if let Some(period) = &self.most_active_period {
178 summary.push_str(&format!("- Most Active Period: {}\n", period.format("%Y-%m-%d %H:%M:%S")));
179 }
180
181 summary.push_str("\nOverall Statistics:\n");
182 summary.push_str(&self.overall_stats.generate_summary());
183
184 summary
185 }
186}