diff --git a/Cargo.lock b/Cargo.lock index 77f975c1..5cc63e1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -738,7 +738,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hl" -version = "0.27.0-beta.4.4" +version = "0.27.0-beta.4.5" dependencies = [ "atoi", "bincode", diff --git a/Cargo.toml b/Cargo.toml index cd796842..79405997 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.27.0-beta.4.4" +version = "0.27.0-beta.4.5" edition = "2021" build = "build.rs" diff --git a/Makefile b/Makefile index 79525876..8ecdd9ec 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,11 @@ bench: @cargo bench .PHONY: bench +## Show usage of the binary +usage: build + @env -i ./target/debug/hl --help +.PHONY: usage + ## Clean build artifacts clean: @cargo clean diff --git a/README.md b/README.md index 6ffa72a4..9b9ebc59 100644 --- a/README.md +++ b/README.md @@ -469,6 +469,7 @@ Options: -Z, --time-zone Time zone name, see column "TZ identifier" at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones [env: HL_TIME_ZONE=] [default: UTC] -L, --local Use local time zone, overrides --time-zone option --no-local Disable local time zone, overrides --local option + --unix-timestamp-unit Unix timestamp unit, [auto, s, ms, us, ns] [env: HL_UNIX_TIMESTAMP_UNIT=] [default: auto] [possible values: auto, s, ms, us, ns] -e, --hide-empty-fields Hide empty fields, applies for null, string, object and array fields only [env: HL_HIDE_EMPTY_FIELDS=] -E, --show-empty-fields Show empty fields, overrides --hide-empty-fields option [env: HL_SHOW_EMPTY_FIELDS=] --input-info Show input number and/or input filename before each message [default: auto] [possible values: auto, none, full, compact, minimal] @@ -480,6 +481,7 @@ Options: -o, --output Output file --delimiter Log message delimiter, [NUL, CR, LF, CRLF] or any custom string --dump-index Dump index metadata and exit + --debug Print debug error messages that can help with troubleshooting --help Print help -V, --version Print version ``` diff --git a/src/app.rs b/src/app.rs index 153ca361..16a68207 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,6 +5,7 @@ use std::convert::{TryFrom, TryInto}; use std::fs; use std::io::{BufWriter, Write}; use std::iter::repeat; +use std::num::NonZeroUsize; use std::ops::Range; use std::path::PathBuf; use std::rc::Rc; @@ -21,15 +22,14 @@ use crossbeam_channel::{self as channel, Receiver, RecvError, RecvTimeoutError, use crossbeam_utils::thread; use itertools::{izip, Itertools}; use platform_dirs::AppDirs; -use sha2::{Digest, Sha256}; -use std::num::{NonZeroU32, NonZeroUsize}; +use serde::{Deserialize, Serialize}; // local imports use crate::datefmt::{DateTimeFormat, DateTimeFormatter}; use crate::fmtx::aligned_left; use crate::formatting::{RawRecordFormatter, RecordFormatter, RecordWithSourceFormatter}; use crate::fsmon::{self, EventKind}; -use crate::index::{Indexer, Timestamp}; +use crate::index::{Indexer, IndexerSettings, Timestamp}; use crate::input::{BlockLine, Input, InputHolder, InputReference}; use crate::model::{Filter, Parser, ParserSettings, RawRecord, Record, RecordFilter, RecordWithSourceConstructor}; use crate::query::Query; @@ -64,9 +64,11 @@ pub struct Options { pub sync_interval: Duration, pub input_info: Option, pub dump_index: bool, + pub debug: bool, pub app_dirs: Option, pub tail: u64, pub delimiter: Delimiter, + pub unix_ts_unit: Option, } impl Options { @@ -93,6 +95,35 @@ pub enum InputInfo { Minimal, } +// --- + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)] +pub enum UnixTimestampUnit { + Seconds, + Milliseconds, + Microseconds, + Nanoseconds, +} + +impl UnixTimestampUnit { + pub fn guess(ts: i64) -> Self { + match ts { + Self::TS_UNIX_AUTO_S_MIN..=Self::TS_UNIX_AUTO_S_MAX => Self::Seconds, + Self::TS_UNIX_AUTO_MS_MIN..=Self::TS_UNIX_AUTO_MS_MAX => Self::Milliseconds, + Self::TS_UNIX_AUTO_US_MIN..=Self::TS_UNIX_AUTO_US_MAX => Self::Microseconds, + _ => Self::Nanoseconds, + } + } + + const TS_UNIX_AUTO_S_MIN: i64 = -62135596800; + const TS_UNIX_AUTO_S_MAX: i64 = 253402300799; + const TS_UNIX_AUTO_MS_MIN: i64 = Self::TS_UNIX_AUTO_S_MIN * 1000; + const TS_UNIX_AUTO_MS_MAX: i64 = Self::TS_UNIX_AUTO_S_MAX * 1000; + const TS_UNIX_AUTO_US_MIN: i64 = Self::TS_UNIX_AUTO_MS_MIN * 1000; + const TS_UNIX_AUTO_US_MAX: i64 = Self::TS_UNIX_AUTO_MS_MAX * 1000; +} +// --- + pub struct App { options: Options, } @@ -188,7 +219,15 @@ impl App { fn sort(&self, inputs: Vec, output: &mut Output) -> Result<()> { let mut output = BufWriter::new(output); - let param_hash = hex::encode(self.parameters_hash()?); + let indexer_settings = IndexerSettings::new( + self.options.buffer_size.try_into()?, + self.options.max_message_size.try_into()?, + &self.options.fields.settings.predefined, + self.options.delimiter.clone(), + self.options.allow_prefix, + self.options.unix_ts_unit, + ); + let param_hash = hex::encode(indexer_settings.hash()?); let cache_dir = self .options .app_dirs @@ -197,16 +236,8 @@ impl App { .unwrap_or_else(|| PathBuf::from(".cache")) .join(param_hash); fs::create_dir_all(&cache_dir)?; - let indexer = Indexer::new( - self.options.concurrency, - NonZeroU32::try_from(self.options.buffer_size)?.try_into()?, - NonZeroU32::try_from(self.options.max_message_size)?.try_into()?, - cache_dir, - &self.options.fields.settings.predefined, - self.options.delimiter.clone(), - self.options.allow_prefix, - ); + let indexer = Indexer::new(self.options.concurrency, cache_dir, indexer_settings); let input_badges = self.input_badges(inputs.iter().map(|x| &x.reference)); let inputs = inputs @@ -309,8 +340,11 @@ impl App { if let Some(ts) = &record.ts { if let Some(unix_ts) = ts.unix_utc() { items.push((unix_ts.into(), location)); - } else { - eprintln!("skipped message because timestamp cannot be parsed: {:#?}", ts) + } else if self.options.debug { + eprintln!( + "skipped a message because its timestamp could not be parsed: {:#?}", + ts.raw() + ) } } }, @@ -571,25 +605,12 @@ impl App { Ok(()) } - fn parameters_hash(&self) -> Result<[u8; 32]> { - let mut hasher = Sha256::new(); - bincode::serialize_into( - &mut hasher, - &( - &self.options.buffer_size, - &self.options.max_message_size, - &self.options.fields.settings.predefined, - &self.options.allow_prefix, - ), - )?; - Ok(hasher.finalize().into()) - } - fn parser(&self) -> Parser { Parser::new(ParserSettings::new( &self.options.fields.settings.predefined, &self.options.fields.settings.ignore, self.options.filter.since.is_some() || self.options.filter.until.is_some() || self.options.follow, + self.options.unix_ts_unit, )) } diff --git a/src/formatting.rs b/src/formatting.rs index 2c6ce8fb..8b05f437 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -462,7 +462,7 @@ mod tests { fn test_nested_objects() { assert_eq!( format(&Record { - ts: Some(Timestamp::new("2000-01-02T03:04:05.123Z", None)), + ts: Some(Timestamp::new("2000-01-02T03:04:05.123Z")), message: Some(RawValue::Json(&json_raw_value(r#""tm""#))), level: Some(Level::Debug), logger: Some("tl"), diff --git a/src/index.rs b/src/index.rs index 36e8ef60..4477b76e 100644 --- a/src/index.rs +++ b/src/index.rs @@ -16,6 +16,7 @@ use std::fmt::{self, Display}; use std::fs::File; use std::io::{Read, Write}; use std::iter::empty; +use std::num::NonZeroU32; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -30,6 +31,7 @@ use itertools::izip; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use crate::app::UnixTimestampUnit; // local imports use crate::error::{Error, Result}; use crate::index_capnp as schema; @@ -120,6 +122,53 @@ impl std::ops::Sub for Timestamp { // --- +pub struct IndexerSettings<'a> { + buffer_size: NonZeroU32, + max_message_size: NonZeroU32, + fields: &'a PredefinedFields, + delimiter: Delimiter, + allow_prefix: bool, + unix_ts_unit: Option, +} + +impl<'a> IndexerSettings<'a> { + pub fn new( + buffer_size: NonZeroU32, + max_message_size: NonZeroU32, + fields: &'a PredefinedFields, + delimiter: Delimiter, + allow_prefix: bool, + unix_ts_unit: Option, + ) -> Self { + Self { + buffer_size, + max_message_size, + fields, + delimiter, + allow_prefix, + unix_ts_unit, + } + } + + pub fn hash(&self) -> Result<[u8; 32]> { + let mut hasher = Sha256::new(); + bincode::serialize_into( + &mut hasher, + &( + &self.buffer_size, + &self.max_message_size, + &self.fields, + &self.delimiter, + &self.allow_prefix, + &self.unix_ts_unit, + ), + )?; + Ok(hasher.finalize().into()) + } +} + +// --- + /// Allows log files indexing to enable message sorting. pub struct Indexer { concurrency: usize, @@ -133,23 +182,20 @@ pub struct Indexer { impl Indexer { /// Returns a new Indexer with the given parameters. - pub fn new( - concurrency: usize, - buffer_size: u32, - max_message_size: u32, - dir: PathBuf, - fields: &PredefinedFields, - delimiter: Delimiter, - allow_prefix: bool, - ) -> Self { + pub fn new(concurrency: usize, dir: PathBuf, settings: IndexerSettings<'_>) -> Self { Self { concurrency, - buffer_size, - max_message_size, + buffer_size: settings.buffer_size.into(), + max_message_size: settings.max_message_size.into(), dir, - parser: Parser::new(ParserSettings::new(&fields, empty(), false)), - delimiter, - allow_prefix, + parser: Parser::new(ParserSettings::new( + &settings.fields, + empty(), + false, + settings.unix_ts_unit, + )), + delimiter: settings.delimiter, + allow_prefix: settings.allow_prefix, } } diff --git a/src/main.rs b/src/main.rs index 8fac2950..6badb6e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,21 +45,20 @@ struct Opt { #[arg(long, default_value = "auto", env = "HL_COLOR", overrides_with = "color")] #[arg(value_enum)] color: ColorOption, - // + /// Handful alias for --color=always, overrides --color option. #[arg(short)] color_always: bool, - // + /// Output paging options. #[arg(long, default_value = "auto", env = "HL_PAGING", overrides_with = "paging")] #[arg(value_enum)] paging: PagingOption, - // + /// Handful alias for --paging=never, overrides --paging option. #[arg(short = 'P')] paging_never: bool, - // - // + /// Color theme. #[arg( long, @@ -68,23 +67,23 @@ struct Opt { overrides_with="theme", )] theme: String, - // + /// Output raw JSON messages instead of formatter messages, it can be useful for applying filters and saving results in original format. #[arg(short, long, overrides_with = "raw")] raw: bool, - // + /// Disable raw JSON messages output, overrides --raw option. #[arg(long, overrides_with = "raw")] _no_raw: bool, - // + /// Disable unescaping and prettifying of field values. #[arg(long, overrides_with = "raw_fields")] raw_fields: bool, - // + /// Allow non-JSON prefixes before JSON messages. #[arg(long, env = "HL_ALLOW_PREFIX", overrides_with = "allow_prefix")] allow_prefix: bool, - // + /// Number of interrupts to ignore, i.e. Ctrl-C (SIGINT). #[arg( long, @@ -93,44 +92,44 @@ struct Opt { overrides_with = "interrupt_ignore_count" )] interrupt_ignore_count: usize, - // + /// Buffer size. #[arg(long, default_value = "256 KiB", env="HL_BUFFER_SIZE", value_parser = parse_non_zero_size, overrides_with="buffer_size")] buffer_size: NonZeroUsize, - // + /// Maximum message size. #[arg(long, default_value = "64 MiB", env="HL_MAX_MESSAGE_SIZE", value_parser = parse_non_zero_size, overrides_with="max_message_size")] max_message_size: NonZeroUsize, - // + /// Number of processing threads. #[arg(long, short = 'C', env = "HL_CONCURRENCY", overrides_with = "concurrency")] concurrency: Option, - // + /// Filtering by field values in one of forms [k=v, k~=v, k~~=v, 'k!=v', 'k!~=v', 'k!~~=v'] where ~ does substring match and ~~ does regular expression match. #[arg(short, long, number_of_values = 1)] filter: Vec, - // + /// Custom query, accepts expressions from --filter and supports '(', ')', 'and', 'or', 'not', 'in', 'contain', 'like', '<', '>', '<=', '>=', etc. #[arg(short, long, number_of_values = 1)] query: Vec, - // + /// Hide or reveal fields with the specified keys, prefix with ! to reveal, specify '!*' to reveal all. #[arg(long, short = 'h', number_of_values = 1)] hide: Vec, - // + /// Filtering by level. #[arg(short, long, env = "HL_LEVEL", overrides_with="level", ignore_case=true, value_parser = LevelValueParser)] #[arg(value_enum)] level: Option, - // + /// Filtering by timestamp >= the value (--time-zone and --local options are honored). #[arg(long, allow_hyphen_values = true, overrides_with = "since")] since: Option, - // + /// Filtering by timestamp <= the value (--time-zone and --local options are honored). #[arg(long, allow_hyphen_values = true, overrides_with = "until")] until: Option, - // + /// Time format, see https://man7.org/linux/man-pages/man1/date.1.html. #[arg( short, @@ -140,23 +139,32 @@ struct Opt { overrides_with = "time_format", )] time_format: String, - // + /// Time zone name, see column "TZ identifier" at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. #[arg(long, short = 'Z', env="HL_TIME_ZONE", default_value = &CONFIG.time_zone.name(), overrides_with="time_zone")] time_zone: chrono_tz::Tz, - // + /// Use local time zone, overrides --time-zone option. #[arg(long, short = 'L', overrides_with = "local")] local: bool, - // + /// Disable local time zone, overrides --local option. #[arg(long, overrides_with = "local")] _no_local: bool, - // + + /// Unix timestamp unit, [auto, s, ms, us, ns]. + #[arg( + long, + default_value = "auto", + overrides_with = "unix_timestamp_unit", + env = "HL_UNIX_TIMESTAMP_UNIT" + )] + unix_timestamp_unit: UnixTimestampUnit, + /// Files to process #[arg(name = "FILE")] files: Vec, - // + /// Hide empty fields, applies for null, string, object and array fields only. #[arg( long, @@ -165,7 +173,7 @@ struct Opt { overrides_with = "hide_empty_fields" )] hide_empty_fields: bool, - // + /// Show empty fields, overrides --hide-empty-fields option. #[arg( long, @@ -179,7 +187,7 @@ struct Opt { #[arg(long, default_value = "auto", overrides_with = "input_info")] #[arg(value_enum)] input_info: InputInfoOption, - // + /// List available themes and exit. #[arg(long)] list_themes: bool, @@ -212,7 +220,10 @@ struct Opt { #[arg(long)] dump_index: bool, - // + /// Print debug error messages that can help with troubleshooting. + #[arg(long)] + debug: bool, + /// Print help. #[arg(long, default_value_t = false, action = ArgAction::SetTrue)] help: bool, @@ -241,6 +252,15 @@ enum InputInfoOption { Minimal, } +#[derive(ValueEnum, Debug, Clone, Copy)] +enum UnixTimestampUnit { + Auto, + S, + Ms, + Us, + Ns, +} + // --- static CONFIG: Lazy = Lazy::new(|| load_config()); @@ -443,9 +463,17 @@ fn run() -> Result<()> { InputInfoOption::Minimal => Some(hl::app::InputInfo::Minimal), }, dump_index: opt.dump_index, + debug: opt.debug, app_dirs: Some(app_dirs), tail: opt.tail, delimiter, + unix_ts_unit: match opt.unix_timestamp_unit { + UnixTimestampUnit::Auto => None, + UnixTimestampUnit::S => Some(hl::app::UnixTimestampUnit::Seconds), + UnixTimestampUnit::Ms => Some(hl::app::UnixTimestampUnit::Milliseconds), + UnixTimestampUnit::Us => Some(hl::app::UnixTimestampUnit::Microseconds), + UnixTimestampUnit::Ns => Some(hl::app::UnixTimestampUnit::Nanoseconds), + }, }); // Configure input. diff --git a/src/model.rs b/src/model.rs index a668feb8..aa3a0d5f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -16,7 +16,6 @@ use serde_json::{ use wildflower::Pattern; // local imports -use crate::error::{Error, Result}; use crate::fmtx::Push; use crate::level; use crate::logfmt; @@ -24,6 +23,10 @@ use crate::serdex::StreamDeserializerWithOffsets; use crate::settings::PredefinedFields; use crate::timestamp::Timestamp; use crate::types::FieldKind; +use crate::{ + app::UnixTimestampUnit, + error::{Error, Result}, +}; // --- @@ -432,6 +435,7 @@ impl RecordFilter for RecordFilterNone { #[derive(Default)] pub struct ParserSettings { pre_parse_time: bool, + unix_ts_unit: Option, level: Vec<(HashMap, Option)>, blocks: Vec, ignore: Vec>, @@ -442,9 +446,11 @@ impl ParserSettings { predefined: &PredefinedFields, ignore: I, pre_parse_time: bool, + unix_ts_unit: Option, ) -> Self { let mut result = Self { pre_parse_time, + unix_ts_unit, level: Vec::new(), blocks: vec![ParserSettingsBlock::default()], ignore: ignore.into_iter().map(|x| Pattern::new(x.to_string())).collect(), @@ -667,9 +673,9 @@ impl FieldSettings { Self::Time => { let s = value.raw_str(); let s = if s.as_bytes()[0] == b'"' { &s[1..s.len() - 1] } else { s }; - let ts = Timestamp::new(s, None); + let ts = Timestamp::new(s).with_unix_unit(ps.unix_ts_unit); if ps.pre_parse_time { - to.ts = Some(Timestamp::new(ts.raw(), Some(ts.parse()))); + to.ts = Some(ts.preparsed()) } else { to.ts = Some(ts); } diff --git a/src/scanning.rs b/src/scanning.rs index 3b4fcaa9..37e6d456 100644 --- a/src/scanning.rs +++ b/src/scanning.rs @@ -8,6 +8,7 @@ use std::sync::Arc; // third-party imports use crossbeam_queue::SegQueue; +use serde::{Deserialize, Serialize}; // local imports use crate::error::*; @@ -36,7 +37,7 @@ impl Scanner { // --- /// Defines a token delimiter for Scanner. -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Delimiter { Byte(u8), Bytes(Vec), diff --git a/src/timestamp.rs b/src/timestamp.rs index 47d608d7..a4d5466a 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -1,40 +1,70 @@ // third-party imports use chrono::{DateTime, Duration, FixedOffset, NaiveDateTime}; +use crate::app::UnixTimestampUnit; + // --- #[derive(Debug)] -pub struct Timestamp<'a>(&'a str, Option>>); +pub struct Timestamp<'a> { + raw: &'a str, + parsed: Option>>, + unix_unit: Option, +} impl<'a> Timestamp<'a> { - pub fn new(value: &'a str, parsed: Option>>) -> Self { - Self(value, parsed) + pub fn new(value: &'a str) -> Self { + Self { + raw: value, + parsed: None, + unix_unit: None, + } } pub fn raw(&self) -> &'a str { - self.0 + self.raw + } + + pub fn preparsed(&self) -> Self { + Self { + raw: self.raw, + parsed: Some(self.parse()), + unix_unit: self.unix_unit, + } + } + + pub fn with_unix_unit(&self, unit: Option) -> Self { + Self { + raw: self.raw, + parsed: if unit == self.unix_unit { self.parsed } else { None }, + unix_unit: unit, + } } pub fn parse(&self) -> Option> { - if let Some(parsed) = self.1 { + if let Some(parsed) = self.parsed { return parsed; } - if let Ok(ts) = self.0.parse() { + if let Ok(ts) = self.raw.parse() { Some(ts) - } else if let Some(nt) = guess_number_type(self.0.as_bytes()) { - let ts = match nt { - NumberType::Integer => self.0.parse::().ok().and_then(|ts| match ts { - Self::TS_UNIX_AUTO_S_MIN..=Self::TS_UNIX_AUTO_S_MAX => NaiveDateTime::from_timestamp_opt(ts, 0), - Self::TS_UNIX_AUTO_MS_MIN..=Self::TS_UNIX_AUTO_MS_MAX => NaiveDateTime::from_timestamp_millis(ts), - Self::TS_UNIX_AUTO_US_MIN..=Self::TS_UNIX_AUTO_US_MAX => NaiveDateTime::from_timestamp_micros(ts), - _ => NaiveDateTime::from_timestamp_nanos(ts), + } else if let Some(nt) = guess_number_type(self.raw.as_bytes()) { + let ts = match (nt, self.unix_unit) { + (NumberType::Integer, unit) => self.raw.parse::().ok().and_then(|ts| { + let unit = unit.unwrap_or_else(|| UnixTimestampUnit::guess(ts)); + match unit { + UnixTimestampUnit::Seconds => NaiveDateTime::from_timestamp_opt(ts, 0), + UnixTimestampUnit::Milliseconds => NaiveDateTime::from_timestamp_millis(ts), + UnixTimestampUnit::Microseconds => NaiveDateTime::from_timestamp_micros(ts), + _ => NaiveDateTime::from_timestamp_nanos(ts), + } }), - NumberType::Float => self.0.bytes().position(|b| b == b'.').and_then(|i| { - let whole = self.0[..i].parse::().ok()?; - let fractional = self.0[i..].parse::().ok()?; - match whole { - Self::TS_UNIX_AUTO_S_MIN..=Self::TS_UNIX_AUTO_S_MAX => { + (NumberType::Float, unit) => self.raw.bytes().position(|b| b == b'.').and_then(|i| { + let whole = self.raw[..i].parse::().ok()?; + let fractional = self.raw[i..].parse::().ok()?; + let unit = unit.unwrap_or_else(|| UnixTimestampUnit::guess(whole)); + match unit { + UnixTimestampUnit::Seconds => { let ns = (fractional * 1e9).round() as u32; let (whole, ns) = if whole < 0 && ns > 0 { (whole - 1, 1_000_000_000 - ns) @@ -43,12 +73,12 @@ impl<'a> Timestamp<'a> { }; NaiveDateTime::from_timestamp_opt(whole, ns) } - Self::TS_UNIX_AUTO_MS_MIN..=Self::TS_UNIX_AUTO_MS_MAX => { + UnixTimestampUnit::Milliseconds => { let ns = (fractional * 1e6).round() as i64; let ns = if whole < 0 { -ns } else { ns }; NaiveDateTime::from_timestamp_millis(whole).map(|ts| ts + Duration::nanoseconds(ns)) } - Self::TS_UNIX_AUTO_US_MIN..=Self::TS_UNIX_AUTO_US_MAX => { + UnixTimestampUnit::Microseconds => { let ns = (fractional * 1e3).round() as i64; let ns = if whole < 0 { -ns } else { ns }; NaiveDateTime::from_timestamp_micros(whole).map(|ts| ts + Duration::nanoseconds(ns)) @@ -59,27 +89,20 @@ impl<'a> Timestamp<'a> { }; ts.and_then(|ts| Some(DateTime::from_naive_utc_and_offset(ts, FixedOffset::east_opt(0)?))) } else { - NaiveDateTime::parse_from_str(self.0, "%Y-%m-%d %H:%M:%S%.f") + NaiveDateTime::parse_from_str(self.raw, "%Y-%m-%d %H:%M:%S%.f") .ok() .map(|ts| ts.and_utc().into()) } } pub fn as_rfc3339(&self) -> Option { - rfc3339::Timestamp::parse(self.0) + rfc3339::Timestamp::parse(self.raw) } pub fn unix_utc(&self) -> Option<(i64, u32)> { self.parse() .and_then(|ts| Some((ts.timestamp(), ts.timestamp_subsec_nanos()))) } - - const TS_UNIX_AUTO_S_MIN: i64 = -62135596800; - const TS_UNIX_AUTO_S_MAX: i64 = 253402300799; - const TS_UNIX_AUTO_MS_MIN: i64 = Self::TS_UNIX_AUTO_S_MIN * 1000; - const TS_UNIX_AUTO_MS_MAX: i64 = Self::TS_UNIX_AUTO_S_MAX * 1000; - const TS_UNIX_AUTO_US_MIN: i64 = Self::TS_UNIX_AUTO_MS_MIN * 1000; - const TS_UNIX_AUTO_US_MAX: i64 = Self::TS_UNIX_AUTO_MS_MAX * 1000; } // --- @@ -467,7 +490,7 @@ mod tests { #[test] fn test_parse() { let test = |s, unix_timestamp, tz| { - let ts = Timestamp(s, None).parse().unwrap(); + let ts = Timestamp::new(s).parse().unwrap(); assert_eq!(ts.timestamp(), unix_timestamp); assert_eq!(ts.timezone().local_minus_utc(), tz); };