nu_data/utils/
mod.rs

1pub mod group;
2pub mod split;
3
4mod internal;
5
6pub use crate::utils::group::group;
7pub use crate::utils::split::split;
8
9pub use crate::utils::internal::Reduction;
10use crate::utils::internal::*;
11
12use derive_new::new;
13use getset::Getters;
14use nu_errors::ShellError;
15use nu_protocol::{UntaggedValue, Value};
16use nu_source::Tag;
17
18#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Getters, Clone, new)]
19pub struct Model {
20    pub labels: Labels,
21    pub ranges: (Range, Range),
22
23    pub data: Value,
24    pub percentages: Value,
25}
26
27#[allow(clippy::type_complexity)]
28pub struct Operation<'a> {
29    pub grouper: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
30    pub splitter: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
31    pub format: &'a Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
32    pub eval: &'a Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
33    pub reduction: &'a Reduction,
34}
35
36pub fn report(
37    values: &Value,
38    options: Operation,
39    tag: impl Into<Tag>,
40) -> Result<Model, ShellError> {
41    let tag = tag.into();
42
43    let grouped = group(values, &options.grouper, &tag)?;
44    let splitted = split(&grouped, &options.splitter, &tag)?;
45
46    let x = grouped
47        .row_entries()
48        .map(|(key, _)| key.clone())
49        .collect::<Vec<_>>();
50
51    let x = sort_columns(&x, options.format)?;
52
53    let mut y = splitted
54        .row_entries()
55        .map(|(key, _)| key.clone())
56        .collect::<Vec<_>>();
57
58    y.sort();
59
60    let planes = Labels { x, y };
61
62    let sorted = sort(&planes, &splitted, &tag)?;
63
64    let evaluated = evaluate(
65        &sorted,
66        if options.eval.is_some() {
67            options.eval
68        } else {
69            &None
70        },
71        &tag,
72    )?;
73
74    let group_labels = planes.grouping_total();
75
76    let reduced = reduce(&evaluated, options.reduction, &tag)?;
77
78    let maxima = max(&reduced, &tag)?;
79
80    let percents = percentages(&maxima, &reduced, &tag)?;
81
82    Ok(Model {
83        labels: planes,
84        ranges: (
85            Range {
86                start: UntaggedValue::int(0).into_untagged_value(),
87                end: group_labels,
88            },
89            Range {
90                start: UntaggedValue::int(0).into_untagged_value(),
91                end: maxima,
92            },
93        ),
94        data: reduced,
95        percentages: percents,
96    })
97}
98
99pub mod helpers {
100    use nu_errors::ShellError;
101    use nu_protocol::{row, Value};
102    use nu_source::{Tag, TaggedItem};
103    use nu_test_support::value::{date, int, string, table};
104    use nu_value_ext::ValueExt;
105
106    pub fn committers() -> Vec<Value> {
107        vec![
108            row! {
109                   "date".into() => date("2019-07-23"),
110                   "name".into() =>       string("AR"),
111                "country".into() =>       string("EC"),
112              "chickens".into() =>             int(10)
113            },
114            row! {
115                   "date".into() => date("2019-07-23"),
116                   "name".into() =>       string("JT"),
117                "country".into() =>       string("NZ"),
118               "chickens".into() =>             int(5)
119            },
120            row! {
121                   "date".into() => date("2019-10-10"),
122                   "name".into() =>       string("YK"),
123                "country".into() =>       string("US"),
124               "chickens".into() =>             int(6)
125            },
126            row! {
127                   "date".into() => date("2019-09-24"),
128                   "name".into() =>       string("AR"),
129                "country".into() =>       string("EC"),
130               "chickens".into() =>            int(20)
131            },
132            row! {
133                   "date".into() => date("2019-10-10"),
134                   "name".into() =>       string("JT"),
135                "country".into() =>       string("NZ"),
136               "chickens".into() =>            int(15)
137            },
138            row! {
139                   "date".into() => date("2019-09-24"),
140                   "name".into() =>       string("YK"),
141                "country".into() =>       string("US"),
142               "chickens".into() =>             int(4)
143            },
144            row! {
145                   "date".into() => date("2019-10-10"),
146                   "name".into() =>       string("AR"),
147                "country".into() =>       string("EC"),
148               "chickens".into() =>            int(30)
149            },
150            row! {
151                   "date".into() => date("2019-09-24"),
152                   "name".into() =>       string("JT"),
153                "country".into() =>       string("NZ"),
154              "chickens".into() =>             int(10)
155            },
156            row! {
157                   "date".into() => date("2019-07-23"),
158                   "name".into() =>       string("YK"),
159                "country".into() =>       string("US"),
160               "chickens".into() =>             int(2)
161            },
162        ]
163    }
164
165    pub fn committers_grouped_by_date() -> Value {
166        let sample = table(&committers());
167
168        let grouper = Box::new(move |_, row: &Value| {
169            let key = String::from("date").tagged_unknown();
170            let group_key = row
171                .get_data_by_key(key.borrow_spanned())
172                .expect("get key failed");
173
174            group_key.format("%Y-%m-%d")
175        });
176
177        crate::utils::group(&sample, &Some(grouper), Tag::unknown())
178            .expect("failed to create group")
179    }
180
181    pub fn date_formatter(
182        fmt: String,
183    ) -> Box<dyn Fn(&Value, String) -> Result<String, ShellError>> {
184        Box::new(move |date: &Value, _: String| {
185            let fmt = fmt.clone();
186            date.format(&fmt)
187        })
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::helpers::{committers, date_formatter};
194    use super::{report, Labels, Model, Operation, Range, Reduction};
195    use nu_errors::ShellError;
196    use nu_protocol::Value;
197    use nu_source::{Tag, TaggedItem};
198    use nu_test_support::value::{decimal_from_float, int, table};
199    use nu_value_ext::ValueExt;
200
201    pub fn assert_without_checking_percentages(report_a: Model, report_b: Model) {
202        assert_eq!(report_a.labels.x, report_b.labels.x);
203        assert_eq!(report_a.labels.y, report_b.labels.y);
204        assert_eq!(report_a.ranges, report_b.ranges);
205        assert_eq!(report_a.data, report_b.data);
206    }
207
208    #[test]
209    fn prepares_report_using_counting_value() {
210        let committers = table(&committers());
211
212        let by_date = Box::new(move |_, row: &Value| {
213            let key = String::from("date").tagged_unknown();
214            let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
215
216            let callback = date_formatter("%Y-%m-%d".to_string());
217            callback(&key, "nothing".to_string())
218        });
219
220        let by_country = Box::new(move |_, row: &Value| {
221            let key = String::from("country").tagged_unknown();
222            let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
223            nu_value_ext::as_string(&key)
224        });
225
226        let options = Operation {
227            grouper: Some(by_date),
228            splitter: Some(by_country),
229            format: &None,
230            eval: /* value to be used for accumulation */ &Some(Box::new(move |_, value: &Value| {
231                let chickens_key = String::from("chickens").tagged_unknown();
232
233                value
234                    .get_data_by_key(chickens_key.borrow_spanned())
235                    .ok_or_else(|| {
236                        ShellError::labeled_error(
237                            "unknown column",
238                            "unknown column",
239                            chickens_key.span(),
240                        )
241                    })
242            })),
243            reduction: &Reduction::Count
244        };
245
246        assert_without_checking_percentages(
247            report(&committers, options, Tag::unknown()).unwrap(),
248            Model {
249                labels: Labels {
250                    x: vec![
251                        String::from("2019-07-23"),
252                        String::from("2019-09-24"),
253                        String::from("2019-10-10"),
254                    ],
255                    y: vec![String::from("EC"), String::from("NZ"), String::from("US")],
256                },
257                ranges: (
258                    Range {
259                        start: int(0),
260                        end: int(3),
261                    },
262                    Range {
263                        start: int(0),
264                        end: int(30),
265                    },
266                ),
267                data: table(&[
268                    table(&[int(10), int(20), int(30)]),
269                    table(&[int(5), int(10), int(15)]),
270                    table(&[int(2), int(4), int(6)]),
271                ]),
272                percentages: table(&[
273                    table(&[
274                        decimal_from_float(33.33),
275                        decimal_from_float(66.66),
276                        decimal_from_float(99.99),
277                    ]),
278                    table(&[
279                        decimal_from_float(16.66),
280                        decimal_from_float(33.33),
281                        decimal_from_float(49.99),
282                    ]),
283                    table(&[
284                        decimal_from_float(6.66),
285                        decimal_from_float(13.33),
286                        decimal_from_float(19.99),
287                    ]),
288                ]),
289            },
290        );
291    }
292}