nu_data/
base.rs

1pub mod shape;
2
3use bigdecimal::BigDecimal;
4use chrono::{DateTime, FixedOffset, Utc};
5use derive_new::new;
6use nu_errors::ShellError;
7use nu_protocol::{
8    hir, Primitive, ShellTypeName, SpannedTypeName, TaggedDictBuilder, UntaggedValue, Value,
9};
10use nu_source::{Span, Tag};
11use nu_value_ext::ValueExt;
12use num_bigint::BigInt;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new, Serialize)]
16pub struct Operation {
17    pub(crate) left: Value,
18    pub(crate) operator: hir::Operator,
19    pub(crate) right: Value,
20}
21
22#[derive(Serialize, Deserialize)]
23pub enum Switch {
24    Present,
25    Absent,
26}
27
28impl std::convert::TryFrom<Option<&Value>> for Switch {
29    type Error = ShellError;
30
31    fn try_from(value: Option<&Value>) -> Result<Switch, ShellError> {
32        match value {
33            None => Ok(Switch::Absent),
34            Some(value) => match &value.value {
35                UntaggedValue::Primitive(Primitive::Boolean(true)) => Ok(Switch::Present),
36                _ => Err(ShellError::type_error("Boolean", value.spanned_type_name())),
37            },
38        }
39    }
40}
41
42pub fn select_fields(obj: &Value, fields: &[String], tag: impl Into<Tag>) -> Value {
43    let mut out = TaggedDictBuilder::new(tag);
44
45    let descs = obj.data_descriptors();
46
47    for column_name in fields {
48        match descs.iter().find(|&d| d == column_name) {
49            None => out.insert_untagged(column_name, UntaggedValue::nothing()),
50            Some(desc) => out.insert_value(desc.clone(), obj.get_data(desc).borrow().clone()),
51        }
52    }
53
54    out.into_value()
55}
56
57pub fn reject_fields(obj: &Value, fields: &[String], tag: impl Into<Tag>) -> Value {
58    let mut out = TaggedDictBuilder::new(tag);
59
60    let descs = obj.data_descriptors();
61
62    for desc in descs {
63        if fields.iter().any(|field| *field == desc) {
64            continue;
65        } else {
66            out.insert_value(desc.clone(), obj.get_data(&desc).borrow().clone())
67        }
68    }
69
70    out.into_value()
71}
72
73pub enum CompareValues {
74    Ints(i64, i64),
75    Filesizes(u64, u64),
76    BigInts(BigInt, BigInt),
77    Decimals(BigDecimal, BigDecimal),
78    String(String, String),
79    Date(DateTime<FixedOffset>, DateTime<FixedOffset>),
80    DateDuration(DateTime<FixedOffset>, BigInt),
81    TimeDuration(BigInt, BigInt),
82    Booleans(bool, bool),
83}
84
85impl CompareValues {
86    pub fn compare(&self) -> std::cmp::Ordering {
87        match self {
88            CompareValues::BigInts(left, right) => left.cmp(right),
89            CompareValues::Ints(left, right) => left.cmp(right),
90            CompareValues::Filesizes(left, right) => left.cmp(right),
91            CompareValues::Decimals(left, right) => left.cmp(right),
92            CompareValues::String(left, right) => left.cmp(right),
93            CompareValues::Date(left, right) => left.cmp(right),
94            CompareValues::DateDuration(left, right) => {
95                // FIXME: Not sure if I could do something better with the Span.
96                let duration = Primitive::into_chrono_duration(
97                    Primitive::Duration(right.clone()),
98                    Span::unknown(),
99                )
100                .expect("Could not convert nushell Duration into chrono Duration.");
101                let right: DateTime<FixedOffset> = Utc::now()
102                    .checked_sub_signed(duration)
103                    .expect("Data overflow")
104                    .into();
105                right.cmp(left)
106            }
107            CompareValues::Booleans(left, right) => left.cmp(right),
108            CompareValues::TimeDuration(left, right) => left.cmp(right),
109        }
110    }
111}
112
113pub fn coerce_compare(
114    left: &UntaggedValue,
115    right: &UntaggedValue,
116) -> Result<CompareValues, (&'static str, &'static str)> {
117    match (left, right) {
118        (UntaggedValue::Primitive(left), UntaggedValue::Primitive(right)) => {
119            coerce_compare_primitive(left, right)
120        }
121
122        _ => Err((left.type_name(), right.type_name())),
123    }
124}
125
126pub fn coerce_compare_primitive(
127    left: &Primitive,
128    right: &Primitive,
129) -> Result<CompareValues, (&'static str, &'static str)> {
130    use Primitive::*;
131
132    Ok(match (left, right) {
133        (Int(left), Int(right)) => CompareValues::Ints(*left, *right),
134        (Int(left), BigInt(right)) => {
135            CompareValues::BigInts(num_bigint::BigInt::from(*left), right.clone())
136        }
137        (BigInt(left), Int(right)) => {
138            CompareValues::BigInts(left.clone(), num_bigint::BigInt::from(*right))
139        }
140        (BigInt(left), BigInt(right)) => CompareValues::BigInts(left.clone(), right.clone()),
141
142        (Int(left), Decimal(right)) => {
143            CompareValues::Decimals(BigDecimal::from(*left), right.clone())
144        }
145        (BigInt(left), Decimal(right)) => {
146            CompareValues::Decimals(BigDecimal::from(left.clone()), right.clone())
147        }
148        (Decimal(left), Decimal(right)) => CompareValues::Decimals(left.clone(), right.clone()),
149        (Decimal(left), Int(right)) => {
150            CompareValues::Decimals(left.clone(), BigDecimal::from(*right))
151        }
152        (Decimal(left), BigInt(right)) => {
153            CompareValues::Decimals(left.clone(), BigDecimal::from(right.clone()))
154        }
155        (Decimal(left), Filesize(right)) => {
156            CompareValues::Decimals(left.clone(), BigDecimal::from(*right))
157        }
158        (Filesize(left), Filesize(right)) => CompareValues::Filesizes(*left, *right),
159        (Filesize(left), Decimal(right)) => {
160            CompareValues::Decimals(BigDecimal::from(*left), right.clone())
161        }
162        (Nothing, Nothing) => CompareValues::Booleans(true, true),
163        (String(left), String(right)) => CompareValues::String(left.clone(), right.clone()),
164        (Date(left), Date(right)) => CompareValues::Date(*left, *right),
165        (Date(left), Duration(right)) => CompareValues::DateDuration(*left, right.clone()),
166        (Boolean(left), Boolean(right)) => CompareValues::Booleans(*left, *right),
167        (Boolean(left), Nothing) => CompareValues::Ints(if *left { 1 } else { 0 }, -1),
168        (Nothing, Boolean(right)) => CompareValues::Ints(-1, if *right { 1 } else { 0 }),
169        (String(left), Nothing) => CompareValues::String(left.clone(), std::string::String::new()),
170        (Nothing, String(right)) => {
171            CompareValues::String(std::string::String::new(), right.clone())
172        }
173        (FilePath(left), String(right)) => {
174            CompareValues::String(left.as_path().display().to_string(), right.clone())
175        }
176        (String(left), FilePath(right)) => {
177            CompareValues::String(left.clone(), right.as_path().display().to_string())
178        }
179        (Duration(left), Duration(right)) => {
180            CompareValues::TimeDuration(left.clone(), right.clone())
181        }
182        _ => return Err((left.type_name(), right.type_name())),
183    })
184}
185#[cfg(test)]
186mod tests {
187    use nu_errors::ShellError;
188    use nu_protocol::UntaggedValue;
189    use nu_source::SpannedItem;
190    use nu_test_support::value::*;
191    use nu_value_ext::ValueExt;
192
193    use indexmap::indexmap;
194
195    #[test]
196    fn gets_matching_field_from_a_row() -> Result<(), ShellError> {
197        let row = UntaggedValue::row(indexmap! {
198            "amigos".into() => table(&[string("andres"),string("jonathan"),string("yehuda")])
199        })
200        .into_untagged_value();
201
202        assert_eq!(
203            row.get_data_by_key("amigos".spanned_unknown())
204                .ok_or_else(|| ShellError::unexpected("Failure during testing"))?,
205            table(&[string("andres"), string("jonathan"), string("yehuda")])
206        );
207
208        Ok(())
209    }
210
211    #[test]
212    fn gets_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> {
213        let field_path = column_path("package.version").as_column_path()?;
214
215        let (version, tag) = string("0.4.0").into_parts();
216
217        let value = UntaggedValue::row(indexmap! {
218            "package".into() =>
219                row(indexmap! {
220                    "name".into()    =>     string("nu"),
221                    "version".into() =>  string("0.4.0")
222                })
223        });
224
225        assert_eq!(
226            *value.into_value(tag).get_data_by_column_path(
227                &field_path.item,
228                Box::new(error_callback("package.version"))
229            )?,
230            version
231        );
232
233        Ok(())
234    }
235
236    #[test]
237    fn gets_first_matching_field_from_rows_with_same_field_inside_a_table() -> Result<(), ShellError>
238    {
239        let field_path = column_path("package.authors.name").as_column_path()?;
240
241        let (_, tag) = string("Andrés N. Robalino").into_parts();
242
243        let value = UntaggedValue::row(indexmap! {
244            "package".into() => row(indexmap! {
245                "name".into() => string("nu"),
246                "version".into() => string("0.4.0"),
247                "authors".into() => table(&[
248                    row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
249                    row(indexmap!{"name".into() => string("Jonathan Turner")}),
250                    row(indexmap!{"name".into() => string("Yehuda Katz")})
251                ])
252            })
253        });
254
255        assert_eq!(
256            value.into_value(tag).get_data_by_column_path(
257                &field_path.item,
258                Box::new(error_callback("package.authors.name"))
259            )?,
260            table(&[
261                string("Andrés N. Robalino"),
262                string("Jonathan Turner"),
263                string("Yehuda Katz")
264            ])
265        );
266
267        Ok(())
268    }
269
270    #[test]
271    fn column_path_that_contains_just_a_number_gets_a_row_from_a_table() -> Result<(), ShellError> {
272        let field_path = column_path("package.authors.0").as_column_path()?;
273
274        let (_, tag) = string("Andrés N. Robalino").into_parts();
275
276        let value = UntaggedValue::row(indexmap! {
277            "package".into() => row(indexmap! {
278                "name".into() => string("nu"),
279                "version".into() => string("0.4.0"),
280                "authors".into() => table(&[
281                    row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
282                    row(indexmap!{"name".into() => string("Jonathan Turner")}),
283                    row(indexmap!{"name".into() => string("Yehuda Katz")})
284                ])
285            })
286        });
287
288        assert_eq!(
289            *value.into_value(tag).get_data_by_column_path(
290                &field_path.item,
291                Box::new(error_callback("package.authors.0"))
292            )?,
293            UntaggedValue::row(indexmap! {
294                "name".into() => string("Andrés N. Robalino")
295            })
296        );
297
298        Ok(())
299    }
300
301    #[test]
302    fn column_path_that_contains_just_a_number_gets_a_row_from_a_row() -> Result<(), ShellError> {
303        let field_path = column_path(r#"package.authors."0""#).as_column_path()?;
304
305        let (_, tag) = string("Andrés N. Robalino").into_parts();
306
307        let value = UntaggedValue::row(indexmap! {
308            "package".into() => row(indexmap! {
309                "name".into() => string("nu"),
310                "version".into() => string("0.4.0"),
311                "authors".into() => row(indexmap! {
312                    "0".into() => row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
313                    "1".into() => row(indexmap!{"name".into() => string("Jonathan Turner")}),
314                    "2".into() => row(indexmap!{"name".into() => string("Yehuda Katz")}),
315                })
316            })
317        });
318
319        assert_eq!(
320            *value.into_value(tag).get_data_by_column_path(
321                &field_path.item,
322                Box::new(error_callback("package.authors.\"0\""))
323            )?,
324            UntaggedValue::row(indexmap! {
325                "name".into() => string("Andrés N. Robalino")
326            })
327        );
328
329        Ok(())
330    }
331
332    #[test]
333    fn replaces_matching_field_from_a_row() -> Result<(), ShellError> {
334        let field_path = column_path("amigos").as_column_path()?;
335
336        let sample = UntaggedValue::row(indexmap! {
337            "amigos".into() => table(&[
338                string("andres"),
339                string("jonathan"),
340                string("yehuda"),
341            ]),
342        });
343
344        let replacement = string("jonas");
345
346        let actual = sample
347            .into_untagged_value()
348            .replace_data_at_column_path(&field_path.item, replacement)
349            .ok_or_else(|| ShellError::untagged_runtime_error("Could not replace column"))?;
350
351        assert_eq!(actual, row(indexmap! {"amigos".into() => string("jonas")}));
352
353        Ok(())
354    }
355
356    #[test]
357    fn replaces_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> {
358        let field_path = column_path(r#"package.authors."los.3.caballeros""#).as_column_path()?;
359
360        let sample = UntaggedValue::row(indexmap! {
361            "package".into() => row(indexmap! {
362                "authors".into() => row(indexmap! {
363                    "los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]),
364                    "los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]),
365                    "los.3.caballeros".into() => table(&[string("andres::yehuda::jonathan")])
366                })
367            })
368        });
369
370        let replacement = table(&[string("yehuda::jonathan::andres")]);
371        let tag = replacement.tag.clone();
372
373        let actual = sample
374            .into_value(&tag)
375            .replace_data_at_column_path(&field_path.item, replacement.clone())
376            .ok_or_else(|| {
377                ShellError::labeled_error(
378                    "Could not replace column",
379                    "could not replace column",
380                    &tag,
381                )
382            })?;
383
384        assert_eq!(
385            actual,
386            UntaggedValue::row(indexmap! {
387            "package".into() => row(indexmap! {
388                "authors".into() => row(indexmap! {
389                    "los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]),
390                    "los.3.amigos".into()      => table(&[string("andres::yehuda::jonathan")]),
391                    "los.3.caballeros".into()  => replacement})})})
392            .into_value(tag)
393        );
394
395        Ok(())
396    }
397    #[test]
398    fn replaces_matching_field_from_rows_inside_a_table() -> Result<(), ShellError> {
399        let field_path =
400            column_path(r#"shell_policy.releases."nu.version.arepa""#).as_column_path()?;
401
402        let sample = UntaggedValue::row(indexmap! {
403            "shell_policy".into() => row(indexmap! {
404                "releases".into() => table(&[
405                    row(indexmap! {
406                        "nu.version.arepa".into() => row(indexmap! {
407                            "code".into() => string("0.4.0"), "tag_line".into() => string("GitHub-era")
408                        })
409                    }),
410                    row(indexmap! {
411                        "nu.version.taco".into() => row(indexmap! {
412                            "code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era")
413                        })
414                    }),
415                    row(indexmap! {
416                        "nu.version.stable".into() => row(indexmap! {
417                            "code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era")
418                        })
419                    })
420                ])
421            })
422        });
423
424        let replacement = row(indexmap! {
425            "code".into() => string("0.5.0"),
426            "tag_line".into() => string("CABALLEROS")
427        });
428        let tag = replacement.tag.clone();
429
430        let actual = sample
431            .into_value(tag.clone())
432            .replace_data_at_column_path(&field_path.item, replacement.clone())
433            .ok_or_else(|| {
434                ShellError::labeled_error(
435                    "Could not replace column",
436                    "could not replace column",
437                    &tag,
438                )
439            })?;
440
441        assert_eq!(
442            actual,
443            UntaggedValue::row(indexmap! {
444                "shell_policy".into() => row(indexmap! {
445                    "releases".into() => table(&[
446                        row(indexmap! {
447                            "nu.version.arepa".into() => replacement
448                        }),
449                        row(indexmap! {
450                            "nu.version.taco".into() => row(indexmap! {
451                                "code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era")
452                            })
453                        }),
454                        row(indexmap! {
455                            "nu.version.stable".into() => row(indexmap! {
456                                "code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era")
457                            })
458                        })
459                    ])
460                })
461            }).into_value(&tag)
462        );
463
464        Ok(())
465    }
466}