Skip to main content

quarb_lib_numpy/
lib.rs

1//! `np:` — numpy as a Quarb command library, by reflection (spec:
2//! Command Libraries, "Reflected commands"): every function of the
3//! package and every method of `ndarray` is a command, named as
4//! numpy names it (`np:mean`, `np:linalg.norm`, `np:percentile(50)`),
5//! its synopsis read from the function's signature when the gate
6//! first meets the name. A method is a function whose first
7//! parameter is the receiver, and the input fills it.
8//!
9//! The stream crosses as an array: a list of numbers is a vector, a
10//! list of lists a matrix, a list of records the matrix of its
11//! numeric fields (the first text field, the label, is dropped: an
12//! array has no names; `pd:` keeps them), instants `datetime64`,
13//! durations `timedelta64`, null NaN. An array coming back is a
14//! library value, kept as it is for the next command and read by
15//! `| @` as nested lists; a numpy scalar crosses as a number.
16
17use quarb::Value;
18use quarb::library::{CommandLibrary, LibCall, LibFailure, Synopsis};
19use quarb_pyvalue::Reflective;
20
21static REFLECT: Reflective = Reflective::new("numpy", &["numpy.ndarray"], "ndarray", false);
22
23/// The library.
24pub struct Numpy;
25
26impl CommandLibrary for Numpy {
27    fn prefix(&self) -> &str {
28        "np"
29    }
30
31    fn commands(&self) -> &[Synopsis] {
32        &[]
33    }
34
35    fn requirements(&self) -> &[&str] {
36        &["numpy"]
37    }
38
39    fn available(&self) -> Result<(), String> {
40        quarb_pyvalue::importable(&["numpy"], "numpy")
41    }
42
43    fn resolve(&self, name: &str) -> Result<&Synopsis, String> {
44        REFLECT.resolve(name)
45    }
46
47    fn call(&self, name: &str, call: LibCall) -> Result<Value, LibFailure> {
48        let syn = REFLECT.resolve(name).map_err(LibFailure::Unknown)?;
49        REFLECT.call(syn, name, call)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    fn nums(v: &[i64]) -> Value {
58        Value::list(v.iter().map(|&n| Value::Int(n)).collect())
59    }
60
61    fn agg(name: &str, input: Value, operands: Vec<Value>) -> Result<Value, LibFailure> {
62        Numpy.call(
63            name,
64            LibCall {
65                agg: true,
66                input: Some(input),
67                operands,
68                options: vec![],
69            },
70        )
71    }
72
73    #[test]
74    fn functions_and_methods_by_reflection() {
75        if Numpy.available().is_err() {
76            eprintln!("numpy not importable; skipping");
77            return;
78        }
79        // A function of the package: the stream its first parameter.
80        assert_eq!(agg("mean", nums(&[1, 2, 3, 6]), vec![]).unwrap(), Value::Float(3.0));
81        // A surplus operand fills the next positional parameter.
82        assert_eq!(
83            agg("percentile", nums(&[1, 2, 3, 4]), vec![Value::Int(50)]).unwrap(),
84            Value::Float(2.5)
85        );
86        // A dotted name walks a submodule.
87        assert_eq!(agg("linalg.norm", nums(&[3, 4]), vec![]).unwrap(), Value::Float(5.0));
88        // A method of ndarray, the input its receiver; the
89        // two-dimensional array it returns comes back as a table
90        // (an array, no header), read by its rows.
91        let t = agg(
92            "transpose",
93            Value::list(vec![nums(&[1, 2, 3]), nums(&[4, 5, 6])]),
94            vec![],
95        )
96        .unwrap();
97        let Value::Table(f) = &t else { panic!("{t:?}") };
98        assert!(f.header.is_none() && f.kind().starts_with("an array 3×2"), "{}", f.kind());
99        assert_eq!(
100            f.rows_value(),
101            Value::list(vec![nums(&[1, 4]), nums(&[2, 5]), nums(&[3, 6])])
102        );
103        // The synopsis comes from the signature.
104        let syn = Numpy.resolve("percentile").unwrap();
105        assert_eq!(syn.operands[0].name, "a");
106        assert_eq!(syn.operands[1].name, "q");
107        assert!(syn.options.iter().any(|o| o.long == "method"), "{:?}", syn.options);
108        let e = Numpy.resolve("meen").unwrap_err();
109        assert!(e.contains("numpy") && e.contains("mean"), "{e}");
110    }
111}