eulumdat/
batch.rs

1//! Batch conversion functionality for Python bindings
2
3use pyo3::prelude::*;
4
5use eulumdat::batch::{
6    self, BatchInput as CoreBatchInput, ConversionFormat as CoreConversionFormat,
7};
8
9/// Input file format for batch conversion
10#[pyclass(eq, eq_int)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum InputFormat {
13    /// EULUMDAT (.ldt) format
14    Ldt = 0,
15    /// IES (.ies) format
16    Ies = 1,
17}
18
19#[pymethods]
20impl InputFormat {
21    fn __repr__(&self) -> String {
22        match self {
23            Self::Ldt => "InputFormat.Ldt".to_string(),
24            Self::Ies => "InputFormat.Ies".to_string(),
25        }
26    }
27}
28
29/// Output format for batch conversion
30#[pyclass(eq, eq_int)]
31#[derive(Clone, Copy, PartialEq, Eq)]
32pub enum ConversionFormat {
33    /// Convert to EULUMDAT (.ldt) format
34    Ldt = 0,
35    /// Convert to IES (.ies) format
36    Ies = 1,
37}
38
39#[pymethods]
40impl ConversionFormat {
41    fn __repr__(&self) -> String {
42        match self {
43            Self::Ldt => "ConversionFormat.Ldt".to_string(),
44            Self::Ies => "ConversionFormat.Ies".to_string(),
45        }
46    }
47}
48
49/// Input file for batch conversion
50#[pyclass]
51#[derive(Clone)]
52pub struct BatchInput {
53    /// File name
54    #[pyo3(get, set)]
55    pub name: String,
56    /// File content
57    #[pyo3(get, set)]
58    pub content: String,
59    /// Optional input format (auto-detected if None)
60    #[pyo3(get, set)]
61    pub format: Option<InputFormat>,
62}
63
64#[pymethods]
65impl BatchInput {
66    #[new]
67    #[pyo3(signature = (name, content, format=None))]
68    fn new(name: String, content: String, format: Option<InputFormat>) -> Self {
69        Self {
70            name,
71            content,
72            format,
73        }
74    }
75
76    fn __repr__(&self) -> String {
77        format!(
78            "BatchInput(name='{}', content_len={}, format={:?})",
79            self.name,
80            self.content.len(),
81            self.format
82        )
83    }
84}
85
86/// Output file from batch conversion
87#[pyclass]
88#[derive(Clone)]
89pub struct BatchOutput {
90    /// Original input file name
91    #[pyo3(get)]
92    pub input_name: String,
93    /// Generated output file name
94    #[pyo3(get)]
95    pub output_name: String,
96    /// Converted content (None if error)
97    #[pyo3(get)]
98    pub content: Option<String>,
99    /// Error message (None if successful)
100    #[pyo3(get)]
101    pub error: Option<String>,
102}
103
104#[pymethods]
105impl BatchOutput {
106    fn __repr__(&self) -> String {
107        if let Some(err) = &self.error {
108            format!("BatchOutput(input='{}', error='{}')", self.input_name, err)
109        } else {
110            format!(
111                "BatchOutput(input='{}', output='{}', content_len={})",
112                self.input_name,
113                self.output_name,
114                self.content.as_ref().map(|c| c.len()).unwrap_or(0)
115            )
116        }
117    }
118
119    /// Check if the conversion was successful
120    #[getter]
121    fn success(&self) -> bool {
122        self.error.is_none()
123    }
124}
125
126/// Statistics from batch conversion
127#[pyclass]
128#[derive(Clone)]
129pub struct BatchStats {
130    /// Total number of files processed
131    #[pyo3(get)]
132    pub total: usize,
133    /// Number of successful conversions
134    #[pyo3(get)]
135    pub successful: usize,
136    /// Number of failed conversions
137    #[pyo3(get)]
138    pub failed: usize,
139}
140
141#[pymethods]
142impl BatchStats {
143    fn __repr__(&self) -> String {
144        format!(
145            "BatchStats(total={}, successful={}, failed={})",
146            self.total, self.successful, self.failed
147        )
148    }
149
150    /// Success rate as a percentage
151    #[getter]
152    fn success_rate(&self) -> f64 {
153        if self.total == 0 {
154            0.0
155        } else {
156            (self.successful as f64 / self.total as f64) * 100.0
157        }
158    }
159}
160
161/// Batch convert multiple photometric files.
162///
163/// This function is significantly faster than converting files one-by-one in Python
164/// because the conversion happens entirely in Rust.
165///
166/// Args:
167///     inputs: List of BatchInput objects containing file names and contents
168///     format: Target conversion format (Ldt or Ies)
169///
170/// Returns:
171///     Tuple of (list of BatchOutput, BatchStats)
172///
173/// Example:
174///     >>> inputs = [
175///     ...     BatchInput("file1.ldt", ldt_content1),
176///     ...     BatchInput("file2.ldt", ldt_content2),
177///     ... ]
178///     >>> outputs, stats = batch_convert(inputs, ConversionFormat.Ies)
179///     >>> print(f"Converted {stats.successful}/{stats.total} files")
180#[pyfunction]
181#[pyo3(signature = (inputs, format))]
182pub fn batch_convert(
183    inputs: Vec<BatchInput>,
184    format: ConversionFormat,
185) -> (Vec<BatchOutput>, BatchStats) {
186    // Convert Python types to Rust types
187    let core_inputs: Vec<CoreBatchInput> = inputs
188        .into_iter()
189        .map(|input| CoreBatchInput {
190            name: input.name,
191            content: input.content,
192            format: input.format.map(|f| match f {
193                InputFormat::Ldt => eulumdat::InputFormat::Ldt,
194                InputFormat::Ies => eulumdat::InputFormat::Ies,
195            }),
196        })
197        .collect();
198
199    let core_format = match format {
200        ConversionFormat::Ldt => CoreConversionFormat::Ldt,
201        ConversionFormat::Ies => CoreConversionFormat::Ies,
202    };
203
204    // Perform batch conversion
205    let (core_outputs, core_stats) = batch::batch_convert_with_stats(&core_inputs, core_format);
206
207    // Convert results back to Python types
208    let outputs: Vec<BatchOutput> = core_outputs
209        .into_iter()
210        .map(|output| BatchOutput {
211            input_name: output.input_name,
212            output_name: output.output_name,
213            content: output.content,
214            error: output.error,
215        })
216        .collect();
217
218    let stats = BatchStats {
219        total: core_stats.total,
220        successful: core_stats.successful,
221        failed: core_stats.failed,
222    };
223
224    (outputs, stats)
225}