lawkit_core/common/output/
formatter.rs

1use crate::laws::benford::BenfordResult;
2use clap::ArgMatches; // clap::ArgMatches をインポート
3use std::io::{self, Write}; // io::Write をインポート
4
5#[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    // outputオプションが存在する場合のみチェック
55    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); // Ensure we don't exceed max width
122
123        // Calculate expected value line position
124        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); // Ensure it's within bounds
126
127        // Create bar with filled portion, expected value line, and background
128        let mut bar_chars = Vec::new();
129        for pos in 0..CHART_WIDTH {
130            if pos == expected_line_pos {
131                bar_chars.push('┃'); // Expected value line (always visible)
132            } else if pos < bar_length {
133                bar_chars.push('█'); // Filled portion
134            } else {
135                bar_chars.push('░'); // Background portion
136            }
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}