Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 155 additions & 3 deletions addin/lambda-boss.Tests/EditLambdaCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,25 +137,145 @@ public void BuildExpandedLet_ZeroParamsZeroArgs_ReturnsBareBody()
}

[Fact]
public void BuildExpandedLet_FewerArgsThanParams_LeavesTrailingUnbound()
public void BuildExpandedLet_FewerArgsThanParams_BindsTrailingToPlaceholder()
{
// Without an IF(ISOMITTED(p), …, p) pattern in the body, the unbound
// params can't have their defaults inferred — bind to "" so the LET
// is at least valid and the holes are obvious.
var sig = new LambdaSignature(["x", "y", "z"], "x + y + z");
var result = EditLambdaCommand.BuildExpandedLet(sig, ["A1"]);

Assert.Equal(Lines(
"=LET(",
" x, A1,",
" y, \"\",",
" z, \"\",",
" x + y + z",
")"), result);
}

[Fact]
public void BuildExpandedLet_ZeroArgsWithParams_OmitsLetWrapper()
public void BuildExpandedLet_ZeroArgsWithParams_BindsAllToPlaceholders()
{
var sig = new LambdaSignature(["x"], "x + 1");
var result = EditLambdaCommand.BuildExpandedLet(sig, []);

Assert.Equal("=x + 1", result);
Assert.Equal(Lines(
"=LET(",
" x, \"\",",
" x + 1",
")"), result);
}

[Fact]
public void BuildExpandedLet_OmittedOptionalWithIsomittedDefault_BindsDefaultAndStripsIf()
{
// Caller omits `y`. Body has the canonical optional pattern
// `IF(ISOMITTED(y), 1, y)`. The pattern's default (1) becomes the
// bound value and the IF wrapper is stripped to a bare `y` so the
// LET round-trips cleanly through LET to LAMBDA.
var sig = LambdaSignatureParser.Parse("=LAMBDA(x, [y], x + IF(ISOMITTED(y), 1, y))");
var result = EditLambdaCommand.BuildExpandedLet(sig, ["A1"]);

Assert.Equal(Lines(
"=LET(",
" x, A1,",
" y, 1,",
" x + y",
")"), result);
}

[Fact]
public void BuildExpandedLet_OmittedOptionalInsideInnerLet_FoldsAndExtracts()
{
// Body is a LET; the IF(ISOMITTED) pattern is inside one of its
// binding RHS values. The inner LET still folds into the outer LET
// and the default is hoisted to the unbound-param binding.
var sig = LambdaSignatureParser.Parse(
"=LAMBDA(text, [chunk_size], LET(_size, IF(ISOMITTED(chunk_size), 1, chunk_size), LEN(text) / _size))");
var result = EditLambdaCommand.BuildExpandedLet(sig, ["A1"]);

Assert.Equal(Lines(
"=LET(",
" text, A1,",
" chunk_size, 1,",
" _size, chunk_size,",
" LEN(text) / _size",
")"), result);
}

[Fact]
public void BuildExpandedLet_MultipleOmittedOptionals_ExtractsAllDefaults()
{
var sig = LambdaSignatureParser.Parse(
"=LAMBDA(text, [chunk_size], [horizontal], "
+ "LET(_size, IF(ISOMITTED(chunk_size), 1, chunk_size), "
+ "_horiz, IF(ISOMITTED(horizontal), FALSE, horizontal), "
+ "text & _size & _horiz))");
var result = EditLambdaCommand.BuildExpandedLet(sig, ["A1"]);

Assert.Equal(Lines(
"=LET(",
" text, A1,",
" chunk_size, 1,",
" horizontal, FALSE,",
" _size, chunk_size,",
" _horiz, horizontal,",
" text & _size & _horiz",
")"), result);
}

[Fact]
public void BuildExpandedLet_OmittedOptionalWithExpressionDefault_PreservesDefault()
{
// The default expression may itself be a function call with commas —
// the extractor must split the IF args at top-level commas.
var sig = LambdaSignatureParser.Parse(
"=LAMBDA([stop], LET(realStop, IF(ISOMITTED(stop), LAMBDA(a, i, FALSE), stop), realStop))");
var result = EditLambdaCommand.BuildExpandedLet(sig, []);

Assert.Equal(Lines(
"=LET(",
" stop, LAMBDA(a, i, FALSE),",
" realStop, stop,",
" realStop",
")"), result);
}

[Fact]
public void BuildExpandedLet_StandaloneIsomittedOnProvidedParam_LeftAlone()
{
// `ISOMITTED(text)` on its own (not wrapped as `IF(ISOMITTED(text), …, text)`)
// is not the canonical default pattern. When text is provided, the
// call evaluates to FALSE in LET context — that's the right semantics
// and we should not rewrite it.
var sig = LambdaSignatureParser.Parse(
"=LAMBDA(text, LET(Help?, ISOMITTED(text), IF(Help?, \"help\", text)))");
var result = EditLambdaCommand.BuildExpandedLet(sig, ["A1"]);

Assert.Equal(Lines(
"=LET(",
" text, A1,",
" Help?, ISOMITTED(text),",
" IF(Help?, \"help\", text)",
")"), result);
}

[Fact]
public void BuildExpandedLet_IsomittedInStringLiteral_NotRewritten()
{
// An IF(ISOMITTED(y), …, y) substring appearing inside a string
// literal in the body must not be detected as the optional pattern.
var sig = LambdaSignatureParser.Parse(
"=LAMBDA(x, [y], CONCAT(\"IF(ISOMITTED(y), 0, y)\", x))");
var result = EditLambdaCommand.BuildExpandedLet(sig, ["A1"]);

Assert.Equal(Lines(
"=LET(",
" x, A1,",
" y, \"\",",
" CONCAT(\"IF(ISOMITTED(y), 0, y)\", x)",
")"), result);
}

[Fact]
Expand Down Expand Up @@ -262,6 +382,38 @@ public void EndToEnd_SpecExample_ProducesExpectedLet()
")"), letFormula);
}

[Fact]
public void RoundTrip_OptionalLetToLambdaThenOmittedEditLambda_RecoversCleanLet()
{
// Spec-0002 path: author marks `y` as optional in LetToLambda. The
// generated LAMBDA wraps `y` with `IF(ISOMITTED(y), 10, y)` as an
// inner LET binding named after the param. When the caller invokes
// it with only `x` (omitting `y`), Edit Lambda should extract the
// default into the outer binding and drop the now-redundant
// `y, y` shadow.
var parsed = LetParser.Parse("=LET(x, 5, y, 10, x + y)");
var request = new LambdaGenerationRequest(
"MyCalc",
parsed,
[
new InputChoice("x", "x", true),
new InputChoice("y", "y", true, IsOptional: true),
]);
var refersTo = LetToLambdaBuilder.Build(request);

var sig = LambdaSignatureParser.Parse(refersTo);
var call = EditLambdaCommand.TryParseLambdaCall("=MyCalc(A1)");
Assert.NotNull(call);
var expanded = EditLambdaCommand.BuildExpandedLet(sig, call.Arguments);

Assert.Equal(Lines(
"=LET(",
" x, A1,",
" y, 10,",
" x + y",
")"), expanded);
}

[Fact]
public void RoundTrip_LetToLambdaThenEditLambda_FlattensToOriginalShape()
{
Expand Down
Loading
Loading