mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 16:33:37 +01:00
add support for timestamp-based time conversion by specifying timezone (#3207)
* add support for timestamp-based time conversion by specifing timezone or 'UTC/Local' * [fix] fix the wrong test sample * code formating * code formating and import missing mod to test * code formating again
This commit is contained in:
parent
f0c7c1b500
commit
589fc0b8ad
@ -8,14 +8,48 @@ use nu_protocol::{
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::ValueExt;
|
||||
|
||||
use chrono::{DateTime, FixedOffset, LocalResult, Offset, TimeZone};
|
||||
use chrono::{DateTime, FixedOffset, Local, LocalResult, Offset, TimeZone, Utc};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
timezone: Option<Tagged<String>>,
|
||||
offset: Option<Tagged<i16>>,
|
||||
format: Option<Tagged<String>>,
|
||||
rest: Vec<ColumnPath>,
|
||||
}
|
||||
|
||||
// In case it may be confused with chrono::TimeZone
|
||||
#[derive(Clone)]
|
||||
enum Zone {
|
||||
Utc,
|
||||
Local,
|
||||
East(u8),
|
||||
West(u8),
|
||||
Error, // we want the nullshell to cast it instead of rust
|
||||
}
|
||||
|
||||
impl Zone {
|
||||
fn new(i: i16) -> Self {
|
||||
if i.abs() <= 12 {
|
||||
// guanranteed here
|
||||
if i >= 0 {
|
||||
Self::East(i as u8) // won't go out of range
|
||||
} else {
|
||||
Self::West(-i as u8) // same here
|
||||
}
|
||||
} else {
|
||||
Self::Error // Out of range
|
||||
}
|
||||
}
|
||||
fn from_string(s: String) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"utc" | "u" => Self::Utc,
|
||||
"local" | "l" => Self::Local,
|
||||
_ => Self::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[async_trait]
|
||||
@ -26,6 +60,18 @@ impl WholeStreamCommand for SubCommand {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("str to-datetime")
|
||||
.named(
|
||||
"timezone",
|
||||
SyntaxShape::String,
|
||||
"Specify timezone if the input is timestamp, like 'UTC/u' or 'LOCAL/l'",
|
||||
Some('z'),
|
||||
)
|
||||
.named(
|
||||
"offset",
|
||||
SyntaxShape::Int,
|
||||
"Specify timezone by offset if the input is timestamp, like '+8', '-4', prior than timezone",
|
||||
Some('o'),
|
||||
)
|
||||
.named(
|
||||
"format",
|
||||
SyntaxShape::String,
|
||||
@ -63,6 +109,17 @@ impl WholeStreamCommand for SubCommand {
|
||||
example: "echo '20200904_163918+0000' | str to-datetime -f '%Y%m%d_%H%M%S%z'",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Convert to datetime using a specified timezone",
|
||||
example: "echo '1614434140' | str to-datetime -z 'UTC'",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description:
|
||||
"Convert to datetime using a specified timezone offset (between -12 and 12)",
|
||||
example: "echo '1614434140' | str to-datetime -o '+9'",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -71,11 +128,42 @@ impl WholeStreamCommand for SubCommand {
|
||||
struct DatetimeFormat(String);
|
||||
|
||||
async fn operate(args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
let (Arguments { format, rest }, input) = args.process().await?;
|
||||
let (
|
||||
Arguments {
|
||||
timezone,
|
||||
offset,
|
||||
format,
|
||||
rest,
|
||||
},
|
||||
input,
|
||||
) = args.process().await?;
|
||||
|
||||
let column_paths: Vec<_> = rest;
|
||||
|
||||
let options = if let Some(Tagged { item: fmt, .. }) = format {
|
||||
// if zone-offset is specified, then zone will be neglected
|
||||
let zone_options = if let Some(Tagged {
|
||||
item: zone_offset,
|
||||
tag: _tag,
|
||||
}) = offset
|
||||
{
|
||||
Some(Tagged {
|
||||
item: Zone::new(zone_offset),
|
||||
tag: _tag,
|
||||
})
|
||||
} else if let Some(Tagged {
|
||||
item: zone,
|
||||
tag: _tag,
|
||||
}) = timezone
|
||||
{
|
||||
Some(Tagged {
|
||||
item: Zone::from_string(zone),
|
||||
tag: _tag,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let format_options = if let Some(Tagged { item: fmt, .. }) = format {
|
||||
Some(DatetimeFormat(fmt))
|
||||
} else {
|
||||
None
|
||||
@ -84,16 +172,17 @@ async fn operate(args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
if column_paths.is_empty() {
|
||||
ReturnSuccess::value(action(&v, &options, v.tag())?)
|
||||
ReturnSuccess::value(action(&v, &zone_options, &format_options, v.tag())?)
|
||||
} else {
|
||||
let mut ret = v;
|
||||
|
||||
for path in &column_paths {
|
||||
let options = options.clone();
|
||||
let zone_options = zone_options.clone();
|
||||
let format_options = format_options.clone();
|
||||
|
||||
ret = ret.swap_data_by_column_path(
|
||||
path,
|
||||
Box::new(move |old| action(old, &options, old.tag())),
|
||||
Box::new(move |old| action(old, &zone_options, &format_options, old.tag())),
|
||||
)?;
|
||||
}
|
||||
|
||||
@ -105,12 +194,41 @@ async fn operate(args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
|
||||
fn action(
|
||||
input: &Value,
|
||||
options: &Option<DatetimeFormat>,
|
||||
timezone: &Option<Tagged<Zone>>,
|
||||
dateformat: &Option<DatetimeFormat>,
|
||||
tag: impl Into<Tag>,
|
||||
) -> Result<Value, ShellError> {
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
let out = match options {
|
||||
let ts = s.parse::<i64>();
|
||||
// if timezone if specified, first check if the input is a timestamp.
|
||||
if let Some(tz) = timezone {
|
||||
if let Ok(t) = ts {
|
||||
const HOUR: i32 = 3600;
|
||||
let stampout = match tz.item {
|
||||
Zone::Utc => UntaggedValue::date(Utc.timestamp(t, 0)),
|
||||
Zone::Local => UntaggedValue::date(Local.timestamp(t, 0)),
|
||||
Zone::East(i) => {
|
||||
let eastoffset = FixedOffset::east((i as i32) * HOUR);
|
||||
UntaggedValue::date(eastoffset.timestamp(t, 0))
|
||||
}
|
||||
Zone::West(i) => {
|
||||
let westoffset = FixedOffset::west((i as i32) * HOUR);
|
||||
UntaggedValue::date(westoffset.timestamp(t, 0))
|
||||
}
|
||||
Zone::Error => {
|
||||
return Err(ShellError::labeled_error(
|
||||
"could not continue to convert timestamp",
|
||||
"given timezone or offset is invalid",
|
||||
tz.tag().span,
|
||||
));
|
||||
}
|
||||
};
|
||||
return Ok(stampout.into_value(tag));
|
||||
}
|
||||
};
|
||||
// if it's not, continue and negelect the timezone option.
|
||||
let out = match dateformat {
|
||||
Some(dt) => match DateTime::parse_from_str(s, &dt.0) {
|
||||
Ok(d) => UntaggedValue::date(d),
|
||||
Err(reason) => {
|
||||
@ -165,9 +283,9 @@ fn action(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ShellError;
|
||||
use super::{action, DatetimeFormat, SubCommand};
|
||||
use super::{action, DatetimeFormat, SubCommand, Zone};
|
||||
use nu_protocol::{Primitive, UntaggedValue};
|
||||
use nu_source::Tag;
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_test_support::value::string;
|
||||
|
||||
#[test]
|
||||
@ -183,7 +301,7 @@ mod tests {
|
||||
|
||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
||||
|
||||
let actual = action(&date_str, &fmt_options, Tag::unknown()).unwrap();
|
||||
let actual = action(&date_str, &None, &fmt_options, Tag::unknown()).unwrap();
|
||||
|
||||
match actual.value {
|
||||
UntaggedValue::Primitive(Primitive::Date(_)) => {}
|
||||
@ -194,7 +312,35 @@ mod tests {
|
||||
#[test]
|
||||
fn takes_iso8601_date_format() {
|
||||
let date_str = string("2020-08-04T16:39:18+00:00");
|
||||
let actual = action(&date_str, &None, Tag::unknown()).unwrap();
|
||||
let actual = action(&date_str, &None, &None, Tag::unknown()).unwrap();
|
||||
match actual.value {
|
||||
UntaggedValue::Primitive(Primitive::Date(_)) => {}
|
||||
_ => panic!("Didn't convert to date"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takes_timestamp_offset() {
|
||||
let date_str = string("1614434140");
|
||||
let timezone_option = Some(Tagged {
|
||||
item: Zone::East(8),
|
||||
tag: Tag::unknown(),
|
||||
});
|
||||
let actual = action(&date_str, &timezone_option, &None, Tag::unknown()).unwrap();
|
||||
match actual.value {
|
||||
UntaggedValue::Primitive(Primitive::Date(_)) => {}
|
||||
_ => panic!("Didn't convert to date"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takes_timestamp() {
|
||||
let date_str = string("1614434140");
|
||||
let timezone_option = Some(Tagged {
|
||||
item: Zone::Local,
|
||||
tag: Tag::unknown(),
|
||||
});
|
||||
let actual = action(&date_str, &timezone_option, &None, Tag::unknown()).unwrap();
|
||||
match actual.value {
|
||||
UntaggedValue::Primitive(Primitive::Date(_)) => {}
|
||||
_ => panic!("Didn't convert to date"),
|
||||
@ -207,7 +353,7 @@ mod tests {
|
||||
|
||||
let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()));
|
||||
|
||||
let actual = action(&date_str, &fmt_options, Tag::unknown());
|
||||
let actual = action(&date_str, &None, &fmt_options, Tag::unknown());
|
||||
|
||||
assert!(actual.is_err());
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user