lawkit_core/common/output/
formatter.rs1use crate::laws::benford::BenfordResult;
2use clap::ArgMatches; use std::io::{self, Write}; #[derive(Debug, Clone)]
6pub enum OutputFormat {
7 Text,
8 Json,
9 Csv,
10 Yaml,
11 Toml,
12 Xml,
13}
14
15impl std::str::FromStr for OutputFormat {
16 type Err = crate::error::BenfError;
17
18 fn from_str(s: &str) -> Result<Self, Self::Err> {
19 match s.to_lowercase().as_str() {
20 "text" => Ok(OutputFormat::Text),
21 "json" => Ok(OutputFormat::Json),
22 "csv" => Ok(OutputFormat::Csv),
23 "yaml" => Ok(OutputFormat::Yaml),
24 "toml" => Ok(OutputFormat::Toml),
25 "xml" => Ok(OutputFormat::Xml),
26 _ => Err(crate::error::BenfError::InvalidInput(format!(
27 "Unsupported format: {s}"
28 ))),
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
34pub struct OutputConfig {
35 pub format: String,
36 pub quiet: bool,
37 pub verbose: bool,
38}
39
40impl OutputConfig {
41 pub fn from_matches(matches: &ArgMatches) -> Self {
42 OutputConfig {
43 format: matches
44 .get_one::<String>("format")
45 .unwrap_or(&"text".to_string())
46 .clone(),
47 quiet: *matches.get_one::<bool>("quiet").unwrap_or(&false),
48 verbose: *matches.get_one::<bool>("verbose").unwrap_or(&false),
49 }
50 }
51}
52
53pub fn create_output_writer(matches: &ArgMatches) -> crate::error::Result<Box<dyn Write>> {
54 let output_path = matches.try_get_one::<String>("output").ok().flatten();
56
57 if let Some(path) = output_path {
58 if path == "-" {
59 Ok(Box::new(io::stdout()))
60 } else {
61 let file = std::fs::File::create(path)?;
62 Ok(Box::new(file))
63 }
64 } else {
65 Ok(Box::new(io::stdout()))
66 }
67}
68
69pub fn format_output(result: &BenfordResult, format: &OutputFormat) -> String {
70 match format {
71 OutputFormat::Text => format_text(result),
72 OutputFormat::Json => format_json(result),
73 OutputFormat::Csv => format_csv(result),
74 OutputFormat::Yaml => format_yaml(result),
75 OutputFormat::Toml => format_toml(result),
76 OutputFormat::Xml => format_xml(result),
77 }
78}
79
80fn format_text(result: &BenfordResult) -> String {
81 format!(
82 "Benford's Law Analysis Results\n\
83 \n\
84 Dataset: {}\n\
85 Numbers analyzed: {}\n\
86 Risk Level: {} {}\n\
87 \n\
88 First Digit Distribution:\n\
89 {}\n\
90 Statistical Tests:\n\
91 Chi-square: {:.2} (p-value: {:.3})\n\
92 Mean Absolute Deviation: {:.1}%\n\
93 \n\
94 Verdict: {}",
95 result.dataset_name,
96 result.numbers_analyzed,
97 result.risk_level,
98 match result.risk_level {
99 crate::common::risk::RiskLevel::Critical => "[CRITICAL]",
100 crate::common::risk::RiskLevel::High => "[HIGH]",
101 crate::common::risk::RiskLevel::Medium => "[MEDIUM]",
102 crate::common::risk::RiskLevel::Low => "[LOW]",
103 },
104 format_distribution_bars(result),
105 result.chi_square,
106 result.p_value,
107 result.mean_absolute_deviation,
108 result.verdict
109 )
110}
111
112fn format_distribution_bars(result: &BenfordResult) -> String {
113 let mut output = String::new();
114 const CHART_WIDTH: usize = 50;
115
116 for i in 0..9 {
117 let digit = i + 1;
118 let observed = result.digit_distribution[i];
119 let expected = result.expected_distribution[i];
120 let bar_length = ((observed / 100.0) * CHART_WIDTH as f64).round() as usize;
121 let bar_length = bar_length.min(CHART_WIDTH); let expected_line_pos = ((expected / 100.0) * CHART_WIDTH as f64).round() as usize;
125 let expected_line_pos = expected_line_pos.min(CHART_WIDTH - 1); let mut bar_chars = Vec::new();
129 for pos in 0..CHART_WIDTH {
130 if pos == expected_line_pos {
131 bar_chars.push('┃'); } else if pos < bar_length {
133 bar_chars.push('█'); } else {
135 bar_chars.push('░'); }
137 }
138 let full_bar: String = bar_chars.iter().collect();
139
140 output.push_str(&format!(
141 "{digit:1}: {full_bar} {observed:>5.1}% (expected: {expected:>5.1}%)\n"
142 ));
143 }
144
145 output
146}
147
148fn format_json(result: &BenfordResult) -> String {
149 format!(
150 r#"{{
151 "dataset": "{}"
152 "numbers_analyzed": {},
153 "risk_level": "{}"
154 "digits": {{
155 "1": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
156 "2": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
157 "3": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
158 "4": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
159 "5": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
160 "6": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
161 "7": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
162 "8": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
163 "9": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}}
164 }},
165 "statistics": {{
166 "chi_square": {:.2},
167 "p_value": {:.3},
168 "mad": {:.1}
169 }},
170 "verdict": "{}"
171}}"#,
172 result.dataset_name,
173 result.numbers_analyzed,
174 result.risk_level,
175 result.digit_distribution[0],
176 result.expected_distribution[0],
177 result.digit_distribution[0] - result.expected_distribution[0],
178 result.digit_distribution[1],
179 result.expected_distribution[1],
180 result.digit_distribution[1] - result.expected_distribution[1],
181 result.digit_distribution[2],
182 result.expected_distribution[2],
183 result.digit_distribution[2] - result.expected_distribution[2],
184 result.digit_distribution[3],
185 result.expected_distribution[3],
186 result.digit_distribution[3] - result.expected_distribution[3],
187 result.digit_distribution[4],
188 result.expected_distribution[4],
189 result.digit_distribution[4] - result.expected_distribution[4],
190 result.digit_distribution[5],
191 result.expected_distribution[5],
192 result.digit_distribution[5] - result.expected_distribution[5],
193 result.digit_distribution[6],
194 result.expected_distribution[6],
195 result.digit_distribution[6] - result.expected_distribution[6],
196 result.digit_distribution[7],
197 result.expected_distribution[7],
198 result.digit_distribution[7] - result.expected_distribution[7],
199 result.digit_distribution[8],
200 result.expected_distribution[8],
201 result.digit_distribution[8] - result.expected_distribution[8],
202 result.chi_square,
203 result.p_value,
204 result.mean_absolute_deviation,
205 result.verdict
206 )
207}
208
209fn format_csv(_result: &BenfordResult) -> String {
210 "CSV format not yet implemented".to_string()
211}
212
213fn format_yaml(_result: &BenfordResult) -> String {
214 "YAML format not yet implemented".to_string()
215}
216
217fn format_toml(_result: &BenfordResult) -> String {
218 "TOML format not yet implemented".to_string()
219}
220
221fn format_xml(_result: &BenfordResult) -> String {
222 "XML format not yet implemented".to_string()
223}