From cebba1e7c65a15d3fe1e08e90ea218d3abc39a26 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Wed, 7 Jul 2021 22:15:02 +0300 Subject: [PATCH] new: time range filter added --- Cargo.lock | 137 ++++++++++++++++++++- Cargo.toml | 6 +- README.md | 36 +++++- src/app.rs | 5 +- src/error.rs | 2 + src/formatting.rs | 2 +- src/lib.rs | 1 + src/main.rs | 46 ++++++-- src/model.rs | 67 +++++++---- src/timeparse.rs | 295 ++++++++++++++++++++++++++++++++++++++++++++++ src/timestamp.rs | 10 +- 11 files changed, 561 insertions(+), 46 deletions(-) create mode 100644 src/timeparse.rs diff --git a/Cargo.lock b/Cargo.lock index 44d1089d..86b13129 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,27 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5da9b3d9f6f585199287a473f4f8dfab6566cf827d15c00c219f53c645687ead" +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + [[package]] name = "bstr" version = "0.2.16" @@ -142,6 +163,12 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "bytefmt" version = "0.1.7" @@ -383,6 +410,15 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] + [[package]] name = "diligent-date-parser" version = "0.1.2" @@ -439,6 +475,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + [[package]] name = "flate2" version = "1.0.20" @@ -451,6 +493,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -514,7 +565,7 @@ dependencies = [ [[package]] name = "hl" -version = "0.9.1" +version = "0.9.2" dependencies = [ "ansi_term", "anyhow", @@ -535,6 +586,8 @@ dependencies = [ "error-chain", "flate2", "heapless", + "htp", + "humantime", "itertools 0.9.0", "itoa", "num_cpus", @@ -548,6 +601,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "htp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f285642c3d0096a62850a9e86ce2ea1fcb2e9d779f23feb57b3f96583f120fd" +dependencies = [ + "chrono", + "pest", + "pest_derive", + "thiserror", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "itertools" version = "0.9.0" @@ -621,6 +692,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "memchr" version = "2.4.0" @@ -725,6 +802,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "parse-zoneinfo" version = "0.3.0" @@ -743,6 +826,40 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + [[package]] name = "platform-dirs" version = "0.3.0" @@ -1032,6 +1149,18 @@ dependencies = [ "serde 1.0.126", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + [[package]] name = "shellwords" version = "1.1.0" @@ -1172,6 +1301,12 @@ dependencies = [ "serde 1.0.126", ] +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + [[package]] name = "ucd-trie" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 8a85af83..c1bf51fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ categories = ["command-line-utilities"] description = "Utility for viewing json-formatted log files." keywords = ["cli", "human", "log"] name = "hl" -version = "0.9.1" +version = "0.9.2" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -13,6 +13,7 @@ edition = "2018" ansi_term = "0" anyhow = "1" atoi = "0" +atty = "0" bitmask = "0" bytefmt = "0" chrono = { version = "0", features = ["serde"] } @@ -26,7 +27,8 @@ derive_deref = "1" error-chain = "0" flate2 = "1" heapless = "0" -atty = "0" +htp = "0" +humantime = "2" itertools = "0" num_cpus = "1" platform-dirs = "0" diff --git a/README.md b/README.md index d720e679..c579fc0e 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,30 @@ Log viewer which translates JSON logs into pretty human-readable representation. Shows only messages with field `provider` containing sub-string `string`. +### Filtering by time range. + +- Command + + ``` + $ hl example.log --since 'Jun 19 11:22:33' --until yesterday + ``` + Shows only messages occurred after Jun 19 11:22:33 UTC of the current year (or of the previous one if current date is less than Jun 19 11:22:33) and until yesterday midnight. + +- Command + + ``` + $ hl example.log --since -3d + ``` + Shows only messages for the last 48 hours. + +- Command + + ``` + $ hl example.log --until '2021-06-01 18:00:00' --local + ``` + Shows only messages occurred before 6 PM on 1st Jun 2021 in local time as well as show timestamps in local time. + + ### Hiding or showing selected fields. - Command @@ -170,7 +194,7 @@ Log viewer which translates JSON logs into pretty human-readable representation. ### Complete set of options and flags ``` -hl 0.8.13 +hl 0.9.2 JSON log converter to human readable representation USAGE: @@ -195,7 +219,7 @@ OPTIONS: -f, --filter ... Filtering by field values in one of forms =, ~=, !=, !~= - -h, --hide ... An exclude-list of keys + -h, --hide ... Hide fields with the specified keys --interrupt-ignore-count Number of interrupts to ignore, i.e. Ctrl-C (SIGINT) [default: 3] @@ -206,7 +230,10 @@ OPTIONS: --paging Output paging options, one of { auto, always, never } [default: auto] - -H, --show ... An include-list of keys + -H, --show ... Hide all fields except fields with the specified keys + --since + Filtering by timestamp >= the value (--time-zone and --local options are honored) + --theme Color theme, one of { auto, dark, dark24, light } [default: dark] @@ -216,6 +243,9 @@ OPTIONS: -Z, --time-zone Time zone name, see column "TZ database name" at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones [default: UTC] + --until + Filtering by timestamp <= the value (--time-zone and --local options are honored) + ARGS: ... Files to process diff --git a/src/app.rs b/src/app.rs index 7f4a724c..c8abe785 100644 --- a/src/app.rs +++ b/src/app.rs @@ -50,7 +50,10 @@ impl App { let n = self.options.concurrency; let sfi = Arc::new(SegmentBufFactory::new(self.options.buffer_size)); let bfo = BufFactory::new(self.options.buffer_size); - let settings = ParserSettings::from(&self.options.settings); + let settings = ParserSettings::new( + &self.options.settings, + self.options.filter.since.is_some() || self.options.filter.until.is_some(), + ); let parser = Parser::new(&settings); thread::scope(|scope| -> Result<()> { // prepare receive/transmit channels for input data diff --git a/src/error.rs b/src/error.rs index caf6e436..ec92a522 100644 --- a/src/error.rs +++ b/src/error.rs @@ -39,6 +39,8 @@ pub enum Error { "64KB" )] InvalidSize(String), + #[error("cannot recognize time {0:?}")] + UnrecognizedTime(String), #[error("zero size")] ZeroSize, } diff --git a/src/formatting.rs b/src/formatting.rs index 812d30fd..5dda46fd 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -57,7 +57,7 @@ impl RecordFormatter { // time // styler.set(buf, Element::Time); - if let Some(ts) = rec.ts() { + if let Some(ts) = &rec.ts { aligned_left(buf, self.ts_width, b' ', |mut buf| { if ts .as_rfc3339() diff --git a/src/lib.rs b/src/lib.rs index ef936f1d..b53576bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod input; pub mod output; pub mod settings; pub mod theme; +pub mod timeparse; pub mod timestamp; pub mod types; diff --git a/src/main.rs b/src/main.rs index 22451a0f..f389d52f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ use std::env; use std::path::PathBuf; use std::process; use std::sync::Arc; -use std::time::Duration; // third-party imports use ansi_term::Colour; @@ -22,6 +21,7 @@ use hl::output::{OutputStream, Pager}; use hl::settings::Settings; use hl::signal::SignalHandler; use hl::theme::Theme; +use hl::timeparse::parse_time; use hl::Level; use hl::{IncludeExcludeKeyFilter, KeyMatchOptions}; @@ -88,6 +88,14 @@ struct Opt { #[structopt(short, long, default_value = "d", overrides_with = "level")] level: Level, // + /// Filtering by timestamp >= the value (--time-zone and --local options are honored). + #[structopt(long, allow_hyphen_values = true)] + since: Option, + // + /// Filtering by timestamp <= the value (--time-zone and --local options are honored). + #[structopt(long, allow_hyphen_values = true)] + until: Option, + // /// Time format, see https://man7.org/linux/man-pages/man1/date.1.html. #[structopt( short, @@ -199,10 +207,30 @@ fn run() -> Result<()> { None | Some(0) => num_cpus::get(), Some(value) => value, }; + // Configure timezone. + let tz = if opt.local { + *Local.timestamp(0, 0).offset() + } else { + let tz = opt.time_zone; + let offset = UTC.ymd(1970, 1, 1).and_hms(0, 0, 0) - tz.ymd(1970, 1, 1).and_hms(0, 0, 0); + FixedOffset::east(offset.num_seconds() as i32) + }; + // Configure time format. + let time_format = LinuxDateFormat::new(&opt.time_format).compile(); // Configure filter. let filter = hl::Filter { fields: hl::FieldFilterSet::new(opt.filter), level: Some(opt.level), + since: if let Some(v) = &opt.since { + Some(parse_time(v, &tz, &time_format)?.into()) + } else { + None + }, + until: if let Some(v) = &opt.until { + Some(parse_time(v, &tz, &time_format)?.into()) + } else { + None + }, }; // Configure hide_empty_fields let hide_empty_fields = !opt.show_empty_fields && opt.hide_empty_fields; @@ -227,19 +255,13 @@ fn run() -> Result<()> { settings: settings, theme: Arc::new(theme), raw_fields: opt.raw_fields, - time_format: LinuxDateFormat::new(&opt.time_format).compile(), + time_format: time_format, buffer_size, max_message_size, concurrency, filter, fields: Arc::new(fields), - time_zone: if opt.local { - *Local.timestamp(0, 0).offset() - } else { - let tz = opt.time_zone; - let offset = UTC.ymd(1970, 1, 1).and_hms(0, 0, 0) - tz.ymd(1970, 1, 1).and_hms(0, 0, 0); - FixedOffset::east(offset.num_seconds() as i32) - }, + time_zone: tz, hide_empty_fields, }); @@ -295,7 +317,11 @@ fn run() -> Result<()> { }; // Run the app with signal handling. - SignalHandler::run(opt.interrupt_ignore_count, Duration::from_secs(1), run) + SignalHandler::run( + opt.interrupt_ignore_count, + std::time::Duration::from_secs(1), + run, + ) } fn main() { diff --git a/src/model.rs b/src/model.rs index 005a72e3..af945017 100644 --- a/src/model.rs +++ b/src/model.rs @@ -5,6 +5,7 @@ use std::iter::IntoIterator; use std::marker::PhantomData; // third-party imports +use chrono::{DateTime, Utc}; use json::value::RawValue; use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor}; use serde_json as json; @@ -21,7 +22,7 @@ pub use types::Level; // --- pub struct Record<'a> { - ts: Option<&'a RawValue>, + pub ts: Option>, pub message: Option<&'a RawValue>, pub level: Option, pub logger: Option<&'a str>, @@ -35,26 +36,26 @@ impl<'a> Record<'a> { self.extra.iter().chain(self.extrax.iter()) } - pub fn ts(&self) -> Option> { - match self.ts { - None => None, - Some(ts) => { - let s = ts.get(); - let s = if s.as_bytes()[0] == b'"' { - &s[1..s.len() - 1] - } else { - s - }; - Some(Timestamp::new(s)) - } - } - } - pub fn matches(&self, filter: &Filter) -> bool { if filter.is_empty() { return true; } + if filter.since.is_some() || filter.until.is_some() { + if let Some(ts) = self.ts.as_ref().and_then(|ts| ts.parse()) { + if let Some(since) = filter.since { + if ts < since { + return false; + } + } + if let Some(until) = filter.until { + if ts > until { + return false; + } + } + } + } + if let Some(bound) = &filter.level { if let Some(level) = self.level.as_ref() { if level > bound { @@ -137,11 +138,11 @@ impl Default for ParserSettings { } } -impl From<&Settings> for ParserSettings { - fn from(s: &Settings) -> Self { +impl ParserSettings { + pub fn new(s: &Settings, preparse_time: bool) -> Self { let mut fields = HashMap::new(); for (i, name) in s.fields.time.names.iter().enumerate() { - fields.insert(name.clone(), (FieldSettings::Time, i)); + fields.insert(name.clone(), (FieldSettings::Time(preparse_time), i)); } let mut j = 0; for variant in &s.fields.level.variants { @@ -167,9 +168,7 @@ impl From<&Settings> for ParserSettings { } Self { fields } } -} -impl ParserSettings { fn apply<'a>( &self, key: &'a str, @@ -236,7 +235,7 @@ impl PriorityContext { // --- enum FieldSettings { - Time, + Time(bool), Level(HashMap), Logger, Message, @@ -246,7 +245,20 @@ enum FieldSettings { impl FieldSettings { fn apply<'a>(&self, value: &'a RawValue, to: &mut Record<'a>) { match self { - Self::Time => to.ts = Some(value), + Self::Time(preparse) => { + let s = value.get(); + let s = if s.as_bytes()[0] == b'"' { + &s[1..s.len() - 1] + } else { + s + }; + let ts = Timestamp::new(s, None); + if *preparse { + to.ts = Some(Timestamp::new(ts.raw(), Some(ts.parse()))); + } else { + to.ts = Some(ts); + } + } Self::Level(values) => { to.level = json::from_str(value.get()) .ok() @@ -260,7 +272,7 @@ impl FieldSettings { fn kind(&self) -> FieldKind { match self { - Self::Time => FieldKind::Time, + Self::Time(_) => FieldKind::Time, Self::Level(_) => FieldKind::Level, Self::Logger => FieldKind::Logger, Self::Message => FieldKind::Message, @@ -531,11 +543,16 @@ impl FieldFilterSet { pub struct Filter { pub fields: FieldFilterSet, pub level: Option, + pub since: Option>, + pub until: Option>, } impl Filter { pub fn is_empty(&self) -> bool { - self.fields.0.is_empty() && self.level.is_none() + self.fields.0.is_empty() + && self.level.is_none() + && self.since.is_none() + && self.until.is_none() } } diff --git a/src/timeparse.rs b/src/timeparse.rs new file mode 100644 index 00000000..7febf8be --- /dev/null +++ b/src/timeparse.rs @@ -0,0 +1,295 @@ +// third-party imports +use chrono::{DateTime, Datelike, Duration, FixedOffset, TimeZone, Utc}; +use humantime::parse_duration; + +// local imports +use crate::datefmt::{DateTimeFormat, Flag, Flags, Item}; +use crate::error::*; + +pub fn parse_time( + s: &str, + tz: &FixedOffset, + format: &DateTimeFormat, +) -> Result> { + let s = s.trim(); + None.or_else(|| relative_past(s)) + .or_else(|| relative_future(s)) + .or_else(|| use_custom_format(s, format, &Utc::now().with_timezone(tz), tz)) + .or_else(|| rfc3339_weak(s, tz)) + .or_else(|| human(s, tz)) + .ok_or(Error::UnrecognizedTime(s.into())) +} + +fn relative_past(s: &str) -> Option> { + if s.starts_with('-') { + let d = parse_duration(&s[1..]).ok()?; + Some((Utc::now() - Duration::from_std(d).ok()?).into()) + } else { + None + } +} + +fn relative_future(s: &str) -> Option> { + if s.starts_with('+') { + let d = parse_duration(&s[1..]).ok()?; + Some((Utc::now() + Duration::from_std(d).ok()?).into()) + } else { + None + } +} + +fn human(s: &str, tz: &FixedOffset) -> Option> { + htp::parse(s, Utc::now().with_timezone(tz)).ok() +} + +fn rfc3339_weak(s: &str, tz: &FixedOffset) -> Option> { + let offset = Duration::seconds(tz.utc_minus_local().into()); + let time = DateTime::::from(humantime::parse_rfc3339_weak(s).ok()?) + offset; + Some(time.into()) +} + +fn use_custom_format( + s: &str, + format: &DateTimeFormat, + now: &DateTime, + tz: &FixedOffset, +) -> Option> { + let unsupported = || None; + let mut buf = Vec::new(); + let mut has_year = false; + let mut has_month = false; + let mut has_day = false; + let mut has_ampm = false; + let mut has_hour = false; + let mut has_minute = false; + let mut has_second = false; + + for item in format { + match *item.as_ref() { + Item::Char(b) => { + buf.push(b); + } + Item::Century(_) => { + return unsupported(); + } + Item::Year(flags) => { + add_format_item(&mut buf, b"Y", flags)?; + has_year = true; + } + Item::YearShort(flags) => { + add_format_item(&mut buf, b"y", flags)?; + has_year = true; + } + Item::YearQuarter(_) => { + return unsupported(); + } + Item::MonthNumeric(flags) => { + if flags.intersects(Flag::FromZero) { + return unsupported(); + } + add_format_item(&mut buf, b"m", flags)?; + has_month = true; + } + Item::MonthShort(flags) => { + if flags != Flags::none() { + return unsupported(); + } + add_format_item(&mut buf, b"b", flags)?; + has_month = true; + } + Item::MonthLong(flags) => { + if flags != Flags::none() { + return unsupported(); + } + add_format_item(&mut buf, b"B", flags)?; + has_month = true; + } + Item::Day(flags) => { + add_format_item(&mut buf, b"d", flags)?; + has_day = true; + } + Item::WeekdayNumeric(flags) => { + let item = if flags.contains(Flag::FromSunday | Flag::FromZero) { + b"w" + } else if !flags.intersects(Flag::FromSunday | Flag::FromZero) { + b"u" + } else { + return None; + }; + add_format_item(&mut buf, item, flags & !(Flag::FromSunday | Flag::FromZero))?; + } + Item::WeekdayShort(flags) => { + if flags != Flags::none() { + return unsupported(); + } + add_format_item(&mut buf, b"a", flags)?; + } + Item::WeekdayLong(flags) => { + if flags != Flags::none() { + return unsupported(); + } + add_format_item(&mut buf, b"A", flags)?; + } + Item::YearDay(_) => { + return unsupported(); + } + Item::IsoWeek(_) => { + return unsupported(); + } + Item::IsoYear(_) => { + return unsupported(); + } + Item::IsoYearShort(_) => { + return unsupported(); + } + Item::Hour(flags) => { + add_format_item(&mut buf, b"H", flags)?; + has_hour = true; + has_ampm = true; + } + Item::Hour12(flags) => { + add_format_item(&mut buf, b"I", flags)?; + has_hour = true; + } + Item::AmPm(flags) => { + let item = if flags.contains(Flag::LowerCase) { + b"p" + } else { + b"P" + }; + add_format_item(&mut buf, item, Flags::none())?; + has_ampm = true; + } + Item::Minute(flags) => { + add_format_item(&mut buf, b"M", flags)?; + has_minute = true; + } + Item::Second(flags) => { + add_format_item(&mut buf, b"S", flags)?; + has_second = true; + } + Item::Nanosecond((_, _)) => { + if buf.len() == 0 || buf[buf.len() - 1] != b'.' { + unsupported(); + } + buf.pop(); + add_format_item(&mut buf, b".f", Flags::none())?; + } + Item::UnixTimestamp(flags) => { + add_format_item(&mut buf, b"s", flags)?; + } + Item::TimeZoneHour(_) => { + return unsupported(); + } + Item::TimeZoneMinute(_) => { + return unsupported(); + } + Item::TimeZoneSecond(_) => { + return unsupported(); + } + Item::TimeZoneName(_) => { + return unsupported(); + } + } + } + let mut extra = Vec::new(); + if !has_year { + buf.extend_from_slice(b" %Y"); + extra.extend_from_slice(b" %Y"); + } + if !has_month { + buf.extend_from_slice(b" %m"); + extra.extend_from_slice(b" %m"); + } + if !has_day { + buf.extend_from_slice(b" %d"); + extra.extend_from_slice(b" %d"); + } + if !has_ampm { + buf.extend_from_slice(b" %p"); + extra.extend_from_slice(b" %p"); + } + if !has_hour { + buf.extend_from_slice(b" %H"); + extra.extend_from_slice(b" 00"); + } + if !has_minute { + buf.extend_from_slice(b" %M"); + extra.extend_from_slice(b" 00"); + } + if !has_second { + buf.extend_from_slice(b" %S"); + extra.extend_from_slice(b" 00"); + } + let f1 = std::str::from_utf8(&buf).ok()?; + let f2 = std::str::from_utf8(&extra).ok()?; + let s = format!("{}{}", s, now.format(f2)); + let result = tz.datetime_from_str(&s, f1).ok()?; + smart_adjust(result, now, has_year, has_month, has_day).or(Some(result)) +} + +fn smart_adjust( + result: DateTime, + now: &DateTime, + has_year: bool, + has_month: bool, + has_day: bool, +) -> Option> { + if &result <= now { + return None; + } + + if !has_day { + let pred = result.date().pred(); + let fixed = result + .timezone() + .ymd(pred.year(), pred.month(), pred.day()) + .and_time(result.time())?; + if &fixed <= now { + return Some(fixed); + } + } + + if !has_month { + let month = result.month(); + let fixed = result + .with_year(if month > 1 { + result.year() + } else { + result.year() - 1 + })? + .with_month(if month > 1 { month - 1 } else { 12 })?; + if &fixed <= now { + return Some(fixed); + } + } + + if !has_year { + let fixed = result.with_year(result.year() - 1)?; + if &fixed <= now { + return Some(fixed); + } + } + + None +} + +fn add_format_item(buf: &mut Vec, item: &[u8], flags: Flags) -> Option<()> { + buf.push(b'%'); + if flags.intersects(Flag::SpacePadding) { + buf.push(b'_'); + } + if flags.intersects(Flag::ZeroPadding) { + buf.push(b'0'); + } + if flags.intersects(Flag::NoPadding) { + buf.push(b'-'); + } + buf.extend_from_slice(item); + + if flags.intersects(Flag::UpperCase | Flag::LowerCase | Flag::FromZero | Flag::FromSunday) { + None + } else { + Some(()) + } +} diff --git a/src/timestamp.rs b/src/timestamp.rs index 0654ecae..e3b2a17e 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -4,11 +4,11 @@ use chrono::{DateTime, FixedOffset}; // --- -pub struct Timestamp<'a>(&'a str); +pub struct Timestamp<'a>(&'a str, Option>>); impl<'a> Timestamp<'a> { - pub fn new(value: &'a str) -> Self { - Self(value) + pub fn new(value: &'a str, parsed: Option>>) -> Self { + Self(value, parsed) } pub fn raw(&self) -> &'a str { @@ -16,6 +16,10 @@ impl<'a> Timestamp<'a> { } pub fn parse(&self) -> Option> { + if let Some(parsed) = self.1 { + return parsed; + } + if let Ok(ts) = self.0.parse::() { let (ts, nsec) = if ts < 100000000000 { (ts, 0)