From 3e76ed9122bc50c17c96a15b41bed60345662fa2 Mon Sep 17 00:00:00 2001
From: Darren Schroeder <343840+fdncred@users.noreply.github.com>
Date: Sat, 26 Nov 2022 09:00:47 -0600
Subject: [PATCH] add `into record` command (#7225)
# Description
This command converts things into records.
It also converts dates into record but I couldn't get the test harness
to accept an example.
Thanks to @WindSoilder for writing the "hard" parts of this. :)
_(Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.)_
_(Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.)_
# User-Facing Changes
_(List of all changes that impact the user experience here. This helps
us keep track of breaking changes.)_
# Tests + Formatting
Don't forget to add tests that cover your changes.
Make sure you've run and fixed any issues with these commands:
- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect` to check that you're using the standard code
style
- `cargo test --workspace` to check that all tests pass
# After Submitting
If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
Co-authored-by: WindSoilder
---
crates/nu-command/src/conversions/into/mod.rs | 2 +
.../nu-command/src/conversions/into/record.rs | 291 ++++++++++++++++++
crates/nu-command/src/default_context.rs | 1 +
crates/nu-protocol/src/value/mod.rs | 55 ++--
4 files changed, 322 insertions(+), 27 deletions(-)
create mode 100644 crates/nu-command/src/conversions/into/record.rs
diff --git a/crates/nu-command/src/conversions/into/mod.rs b/crates/nu-command/src/conversions/into/mod.rs
index dc1281494c..ee0ad71d5c 100644
--- a/crates/nu-command/src/conversions/into/mod.rs
+++ b/crates/nu-command/src/conversions/into/mod.rs
@@ -6,6 +6,7 @@ mod decimal;
mod duration;
mod filesize;
mod int;
+mod record;
mod string;
pub use self::bool::SubCommand as IntoBool;
@@ -16,4 +17,5 @@ pub use datetime::SubCommand as IntoDatetime;
pub use decimal::SubCommand as IntoDecimal;
pub use duration::SubCommand as IntoDuration;
pub use int::SubCommand as IntoInt;
+pub use record::SubCommand as IntoRecord;
pub use string::SubCommand as IntoString;
diff --git a/crates/nu-command/src/conversions/into/record.rs b/crates/nu-command/src/conversions/into/record.rs
new file mode 100644
index 0000000000..c049658ece
--- /dev/null
+++ b/crates/nu-command/src/conversions/into/record.rs
@@ -0,0 +1,291 @@
+use chrono::{DateTime, Datelike, FixedOffset, Timelike};
+use nu_protocol::format_duration_as_timeperiod;
+use nu_protocol::{
+ ast::Call,
+ engine::{Command, EngineState, Stack},
+ Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value,
+};
+#[derive(Clone)]
+pub struct SubCommand;
+
+impl Command for SubCommand {
+ fn name(&self) -> &str {
+ "into record"
+ }
+
+ fn signature(&self) -> Signature {
+ Signature::build("into record")
+ .input_output_types(vec![
+ (Type::Date, Type::Record(vec![])),
+ (Type::Duration, Type::Record(vec![])),
+ (Type::List(Box::new(Type::Any)), Type::Record(vec![])),
+ (Type::Range, Type::Record(vec![])),
+ (Type::Record(vec![]), Type::Record(vec![])),
+ (Type::Table(vec![]), Type::Record(vec![])),
+ ])
+ .category(Category::Conversions)
+ }
+
+ fn usage(&self) -> &str {
+ "Convert value to record"
+ }
+
+ fn search_terms(&self) -> Vec<&str> {
+ vec!["convert"]
+ }
+
+ fn run(
+ &self,
+ engine_state: &EngineState,
+ _stack: &mut Stack,
+ call: &Call,
+ input: PipelineData,
+ ) -> Result {
+ into_record(engine_state, call, input)
+ }
+
+ fn examples(&self) -> Vec {
+ let span = Span::test_data();
+ vec![
+ Example {
+ description: "Convert from one row table to record",
+ example: "echo [[value]; [false]] | into record",
+ result: Some(Value::Record {
+ cols: vec!["value".to_string()],
+ vals: vec![Value::boolean(false, span)],
+ span,
+ }),
+ },
+ Example {
+ description: "Convert from list to record",
+ example: "[1 2 3] | into record",
+ result: Some(Value::Record {
+ cols: vec!["0".to_string(), "1".to_string(), "2".to_string()],
+ vals: vec![
+ Value::Int { val: 1, span },
+ Value::Int { val: 2, span },
+ Value::Int { val: 3, span },
+ ],
+ span,
+ }),
+ },
+ Example {
+ description: "Convert from range to record",
+ example: "0..2 | into record",
+ result: Some(Value::Record {
+ cols: vec!["0".to_string(), "1".to_string(), "2".to_string()],
+ vals: vec![
+ Value::Int { val: 0, span },
+ Value::Int { val: 1, span },
+ Value::Int { val: 2, span },
+ ],
+ span,
+ }),
+ },
+ Example {
+ description: "convert duration to record",
+ example: "-500day | into record",
+ result: Some(Value::Record {
+ cols: vec![
+ "year".into(),
+ "month".into(),
+ "week".into(),
+ "day".into(),
+ "sign".into(),
+ ],
+ vals: vec![
+ Value::Int { val: 1, span },
+ Value::Int { val: 4, span },
+ Value::Int { val: 2, span },
+ Value::Int { val: 1, span },
+ Value::String {
+ val: "-".into(),
+ span,
+ },
+ ],
+ span,
+ }),
+ },
+ Example {
+ description: "convert record to record",
+ example: "{a: 1, b: 2} | into record",
+ result: Some(Value::Record {
+ cols: vec!["a".to_string(), "b".to_string()],
+ vals: vec![Value::Int { val: 1, span }, Value::Int { val: 2, span }],
+ span,
+ }),
+ },
+ Example {
+ description: "convert date to record",
+ example: "2020-04-12T22:10:57+02:00 | into record",
+ result: Some(Value::Record {
+ cols: vec![
+ "year".into(),
+ "month".into(),
+ "day".into(),
+ "hour".into(),
+ "minute".into(),
+ "second".into(),
+ "timezone".into(),
+ ],
+ vals: vec![
+ Value::Int { val: 2020, span },
+ Value::Int { val: 4, span },
+ Value::Int { val: 12, span },
+ Value::Int { val: 22, span },
+ Value::Int { val: 10, span },
+ Value::Int { val: 57, span },
+ Value::String {
+ val: "+02:00".to_string(),
+ span,
+ },
+ ],
+ span,
+ }),
+ },
+ ]
+ }
+}
+
+fn into_record(
+ engine_state: &EngineState,
+ call: &Call,
+ input: PipelineData,
+) -> Result {
+ let input = input.into_value(call.head);
+ let input_type = input.get_type();
+ let res = match input {
+ Value::Date { val, span } => parse_date_into_record(Ok(val), span),
+ Value::Duration { val, span } => parse_duration_into_record(val, span),
+ Value::List { mut vals, span } => match input_type {
+ Type::Table(..) if vals.len() == 1 => vals.pop().expect("already checked 1 item"),
+ _ => {
+ let mut cols = vec![];
+ let mut values = vec![];
+ for (idx, val) in vals.into_iter().enumerate() {
+ cols.push(format!("{idx}"));
+ values.push(val);
+ }
+ Value::Record {
+ cols,
+ vals: values,
+ span,
+ }
+ }
+ },
+ Value::Range { val, span } => {
+ let mut cols = vec![];
+ let mut vals = vec![];
+ for (idx, val) in val.into_range_iter(engine_state.ctrlc.clone())?.enumerate() {
+ cols.push(format!("{idx}"));
+ vals.push(val);
+ }
+ Value::Record { cols, vals, span }
+ }
+ Value::Record { cols, vals, span } => Value::Record { cols, vals, span },
+ other => Value::Error {
+ error: ShellError::UnsupportedInput(
+ "'into record' does not support this input".into(),
+ other.span().unwrap_or(call.head),
+ ),
+ },
+ };
+ Ok(res.into_pipeline_data())
+}
+
+fn parse_date_into_record(date: Result, Value>, span: Span) -> Value {
+ let cols = vec![
+ "year".into(),
+ "month".into(),
+ "day".into(),
+ "hour".into(),
+ "minute".into(),
+ "second".into(),
+ "timezone".into(),
+ ];
+ match date {
+ Ok(x) => {
+ let vals = vec![
+ Value::Int {
+ val: x.year() as i64,
+ span,
+ },
+ Value::Int {
+ val: x.month() as i64,
+ span,
+ },
+ Value::Int {
+ val: x.day() as i64,
+ span,
+ },
+ Value::Int {
+ val: x.hour() as i64,
+ span,
+ },
+ Value::Int {
+ val: x.minute() as i64,
+ span,
+ },
+ Value::Int {
+ val: x.second() as i64,
+ span,
+ },
+ Value::String {
+ val: x.offset().to_string(),
+ span,
+ },
+ ];
+ Value::Record { cols, vals, span }
+ }
+ Err(e) => e,
+ }
+}
+
+fn parse_duration_into_record(duration: i64, span: Span) -> Value {
+ let (sign, periods) = format_duration_as_timeperiod(duration);
+
+ let mut cols = vec![];
+ let mut vals = vec![];
+ for p in periods {
+ let num_with_unit = p.to_text().to_string();
+ let split = num_with_unit.split(' ').collect::>();
+ cols.push(match split[1] {
+ "ns" => "nanosecond".into(),
+ "µs" => "microsecond".into(),
+ "ms" => "millisecond".into(),
+ "sec" => "second".into(),
+ "min" => "minute".into(),
+ "hr" => "hour".into(),
+ "day" => "day".into(),
+ "wk" => "week".into(),
+ "month" => "month".into(),
+ "yr" => "year".into(),
+ _ => "unknown".into(),
+ });
+
+ vals.push(Value::Int {
+ val: split[0].parse::().unwrap_or(0),
+ span,
+ });
+ }
+
+ cols.push("sign".into());
+ vals.push(Value::String {
+ val: if sign == -1 { "-".into() } else { "+".into() },
+ span,
+ });
+
+ Value::Record { cols, vals, span }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_examples() {
+ use crate::test_examples;
+
+ test_examples(SubCommand {})
+ }
+}
diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs
index 66c04de711..f7acf9dce7 100644
--- a/crates/nu-command/src/default_context.rs
+++ b/crates/nu-command/src/default_context.rs
@@ -368,6 +368,7 @@ pub fn create_default_context() -> EngineState {
IntoDuration,
IntoFilesize,
IntoInt,
+ IntoRecord,
IntoString,
};
diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs
index b4ccb6e443..691146a79c 100644
--- a/crates/nu-protocol/src/value/mod.rs
+++ b/crates/nu-protocol/src/value/mod.rs
@@ -2968,7 +2968,7 @@ pub fn is_leap_year(year: i32) -> bool {
}
#[derive(Clone, Copy)]
-enum TimePeriod {
+pub enum TimePeriod {
Nanos(i64),
Micros(i64),
Millis(i64),
@@ -2982,18 +2982,18 @@ enum TimePeriod {
}
impl TimePeriod {
- fn to_text(self) -> Cow<'static, str> {
+ pub fn to_text(self) -> Cow<'static, str> {
match self {
- Self::Nanos(n) => format!("{}ns", n).into(),
- Self::Micros(n) => format!("{}µs", n).into(),
- Self::Millis(n) => format!("{}ms", n).into(),
- Self::Seconds(n) => format!("{}sec", n).into(),
- Self::Minutes(n) => format!("{}min", n).into(),
- Self::Hours(n) => format!("{}hr", n).into(),
- Self::Days(n) => format!("{}day", n).into(),
- Self::Weeks(n) => format!("{}wk", n).into(),
- Self::Months(n) => format!("{}month", n).into(),
- Self::Years(n) => format!("{}yr", n).into(),
+ Self::Nanos(n) => format!("{} ns", n).into(),
+ Self::Micros(n) => format!("{} µs", n).into(),
+ Self::Millis(n) => format!("{} ms", n).into(),
+ Self::Seconds(n) => format!("{} sec", n).into(),
+ Self::Minutes(n) => format!("{} min", n).into(),
+ Self::Hours(n) => format!("{} hr", n).into(),
+ Self::Days(n) => format!("{} day", n).into(),
+ Self::Weeks(n) => format!("{} wk", n).into(),
+ Self::Months(n) => format!("{} month", n).into(),
+ Self::Years(n) => format!("{} yr", n).into(),
}
}
}
@@ -3005,6 +3005,21 @@ impl Display for TimePeriod {
}
pub fn format_duration(duration: i64) -> String {
+ let (sign, periods) = format_duration_as_timeperiod(duration);
+
+ let text = periods
+ .into_iter()
+ .map(|p| p.to_text().to_string().replace(' ', ""))
+ .collect::>();
+
+ format!(
+ "{}{}",
+ if sign == -1 { "-" } else { "" },
+ text.join(" ").trim()
+ )
+}
+
+pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec) {
// Attribution: most of this is taken from chrono-humanize-rs. Thanks!
// https://gitlab.com/imp/chrono-humanize-rs/-/blob/master/src/humantime.rs
const DAYS_IN_YEAR: i64 = 365;
@@ -3151,21 +3166,7 @@ pub fn format_duration(duration: i64) -> String {
periods.push(TimePeriod::Seconds(0));
}
- // let last = periods.pop().map(|last| last.to_text().to_string());
- let text = periods
- .into_iter()
- .map(|p| p.to_text().to_string())
- .collect::>();
-
- // if let Some(last) = last {
- // text.push(format!("and {}", last));
- // }
-
- format!(
- "{}{}",
- if sign == -1 { "-" } else { "" },
- text.join(" ").trim()
- )
+ (sign, periods)
}
pub fn format_filesize_from_conf(num_bytes: i64, config: &Config) -> String {