dzn_rs/
ast.rs

1use std::collections::HashMap;
2
3use crate::{
4    numbers::Integer,
5    value::{GetValue, ShapedArray, Value, ValueArray},
6};
7
8/// The top-level structure which represents a data file.
9///
10/// A data file is a key-value store, where the key is a MiniZinc identifier, and the value is one
11/// of:
12/// - `int`
13/// - `bool`
14/// - `array of` one of the above
15///
16/// Conceptually, the integers in the MiniZinc specification are unbounded, which means the scalar
17/// signed integers not model the DZN integers well. However, from a practical standpoint, many
18/// uses of DZN files do only deal with [`i32`] or others. Therefore, [`DataFile`] is generic over
19/// the integer type to allow the user to decide how big the integers can be.
20#[derive(Clone, Debug, Default)]
21pub struct DataFile<Int> {
22    pub(crate) values: HashMap<String, Value<Int>>,
23    pub(crate) arrays_1d: HashMap<String, ValueArray<Int, 1>>,
24    pub(crate) arrays_2d: HashMap<String, ValueArray<Int, 2>>,
25}
26
27impl<Int: Integer> DataFile<Int> {
28    /// Get a value from the data file with the given `key`.
29    ///
30    /// When attempting to get a specific type, this method does not discriminate to the key not
31    /// existing at all, or whether the value is a different type. In either situation, [`None`] is
32    /// returned.
33    pub fn get<T>(&self, key: &str) -> Option<&T>
34    where
35        Value<Int>: GetValue<T>,
36    {
37        self.values.get(key).and_then(|value| value.try_get())
38    }
39
40    /// Get a 1-dimensional array from the data file with the given `key` and `length`.
41    pub fn array_1d<T>(&self, key: &str, length: usize) -> Option<&ShapedArray<T, 1>>
42    where
43        ValueArray<Int, 1>: GetValue<ShapedArray<T, 1>>,
44    {
45        self.arrays_1d
46            .get(key)
47            .and_then(|array| array.try_get())
48            .filter(move |&array| array.shape == [length])
49    }
50
51    /// Get a 2-dimensional array from the data file with the given `key`.
52    ///
53    /// The array shape should match `shape`.
54    pub fn array_2d<T>(&self, key: &str, shape: [usize; 2]) -> Option<&ShapedArray<T, 2>>
55    where
56        ValueArray<Int, 2>: GetValue<ShapedArray<T, 2>>,
57    {
58        self.arrays_2d
59            .get(key)
60            .and_then(|array| array.try_get())
61            .filter(move |&array| array.shape == shape)
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use proptest::{proptest, strategy::Strategy};
68
69    use super::*;
70
71    fn ident() -> impl Strategy<Value = String> {
72        proptest::string::string_regex("[A-Za-z][A-Za-z0-9_]*").expect("valid regex")
73    }
74
75    fn int_map() -> impl Strategy<Value = HashMap<String, i32>> {
76        proptest::collection::hash_map(ident(), proptest::prelude::any::<i32>(), 1..5)
77    }
78
79    proptest! {
80        #[test]
81        fn integers_are_found(values in int_map()) {
82            let data_file = DataFile {
83                values: values.iter().map(|(k, v)| (k.clone(), Value::Int(*v))).collect(),
84                ..Default::default()
85            };
86
87            for (key, value) in values.iter() {
88                assert_eq!(Some(value), data_file.get::<i32>(key.as_str()));
89            }
90        }
91    }
92
93    proptest! {
94        #[test]
95        fn nonexistent_integer_values_return_none(label in ident()) {
96            let data_file: DataFile<i32> = DataFile { values: [].into(), ..Default::default() };
97
98            assert_eq!(None, data_file.get::<i32>(&label));
99        }
100    }
101}