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: &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}