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 BAR_WIDTH: usize = 40;
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) * BAR_WIDTH as f64).round() as usize;
121 let bar_length = bar_length.min(BAR_WIDTH); let filled_bar = "█".repeat(bar_length);
125 let background_bar = "░".repeat(BAR_WIDTH - bar_length);
126 let full_bar = format!("{filled_bar}{background_bar}");
127
128 output.push_str(&format!(
129 "{digit:1}: {full_bar} {observed:>5.1}% (expected: {expected:>5.1}%)\n"
130 ));
131 }
132
133 output
134}
135
136fn format_json(result: &BenfordResult) -> String {
137 format!(
138 r#"{{
139 "dataset": "{}"
140 "numbers_analyzed": {},
141 "risk_level": "{}"
142 "digits": {{
143 "1": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
144 "2": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
145 "3": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
146 "4": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
147 "5": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
148 "6": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
149 "7": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
150 "8": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}},
151 "9": {{"observed": {:.1}, "expected": {:.1}, "deviation": {:.1}}}
152 }},
153 "statistics": {{
154 "chi_square": {:.2},
155 "p_value": {:.3},
156 "mad": {:.1}
157 }},
158 "verdict": "{}"
159}}"#,
160 result.dataset_name,
161 result.numbers_analyzed,
162 result.risk_level,
163 result.digit_distribution[0],
164 result.expected_distribution[0],
165 result.digit_distribution[0] - result.expected_distribution[0],
166 result.digit_distribution[1],
167 result.expected_distribution[1],
168 result.digit_distribution[1] - result.expected_distribution[1],
169 result.digit_distribution[2],
170 result.expected_distribution[2],
171 result.digit_distribution[2] - result.expected_distribution[2],
172 result.digit_distribution[3],
173 result.expected_distribution[3],
174 result.digit_distribution[3] - result.expected_distribution[3],
175 result.digit_distribution[4],
176 result.expected_distribution[4],
177 result.digit_distribution[4] - result.expected_distribution[4],
178 result.digit_distribution[5],
179 result.expected_distribution[5],
180 result.digit_distribution[5] - result.expected_distribution[5],
181 result.digit_distribution[6],
182 result.expected_distribution[6],
183 result.digit_distribution[6] - result.expected_distribution[6],
184 result.digit_distribution[7],
185 result.expected_distribution[7],
186 result.digit_distribution[7] - result.expected_distribution[7],
187 result.digit_distribution[8],
188 result.expected_distribution[8],
189 result.digit_distribution[8] - result.expected_distribution[8],
190 result.chi_square,
191 result.p_value,
192 result.mean_absolute_deviation,
193 result.verdict
194 )
195}
196
197fn format_csv(_result: &BenfordResult) -> String {
198 "CSV format not yet implemented".to_string()
199}
200
201fn format_yaml(_result: &BenfordResult) -> String {
202 "YAML format not yet implemented".to_string()
203}
204
205fn format_toml(_result: &BenfordResult) -> String {
206 "TOML format not yet implemented".to_string()
207}
208
209fn format_xml(_result: &BenfordResult) -> String {
210 "XML format not yet implemented".to_string()
211}