Skip to content

Commit 50d3dd7

Browse files
authored
Merge pull request #283 from dnelson-1901/timefuncs
Implement more time-related functions
2 parents df076dd + 881dc2b commit 50d3dd7

File tree

6 files changed

+252
-12
lines changed

6 files changed

+252
-12
lines changed

Cargo.lock

Lines changed: 107 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ Here is an overview that summarises:
285285
- [x] Stream generators (`range`, `recurse`)
286286
- [x] Time (`now`, `fromdateiso8601`, `todateiso8601`)
287287
- [x] More numeric filters (`sqrt`, `sin`, `log`, `pow`, ...) ([list of numeric filters](#numeric-filters))
288-
- [ ] More time filters (`strptime`, `strftime`, `strflocaltime`, `mktime`, `gmtime`, and `localtime`)
288+
- [x] More time filters (`strptime`, `strftime`, `strflocaltime`, `mktime`, `gmtime`, and `localtime`)
289289

290290
## Standard filters
291291

jaq-std/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ time = ["chrono"]
2222
jaq-core = { version = "2.1.0", path = "../jaq-core" }
2323

2424
hifijson = { version = "0.2.0", optional = true }
25-
chrono = { version = "0.4.38", default-features = false, features = ["alloc"], optional = true }
25+
chrono = { version = "0.4.38", default-features = false, features = ["alloc", "clock"], optional = true }
2626
regex-lite = { version = "0.1", optional = true }
2727
log = { version = "0.4.17", optional = true }
2828
libm = { version = "0.2.7", optional = true }

jaq-std/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,13 +591,28 @@ fn regex<V: ValT>() -> Box<[Filter<RunPtr<V>>]> {
591591

592592
#[cfg(feature = "time")]
593593
fn time<V: ValT>() -> Box<[Filter<RunPtr<V>>]> {
594+
use chrono::{Local, Utc};
594595
Box::new([
595596
("fromdateiso8601", v(0), |_, cv| {
596597
bome(cv.1.try_as_str().and_then(time::from_iso8601))
597598
}),
598599
("todateiso8601", v(0), |_, cv| {
599600
bome(time::to_iso8601(&cv.1).map(V::from))
600601
}),
602+
("strftime", v(1), |_, cv| {
603+
unary(cv, |v, fmt| time::strftime(&v, fmt.try_as_str()?, Utc))
604+
}),
605+
("strflocaltime", v(1), |_, cv| {
606+
unary(cv, |v, fmt| time::strftime(&v, fmt.try_as_str()?, Local))
607+
}),
608+
("gmtime", v(0), |_, cv| bome(time::gmtime(&cv.1, Utc))),
609+
("localtime", v(0), |_, cv| bome(time::gmtime(&cv.1, Local))),
610+
("strptime", v(1), |_, cv| {
611+
unary(cv, |v, fmt| {
612+
time::strptime(v.try_as_str()?, fmt.try_as_str()?)
613+
})
614+
}),
615+
("mktime", v(0), |_, cv| bome(time::mktime(&cv.1))),
601616
])
602617
}
603618

jaq-std/src/time.rs

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,66 @@
1-
use crate::{Error, ValR, ValT};
1+
use crate::{Error, ValR, ValT, ValTx};
22
use alloc::string::{String, ToString};
3-
use chrono::DateTime;
3+
use chrono::{DateTime, Datelike, FixedOffset, NaiveDateTime, TimeZone, Timelike, Utc};
4+
5+
/// Convert a UNIX epoch timestamp with optional fractions.
6+
fn epoch_to_datetime<V: ValT>(v: &V) -> Result<DateTime<Utc>, Error<V>> {
7+
let fail = || Error::str(format_args!("cannot parse {v} as epoch timestamp"));
8+
let val = if let Some(i) = v.as_isize() {
9+
(i * 1000000) as i64
10+
} else {
11+
(v.as_f64()? * 1000000.0) as i64
12+
};
13+
14+
DateTime::from_timestamp_micros(val).ok_or_else(fail)
15+
}
16+
17+
/// Convert a date-time pair to a UNIX epoch timestamp.
18+
fn datetime_to_epoch<Tz: TimeZone, V: ValT>(dt: DateTime<Tz>, frac: bool) -> ValR<V> {
19+
if frac {
20+
Ok((dt.timestamp_micros() as f64 / 1e6).into())
21+
} else {
22+
let seconds = dt.timestamp();
23+
isize::try_from(seconds)
24+
.map(V::from)
25+
.or_else(|_| V::from_num(&seconds.to_string()))
26+
}
27+
}
28+
29+
/// Parse a "broken down time" array.
30+
fn array_to_datetime<V: ValT>(v: &[V]) -> Option<DateTime<Utc>> {
31+
let [year, month, day, hour, min, sec]: &[V; 6] = v.get(..6)?.try_into().ok()?;
32+
let sec = sec.as_f64().ok()?;
33+
let u32 = |v: &V| -> Option<u32> { v.as_isize()?.try_into().ok() };
34+
Utc.with_ymd_and_hms(
35+
year.as_isize()?.try_into().ok()?,
36+
u32(month)? + 1,
37+
u32(day)?,
38+
u32(hour)?,
39+
u32(min)?,
40+
// the `as i8` cast saturates, returning a number in the range [-128, 128]
41+
(sec.floor() as i8).try_into().ok()?,
42+
)
43+
.single()?
44+
.with_nanosecond((sec.fract() * 1e9) as u32)
45+
}
46+
47+
/// Convert a DateTime<FixedOffset> to a "broken down time" array
48+
fn datetime_to_array<V: ValT>(dt: DateTime<FixedOffset>) -> [V; 8] {
49+
[
50+
V::from(dt.year() as isize),
51+
V::from(dt.month0() as isize),
52+
V::from(dt.day() as isize),
53+
V::from(dt.hour() as isize),
54+
V::from(dt.minute() as isize),
55+
if dt.nanosecond() > 0 {
56+
V::from(dt.second() as f64 + dt.timestamp_subsec_micros() as f64 / 1e6)
57+
} else {
58+
V::from(dt.second() as isize)
59+
},
60+
V::from(dt.weekday().num_days_from_sunday() as isize),
61+
V::from(dt.ordinal0() as isize),
62+
]
63+
}
464

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

2477
/// Format a number as an ISO 8601 timestamp string.
@@ -33,3 +86,36 @@ pub fn to_iso8601<V: ValT>(v: &V) -> Result<String, Error<V>> {
3386
Ok(dt.format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string())
3487
}
3588
}
89+
90+
/// Format a date (either number or array) in a given timezone.
91+
pub fn strftime<V: ValT>(v: &V, fmt: &str, tz: impl TimeZone) -> ValR<V> {
92+
let fail = || Error::str(format_args!("cannot convert {v} to time"));
93+
let dt = match v.clone().into_vec() {
94+
Ok(v) => array_to_datetime(&v).ok_or_else(fail),
95+
Err(_) => epoch_to_datetime(v),
96+
}?;
97+
let dt = dt.with_timezone(&tz).fixed_offset();
98+
Ok(dt.format(fmt).to_string().into())
99+
}
100+
101+
/// Convert an epoch timestamp to a "broken down time" array.
102+
pub fn gmtime<V: ValT>(v: &V, tz: impl TimeZone) -> ValR<V> {
103+
let dt = epoch_to_datetime(v)?;
104+
let dt = dt.with_timezone(&tz).fixed_offset();
105+
datetime_to_array(dt).into_iter().map(Ok).collect()
106+
}
107+
108+
/// Parse a string into a "broken down time" array.
109+
pub fn strptime<V: ValT>(s: &str, fmt: &str) -> ValR<V> {
110+
let dt = NaiveDateTime::parse_from_str(s, fmt)
111+
.map_err(|e| Error::str(format_args!("cannot parse {s} using {fmt}: {e}")))?;
112+
let dt = dt.and_utc().fixed_offset();
113+
datetime_to_array(dt).into_iter().map(Ok).collect()
114+
}
115+
116+
/// Parse an array into a UNIX epoch timestamp.
117+
pub fn mktime<V: ValT>(v: &V) -> ValR<V> {
118+
let fail = || Error::str(format_args!("cannot convert {v} to time"));
119+
let dt = array_to_datetime(&v.clone().into_vec()?).ok_or_else(fail)?;
120+
datetime_to_epoch(dt, dt.timestamp_subsec_micros() > 0)
121+
}

jaq-std/tests/funs.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,38 @@ yields!(
5151
"86400.123456 | todateiso8601",
5252
"1970-01-02T00:00:00.123456Z"
5353
);
54+
yields!(
55+
strftime,
56+
r#"86400 | strftime("%F %T")"#,
57+
"1970-01-02 00:00:00"
58+
);
59+
yields!(
60+
strftime_arr,
61+
r#"[ 1970, 0, 2, 0, 0, 0, 5, 1 ] | strftime("%F %T")"#,
62+
"1970-01-02 00:00:00"
63+
);
64+
yields!(
65+
strftime_mu,
66+
r#"86400.123456 | strftime("%F %T.%6f")"#,
67+
"1970-01-02 00:00:00.123456"
68+
);
69+
yields!(gmtime, r"86400 | gmtime", [1970, 0, 2, 0, 0, 0, 5, 1]);
70+
yields!(
71+
gmtime_mu,
72+
r"86400.123456 | gmtime",
73+
json!([1970, 0, 2, 0, 0, 0.123456, 5, 1])
74+
);
75+
yields!(
76+
gmtime_mktime_mu,
77+
r"86400.123456 | gmtime | mktime",
78+
86400.123456
79+
);
80+
yields!(
81+
strptime,
82+
r#""1970-01-02T00:00:00Z" | strptime("%Y-%m-%dT%H:%M:%SZ")"#,
83+
[1970, 0, 2, 0, 0, 0, 5, 1]
84+
);
85+
yields!(mktime, "[ 1970, 0, 2, 0, 0, 0, 5, 1 ] | mktime", 86400);
5486

5587
#[test]
5688
fn fromtodate() {

0 commit comments

Comments
 (0)