Skip to main content

henad_core/explore/
value.rs

1//! Parameter values written as text, read and checked against their descriptors.
2
3use std::fmt;
4use std::num::{ParseFloatError, ParseIntError};
5use std::str::ParseBoolError;
6
7use crate::params::{ParamDescriptor, ParamKind, ParamValue};
8
9/// A parameter value that cannot be read, or does not fit its descriptor.
10///
11/// The `source` of text that cannot be parsed as its kind is the parser's error.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ValueError {
14    /// Text that cannot be parsed as an `f32`.
15    NotANumber {
16        /// Text as given.
17        raw: String,
18        /// Error of the `f32` parser.
19        source: ParseFloatError,
20    },
21    /// Text that cannot be parsed as a `u32`.
22    NotAnInteger {
23        /// Text as given.
24        raw: String,
25        /// Error of the `u32` parser.
26        source: ParseIntError,
27    },
28    /// Text that cannot be parsed as a `bool`.
29    NotABool {
30        /// Text as given.
31        raw: String,
32        /// Error of the `bool` parser.
33        source: ParseBoolError,
34    },
35    /// A number outside the descriptor's inclusive bounds. A non-finite number is outside every bound.
36    OutOfRange {
37        /// Number as text.
38        value: String,
39        /// Lower bound as text.
40        min: String,
41        /// Upper bound as text.
42        max: String,
43    },
44    /// Neither the index nor the name of an option.
45    UnknownOption {
46        /// Text as given, or the index as text.
47        raw: String,
48        /// Options of the parameter.
49        options: &'static [&'static str],
50    },
51    /// An id no descriptor has.
52    UnknownParam {
53        /// Id as given.
54        id: String,
55        /// Ids the descriptors have.
56        known: Vec<&'static str>,
57    },
58    /// An override with no `=` between the id and the value.
59    BadOverride {
60        /// Override as given.
61        raw: String,
62    },
63    /// A value that parameter `id` rejects.
64    Param {
65        /// Id of the parameter.
66        id: String,
67        /// Reason the descriptor rejects the value.
68        source: Box<Self>,
69    },
70    /// A value passed to [`check_value`] whose kind differs from the descriptor's kind.
71    WrongKind {
72        /// Kind that the descriptor accepts.
73        expected: ValueKind,
74        /// Kind of the value.
75        found: ValueKind,
76        /// Value as text.
77        value: String,
78    },
79}
80
81/// Kind of a parameter value, as [`ValueError::WrongKind`] reports it.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum ValueKind {
84    /// An `f32`.
85    F32,
86    /// A `u32`.
87    U32,
88    /// A `bool`.
89    Bool,
90    /// An index into a choice's options.
91    Choice,
92}
93
94impl fmt::Display for ValueKind {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str(match self {
97            Self::F32 => "f32",
98            Self::U32 => "u32",
99            Self::Bool => "bool",
100            Self::Choice => "choice",
101        })
102    }
103}
104
105impl fmt::Display for ValueError {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::NotANumber { raw, .. } => write!(f, "'{raw}' is not a number"),
109            Self::NotAnInteger { raw, .. } => write!(f, "'{raw}' is not an integer"),
110            Self::NotABool { raw, .. } => write!(f, "'{raw}' is not a bool"),
111            Self::OutOfRange { value, min, max } => write!(f, "{value} is outside {min}..={max}"),
112            Self::UnknownOption { raw, options } if raw.parse::<usize>().is_ok() => {
113                write!(f, "index {raw} is not one of {options:?}")
114            }
115            Self::UnknownOption { raw, options } => write!(f, "'{raw}' is not one of {options:?}"),
116            Self::UnknownParam { id, .. } => write!(f, "model has no parameter '{id}'"),
117            Self::BadOverride { raw } => write!(f, "bad --set '{raw}', expected ID=VALUE"),
118            Self::Param { id, .. } => write!(f, "parameter '{id}'"),
119            Self::WrongKind { expected, found, value } => {
120                let example = match expected {
121                    ValueKind::F32 => "an f32 value such as 1.0f32",
122                    ValueKind::U32 => "a u32 value such as 1u32",
123                    ValueKind::Bool => "a bool value",
124                    ValueKind::Choice => "a choice such as ParamValue::Choice(0)",
125                };
126                write!(f, "expected {example}, found the {found} {value}")
127            }
128        }
129    }
130}
131
132impl std::error::Error for ValueError {
133    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
134        match self {
135            Self::NotANumber { source, .. } => Some(source),
136            Self::NotAnInteger { source, .. } => Some(source),
137            Self::NotABool { source, .. } => Some(source),
138            Self::Param { source, .. } => Some(source),
139            _ => None,
140        }
141    }
142}
143
144/// Splits each `--set ID=VALUE` string into an `(id, value)` pair at its first `=`.
145///
146/// # Errors
147///
148/// Returns [`ValueError::BadOverride`] for a string with no `=`.
149pub fn parse_overrides(raw: &[String]) -> Result<Vec<(String, String)>, ValueError> {
150    raw.iter()
151        .map(|pair| {
152            let (id, value) = pair
153                .split_once('=')
154                .ok_or_else(|| ValueError::BadOverride { raw: pair.clone() })?;
155            Ok((id.to_owned(), value.to_owned()))
156        })
157        .collect()
158}
159
160/// Returns every descriptor's default value, with `overrides` applied in order by id.
161///
162/// # Errors
163///
164/// Returns [`ValueError::UnknownParam`] for an id no descriptor has, and [`ValueError::Param`] for a
165/// value that its descriptor rejects.
166pub fn resolve_params(
167    descriptors: &[ParamDescriptor],
168    overrides: &[(String, String)],
169) -> Result<Vec<ParamValue>, ValueError> {
170    let mut values: Vec<ParamValue> = descriptors
171        .iter()
172        .map(|descriptor| descriptor.kind.default_value())
173        .collect();
174
175    for (id, raw) in overrides {
176        let index = descriptors
177            .iter()
178            .position(|descriptor| descriptor.id == *id)
179            .ok_or_else(|| ValueError::UnknownParam {
180                id: id.clone(),
181                known: descriptors.iter().map(|descriptor| descriptor.id).collect(),
182            })?;
183        values[index] = parse_value(&descriptors[index].kind, raw).map_err(|error| ValueError::Param {
184            id: id.clone(),
185            source: Box::new(error),
186        })?;
187    }
188
189    Ok(values)
190}
191
192/// Reads `raw` as a value of `kind`, and checks it with [`check_value`].
193///
194/// For a choice, `raw` is read as an option name first, then as an option index.
195///
196/// # Errors
197///
198/// Returns [`ValueError`] when `raw` cannot be parsed as the kind, or [`check_value`] rejects it.
199pub fn parse_value(kind: &ParamKind, raw: &str) -> Result<ParamValue, ValueError> {
200    let value = match kind {
201        ParamKind::F32 { .. } => ParamValue::F32(raw.parse().map_err(|source| ValueError::NotANumber {
202            raw: raw.to_owned(),
203            source,
204        })?),
205        ParamKind::U32 { .. } => ParamValue::U32(raw.parse().map_err(|source| ValueError::NotAnInteger {
206            raw: raw.to_owned(),
207            source,
208        })?),
209        ParamKind::Bool { .. } => ParamValue::Bool(raw.parse().map_err(|source| ValueError::NotABool {
210            raw: raw.to_owned(),
211            source,
212        })?),
213        ParamKind::Choice { options, .. } => {
214            let index = options
215                .iter()
216                .position(|option| *option == raw)
217                .or_else(|| raw.parse::<usize>().ok())
218                .ok_or_else(|| ValueError::UnknownOption {
219                    raw: raw.to_owned(),
220                    options,
221                })?;
222            ParamValue::Choice(index)
223        }
224    };
225    check_value(kind, &value)?;
226    Ok(value)
227}
228
229/// Checks `value` against the type, bounds and options of `kind`.
230///
231/// # Errors
232///
233/// Returns [`ValueError::OutOfRange`] for a number outside the bounds or not finite,
234/// [`ValueError::UnknownOption`] for an index past the options, and [`ValueError::WrongKind`] for a value of another
235/// kind. A `u32` is never accepted as a choice index.
236pub fn check_value(kind: &ParamKind, value: &ParamValue) -> Result<(), ValueError> {
237    let out_of_range = |value: &dyn fmt::Display, min: &dyn fmt::Display, max: &dyn fmt::Display| {
238        Err(ValueError::OutOfRange {
239            value: value.to_string(),
240            min: min.to_string(),
241            max: max.to_string(),
242        })
243    };
244    match (kind, value) {
245        (ParamKind::F32 { min, max, .. }, ParamValue::F32(number)) => {
246            if number.is_finite() && number >= min && number <= max {
247                Ok(())
248            } else {
249                out_of_range(number, min, max)
250            }
251        }
252        (ParamKind::U32 { min, max, .. }, ParamValue::U32(number)) => {
253            if number >= min && number <= max {
254                Ok(())
255            } else {
256                out_of_range(number, min, max)
257            }
258        }
259        (ParamKind::Bool { .. }, ParamValue::Bool(_)) => Ok(()),
260        (ParamKind::Choice { options, .. }, ParamValue::Choice(index)) if *index < options.len() => Ok(()),
261        (ParamKind::Choice { options, .. }, ParamValue::Choice(index)) => Err(ValueError::UnknownOption {
262            raw: index.to_string(),
263            options,
264        }),
265        (kind, other) => Err(ValueError::WrongKind {
266            expected: param_kind(kind),
267            found: value_kind(other),
268            value: plain_text(other),
269        }),
270    }
271}
272
273/// Returns the kind of value that `kind` accepts.
274fn param_kind(kind: &ParamKind) -> ValueKind {
275    match kind {
276        ParamKind::F32 { .. } => ValueKind::F32,
277        ParamKind::U32 { .. } => ValueKind::U32,
278        ParamKind::Bool { .. } => ValueKind::Bool,
279        ParamKind::Choice { .. } => ValueKind::Choice,
280    }
281}
282
283/// Returns the kind of `value`.
284fn value_kind(value: &ParamValue) -> ValueKind {
285    match value {
286        ParamValue::F32(_) => ValueKind::F32,
287        ParamValue::U32(_) => ValueKind::U32,
288        ParamValue::Bool(_) => ValueKind::Bool,
289        ParamValue::Choice(_) => ValueKind::Choice,
290    }
291}
292
293/// Returns `value` as text that [`parse_value`] reads back unchanged.
294///
295/// A number is written in its shortest round-trip form, and a choice as its option name.
296pub fn format_value(kind: &ParamKind, value: &ParamValue) -> String {
297    match (kind, value) {
298        (ParamKind::Choice { options, .. }, ParamValue::Choice(index)) => options
299            .get(*index)
300            .map_or_else(|| index.to_string(), |&name| name.to_owned()),
301        _ => plain_text(value),
302    }
303}
304
305/// Returns `value` as text without its descriptor, so a choice is written as its index.
306fn plain_text(value: &ParamValue) -> String {
307    match value {
308        ParamValue::F32(number) => number.to_string(),
309        ParamValue::U32(number) => number.to_string(),
310        ParamValue::Bool(flag) => flag.to_string(),
311        ParamValue::Choice(index) => index.to_string(),
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::{ValueError, ValueKind, check_value, format_value, parse_overrides, parse_value, resolve_params};
318    use crate::helpers::{bool_param, f32_param, u32_param};
319    use crate::params::{ParamApply, ParamDescriptor, ParamFormat, ParamKind, ParamValue};
320
321    const OPTIONS: &[&str] = &["moore", "von_neumann"];
322
323    fn descriptors() -> Vec<ParamDescriptor> {
324        vec![
325            u32_param("num_agents", "Agents", 1000, 1, 5_000_000),
326            f32_param("cohesion", "Cohesion", 0.5, 0.0, 1.0, None),
327            ParamDescriptor {
328                id: "neighborhood",
329                label: "Neighborhood",
330                kind: ParamKind::Choice {
331                    options: OPTIONS,
332                    default: 0,
333                },
334                apply: ParamApply::Live,
335                format: ParamFormat::Plain,
336            },
337        ]
338    }
339
340    fn resolve(raw: &str) -> Result<Vec<ParamValue>, ValueError> {
341        let overrides = parse_overrides(&[raw.to_owned()])?;
342        resolve_params(&descriptors(), &overrides)
343    }
344
345    /// Returns the error's message followed by each source's message, joined by ": ".
346    fn message(error: &ValueError) -> String {
347        let mut text = error.to_string();
348        let mut source = std::error::Error::source(error);
349        while let Some(inner) = source {
350            text = format!("{text}: {inner}");
351            source = inner.source();
352        }
353        text
354    }
355
356    #[test]
357    fn an_override_replaces_one_default_and_leaves_the_rest() {
358        let values = resolve("num_agents=2500").expect("2500 agents is in range");
359        assert_eq!(values[0], ParamValue::U32(2500));
360        assert_eq!(values[1], ParamValue::F32(0.5));
361        assert_eq!(values[2], ParamValue::Choice(0));
362    }
363
364    /// A slider cannot request four billion agents, and `--set` cannot either. Unchecked, the number would go
365    /// straight to `init`.
366    #[test]
367    fn a_value_outside_the_descriptor_range_is_refused() {
368        let error = resolve("num_agents=4000000000").expect_err("4e9 agents is over the maximum");
369        assert!(message(&error).contains("5000000"), "{}", message(&error));
370
371        let error = resolve("num_agents=0").expect_err("0 agents is under the minimum");
372        assert!(message(&error).contains("num_agents"), "{}", message(&error));
373
374        assert!(resolve("cohesion=1.5").is_err(), "1.5 is over the maximum");
375        assert!(resolve("cohesion=-0.5").is_err(), "-0.5 is under the minimum");
376        assert!(resolve("cohesion=nan").is_err(), "NaN is in no range");
377    }
378
379    /// Both ends of the range are allowed.
380    #[test]
381    fn the_limits_themselves_are_accepted() {
382        assert_eq!(resolve("cohesion=0").expect("the minimum")[1], ParamValue::F32(0.0));
383        assert_eq!(resolve("cohesion=1").expect("the maximum")[1], ParamValue::F32(1.0));
384        assert_eq!(
385            resolve("num_agents=5000000").expect("the maximum")[0],
386            ParamValue::U32(5_000_000)
387        );
388    }
389
390    /// A choice is an index into the option list, by number or by label.
391    #[test]
392    fn a_choice_index_is_checked_against_the_options() {
393        assert_eq!(
394            resolve("neighborhood=1").expect("index 1 exists")[2],
395            ParamValue::Choice(1)
396        );
397        assert_eq!(
398            resolve("neighborhood=von_neumann").expect("a label")[2],
399            ParamValue::Choice(1)
400        );
401        assert!(resolve("neighborhood=2").is_err(), "there is no third option");
402        assert!(resolve("neighborhood=hexagonal").is_err(), "no such label");
403    }
404
405    #[test]
406    fn an_unknown_parameter_is_refused() {
407        assert!(resolve("no_such_param=1").is_err());
408        assert!(
409            parse_overrides(&["num_agents".to_owned()]).is_err(),
410            "no '=' in the pair"
411        );
412    }
413
414    #[test]
415    fn text_of_the_wrong_kind_keeps_the_parser_error() {
416        let error = resolve("num_agents=abc").expect_err("not an integer");
417        assert_eq!(
418            message(&error),
419            "parameter 'num_agents': 'abc' is not an integer: invalid digit found in string",
420            "the parser's error is the last source"
421        );
422        let error = resolve("cohesion=high").expect_err("not a number");
423        assert!(
424            message(&error).ends_with("invalid float literal"),
425            "{}",
426            message(&error)
427        );
428    }
429
430    #[test]
431    fn a_formatted_value_parses_back_to_itself() {
432        let mut descriptors = descriptors();
433        descriptors.push(bool_param("wrap", "Wrap", true));
434        let cases = [
435            (0, ParamValue::U32(1)),
436            (0, ParamValue::U32(5_000_000)),
437            (1, ParamValue::F32(0.0)),
438            (1, ParamValue::F32(0.1)),
439            (1, ParamValue::F32(1.0 / 3.0)),
440            (1, ParamValue::F32(f32::from_bits(1))),
441            (1, ParamValue::F32(1.0)),
442            (2, ParamValue::Choice(0)),
443            (2, ParamValue::Choice(1)),
444            (3, ParamValue::Bool(false)),
445            (3, ParamValue::Bool(true)),
446        ];
447        for (index, value) in cases {
448            let kind = &descriptors[index].kind;
449            let text = format_value(kind, &value);
450            assert_eq!(parse_value(kind, &text), Ok(value), "{text}");
451        }
452        assert_eq!(
453            format_value(&descriptors[2].kind, &ParamValue::Choice(1)),
454            "von_neumann"
455        );
456    }
457
458    /// A name that looks like a number is matched as a name first. Read as an index, option "0" at index 1 would
459    /// come back as option 0.
460    #[test]
461    fn a_choice_reads_an_option_name_before_an_index() {
462        let kind = ParamKind::Choice {
463            options: &["low", "0"],
464            default: 0,
465        };
466        assert_eq!(parse_value(&kind, "0"), Ok(ParamValue::Choice(1)), "the name wins");
467        assert_eq!(
468            parse_value(&kind, "1"),
469            Ok(ParamValue::Choice(1)),
470            "an index still reads"
471        );
472        let kind = ParamKind::Choice {
473            options: &["low", "2"],
474            default: 0,
475        };
476        for index in 0..2 {
477            let value = ParamValue::Choice(index);
478            assert_eq!(parse_value(&kind, &format_value(&kind, &value)), Ok(value));
479        }
480    }
481
482    #[test]
483    fn a_value_that_does_not_fit_its_kind_is_refused() {
484        let descriptors = descriptors();
485        assert!(check_value(&descriptors[0].kind, &ParamValue::F32(10.0)).is_err());
486        assert!(check_value(&descriptors[1].kind, &ParamValue::U32(0)).is_err());
487        assert!(check_value(&descriptors[1].kind, &ParamValue::F32(f32::INFINITY)).is_err());
488        assert!(check_value(&descriptors[2].kind, &ParamValue::Choice(2)).is_err());
489        assert_eq!(check_value(&descriptors[2].kind, &ParamValue::Choice(1)), Ok(()));
490    }
491
492    /// The message for a value of another kind includes the literal that fits.
493    #[test]
494    fn a_value_of_another_kind_names_both_kinds() {
495        let descriptors = descriptors();
496        let error = check_value(&descriptors[1].kind, &ParamValue::U32(1)).expect_err("a u32 for an f32");
497        assert_eq!(
498            error,
499            ValueError::WrongKind {
500                expected: ValueKind::F32,
501                found: ValueKind::U32,
502                value: "1".to_owned(),
503            }
504        );
505        assert_eq!(
506            error.to_string(),
507            "expected an f32 value such as 1.0f32, found the u32 1"
508        );
509        let error = check_value(&descriptors[0].kind, &ParamValue::F32(256.0)).expect_err("an f32 for a u32");
510        assert_eq!(
511            error.to_string(),
512            "expected a u32 value such as 1u32, found the f32 256"
513        );
514        let error = check_value(&descriptors[2].kind, &ParamValue::U32(1)).expect_err("a u32 for a choice");
515        assert!(
516            matches!(
517                error,
518                ValueError::WrongKind {
519                    expected: ValueKind::Choice,
520                    found: ValueKind::U32,
521                    ..
522                }
523            ),
524            "{error:?}"
525        );
526        assert_eq!(
527            error.to_string(),
528            "expected a choice such as ParamValue::Choice(0), found the u32 1"
529        );
530        let wrap = bool_param("wrap", "Wrap", true);
531        let error = check_value(&wrap.kind, &ParamValue::Choice(0)).expect_err("a choice for a bool");
532        assert_eq!(error.to_string(), "expected a bool value, found the choice 0");
533    }
534}