seam

Symbolic-Expressions As Markup.
git clone git://git.knutsen.co/seam
Log | Files | Refs | README | LICENSE

commit a0660bbdd206cc0596ca38acd4149fc2658501c6
parent 628c1342b53897f015b3a4b5bef2e429e8868a2e
Author: Demonstrandum <samuel@knutsen.co>
Date:   Mon,  9 Dec 2024 21:18:53 +0000

Extended %date macro to handle epoch timestamps.

Diffstat:
MCargo.lock | 2+-
MREADME.md | 1+
Mcrates/seam/Cargo.toml | 2+-
Mcrates/seam/src/parse/expander.rs | 34++++++++++++++++++++--------------
Mcrates/seam/src/parse/parser.rs | 6+++---
Mcrates/seam/src/tests.rs | 42+++++++++++++++++++++++++++++++-----------
6 files changed, 57 insertions(+), 30 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -297,7 +297,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "seam" -version = "0.4.1" +version = "0.4.2" dependencies = [ "chrono", "colored", diff --git a/README.md b/README.md @@ -93,6 +93,7 @@ seam --sexp <<< '(hello (%define subject world) %subject)' ## Checklist - [ ] User `(%error msg)` macro for aborting compilation. - [x] List reverse macro `(%reverse (...))`. + - [ ] Literal/atomic conversion macros: `(%symbol lit)`, `(%number lit)`, `(%string lit)`, `(%raw lit)`. - [x] Sorting macro `(%sort (...))` which sorts alphanumerically on literals. Allow providing a `:key` to sort "by field": e.g. sort by title name `(%sort :key (%lambda ((:title _ &&_)) %title) %posts)` - [ ] Extend the strftime-style `(%date)` to be able to read UNIX numeric timestamps and display relative to timezones. diff --git a/crates/seam/Cargo.toml b/crates/seam/Cargo.toml @@ -5,7 +5,7 @@ keywords = ["markup", "lisp", "macro", "symbolic-expression", "sexp"] license-file = "../../LICENSE" readme = "../../README.md" homepage = "https://git.knutsen.co/seam" -version = "0.4.1" +version = "0.4.2" authors = ["Demonstrandum <samuel@knutsen.co>"] edition = "2021" diff --git a/crates/seam/src/parse/expander.rs b/crates/seam/src/parse/expander.rs @@ -875,20 +875,26 @@ impl<'a> Expander<'a> { fn expand_date_macro(&self, node: &ParseNode<'a>, params: Box<[ParseNode<'a>]>) -> Result<ParseTree<'a>, ExpansionError<'a>> { let params = self.expand_nodes(params)?; - let [date_format] = &*params else { - return Err(ExpansionError::new( - "`%date' macro only expects one formatting argument.", - node.site())) - }; - - let Some(Node { value: date_format, .. }) = date_format.atomic() else { - return Err(ExpansionError::new( - "`%date' macro needs string (or atomic) \ - formatting argument.", node.site())) + let (_, args) = arguments! { [&params] + mandatory(1): literal, + optional(2): number, + }?; + let date_format = args.number.1.value; + let time = if let Some(time) = args.number.2 { + let Ok(secs) = time.value.parse::<i64>() else { + return Err(ExpansionError::new( + "Timestamp not a valid UNIX epoch signed integer.", + &time.site, + )); + }; + match chrono::DateTime::from_timestamp(secs, 0) { + Some(time) => time, + None => return Err(ExpansionError::new("Invalid timestamp.", &time.site)), + } + } else { + chrono::Utc::now() }; - - let now = chrono::Local::now(); - let formatted = now.format(&date_format).to_string(); + let formatted = time.format(&date_format).to_string(); let date_string_node = ParseNode::String(Node { value: formatted, site: node.site().clone(), @@ -898,7 +904,7 @@ impl<'a> Expander<'a> { } /// `(%log ...)` logs to `STDERR` when called and leaves *no* node behind. - /// This means whitespace preceeding `(%log ...)` will be removed! + /// This means whitespace preceding `(%log ...)` will be removed! fn expand_log_macro(&self, node: &ParseNode<'a>, params: ParseTree<'a>) -> Result<ParseTree<'a>, ExpansionError<'a>> { let mut words = Vec::with_capacity(params.len()); diff --git a/crates/seam/src/parse/parser.rs b/crates/seam/src/parse/parser.rs @@ -236,9 +236,9 @@ impl<'a> ParseNode<'a> { Self::Symbol(ref node) | Self::Number(ref node) | Self::String(ref node) - | Self::Raw(ref node) => &node.leading_whitespace, - Self::List { ref leading_whitespace, .. } => leading_whitespace, - Self::Attribute { ref leading_whitespace, .. } => leading_whitespace, + | Self::Raw(ref node) => node.leading_whitespace.as_str(), + Self::List { leading_whitespace, .. } => leading_whitespace.as_str(), + Self::Attribute { leading_whitespace, .. } => leading_whitespace.as_str(), } } diff --git a/crates/seam/src/tests.rs b/crates/seam/src/tests.rs @@ -1,19 +1,24 @@ use super::*; -fn expand<'a>(source: String) -> Result<String, Box<dyn 'a + std::error::Error>> { +fn expand(source: String) -> Result<String, String> { let expander = tree_builder(Some("<test>"), source); - let expander = Box::leak(Box::new(expander)); - let tree = expander.expand()?; - let mut ret = String::new(); + let tree = match expander.expand() { + Ok(tree) => tree, + Err(err) => return Err(format!("{}", err)), + }; + let mut out = String::new(); for node in tree { - ret += &format!("{}", node); + out += &format!("{}", node); } - Ok(ret) + Ok(out) } -fn test_output_eq(input: &str, expect: &str) { +fn assert_output_eq(input: &str, expect: &str) { let source = String::from(input); - let output = expand(source).unwrap(); + let output = match expand(source) { + Ok(o) => o, + Err(o) => o, + }; assert_eq!(output, expect); } @@ -22,7 +27,8 @@ mod tests { #[test] fn test_sort_macro_alphanumeric() { - test_output_eq( + // sort all signed, unsigened, floats and text. + assert_output_eq( "(%sort (13 d 8 e 14 f 9 10 g 11 12 a b c 0 0.1 1.5 0.2 1 2 -1 -10 -2 -1.4 -0.3) :order ascending)", "(-10 -2 -1.4 -1 -0.3 0 0.1 0.2 1 1.5 2 8 9 10 11 12 13 14 a b c d e f g)" ); @@ -31,7 +37,7 @@ mod tests { #[test] fn test_sort_macro_key() { // sort by the second element in 3-tuples. - test_output_eq( + assert_output_eq( "(%sort :key (%lambda ((x y z)) %y) ((x 3 b) (z 1 a) (y 2 c)))", "((z 1 a) (y 2 c) (x 3 b))" ); @@ -39,9 +45,23 @@ mod tests { #[test] fn test_reverse_macro() { - test_output_eq( + // reverse a list while preserving whitespace-ordering. + assert_output_eq( "(%reverse (a 3 2 1 2 s))", "(s 2 1 2 3 a)" ); } + + #[test] + fn test_date_timestamps() { + // check %date works with i64 unix timestamps. + assert_output_eq( + "(%date \"%Y-%m-%d\" 0)", + "\"1970-01-01\"" + ); + assert_output_eq( + "(%date \"%Y-%m-%d %H:%M:%S\" 1733776966)", + "\"2024-12-09 20:42:46\"" + ); + } }