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 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); // Ensure we don't exceed max width
122
123        // Create bar with filled and background portions
124        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}