Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b47873b
Add strftime and strflocaltime functions
dnelson-1901 Apr 27, 2025
d80b54c
Add gmtime function
dnelson-1901 Apr 27, 2025
aa02ab2
gmtime: handle fractional seconds
dnelson-1901 May 3, 2025
7031924
add helper function for parsing an epoch timestamp
dnelson-1901 May 3, 2025
7ca63ea
collapse strftime and strflocaltime into one function
dnelson-1901 May 3, 2025
03f05b0
add localtime, and collapse gmtime and localtime into one function
dnelson-1901 May 3, 2025
b38a855
add helper function for creating a date-time array
dnelson-1901 May 3, 2025
973b7a1
add strptime function
dnelson-1901 May 3, 2025
309e3c9
add mktime function
dnelson-1901 May 3, 2025
4a51944
allow strftime/strflocaltime to handle incoming arrays
dnelson-1901 May 4, 2025
437826b
Document implementation of "More time filters"
dnelson-1901 May 4, 2025
1a94489
Make chrono imports explicit.
01mf02 May 7, 2025
1d0ec8b
Run `cargo fmt`.
01mf02 May 7, 2025
d65e7b1
Simplify timezone passing.
01mf02 May 7, 2025
0f8a44e
Simplify `datetime_to_array`.
01mf02 May 7, 2025
d8090ac
Simplify `array_to_datetime`.
01mf02 May 7, 2025
3c5d8ef
Comments.
01mf02 May 7, 2025
105f51f
Clippy.
01mf02 May 7, 2025
056c98e
Make `strftime` a bit more performant.
01mf02 May 7, 2025
66eec4d
Shorten formatting.
01mf02 May 7, 2025
607ae22
mktime: handle fractional seconds
dnelson-1901 May 7, 2025
5701f51
Code deduplication.
01mf02 May 8, 2025
3cde63d
Avoid double calls and be more explicit about truncation.
01mf02 May 8, 2025
0842e29
Unix is capital.
01mf02 May 8, 2025
bff22e2
Omit multiplication.
01mf02 May 8, 2025
6e16f1b
Make it compile with Rust 1.65 & simplify array construction.
01mf02 May 9, 2025
dc1e6b7
Reject negative date-time components.
01mf02 May 9, 2025
881dc2b
Thanks, clippy!
01mf02 May 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ Here is an overview that summarises:
- [x] Stream generators (`range`, `recurse`)
- [x] Time (`now`, `fromdateiso8601`, `todateiso8601`)
- [x] More numeric filters (`sqrt`, `sin`, `log`, `pow`, ...) ([list of numeric filters](#numeric-filters))
- [ ] More time filters (`strptime`, `strftime`, `strflocaltime`, `mktime`, `gmtime`, and `localtime`)
- [x] More time filters (`strptime`, `strftime`, `strflocaltime`, `mktime`, `gmtime`, and `localtime`)

## Standard filters

Expand Down
2 changes: 1 addition & 1 deletion jaq-std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ time = ["chrono"]
jaq-core = { version = "2.1.0", path = "../jaq-core" }

hifijson = { version = "0.2.0", optional = true }
chrono = { version = "0.4.38", default-features = false, features = ["alloc"], optional = true }
chrono = { version = "0.4.38", default-features = false, features = ["alloc", "clock"], optional = true }
regex-lite = { version = "0.1", optional = true }
log = { version = "0.4.17", optional = true }
libm = { version = "0.2.7", optional = true }
Expand Down
15 changes: 15 additions & 0 deletions jaq-std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,13 +591,28 @@ fn regex<V: ValT>() -> Box<[Filter<RunPtr<V>>]> {

#[cfg(feature = "time")]
fn time<V: ValT>() -> Box<[Filter<RunPtr<V>>]> {
use chrono::{Local, Utc};
Box::new([
("fromdateiso8601", v(0), |_, cv| {
bome(cv.1.try_as_str().and_then(time::from_iso8601))
}),
("todateiso8601", v(0), |_, cv| {
bome(time::to_iso8601(&cv.1).map(V::from))
}),
("strftime", v(1), |_, cv| {
unary(cv, |v, fmt| time::strftime(&v, fmt.try_as_str()?, Utc))
}),
("strflocaltime", v(1), |_, cv| {
unary(cv, |v, fmt| time::strftime(&v, fmt.try_as_str()?, Local))
}),
("gmtime", v(0), |_, cv| bome(time::gmtime(&cv.1, Utc))),
("localtime", v(0), |_, cv| bome(time::gmtime(&cv.1, Local))),
("strptime", v(1), |_, cv| {
unary(cv, |v, fmt| {
time::strptime(v.try_as_str()?, fmt.try_as_str()?)
})
}),
("mktime", v(0), |_, cv| bome(time::mktime(&cv.1))),
])
}

Expand Down
106 changes: 96 additions & 10 deletions jaq-std/src/time.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,66 @@
use crate::{Error, ValR, ValT};
use crate::{Error, ValR, ValT, ValTx};
use alloc::string::{String, ToString};
use chrono::DateTime;
use chrono::{DateTime, Datelike, FixedOffset, NaiveDateTime, TimeZone, Timelike, Utc};

/// Convert a UNIX epoch timestamp with optional fractions.
fn epoch_to_datetime<V: ValT>(v: &V) -> Result<DateTime<Utc>, Error<V>> {
let fail = || Error::str(format_args!("cannot parse {v} as epoch timestamp"));
let val = if let Some(i) = v.as_isize() {
(i * 1000000) as i64
} else {
(v.as_f64()? * 1000000.0) as i64
};

DateTime::from_timestamp_micros(val).ok_or_else(fail)
}

/// Convert a date-time pair to a UNIX epoch timestamp.
fn datetime_to_epoch<Tz: TimeZone, V: ValT>(dt: DateTime<Tz>, frac: bool) -> ValR<V> {
if frac {
Ok((dt.timestamp_micros() as f64 / 1e6).into())
} else {
let seconds = dt.timestamp();
isize::try_from(seconds)
.map(V::from)
.or_else(|_| V::from_num(&seconds.to_string()))
}
}

/// Parse a "broken down time" array.
fn array_to_datetime<V: ValT>(v: &[V]) -> Option<DateTime<Utc>> {
let [year, month, day, hour, min, sec]: &[V; 6] = v.get(..6)?.try_into().ok()?;
let sec = sec.as_f64().ok()?;
let u32 = |v: &V| -> Option<u32> { v.as_isize()?.try_into().ok() };
Utc.with_ymd_and_hms(
year.as_isize()?.try_into().ok()?,
u32(month)? + 1,
u32(day)?,
u32(hour)?,
u32(min)?,
// the `as i8` cast saturates, returning a number in the range [-128, 128]
(sec.floor() as i8).try_into().ok()?,
)
.single()?
.with_nanosecond((sec.fract() * 1e9) as u32)
}

/// Convert a DateTime<FixedOffset> to a "broken down time" array
fn datetime_to_array<V: ValT>(dt: DateTime<FixedOffset>) -> [V; 8] {
[
V::from(dt.year() as isize),
V::from(dt.month0() as isize),
V::from(dt.day() as isize),
V::from(dt.hour() as isize),
V::from(dt.minute() as isize),
if dt.nanosecond() > 0 {
V::from(dt.second() as f64 + dt.timestamp_subsec_micros() as f64 / 1e6)
} else {
V::from(dt.second() as isize)
},
V::from(dt.weekday().num_days_from_sunday() as isize),
V::from(dt.ordinal0() as isize),
]
}

/// Parse an ISO 8601 timestamp string to a number holding the equivalent UNIX timestamp
/// (seconds elapsed since 1970/01/01).
Expand All @@ -11,14 +71,7 @@ use chrono::DateTime;
pub fn from_iso8601<V: ValT>(s: &str) -> ValR<V> {
let dt = DateTime::parse_from_rfc3339(s)
.map_err(|e| Error::str(format_args!("cannot parse {s} as ISO-8601 timestamp: {e}")))?;
if s.contains('.') {
Ok((dt.timestamp_micros() as f64 / 1e6).into())
} else {
let seconds = dt.timestamp();
isize::try_from(seconds)
.map(V::from)
.or_else(|_| V::from_num(&seconds.to_string()))
}
datetime_to_epoch(dt, s.contains('.'))
}

/// Format a number as an ISO 8601 timestamp string.
Expand All @@ -33,3 +86,36 @@ pub fn to_iso8601<V: ValT>(v: &V) -> Result<String, Error<V>> {
Ok(dt.format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string())
}
}

/// Format a date (either number or array) in a given timezone.
pub fn strftime<V: ValT>(v: &V, fmt: &str, tz: impl TimeZone) -> ValR<V> {
let fail = || Error::str(format_args!("cannot convert {v} to time"));
let dt = match v.clone().into_vec() {
Ok(v) => array_to_datetime(&v).ok_or_else(fail),
Err(_) => epoch_to_datetime(v),
}?;
let dt = dt.with_timezone(&tz).fixed_offset();
Ok(dt.format(fmt).to_string().into())
}

/// Convert an epoch timestamp to a "broken down time" array.
pub fn gmtime<V: ValT>(v: &V, tz: impl TimeZone) -> ValR<V> {
let dt = epoch_to_datetime(v)?;
let dt = dt.with_timezone(&tz).fixed_offset();
datetime_to_array(dt).into_iter().map(Ok).collect()
}

/// Parse a string into a "broken down time" array.
pub fn strptime<V: ValT>(s: &str, fmt: &str) -> ValR<V> {
let dt = NaiveDateTime::parse_from_str(s, fmt)
.map_err(|e| Error::str(format_args!("cannot parse {s} using {fmt}: {e}")))?;
let dt = dt.and_utc().fixed_offset();
datetime_to_array(dt).into_iter().map(Ok).collect()
}

/// Parse an array into a UNIX epoch timestamp.
pub fn mktime<V: ValT>(v: &V) -> ValR<V> {
let fail = || Error::str(format_args!("cannot convert {v} to time"));
let dt = array_to_datetime(&v.clone().into_vec()?).ok_or_else(fail)?;
datetime_to_epoch(dt, dt.timestamp_subsec_micros() > 0)
}
32 changes: 32 additions & 0 deletions jaq-std/tests/funs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ yields!(
"86400.123456 | todateiso8601",
"1970-01-02T00:00:00.123456Z"
);
yields!(
strftime,
r#"86400 | strftime("%F %T")"#,
"1970-01-02 00:00:00"
);
yields!(
strftime_arr,
r#"[ 1970, 0, 2, 0, 0, 0, 5, 1 ] | strftime("%F %T")"#,
"1970-01-02 00:00:00"
);
yields!(
strftime_mu,
r#"86400.123456 | strftime("%F %T.%6f")"#,
"1970-01-02 00:00:00.123456"
);
yields!(gmtime, r"86400 | gmtime", [1970, 0, 2, 0, 0, 0, 5, 1]);
yields!(
gmtime_mu,
r"86400.123456 | gmtime",
json!([1970, 0, 2, 0, 0, 0.123456, 5, 1])
);
yields!(
gmtime_mktime_mu,
r"86400.123456 | gmtime | mktime",
86400.123456
);
yields!(
strptime,
r#""1970-01-02T00:00:00Z" | strptime("%Y-%m-%dT%H:%M:%SZ")"#,
[1970, 0, 2, 0, 0, 0, 5, 1]
);
yields!(mktime, "[ 1970, 0, 2, 0, 0, 0, 5, 1 ] | mktime", 86400);

#[test]
fn fromtodate() {
Expand Down