mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Merge pull request #65 from MarekSpodBiedry/issue-63
XLSX: write _xlpm. prefix on LET/LAMBDA parameter names, strip it on read (#63)
This commit is contained in:
+76
-1
@@ -21,7 +21,56 @@ pub fn text_to_file(text: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
const PARAM_PREFIX: &str = "_xlpm.";
|
||||
|
||||
/// Adds Excel's `_xlpm.` prefix to LET/LAMBDA parameter names, at the declaration and at every use in scope.
|
||||
fn prefix_params(e: Expr, scope: &[String]) -> Expr {
|
||||
let sub = |x: Box<Expr>, sc: &[String]| Box::new(prefix_params(*x, sc));
|
||||
match e {
|
||||
Expr::Name(n) if scope.iter().any(|s| s.eq_ignore_ascii_case(&n)) => Expr::Name(format!("{PARAM_PREFIX}{n}")),
|
||||
Expr::Unary(op, x) => Expr::Unary(op, sub(x, scope)),
|
||||
Expr::Paren(x) => Expr::Paren(sub(x, scope)),
|
||||
Expr::Binary(op, a, b) => Expr::Binary(op, sub(a, scope), sub(b, scope)),
|
||||
Expr::Invoke(c, args) => Expr::Invoke(sub(c, scope), args.into_iter().map(|a| prefix_params(a, scope)).collect()),
|
||||
Expr::Call(name, args) if name == "LET" || name == "LAMBDA" => {
|
||||
let last = args.len().saturating_sub(1);
|
||||
let is_let = name == "LET";
|
||||
let mut sc = scope.to_vec();
|
||||
let mut pending: Option<String> = None;
|
||||
let mut out = Vec::with_capacity(args.len());
|
||||
for (i, a) in args.into_iter().enumerate() {
|
||||
let declares = i < last && (!is_let || i % 2 == 0);
|
||||
match a {
|
||||
Expr::Name(n) if declares => {
|
||||
out.push(Expr::Name(format!("{PARAM_PREFIX}{n}")));
|
||||
if is_let {
|
||||
pending = Some(n);
|
||||
} else {
|
||||
sc.push(n);
|
||||
}
|
||||
}
|
||||
other => {
|
||||
out.push(prefix_params(other, &sc));
|
||||
// A LET name is in scope only after its value expression.
|
||||
if let Some(n) = pending.take() {
|
||||
sc.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Call(name, out)
|
||||
}
|
||||
// A LET-bound LAMBDA called by name, e.g. `f(3)`.
|
||||
Expr::Call(name, args) if scope.iter().any(|s| s.eq_ignore_ascii_case(&name)) => {
|
||||
Expr::Call(format!("{PARAM_PREFIX}{name}"), args.into_iter().map(|a| prefix_params(a, scope)).collect())
|
||||
}
|
||||
Expr::Call(name, args) => Expr::Call(name, args.into_iter().map(|a| prefix_params(a, scope)).collect()),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn expr_to_file(e: Expr) -> String {
|
||||
let e = prefix_params(e, &[]);
|
||||
let e = e.map(&mut |x| match x {
|
||||
Expr::Call(name, args) => match crate::tables::file_function_name(&name) {
|
||||
Some(n) => Expr::Call(n, args),
|
||||
@@ -34,11 +83,23 @@ fn expr_to_file(e: Expr) -> String {
|
||||
gridcraft_formula::print(&e)
|
||||
}
|
||||
|
||||
/// `_xlpm.x` → `x` (any case, as the parser upper-cases function names).
|
||||
fn strip_param_prefix(n: String) -> String {
|
||||
match (n.get(..PARAM_PREFIX.len()), n.get(PARAM_PREFIX.len()..)) {
|
||||
(Some(p), Some(rest)) if p.eq_ignore_ascii_case(PARAM_PREFIX) => rest.to_string(),
|
||||
_ => n,
|
||||
}
|
||||
}
|
||||
|
||||
/// Formula text read from a file → model text (strips prefixes when it parses).
|
||||
pub fn from_file(text: &str) -> String {
|
||||
let body = text.strip_prefix('=').unwrap_or(text);
|
||||
match gridcraft_formula::parse(body) {
|
||||
Ok(e) => gridcraft_formula::print(&e),
|
||||
Ok(e) => gridcraft_formula::print(&e.map(&mut |x| match x {
|
||||
Expr::Name(n) => Expr::Name(strip_param_prefix(n)),
|
||||
Expr::Call(n, args) => Expr::Call(strip_param_prefix(n), args),
|
||||
other => other,
|
||||
})),
|
||||
Err(_) => body.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -61,4 +122,18 @@ mod tests {
|
||||
assert_eq!(from_file("_xlfn.XLOOKUP(1,A:A,B:B)"), "XLOOKUP(1,A:A,B:B)");
|
||||
assert_eq!(text_to_file("=IFS(A1>0,1)"), "_xlfn.IFS(A1>0,1)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn let_lambda_params() {
|
||||
assert_eq!(to_file(&Formula::new("LET(x,1,x+1)")), "_xlfn.LET(_xlpm.x,1,_xlpm.x+1)");
|
||||
assert_eq!(to_file(&Formula::new("LET(x,A1*2,y,x+1,x*y)")), "_xlfn.LET(_xlpm.x,A1*2,_xlpm.y,_xlpm.x+1,_xlpm.x*_xlpm.y)");
|
||||
assert_eq!(to_file(&Formula::new("LAMBDA(a,b,a^2+b)(3,4)")), "_xlfn.LAMBDA(_xlpm.a,_xlpm.b,_xlpm.a^2+_xlpm.b)(3,4)");
|
||||
// A name outside the scope stays a plain defined name.
|
||||
assert_eq!(to_file(&Formula::new("LET(x,1,x)+x")), "_xlfn.LET(_xlpm.x,1,_xlpm.x)+x");
|
||||
assert_eq!(from_file("_xlfn.LET(_xlpm.x,1,_xlpm.x+1)"), "LET(x,1,x+1)");
|
||||
assert_eq!(from_file("_xlfn.LAMBDA(_xlpm.a,_xlpm.a*2)(3)"), "LAMBDA(a,a*2)(3)");
|
||||
// A LET-bound LAMBDA called by name.
|
||||
assert_eq!(to_file(&Formula::new("LET(f,LAMBDA(x,x*2),f(3))")), "_xlfn.LET(_xlpm.f,_xlfn.LAMBDA(_xlpm.x,_xlpm.x*2),_xlpm.F(3))");
|
||||
assert_eq!(from_file("_xlfn.LET(_xlpm.f,_xlfn.LAMBDA(_xlpm.x,_xlpm.x*2),_xlpm.f(3))"), "LET(f,LAMBDA(x,x*2),F(3))");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user