diff --git a/addin/lambda-boss.Tests/LetToLambdaBuilderTests.cs b/addin/lambda-boss.Tests/LetToLambdaBuilderTests.cs
index 07410b4..1caaf8e 100644
--- a/addin/lambda-boss.Tests/LetToLambdaBuilderTests.cs
+++ b/addin/lambda-boss.Tests/LetToLambdaBuilderTests.cs
@@ -22,6 +22,16 @@ private static string BuildWithOptional(string formula, string lambdaName,
return LetToLambdaBuilder.Build(new LambdaGenerationRequest(lambdaName, parsed, inputs));
}
+ private static string BuildWithDefault(string formula, string lambdaName,
+ params (string name, string paramName, bool keep, bool isOptional, string? defaultExpr)[] choices)
+ {
+ var parsed = LetParser.Parse(formula);
+ var inputs = choices
+ .Select(c => new InputChoice(c.name, c.paramName, c.keep, c.isOptional, c.defaultExpr))
+ .ToList();
+ return LetToLambdaBuilder.Build(new LambdaGenerationRequest(lambdaName, parsed, inputs));
+ }
+
private static string Lines(params string[] lines) => string.Join("\n", lines);
[Fact]
@@ -369,6 +379,78 @@ public void OptionalWithReorderAndRename_WorksTogether()
")"), result);
}
+ [Fact]
+ public void OptionalWithCustomDefault_UsesTypedDefault()
+ {
+ // The original RHS is A1, but the user overrides the default to 0.
+ var result = BuildWithDefault("=LET(x, 10, y, A1, x + y)", "Adder",
+ ("x", "x", true, false, null), ("y", "offset", true, true, "0"));
+
+ Assert.Equal(Lines(
+ "=LAMBDA(",
+ " x,",
+ " [offset],",
+ " LET(",
+ " offset, IF(ISOMITTED(offset), 0, offset),",
+ " x + offset",
+ " )",
+ ")"), result);
+ }
+
+ [Fact]
+ public void OptionalWithCustomDefault_CellRefAbsolutized()
+ {
+ // A cell ref typed as a custom default is absolutized like any other.
+ var result = BuildWithDefault("=LET(x, 10, y, A1, x + y)", "Adder",
+ ("x", "x", true, false, null), ("y", "offset", true, true, "B2"));
+
+ Assert.Equal(Lines(
+ "=LAMBDA(",
+ " x,",
+ " [offset],",
+ " LET(",
+ " offset, IF(ISOMITTED(offset), $B$2, offset),",
+ " x + offset",
+ " )",
+ ")"), result);
+ }
+
+ [Fact]
+ public void OptionalWithBlankCustomDefault_FallsBackToOriginalRhs()
+ {
+ // A null/blank custom default preserves the pre-edit behaviour.
+ var result = BuildWithDefault("=LET(x, 10, y, A1, x + y)", "Adder",
+ ("x", "x", true, false, null), ("y", "offset", true, true, " "));
+
+ Assert.Equal(Lines(
+ "=LAMBDA(",
+ " x,",
+ " [offset],",
+ " LET(",
+ " offset, IF(ISOMITTED(offset), $A$1, offset),",
+ " x + offset",
+ " )",
+ ")"), result);
+ }
+
+ [Fact]
+ public void OptionalWithCustomDefault_AppliesRenames()
+ {
+ // The typed default references binding x, which is renamed to base.
+ var result = BuildWithDefault("=LET(x, 5, y, 0, x + y)", "Calc",
+ ("x", "base", true, false, null), ("y", "offset", true, true, "x + 1"));
+
+ Assert.Equal(Lines(
+ "=LAMBDA(",
+ " base,",
+ " [offset],",
+ " LET(",
+ " offset, IF(ISOMITTED(offset), base + 1, offset),",
+ " base + offset",
+ " )",
+ ")"), result);
+ }
+
[Fact]
public void OptionalOnUncheckedRow_Throws()
{
diff --git a/addin/lambda-boss/LetToLambdaBuilder.cs b/addin/lambda-boss/LetToLambdaBuilder.cs
index 9f02dcc..5bd8abe 100644
--- a/addin/lambda-boss/LetToLambdaBuilder.cs
+++ b/addin/lambda-boss/LetToLambdaBuilder.cs
@@ -6,11 +6,18 @@ namespace LambdaBoss;
///
/// User's decision for a single LET binding whose RHS is a value.
///
+///
+/// Custom default for an optional param, as typed by the user in the
+/// dialog. When null or blank the builder falls back to the binding's
+/// original RHS, preserving the pre-edit behaviour. Only consulted when
+/// is true.
+///
public record InputChoice(
string OriginalBindingName,
string ParamName,
bool Keep,
- bool IsOptional = false);
+ bool IsOptional = false,
+ string? DefaultExpression = null);
public record LambdaGenerationRequest(
string LambdaName,
@@ -99,13 +106,10 @@ public static string Build(LambdaGenerationRequest request)
.ToList();
// Optional bindings wrap each optional kept param with an
- // IF(ISOMITTED(...)) defaulting to the original RHS (with renames).
- // They appear before internal bindings so internal bindings can
- // reference the defaulted value.
- // Optional bindings wrap each optional kept param with an
- // IF(ISOMITTED(...)) defaulting to the original RHS (with renames).
- // They appear before internal bindings so internal bindings can
- // reference the defaulted value. Cell references in the default are
+ // IF(ISOMITTED(...)) defaulting to the user's custom default (or the
+ // original RHS when none was supplied), with renames applied. They
+ // appear before internal bindings so internal bindings can reference
+ // the defaulted value. Cell references in the default are
// forced absolute: when Excel stores a LAMBDA as a workbook Name,
// relative refs shift by the offset between the active cell at
// registration time and the calling cell, which in practice baked
@@ -113,10 +117,18 @@ public static string Build(LambdaGenerationRequest request)
// regardless of where the LAMBDA is invoked.
var optionalBindings = kept
.Where(k => k.Choice.IsOptional)
- .Select(k => new LetBinding(
- k.Choice.ParamName,
- $"IF(ISOMITTED({k.Choice.ParamName}), {AbsolutizeCellRefs(ApplyRenames(k.Binding.RhsText, renames))}, {k.Choice.ParamName})",
- IsCalculation: false))
+ .Select(k =>
+ {
+ // A blank custom default falls back to the original RHS so the
+ // builder behaves exactly as before when no edit was made.
+ var defaultExpr = string.IsNullOrWhiteSpace(k.Choice.DefaultExpression)
+ ? k.Binding.RhsText
+ : k.Choice.DefaultExpression!;
+ return new LetBinding(
+ k.Choice.ParamName,
+ $"IF(ISOMITTED({k.Choice.ParamName}), {AbsolutizeCellRefs(ApplyRenames(defaultExpr, renames))}, {k.Choice.ParamName})",
+ IsCalculation: false);
+ })
.ToList();
var body = ApplyRenames(parsed.Body, renames);
diff --git a/addin/lambda-boss/UI/LetToLambdaWindow.xaml b/addin/lambda-boss/UI/LetToLambdaWindow.xaml
index dc28aa7..e4a3d7b 100644
--- a/addin/lambda-boss/UI/LetToLambdaWindow.xaml
+++ b/addin/lambda-boss/UI/LetToLambdaWindow.xaml
@@ -103,9 +103,10 @@
Foreground="#cccccc"
VerticalAlignment="Center"
Margin="0,0,8,0"
- ToolTip="Caller may omit this argument; defaults to the original RHS expression" />
+ ToolTip="Caller may omit this argument; edit the default it falls back to" />
+
- /// Display form of used by the row template.
- /// When the row is marked optional, the RHS becomes the default
- /// expression in the generated LAMBDA, so we prefix "default:" to
- /// make that role explicit.
+ /// The default expression for an optional param, editable in the row
+ /// template when is set. Seeded with the
+ /// original RHS so the dialog opens showing the existing behaviour.
///
- public string RhsPreviewDisplay => IsOptional ? $"default: {RhsPreview}" : RhsPreview;
+ public string DefaultExpression
+ {
+ get => _defaultExpression;
+ set
+ {
+ _defaultExpression = value;
+ OnChanged();
+ }
+ }
+
+ ///
+ /// The read-only RHS preview is shown only while the row is not
+ /// optional; when optional, the editable default box takes its place.
+ ///
+ public bool ShowRhsPreview => !IsOptional;
///
/// Zero-based position in the original LET source order. Used to keep
@@ -54,7 +68,7 @@ public bool Keep
{
_isOptional = false;
OnChanged(nameof(IsOptional));
- OnChanged(nameof(RhsPreviewDisplay));
+ OnChanged(nameof(ShowRhsPreview));
}
OnChanged();
}
@@ -68,7 +82,7 @@ public bool IsOptional
if (_isOptional == value) return;
_isOptional = value;
OnChanged();
- OnChanged(nameof(RhsPreviewDisplay));
+ OnChanged(nameof(ShowRhsPreview));
}
}
@@ -114,6 +128,7 @@ public LetToLambdaWindow(ParsedLet parsed, Func resolveExisting
BindingName = b.Name,
ParamName = b.Name,
RhsPreview = b.RhsText,
+ DefaultExpression = b.RhsText,
Keep = true,
SourceIndex = i
}));
@@ -314,6 +329,15 @@ private void UpdateSaveEnabled()
return;
}
+ var blankDefault = keptRows
+ .FirstOrDefault(r => r.IsOptional && string.IsNullOrWhiteSpace(r.DefaultExpression));
+ if (blankDefault != null)
+ {
+ StatusText.Text = $"Optional input '{blankDefault.ParamName}' needs a default value.";
+ SaveButton.IsEnabled = false;
+ return;
+ }
+
var retainedBindingNames = _parsed.Bindings
.Where(b => b.IsCalculation || _rows.Any(r => r.BindingName == b.Name && !r.Keep))
.Select(b => b.Name)
@@ -355,7 +379,12 @@ private void HideNameError()
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
var inputs = _rows
- .Select(r => new InputChoice(r.BindingName, r.ParamName.Trim(), r.Keep, r.IsOptional))
+ .Select(r => new InputChoice(
+ r.BindingName,
+ r.ParamName.Trim(),
+ r.Keep,
+ r.IsOptional,
+ r.IsOptional ? r.DefaultExpression.Trim() : null))
.ToList();
Result = new LambdaGenerationRequest(LambdaNameBox.Text.Trim(), _parsed, inputs);