1use crate::{Point, Setting};
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq)]
14pub enum Dimension {
15 Real {
17 low: f64,
19 high: f64,
21 log: bool,
25 },
26 Int {
28 low: i64,
30 high: i64,
32 },
33 Choice(Vec<String>),
35}
36
37impl Dimension {
38 fn sound(&self) -> bool {
40 match self {
41 Self::Real { low, high, log } => {
42 low < high && low.is_finite() && high.is_finite() && (!log || *low > 0.0)
43 }
44 Self::Int { low, high } => low < high,
45 Self::Choice(options) => !options.is_empty(),
46 }
47 }
48
49 pub fn grid_of(&self, steps: usize) -> usize {
53 match self {
54 Self::Real { .. } => steps.max(1),
55 Self::Int { low, high } => ((high - low + 1) as usize).min(steps.max(1)),
56 Self::Choice(options) => options.len(),
57 }
58 }
59}
60
61impl fmt::Display for Dimension {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Real {
65 low,
66 high,
67 log: false,
68 } => write!(f, "real({low},{high})"),
69 Self::Real {
70 low,
71 high,
72 log: true,
73 } => write!(f, "logreal({low},{high})"),
74 Self::Int { low, high } => write!(f, "int({low},{high})"),
75 Self::Choice(options) => write!(f, "choice({})", options.join("|")),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Default, PartialEq)]
93pub struct Space {
94 dimensions: Vec<(String, Dimension)>,
95}
96
97impl Space {
98 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn with(
108 mut self,
109 name: impl Into<String>,
110 dimension: Dimension,
111 ) -> Result<Self, SpaceError> {
112 let name = name.into();
113 if self.dimensions.iter().any(|(taken, _)| taken == &name) {
114 return Err(SpaceError::Taken(name));
115 }
116 if !dimension.sound() {
117 return Err(SpaceError::Empty(name, dimension));
118 }
119 if let Some(text) = punctuated(&name, &dimension) {
124 return Err(SpaceError::Unreadable(name, text));
125 }
126 self.dimensions.push((name, dimension));
127 Ok(self)
128 }
129
130 pub fn read(&self, said: &str) -> Result<Point, ReadError> {
151 let mut given: Vec<(&str, &str)> = Vec::new();
152 for piece in said.split(',').filter(|piece| !piece.trim().is_empty()) {
153 let (name, value) = piece
154 .split_once('=')
155 .ok_or_else(|| ReadError::Shapeless(piece.trim().to_string()))?;
156 given.push((name.trim(), value.trim()));
157 }
158
159 let mut settings = Vec::with_capacity(self.dimensions.len());
160 for (name, dimension) in &self.dimensions {
161 let value = given
162 .iter()
163 .find(|(said, _)| said == name)
164 .ok_or_else(|| ReadError::Missing(name.clone()))?
165 .1;
166 let setting = understand(dimension, value).ok_or_else(|| {
167 ReadError::NotIn(name.clone(), dimension.clone(), value.to_string())
168 })?;
169 settings.push((name.clone(), setting));
170 }
171
172 if let Some((stranger, _)) = given
173 .iter()
174 .find(|(name, _)| !self.dimensions.iter().any(|(taken, _)| taken == name))
175 {
176 return Err(ReadError::Stranger(stranger.to_string()));
177 }
178 Ok(Point::of(settings))
179 }
180
181 pub fn dimensions(&self) -> &[(String, Dimension)] {
183 &self.dimensions
184 }
185
186 pub fn len(&self) -> usize {
188 self.dimensions.len()
189 }
190
191 pub fn is_empty(&self) -> bool {
193 self.dimensions.is_empty()
194 }
195}
196
197impl fmt::Display for Space {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 let said: Vec<String> = self
200 .dimensions
201 .iter()
202 .map(|(name, dimension)| format!("{name}={dimension}"))
203 .collect();
204 f.write_str(&said.join(","))
205 }
206}
207
208fn punctuated(name: &str, dimension: &Dimension) -> Option<String> {
210 let loud = |text: &str| text.contains(',') || text.contains('=');
211 if loud(name) {
212 return Some(name.to_string());
213 }
214 match dimension {
215 Dimension::Choice(options) => options.iter().find(|option| loud(option)).cloned(),
216 _ => None,
217 }
218}
219
220fn understand(dimension: &Dimension, value: &str) -> Option<Setting> {
225 match dimension {
226 Dimension::Real { low, high, .. } => value
227 .parse::<f64>()
228 .ok()
229 .filter(|read| (low..=high).contains(&read))
230 .map(Setting::Real),
231 Dimension::Int { low, high } => value
232 .parse::<i64>()
233 .ok()
234 .filter(|read| (low..=high).contains(&read))
235 .map(Setting::Int),
236 Dimension::Choice(options) => options
237 .iter()
238 .find(|option| *option == value)
239 .cloned()
240 .map(Setting::Choice),
241 }
242}
243
244#[derive(Debug, Clone, PartialEq)]
246pub enum SpaceError {
247 Taken(String),
249 Empty(String, Dimension),
251 Unreadable(String, String),
254}
255
256impl fmt::Display for SpaceError {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 match self {
259 Self::Taken(name) => write!(
260 f,
261 "`{name}` is already a dimension of this space: a point would have two \
262 values for it and no way to say which"
263 ),
264 Self::Empty(name, dimension) => write!(
265 f,
266 "`{name}` as `{dimension}` has nothing to draw from: a range needs its \
267 bottom below its top, a choice needs an option, and a logarithmic range \
268 needs to start above zero"
269 ),
270 Self::Unreadable(name, text) => write!(
271 f,
272 "`{text}`, of the knob `{name}`, has a `,` or an `=` in it, and a point \
273 writes itself down as `name=value,name=value`: kept, it would be a trial \
274 name that cannot be read back, and by then which knob was meant is gone"
275 ),
276 }
277 }
278}
279
280impl std::error::Error for SpaceError {}
281
282#[derive(Debug, Clone, PartialEq)]
284pub enum ReadError {
285 Shapeless(String),
287 Missing(String),
289 Stranger(String),
291 NotIn(String, Dimension, String),
293}
294
295impl fmt::Display for ReadError {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 match self {
298 Self::Shapeless(piece) => write!(
299 f,
300 "`{piece}` is not `name=value`, and a point is written down as those \
301 separated by commas"
302 ),
303 Self::Missing(name) => write!(
304 f,
305 "this space has a knob `{name}` and the text sets nothing for it: a point \
306 of a space sets every one of its knobs, so this was written against \
307 another space"
308 ),
309 Self::Stranger(name) => write!(
310 f,
311 "`{name}` is not a knob of this space: the text was written against \
312 another one, and reading the part that does match would be a point of \
313 neither"
314 ),
315 Self::NotIn(name, dimension, value) => write!(
316 f,
317 "`{value}` is not a value of `{name}`, which is `{dimension}`: it is \
318 either the wrong kind, an option nobody declared, or outside the range"
319 ),
320 }
321 }
322}
323
324impl std::error::Error for ReadError {}