lib_efo/stats/implementations/
from_impl.rs

1//! All from implementations
2
3use crate::stats::FactionId;
4use crate::{
5    error::EpochError,
6    stats::{self, GenericStat, ListStats, Position, PositionGrid, PositionIdentifier, StatPair},
7};
8use std::str::FromStr;
9
10impl FromStr for StatPair {
11    type Err = EpochError;
12
13    /// Parse a string into a `StatPair`.
14    ///
15    /// The input can be either in the form `"stat:value"` or `"stat value"`.
16    /// If the format is invalid, an `EpochError::IllFormedStatQualifier`
17    /// is returned.
18    fn from_str(s: &str) -> Result<Self, Self::Err> {
19        let (stat_str, val_str) = if let Some(idx) = s.find([':', '=']) {
20            (&s[..idx], &s[idx + 1..])
21        } else {
22            let parts: Vec<&str> = s.split_whitespace().collect();
23            if parts.len() != 2 {
24                return Err(EpochError::IllFormedStatQualifier(s.to_owned()));
25            }
26            (parts[0], parts[1])
27        };
28
29        let stat: ListStats = stat_str.parse()?;
30        let value: isize = val_str
31            .parse()
32            .map_err(|_| EpochError::IllFormedStatQualifier(val_str.to_owned()))?;
33        Ok(StatPair {
34            stat,
35            value: stats::GenericStat(value),
36        })
37    }
38}
39
40/// Parse a string into a `Position`.
41///
42/// The input must be two signed integers separated by a comma, optionally
43/// surrounded by whitespace.  Leading and trailing whitespace around each
44/// component is ignored.
45///
46/// # Errors
47///
48/// * `ParsePositionError::InvalidFormat` - the string does not contain
49///   exactly one comma or there are not exactly two components.
50/// * `ParsePositionError::ParseIntError` - one of the components cannot be
51///   parsed as an `isize`.
52impl std::str::FromStr for Position {
53    type Err = EpochError;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        let parts: Vec<&str> = s.split(',').collect();
57        if parts.len() != 2 {
58            return Err(EpochError::InvalidPositionFormat(s.to_string()));
59        }
60        let x: isize = parts[0]
61            .trim()
62            .parse()
63            .map_err(|_| EpochError::ParsePositionError(s.to_string()))?;
64        let y: isize = parts[1]
65            .trim()
66            .parse()
67            .map_err(|_| EpochError::ParsePositionError(s.to_string()))?;
68        Ok(Position(x, y))
69    }
70}
71
72/// Parse a string into a `PositionGrid`.  
73///  
74/// The string is expected to contain two signed integers separated by a comma,
75/// optionally surrounded by whitespace.  Each component is trimmed before
76/// parsing, so leading and trailing whitespace is ignored.  
77///  
78/// The parsing logic is the same as for `Position`:  
79/// * Split on the comma; there must be exactly one comma.  
80/// * Trim each component and parse it as an `isize`.  
81/// * Convert the resulting `Position` to a grid distance with
82///   `Position::to_grid_distance`.  
83///  
84/// # Errors  
85///  
86/// * `EpochError::InvalidPositionFormat` - the string does not contain
87///   exactly one comma or there are not exactly two components.  
88/// * `EpochError::ParsePositionError` - one of the components cannot be
89///   parsed as an `isize`.  (These errors come from the underlying `Position`
90///   implementation.)
91impl std::str::FromStr for PositionGrid {
92    type Err = EpochError;
93
94    fn from_str(s: &str) -> Result<Self, Self::Err> {
95        s.parse::<Position>().map(|p| PositionGrid(p.0, p.1))
96    }
97}
98
99impl FromStr for PositionIdentifier {
100    type Err = EpochError;
101
102    /// Parses a string into one of the three variants.
103    ///
104    /// The parsing rules are described in the table above.
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        let s = s.trim();
107
108        if let Some(_rest) = s.find('@') {
109            let id = s.parse().map_err(|_| EpochError::ParsingArgument {
110                argument_parsed: s.to_string(),
111                while_parsing: "PositionIdentifier::EpochId",
112            })?;
113            return Ok(Self::Id(id));
114        }
115
116        if let Some(rest) = s.strip_prefix('~').or_else(|| s.strip_prefix('g')) {
117            let grid = rest.parse().map_err(|_| EpochError::ParsingArgument {
118                argument_parsed: s.to_string(),
119                while_parsing: "PositionIdentifier::PositionGrid",
120            })?;
121            return Ok(Self::Grid(grid));
122        }
123
124        let exact = s.parse().map_err(|_| EpochError::ParsingArgument {
125            argument_parsed: s.to_string(),
126            while_parsing: "PositionIdentifier::Position",
127        })?;
128        Ok(Self::Exact(exact))
129    }
130}
131
132/// Convert a tuple of two signed integers into a `Position`.
133///
134/// This implementation simply wraps the `(isize, isize)` tuple into a
135/// `Position` struct. It is equivalent to `Position(x, y)` where `x` is the
136/// first element and `y` is the second.
137impl From<(isize, isize)> for Position {
138    fn from(value: (isize, isize)) -> Self {
139        Self(value.0, value.1)
140    }
141}
142/// Convert a tuple of two signed integers into a `PositionGrid`.
143///
144/// This implementation simply wraps the `(isize, isize)` tuple into a
145/// `Position` struct. It is equivalent to `Position(x, y)` where `x` is the
146/// first element and `y` is the second.
147impl From<(isize, isize)> for PositionGrid {
148    fn from(value: (isize, isize)) -> Self {
149        Self(value.0, value.1)
150    }
151}
152impl std::str::FromStr for stats::Reach {
153    type Err = EpochError; // Clap accepts any type that implements `Display`
154
155    fn from_str(s: &str) -> Result<Self, Self::Err> {
156        let s = s.trim().to_ascii_lowercase();
157
158        match s.as_str() {
159            "g" | "global" => return Ok(stats::Reach::Global),
160            "n" | "none" => return Ok(stats::Reach::None),
161            _ => {}
162        }
163
164        if s.starts_with('l') {
165            if let Some(idx) = s.find(':') {
166                let rad_str = &s[idx + 1..];
167                let radius: usize = rad_str.parse().map_err(|_| EpochError::ParsingArgument {
168                    argument_parsed: s,
169                    while_parsing: "Reach::Local",
170                })?;
171                return Ok(stats::Reach::Local(radius));
172            } else {
173                return Err(EpochError::ParsingArgument {
174                    argument_parsed: s,
175                    while_parsing: "Local reach requires a radius, e.g. `l:5` or `local:5`",
176                });
177            }
178        }
179
180        if s.starts_with('f') {
181            if let Some(idx) = s.find(':') {
182                let id_str = &s[idx + 1..];
183                let id: usize = id_str.parse().map_err(|_| EpochError::ParsingArgument {
184                    argument_parsed: s,
185                    while_parsing: "Reach::Faction",
186                })?;
187                return Ok(stats::Reach::Faction(stats::FactionId(id)));
188            } else {
189                return Err(EpochError::ParsingArgument {
190                    argument_parsed: s,
191                    while_parsing: "Faction reach requires an id, e.g. `f:3` or `faction:3`",
192                });
193            }
194        }
195
196        Err(EpochError::ParsingArgument {
197            argument_parsed: s,
198            while_parsing: "Unknown error",
199        })
200    }
201}
202
203impl FromStr for GenericStat {
204    type Err = EpochError;
205
206    fn from_str(s: &str) -> Result<Self, Self::Err> {
207        s.parse::<isize>()
208            .map_err(|_| EpochError::ParsingArgument {
209                argument_parsed: s.to_owned(),
210                while_parsing: "GenericStat",
211            })
212            .map(Self)
213    }
214}
215
216impl std::str::FromStr for stats::Modification {
217    type Err = EpochError; // placeholder – you can make a custom error
218
219    fn from_str(s: &str) -> Result<Self, Self::Err> {
220        match s.to_ascii_lowercase().as_str() {
221            w if w.starts_with("delt") => Ok(stats::Modification::Delta),
222            "add" => Ok(stats::Modification::Delta),
223            w if w.starts_with("repl") => Ok(stats::Modification::Replace),
224            "change" => Ok(stats::Modification::Replace),
225            w if w.starts_with("del") => Ok(stats::Modification::Delete),
226            _ => Err(EpochError::ParsingArgument {
227                argument_parsed: s.to_owned(),
228                while_parsing: "Modification",
229            }), // trick: convert a bad string into a ParseIntError
230        }
231    }
232}
233
234impl From<bool> for stats::Modification {
235    fn from(value: bool) -> Self {
236        match value {
237            true => stats::Modification::Delta,
238            false => stats::Modification::Replace,
239        }
240    }
241}
242
243impl FromStr for FactionId {
244    type Err = EpochError; // the error we get from `usize::parse`
245
246    /// Parse a string slice into a `FactionId`.
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        // Trim whitespace just in case and attempt to parse.
249        let inner = s
250            .trim()
251            .parse::<usize>()
252            .map_err(|_| EpochError::ParsingArgument {
253                argument_parsed: s.to_string(),
254                while_parsing: "FactionId",
255            })?;
256        Ok(FactionId(inner))
257    }
258}
259
260impl FromStr for ListStats {
261    type Err = EpochError;
262
263    /// Convert an acronym (e.g. `"cri"`) into the corresponding enum variant.
264    fn from_str(s: &str) -> Result<Self, Self::Err> {
265        match s.to_ascii_lowercase().as_str() {
266            "dem" | "demog" | "demographics" | "demo" => Ok(ListStats::Demographics),
267            "pop" | "popul" | "populations" => Ok(ListStats::Population),
268            "grow" | "grw" | "growth" => Ok(ListStats::Growthrate),
269            "cri" | "crim" | "crimes" => Ok(ListStats::Crime),
270            "hapn" | "happines" | "hap" => Ok(ListStats::Happiness),
271            "eco" | "economy" | "econ" => Ok(ListStats::Economy),
272            "food" | "fo" | "agr" | "agri" | "fod" => Ok(ListStats::Food),
273            "industry" | "indus" | "ind" => Ok(ListStats::Industry),
274            "science" | "sci" | "sc" => Ok(ListStats::Science),
275            "trade" | "trd" | "tr" => Ok(ListStats::Trade),
276            "social" | "soc" | "sl" => Ok(ListStats::Social),
277            "culture" | "cul" | "cl" => Ok(ListStats::Culture),
278            "education" | "edu" | "ed" => Ok(ListStats::Education),
279            "tourism" | "tour" | "tm" => Ok(ListStats::Tourism),
280            "influence" | "infl" | "influ" => Ok(ListStats::Influence),
281            "military" | "mil" | "mt" => Ok(ListStats::Military),
282            "army" | "arm" | "ar" => Ok(ListStats::Army),
283            "strength" | "str" | "st" => Ok(ListStats::Strength),
284            "fortification" | "fort" | "ft" => Ok(ListStats::Fortification),
285            "threat" | "thr" | "th" => Ok(ListStats::Threat),
286            "ifr" | "inf" | "infrastructure" | "infra" | "infr" => Ok(ListStats::Infrastructure),
287            "bureaucracy" | "bureau" | "bur" => Ok(ListStats::Bureaucracy),
288            "transportation" | "trans" | "trns" => Ok(ListStats::Transportation),
289            "sprawl" | "spr" | "spra" => Ok(ListStats::Sprawl),
290            "health" | "hlth" | "hlt" => Ok(ListStats::Health),
291            _ => Err(EpochError::UnknownStatAcronim(s.to_owned())),
292        }
293    }
294}
295
296impl From<ListStats> for &str {
297    fn from(value: ListStats) -> Self {
298        match value {
299            ListStats::Demographics => "demographics",
300            ListStats::Population => "population",
301            ListStats::Growthrate => "growthrate",
302            ListStats::Crime => "crime",
303            ListStats::Happiness => "happiness",
304            ListStats::Economy => "economy",
305            ListStats::Food => "food",
306            ListStats::Industry => "industry",
307            ListStats::Science => "science",
308            ListStats::Trade => "trade",
309            ListStats::Social => "social",
310            ListStats::Culture => "culture",
311            ListStats::Education => "education",
312            ListStats::Tourism => "tourism",
313            ListStats::Influence => "influence",
314            ListStats::Military => "military",
315            ListStats::Army => "army",
316            ListStats::Strength => "strength",
317            ListStats::Fortification => "fortification",
318            ListStats::Threat => "threat",
319            ListStats::Infrastructure => "infrastructure",
320            ListStats::Bureaucracy => "bureaucracy",
321            ListStats::Transportation => "transportation",
322            ListStats::Sprawl => "sprawl",
323            ListStats::Health => "health",
324        }
325    }
326}