commit c100c7a77d1bb443010b63572a8b045dcf9c35e9
parent 1c652410814e5b69b9e09c12180e06343e2fee53
Author: Demonstrandum <samuel@knutsen.co>
Date: Mon, 9 Dec 2024 22:45:43 +0000
Added %timestamp macro with timezone capability.
Diffstat:
2 files changed, 87 insertions(+), 1 deletion(-)
diff --git a/crates/seam/src/parse/expander.rs b/crates/seam/src/parse/expander.rs
@@ -14,6 +14,7 @@ use std::{
},
};
+use chrono::TimeZone;
use colored::*;
use fixed;
use formatx;
@@ -889,7 +890,7 @@ impl<'a> Expander<'a> {
&offset.site,
)),
},
- None => 0,
+ None => chrono::Local::now().offset().local_minus_utc(),
};
let date_format = args.number.1.value;
let time = if let Some(time) = args.number.2 {
@@ -923,6 +924,67 @@ impl<'a> Expander<'a> {
Ok(Box::new([date_string_node]))
}
+ fn expand_timestamp_macro(&self, node: &ParseNode<'a>, params: Box<[ParseNode<'a>]>)
+ -> Result<ParseTree<'a>, ExpansionError<'a>> {
+ let params = self.expand_nodes(params)?;
+ let (_, args) = arguments! { [¶ms]
+ mandatory(1): literal,
+ mandatory(2): literal,
+ optional("timezone"): number,
+ }?;
+ let format = args.number.1.value;
+ let date_string = args.number.2.value;
+
+ let timezone_offset: i32 = match args.timezone {
+ Some(ref offset) => match offset.value.parse::<fixed::types::I32F32>() {
+ Ok(offset) => (offset * 60 * 60).round().to_num(),
+ Err(_) => return Err(ExpansionError::new(
+ "Invalid (decimal) timezone offset in hours.",
+ &offset.site,
+ )),
+ },
+ None => chrono::Local::now().offset().local_minus_utc(),
+ };
+
+ let Some(timezone) = chrono::FixedOffset::east_opt(timezone_offset) else {
+ return Err(ExpansionError(
+ format!("Failed to compute UTC+(east) offset of {} seconds", timezone_offset),
+ args.timezone.map_or(node.owned_site(), |node| node.site),
+ ));
+ };
+
+ let timestamp = if let Ok(datetime) = chrono::DateTime::parse_from_str(&date_string, &format) {
+ // Timezone information already given. Ignore any `:timezone` attribute.
+ datetime.timestamp()
+ } else {
+ // Parse NaiveDateTime instead and get timzone from attribute, or default to local time.
+ let datetime = match chrono::NaiveDateTime::parse_from_str(&date_string, &format) {
+ Ok(datetime) => datetime,
+ Err(err) => return Err(ExpansionError(
+ format!("Failed to parse date: {}", err),
+ node.owned_site(),
+ )),
+ };
+ let datetime = timezone.from_local_datetime(&datetime);
+ let datetime = match datetime.earliest() {
+ Some(local) => local,
+ None => return Err(ExpansionError::new(
+ "Timezone: local time falls in a gap in local time.",
+ node.site(),
+ ))
+ };
+ datetime.timestamp()
+ };
+
+ Ok(Box::new([
+ ParseNode::Number(Node {
+ value: format!("{}", timestamp),
+ site: node.owned_site(),
+ leading_whitespace: node.leading_whitespace().to_owned(),
+ })
+ ]))
+ }
+
/// `(%log ...)` logs to `STDERR` when called and leaves *no* node behind.
/// This means whitespace preceding `(%log ...)` will be removed!
fn expand_log_macro(&self, node: &ParseNode<'a>, params: ParseTree<'a>)
@@ -1658,6 +1720,7 @@ impl<'a> Expander<'a> {
"sort" => self.expand_sort_macro(node, params),
"reverse" => self.expand_reverse_macro(node, params),
"date" => self.expand_date_macro(node, params),
+ "timestamp" => self.expand_timestamp_macro(node, params),
"join" => self.expand_join_macro(node, params),
"concat" => self.expand_concat_macro(node, params),
"map" => self.expand_map_macro(node, params),
diff --git a/crates/seam/src/tests.rs b/crates/seam/src/tests.rs
@@ -80,4 +80,27 @@ mod tests {
"\"2001-09-08 23:31:39-02:15\""
);
}
+
+ #[test]
+ fn test_timestamp_macro() {
+ let timestamp = "419083754"; // 13 Apr 1983.
+ assert_output_eq(
+ "(%timestamp \"%Y %b %d %H:%M:%S%.3f %z\" \"1983 Apr 13 12:09:14.274 +0000\")",
+ timestamp
+ );
+ assert_output_eq(
+ "(%timestamp \"%Y %b %d %H:%M:%S%.3f\" \"1983 Apr 13 12:09:14.274\" :timezone 0)",
+ timestamp
+ );
+ // Same time *locally* but in a timezone of -02:00.
+ let timestamp_minus_2h = "419090954";
+ assert_output_eq(
+ "(%timestamp \"%Y %b %d %H:%M:%S%.3f %z\" \"1983 Apr 13 12:09:14.274 -0200\")",
+ timestamp_minus_2h
+ );
+ assert_output_eq(
+ "(%timestamp \"%Y %b %d %H:%M:%S%.3f\" \"1983 Apr 13 12:09:14.274\" :timezone -2)",
+ timestamp_minus_2h
+ );
+ }
}