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:
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! { [¶ms]
+ 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\""
+ );
+ }
}