mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-10-16 03:25:04 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb1cb02a33 | ||
|
|
52554677dd | ||
|
|
516a3a934d | ||
|
|
1b014a15a3 | ||
|
|
bf5cf43375 | ||
|
|
edb83cceba | ||
|
|
a3b8d128fa | ||
|
|
e717803a5e | ||
|
|
b6c2256daf | ||
|
|
3355829f3d | ||
|
|
80556965f3 | ||
|
|
b83d7a953c | ||
|
|
4b412e03d2 | ||
|
|
c312311cf0 | ||
|
|
672a2acbc4 | ||
|
|
665eb95a13 | ||
|
|
0371374e7e | ||
|
|
1129eeaeaf | ||
|
|
400969043d | ||
|
|
d464f3a2b1 | ||
|
|
025feb86f8 | ||
|
|
53ed79bcfc | ||
|
|
d5bd1e8e4a | ||
|
|
7e9a8b502c |
No files matched your search
@@ -0,0 +1,15 @@
|
||||
name: Security audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rustsec/audit-check@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo check
|
||||
|
||||
test:
|
||||
@@ -25,14 +25,14 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo test
|
||||
|
||||
fmt:
|
||||
name: cargo fmt --all -- --check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
min_version:
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ env.RUST_MIN_SRV }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
- { os: macos-latest , features: macos }
|
||||
- { os: windows-latest , features: windows }
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: emulate nightly toolchain
|
||||
run: echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
|
||||
- name: Initialize workflow variables
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" --source-dir .
|
||||
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
|
||||
- name: Upload coverage results (to Codecov.io)
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ${{ steps.coverage.outputs.report }}
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
env:
|
||||
RUN_FOR: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: emulate nightly toolchain
|
||||
run: echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
|
||||
- name: Install `cargo-fuzz`
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
name: Run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@v1
|
||||
|
||||
Generated
+179
-151
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -17,7 +17,7 @@ jiff = { version = "0.2.15", default-features = false, features = ["tz-system",
|
||||
rstest = "0.26"
|
||||
|
||||
[dev-dependencies.codspeed-criterion-compat]
|
||||
version = "4.0"
|
||||
version = "5.0"
|
||||
|
||||
[[bench]]
|
||||
name = "parse_datetime"
|
||||
|
||||
Generated
+102
-30
@@ -13,15 +13,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.58"
|
||||
version = "1.2.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -35,6 +41,38 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"defmt-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-parser"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -76,24 +114,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.23"
|
||||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
|
||||
checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.23"
|
||||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
|
||||
checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -127,15 +166,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.12"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||
checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
@@ -179,13 +218,35 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
||||
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -247,6 +308,26 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -255,9 +336,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
@@ -268,26 +349,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
+13
-10
@@ -189,8 +189,8 @@ impl DateTimeBuilder {
|
||||
/// - b. Apply time. If time carries an explicit numeric offset, apply the
|
||||
/// offset before setting time.
|
||||
/// - c. Apply weekday (e.g., "next Friday" or "last Monday").
|
||||
/// - d. Apply relative adjustments (e.g., "+3 days", "-2 months").
|
||||
/// - e. Apply final fixed offset if present.
|
||||
/// - d. Apply fixed offset if present (anchors the instant).
|
||||
/// - e. Apply relative adjustments (e.g., "+3 days", "-2 months").
|
||||
pub(super) fn build(self) -> Result<ParsedDateTime, error::Error> {
|
||||
if let Some(date) = self.date.as_ref() {
|
||||
if date.year.unwrap_or(0) > 9999 {
|
||||
@@ -317,7 +317,17 @@ impl DateTimeBuilder {
|
||||
dt = dt.checked_add(Span::new().try_days(delta)?)?;
|
||||
}
|
||||
|
||||
// 4d. Apply relative adjustments.
|
||||
// 4d. Apply fixed offset. This anchors the instant before relative
|
||||
// adjustments so that "1970/01/01 UTC N seconds" adds N seconds to the
|
||||
// fixed-offset instant rather than to a local wall-clock that may drift
|
||||
// across a DST boundary (see issue uutils/coreutils#12555).
|
||||
if let Some(offset) = self.offset {
|
||||
let (offset, hour_adjustment) = offset.normalize();
|
||||
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
|
||||
dt = dt.datetime().to_zoned((&offset).try_into()?)?;
|
||||
}
|
||||
|
||||
// 4e. Apply relative adjustments.
|
||||
for rel in self.relative {
|
||||
dt = match rel {
|
||||
relative::Relative::Years(_) | relative::Relative::Months(_) => {
|
||||
@@ -338,13 +348,6 @@ impl DateTimeBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
// 4e. Apply final fixed offset.
|
||||
if let Some(offset) = self.offset {
|
||||
let (offset, hour_adjustment) = offset.normalize();
|
||||
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
|
||||
dt = dt.datetime().to_zoned((&offset).try_into()?)?;
|
||||
}
|
||||
|
||||
Ok(dt)
|
||||
}
|
||||
|
||||
|
||||
+53
-8
@@ -16,7 +16,7 @@
|
||||
//! > discarded.
|
||||
use winnow::{
|
||||
combinator::{alt, trace},
|
||||
seq, ModalResult, Parser,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use crate::items::space;
|
||||
@@ -29,14 +29,32 @@ pub(crate) struct DateTime {
|
||||
pub(crate) time: time::Time,
|
||||
}
|
||||
|
||||
fn remaining_starts_with_meridiem(input: &str) -> bool {
|
||||
let trimmed = input.trim_start();
|
||||
trimmed.starts_with("am")
|
||||
|| trimmed.starts_with("pm")
|
||||
|| trimmed.starts_with("a.m.")
|
||||
|| trimmed.starts_with("p.m.")
|
||||
}
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
||||
seq!(DateTime {
|
||||
date: trace("iso_date", alt((date::iso1, date::iso2))),
|
||||
// Note: the `T` is lowercased by the main parse function
|
||||
_: alt((s('t').void(), (' ', space).void())),
|
||||
time: trace("iso_time", time::iso),
|
||||
})
|
||||
.parse_next(input)
|
||||
let date = trace("iso_date", alt((date::iso1, date::iso2))).parse_next(input)?;
|
||||
// Note: the `T` is lowercased by the main parse function
|
||||
alt((s('t').void(), (' ', space).void())).parse_next(input)?;
|
||||
|
||||
let mut iso_input = *input;
|
||||
if let Ok(parsed_time) = trace("iso_time", time::iso).parse_next(&mut iso_input) {
|
||||
if !remaining_starts_with_meridiem(iso_input) {
|
||||
*input = iso_input;
|
||||
return Ok(DateTime {
|
||||
date,
|
||||
time: parsed_time,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let time = trace("iso_time", time::parse).parse_next(input)?;
|
||||
Ok(DateTime { date, time })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -73,4 +91,31 @@ mod tests {
|
||||
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_and_time_ampm() {
|
||||
let reference = Some(DateTime {
|
||||
date: Date {
|
||||
day: 15,
|
||||
month: 6,
|
||||
year: Some(2024),
|
||||
},
|
||||
time: Time {
|
||||
hour: 15,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
},
|
||||
});
|
||||
|
||||
for mut s in [
|
||||
"2024-06-15 3:00 pm",
|
||||
"2024-06-15 3:00pm",
|
||||
"2024-06-15 3:00 p.m.",
|
||||
] {
|
||||
let old_s = s.to_owned();
|
||||
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,6 +377,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_and_time_ampm() {
|
||||
// https://github.com/uutils/parse_datetime/issues/282
|
||||
assert_eq!("12:00", test_eq_fmt("%H:%M", "2024-06-15 12:00 PM"));
|
||||
assert_eq!("11:30", test_eq_fmt("%H:%M", "2024-06-15 11:30am"));
|
||||
assert_eq!("15:00", test_eq_fmt("%H:%M", "2024-06-15 3:00 PM"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let result = parse(&mut "");
|
||||
|
||||
@@ -281,6 +281,7 @@ fn timezone_name_to_offset(input: &str) -> ModalResult<Offset> {
|
||||
"w" => Ok("-10"),
|
||||
"v" => Ok("-9"),
|
||||
"utc" => Ok("+0"),
|
||||
"ut" => Ok("+0"),
|
||||
"u" => Ok("-8"),
|
||||
"t" => Ok("-7"),
|
||||
"sst" => Ok("-11"),
|
||||
@@ -423,6 +424,7 @@ mod tests {
|
||||
fn timezone_name_without_offset() {
|
||||
for (input, expected) in [
|
||||
("utc", off(false, 0, 0)), // UTC
|
||||
("ut", off(false, 0, 0)), // Universal Time = UTC (issue #280)
|
||||
("gmt", off(false, 0, 0)), // UTC
|
||||
("z", off(false, 0, 0)), // UTC
|
||||
("west", off(false, 1, 0)), // positive offset
|
||||
|
||||
+168
@@ -49,6 +49,57 @@ impl ParsedDateTime {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of seconds since the Unix epoch, using floor
|
||||
/// division (rounding toward negative infinity).
|
||||
///
|
||||
/// This matches the behavior of GNU `date +%s` for negative fractional
|
||||
/// timestamps. For example, a timestamp of −1.5 seconds returns −2
|
||||
/// (not −1 as truncation toward zero would give).
|
||||
///
|
||||
/// Note: `jiff::Timestamp::as_second()` uses truncation toward zero,
|
||||
/// which differs from the POSIX/GNU convention for `%s`. This method
|
||||
/// corrects for that.
|
||||
pub fn unix_epoch_second(&self) -> i64 {
|
||||
match self {
|
||||
ParsedDateTime::InRange(z) => {
|
||||
let ts = z.timestamp();
|
||||
let s = ts.as_second();
|
||||
let ns = ts.subsec_nanosecond();
|
||||
// jiff stores negative fractional timestamps with a negative
|
||||
// subsec_nanosecond (e.g. -1.5s → as_second=-1, subsec=-500ms).
|
||||
// Floor semantics require subtracting 1 from the second when
|
||||
// the nanosecond component is negative.
|
||||
if ns < 0 {
|
||||
s - 1
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
ParsedDateTime::Extended(ext) => ext.unix_seconds(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the sub-second nanosecond component, always in `0..1_000_000_000`.
|
||||
///
|
||||
/// This is the complement of [`unix_epoch_second`](Self::unix_epoch_second):
|
||||
/// the full timestamp equals `unix_epoch_second() + subsec_nanosecond() / 1e9`.
|
||||
///
|
||||
/// For negative fractional timestamps the nanosecond is re-normalized to be
|
||||
/// non-negative (matching the GNU `timespec` convention).
|
||||
pub fn subsec_nanosecond(&self) -> i32 {
|
||||
match self {
|
||||
ParsedDateTime::InRange(z) => {
|
||||
let ns = z.timestamp().subsec_nanosecond();
|
||||
if ns < 0 {
|
||||
1_000_000_000 + ns
|
||||
} else {
|
||||
ns
|
||||
}
|
||||
}
|
||||
ParsedDateTime::Extended(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwraps the `InRange` variant, panicking if this is an `Extended` value.
|
||||
///
|
||||
/// This is a convenience for contexts where the caller is certain the result
|
||||
@@ -694,6 +745,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// GNU `date` accepts bare `ut` / `UT` as a synonym for UTC (issue #280).
|
||||
/// `gmt` and `GMT` already worked; this test covers all four forms.
|
||||
#[test]
|
||||
fn test_bare_utc_timezone_abbreviations() {
|
||||
// All four should parse without error and produce a result in UTC (+00:00).
|
||||
for abbr in ["ut", "UT", "gmt", "GMT"] {
|
||||
let result = parse_datetime(abbr)
|
||||
.unwrap_or_else(|_| panic!("bare timezone '{abbr}' should be accepted"));
|
||||
let offset_secs = result.expect_in_range().offset().seconds();
|
||||
assert_eq!(offset_secs, 0, "'{abbr}' should resolve to UTC (+0)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weekday_only() {
|
||||
let now = Zoned::now();
|
||||
@@ -887,4 +951,108 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod unix_epoch_second {
|
||||
use jiff::{civil::DateTime, tz::TimeZone};
|
||||
|
||||
use crate::{parse_datetime, parse_datetime_at_date, DateParts, ParsedDateTime, TimeParts};
|
||||
|
||||
fn epoch_second(input: &str) -> (i64, i32) {
|
||||
let base = "2024-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let parsed = parse_datetime_at_date(base, input).unwrap();
|
||||
(parsed.unix_epoch_second(), parsed.subsec_nanosecond())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positive_integer_epoch() {
|
||||
let (sec, nsec) = epoch_second("@1690466034");
|
||||
assert_eq!(sec, 1690466034);
|
||||
assert_eq!(nsec, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positive_fractional_epoch() {
|
||||
let (sec, nsec) = epoch_second("@1690466034.5");
|
||||
assert_eq!(sec, 1690466034);
|
||||
assert_eq!(nsec, 500_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_integer_epoch() {
|
||||
let (sec, nsec) = epoch_second("@-1");
|
||||
assert_eq!(sec, -1);
|
||||
assert_eq!(nsec, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_fractional_epoch_floors() {
|
||||
// Issue #283: @-1.5 must floor to -2, not truncate to -1.
|
||||
let (sec, nsec) = epoch_second("@-1.5");
|
||||
assert_eq!(sec, -2, "floor(-1.5) should be -2, not -1");
|
||||
assert_eq!(nsec, 500_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_fractional_epoch_large() {
|
||||
// From issue #283: @-893375784.554767216
|
||||
let (sec, nsec) = epoch_second("@-893375784.554767216");
|
||||
assert_eq!(sec, -893375785);
|
||||
assert_eq!(nsec, 445_232_784);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_epoch() {
|
||||
let (sec, nsec) = epoch_second("@0");
|
||||
assert_eq!(sec, 0);
|
||||
assert_eq!(nsec, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_zero_fractional() {
|
||||
// @-0.5 should floor to -1
|
||||
let (sec, nsec) = epoch_second("@-0.5");
|
||||
assert_eq!(sec, -1);
|
||||
assert_eq!(nsec, 500_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_values_use_extended_epoch_path() {
|
||||
let parsed = parse_datetime("10000-01-01").unwrap();
|
||||
// Year 10000 is beyond jiff::Zoned's representable range, so
|
||||
// parse_datetime must route through the ExtendedDateTime branch
|
||||
// of both accessors.
|
||||
assert!(matches!(parsed, ParsedDateTime::Extended(_)));
|
||||
assert_eq!(parsed.subsec_nanosecond(), 0);
|
||||
|
||||
// Year 10000 UTC in seconds since 1970 is well past 2.5e11.
|
||||
assert!(parsed.unix_epoch_second() > 250_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manually_constructed_extended_values_use_extended_epoch_path() {
|
||||
let ext = crate::ExtendedDateTime::new(
|
||||
DateParts {
|
||||
year: 10_000,
|
||||
month: 1,
|
||||
day: 1,
|
||||
},
|
||||
TimeParts {
|
||||
hour: 12,
|
||||
minute: 34,
|
||||
second: 56,
|
||||
nanosecond: 789_000_000,
|
||||
},
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
let parsed = ParsedDateTime::Extended(ext.clone());
|
||||
|
||||
assert_eq!(parsed.unix_epoch_second(), ext.unix_seconds());
|
||||
assert_eq!(parsed.subsec_nanosecond(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,3 +285,31 @@ fn test_multiple_month_skip(#[case] base: &str, #[case] input: &str, #[case] exp
|
||||
fn test_embedded_timezone(#[case] input: &str, #[case] expected: &str) {
|
||||
check_absolute(input, expected);
|
||||
}
|
||||
|
||||
// Regression test for uutils/coreutils#12555.
|
||||
// A fixed offset (e.g. the "UTC" keyword) must anchor the instant *before*
|
||||
// relative adjustments are applied. Otherwise, when the base zone observes DST,
|
||||
// adding a large relative offset drifts the wall-clock across a DST boundary
|
||||
// and re-anchoring at the fixed offset bakes in the extra hour.
|
||||
// "1970-01-01 UTC + N seconds" must equal exactly N seconds after the epoch,
|
||||
// regardless of the base zone's DST rules.
|
||||
#[test]
|
||||
fn test_utc_keyword_plus_relative_seconds_across_dst() {
|
||||
// Base in a DST-observing zone (Europe/Berlin); the value only matters as
|
||||
// the default zone for interpreting the date, not the result.
|
||||
let base = "2020-06-15 12:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::get("Europe/Berlin").unwrap())
|
||||
.unwrap();
|
||||
|
||||
// A timestamp landing in summer (CEST, UTC+2) while the epoch base is in
|
||||
// winter (CET, UTC+1), so the DST offset differs between base and result.
|
||||
let seconds = 1_780_318_971_i64;
|
||||
let input = format!("1970-01-01 UTC {seconds} seconds");
|
||||
|
||||
let parsed = parse_datetime::parse_datetime_at_date(base, input)
|
||||
.unwrap()
|
||||
.expect_in_range();
|
||||
assert_eq!(parsed.timestamp().as_second(), seconds);
|
||||
}
|
||||
Reference in new issue
Block a user