Add custom datetime format through strftime strings (#9500)

- improves usability of datetime's in displayed text
- 
# Description
Creates a config point for specifying long / short date time formats.
Defaults to humanized as we have today.

Provides for adding strftime formats into config.nu such as:
```nu
  datetime_format: {
    normal: "%Y-%m-%d %H:%M:%S"
    table: "%Y-%m-%d"
  }
```

Example:
```bash
> $env.config.datetime_format                                                                                                                         
┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ normal ┃ %a, %d %b %Y %H:%M:%S %z ┃
┃ table  ┃ %m/%d/%y %I:%M:%S%p      ┃
┗━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━┛
> let a = (date now)                                                                                                                                  
> echo $a                                                                                                                                             
Thu, 22 Jun 2023 10:21:23 -0700
> echo [$a]                                                                                                                                           
┏━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ 0 ┃ 06/22/23 10:21:23AM ┃
┗━━━┻━━━━━━━━━━━━━━━━━━━━━┛
```

# User-Facing Changes
Any place converting a datetime to a user displayed value should be
impacted.

# Tests + Formatting

- `cargo fmt --all -- --check` Done
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect -A clippy::result_large_err` Done
- `cargo test --workspace` Done 
- `cargo run -- crates/nu-std/tests/run.nu` Not done - doesn't seem to
work

```bash
> use toolkit.nu  # or use an `env_change` hook to activate it automatically
> toolkit check pr
``` - Done

---------

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
Co-authored-by: Antoine Stevan <44101798+amtoine@users.noreply.github.com>
This commit is contained in:
WMR 2023-06-23 13:05:04 -07:00 committed by GitHub
parent e16c1b7c88
commit 0c888486c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 94 additions and 6 deletions

1
.gitignore vendored
View file

@ -42,6 +42,7 @@ tarpaulin-report.html
*.rsproj
*.rsproj.user
*.sln
*.code-workspace
# direnv
.direnv/

View file

@ -16,7 +16,7 @@ bench = false
nu-utils = { path = "../nu-utils", version = "0.81.1" }
byte-unit = "4.0"
chrono = { version = "0.4", features = [ "serde", "std", ], default-features = false }
chrono = { version = "0.4", features = [ "serde", "std", "unstable-locales" ], default-features = false }
chrono-humanize = "0.2"
fancy-regex = "0.11"
indexmap = { version = "1.7" }

View file

@ -106,6 +106,8 @@ pub struct Config {
pub cursor_shape_vi_insert: NuCursorShape,
pub cursor_shape_vi_normal: NuCursorShape,
pub cursor_shape_emacs: NuCursorShape,
pub datetime_normal_format: Option<String>,
pub datetime_table_format: Option<String>,
}
impl Default for Config {
@ -150,6 +152,8 @@ impl Default for Config {
cursor_shape_vi_insert: NuCursorShape::Block,
cursor_shape_vi_normal: NuCursorShape::UnderScore,
cursor_shape_emacs: NuCursorShape::Line,
datetime_normal_format: None,
datetime_table_format: None,
}
}
}
@ -1215,6 +1219,50 @@ impl Value {
});
}
},
"datetime_format" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
for index in (0..cols.len()).rev() {
let value = &vals[index];
let key2 = cols[index].as_str();
match key2 {
"normal" => {
if let Ok(v) = value.as_string() {
config.datetime_normal_format = Some(v);
} else {
invalid!(Some(*span), "should be a string");
}
}
"table" => {
if let Ok(v) = value.as_string() {
config.datetime_table_format = Some(v);
} else {
invalid!(Some(*span), "should be a string");
}
}
x => {
invalid_key!(
cols,
vals,
index,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["metric".into(), "format".into()],
vec![
Value::boolean(config.filesize_metric, *span),
Value::string(config.filesize_format.clone(), *span),
],
*span,
);
}
}
// Catch all
x => {
invalid_key!(

View file

@ -12,7 +12,7 @@ use crate::engine::EngineState;
use crate::ShellError;
use crate::{did_you_mean, BlockId, Config, Span, Spanned, Type, VarId};
use byte_unit::ByteUnit;
use chrono::{DateTime, Duration, FixedOffset};
use chrono::{DateTime, Duration, FixedOffset, Locale, TimeZone};
use chrono_humanize::HumanTime;
pub use custom_value::CustomValue;
use fancy_regex::Regex;
@ -20,10 +20,12 @@ pub use from_value::FromValue;
use indexmap::map::IndexMap;
pub use lazy_record::LazyRecord;
use nu_utils::get_system_locale;
use nu_utils::locale::get_system_locale_string;
use num_format::ToFormattedString;
pub use range::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Write;
use std::{
borrow::Cow,
fmt::{Display, Formatter, Result as FmtResult},
@ -534,7 +536,12 @@ impl Value {
Value::Float { val, .. } => val.to_string(),
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
Value::Duration { val, .. } => format_duration(*val),
Value::Date { val, .. } => format!("{} ({})", val.to_rfc2822(), HumanTime::from(*val)),
Value::Date { val, .. } => match &config.datetime_normal_format {
Some(format) => self.format_datetime(val, format),
None => {
format!("{} ({})", val.to_rfc2822(), HumanTime::from(*val))
}
},
Value::Range { val, .. } => {
format!(
"{}..{}",
@ -586,7 +593,10 @@ impl Value {
Value::Float { val, .. } => val.to_string(),
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
Value::Duration { val, .. } => format_duration(*val),
Value::Date { val, .. } => HumanTime::from(*val).to_string(),
Value::Date { val, .. } => match &config.datetime_table_format {
Some(format) => self.format_datetime(val, format),
None => HumanTime::from(*val).to_string(),
},
Value::Range { val, .. } => {
format!(
"{}..{}",
@ -630,6 +640,26 @@ impl Value {
}
}
fn format_datetime<Tz: TimeZone>(&self, date_time: &DateTime<Tz>, formatter: &str) -> String
where
Tz::Offset: Display,
{
let mut formatter_buf = String::new();
let locale: Locale = get_system_locale_string()
.map(|l| l.replace('-', "_")) // `chrono::Locale` needs something like `xx_xx`, rather than `xx-xx`
.unwrap_or_else(|| String::from("en_US"))
.as_str()
.try_into()
.unwrap_or(Locale::en_US);
let format = date_time.format_localized(formatter, locale);
match formatter_buf.write_fmt(format_args!("{format}")) {
Ok(_) => (),
Err(_) => formatter_buf = format!("Invalid format string {}", formatter),
}
formatter_buf
}
/// Convert Value into a debug string
pub fn debug_value(&self) -> String {
format!("{self:#?}")
@ -798,8 +828,8 @@ impl Value {
return Ok(Value::nothing(*origin_span)); // short-circuit
} else {
return Err(ShellError::AccessBeyondEndOfStream {
span: *origin_span
});
span: *origin_span
});
}
}
Value::CustomValue { val, .. } => {

View file

@ -207,6 +207,15 @@ let-env config = {
}
}
# datetime_format determines what a datetime rendered in the shell would look like.
# Behavior without this configuration point will be to "humanize" the datetime display,
# showing something like "a day ago."
datetime_format: {
normal: '%a, %d %b %Y %H:%M:%S %z' # shows up in displays of variables or other datetime's outside of tables
# table: '%m/%d/%y %I:%M:%S%p' # generally shows up in tabular outputs such as ls. commenting this out will change it to the default human readable datetime format
}
explore: {
help_banner: true
exit_esc: true