Skip to main content

_formatparse/
results.rs

1use crate::parser::raw_match::RawMatchData;
2use pyo3::exceptions::{PyIndexError, PyTypeError};
3use pyo3::prelude::*;
4use pyo3::types::PyList;
5use pyo3::IntoPyObjectExt;
6
7/// Results container that stores raw match data and lazily converts to ParseResult
8/// This avoids creating all ParseResult objects upfront, improving performance
9/// The struct itself is lightweight - just a Vec of raw data
10#[pyclass]
11pub struct Results {
12    raw_data: Vec<RawMatchData>,
13    // Cache for converted ParseResult objects (lazy evaluation)
14    cached_results: Option<PyObject>,
15}
16
17impl Results {
18    pub fn new(raw_data: Vec<RawMatchData>) -> Self {
19        Self {
20            raw_data,
21            cached_results: None,
22        }
23    }
24
25    /// Convert all raw data to ParseResult objects (called lazily)
26    fn convert_all(&mut self, py: Python) -> PyResult<PyObject> {
27        if let Some(ref cached) = self.cached_results {
28            return Ok(cached.clone_ref(py));
29        }
30
31        let mut py_results: Vec<PyObject> = Vec::with_capacity(self.raw_data.len());
32        for raw_data in &self.raw_data {
33            let parse_result = raw_data.to_parse_result(py)?;
34            py_results.push(parse_result.into_py_any(py)?);
35        }
36
37        let items: Vec<_> = py_results.iter().map(|obj| obj.bind(py)).collect();
38        let results_list = PyList::new(py, items)?;
39        let list_obj = results_list.into_py_any(py)?;
40
41        // Cache the result
42        self.cached_results = Some(list_obj.clone_ref(py));
43        Ok(list_obj)
44    }
45
46    /// Convert a single raw data item to ParseResult (for lazy indexing)
47    pub fn convert_item(&self, index: usize, py: Python) -> PyResult<PyObject> {
48        if index >= self.raw_data.len() {
49            return Err(PyIndexError::new_err("list index out of range"));
50        }
51
52        let raw_data = &self.raw_data[index];
53        let parse_result = raw_data.to_parse_result(py)?;
54        parse_result.into_py_any(py)
55    }
56}
57
58#[pymethods]
59impl Results {
60    /// Get the length (no conversion needed)
61    fn __len__(&self) -> usize {
62        self.raw_data.len()
63    }
64
65    /// Get an item by index (lazy conversion - only converts the requested item)
66    fn __getitem__(&self, key: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
67        // Try to extract as usize first (positive index)
68        if let Ok(index) = key.extract::<usize>() {
69            // Single item access - convert only this item
70            self.convert_item(index, py)
71        } else if let Ok(index) = key.extract::<isize>() {
72            let len = self.raw_data.len();
73            let actual_index = if index < 0 {
74                match (len as i128).checked_add(index as i128) {
75                    Some(sum) if sum >= 0 && (sum as usize) < len => sum as usize,
76                    _ => {
77                        return Err(PyIndexError::new_err("list index out of range"));
78                    }
79                }
80            } else {
81                let u = index as usize;
82                if u >= len {
83                    return Err(PyIndexError::new_err("list index out of range"));
84                }
85                u
86            };
87            self.convert_item(actual_index, py)
88        } else if key.is_instance_of::<pyo3::types::PySlice>() {
89            // Slice access - convert all items to a list and let Python handle slicing
90            // This is less optimal but necessary for slice support
91            let mut results = Results::new(self.raw_data.clone());
92            let list = results.convert_all(py)?;
93            // Use Python's __getitem__ to handle the slice
94            let list_bound = list.bind(py);
95            let slice_result = list_bound.get_item(key)?;
96            Ok(slice_result.into_py_any(py)?)
97        } else {
98            Err(PyTypeError::new_err(
99                "list indices must be integers or slices",
100            ))
101        }
102    }
103
104    /// Iterator support (batch converts on first iteration)
105    fn __iter__(slf: PyRef<'_, Self>) -> PyResult<ResultsIterator> {
106        Ok(ResultsIterator {
107            results: slf.into(),
108            cached_list: None,
109            index: 0,
110        })
111    }
112
113    /// Convert to list (forces conversion of all items).
114    /// Name matches the `parse` library Python API (`results.to_list()`).
115    #[allow(clippy::wrong_self_convention)] // `to_*` + `&mut self` is required by pymethods / API parity
116    fn to_list(&mut self, py: Python) -> PyResult<PyObject> {
117        self.convert_all(py)
118    }
119
120    /// String representation
121    fn __repr__(&self) -> String {
122        format!("<Results {} matches>", self.raw_data.len())
123    }
124
125    fn __str__(&self) -> String {
126        self.__repr__()
127    }
128}
129
130/// Iterator for Results (batch conversion on first iteration)
131/// This avoids FFI overhead by converting all items at once when iteration starts
132#[pyclass]
133pub struct ResultsIterator {
134    results: Py<Results>,
135    cached_list: Option<PyObject>, // Cached list of converted items
136    index: usize,
137}
138
139#[pymethods]
140impl ResultsIterator {
141    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
142        slf
143    }
144
145    fn __next__(&mut self, py: Python) -> PyResult<Option<PyObject>> {
146        // On first iteration, batch convert all items at once
147        if self.cached_list.is_none() {
148            let results = self.results.bind(py);
149            // Convert all items in a single batch (one GIL block)
150            let list = results.call_method0("to_list")?;
151            self.cached_list = Some(list.into_py_any(py)?);
152        }
153
154        // Now iterate over the cached list (no FFI overhead)
155        let list_bound = self
156            .cached_list
157            .as_ref()
158            .expect("cached_list set immediately above when None")
159            .bind(py)
160            .downcast::<pyo3::types::PyList>()?;
161        let len = list_bound.len();
162
163        if self.index >= len {
164            return Ok(None);
165        }
166
167        let item = list_bound.get_item(self.index)?;
168        self.index += 1;
169        Ok(Some(item.into_py_any(py)?))
170    }
171}