Implement subst and where

Implement the `subst` and `where` commands.

The `where` command can also be used as an operator `|` in
expressions, for example `'A+B|A=X-3'` will evaluate as `'X-3+B'`.

The HP syntax with parentheses for multiple substitutions, for example
`'A+B|(A=3;B=2)'`, is not implemented in this commit. It can be
replaced with sequences, like `'A+B|A=3|B=2'`.

Fixes: #1187

Signed-off-by: Christophe de Dinechin <christophe@dinechin.org>
This commit is contained in:
Christophe de Dinechin 2024-09-15 23:07:19 +02:00
parent 9812e0532f
commit 3c79ef7f9c
6 changed files with 208 additions and 12 deletions

View file

@ -2424,6 +2424,7 @@ COMMAND_BODY(Apply)
}
// ============================================================================
//
// User-accessible match commands
@ -2965,3 +2966,152 @@ FUNCTION_BODY(Simplify)
{
return do_rewrite(x, &expression::simplify);
}
static expression_p substitute(expression_r pattern,
symbol_r name,
expression_r to)
// ----------------------------------------------------------------------------
// Substitute a single name with the corresponding expression
// ----------------------------------------------------------------------------
{
scribble scr;
size_t replsz = 0;
object_g replobj = to->objects(&replsz);
for (object_g obj : *pattern)
{
symbol_p oname = obj->as<symbol>();
if (oname && name->is_same_as(oname))
{
byte *objcopy = rt.allocate(replsz);
if (!objcopy)
return nullptr;
memmove(objcopy, +replobj, replsz);
}
else
{
size_t sz = obj->size();
byte *objcopy = rt.allocate(sz);
if (!objcopy)
return nullptr;
memmove(objcopy, +obj, sz);
}
}
expression_g result = expression_p(list::make(object::ID_expression,
scr.scratch(), scr.growth()));
return result;
}
static expression_p substitute(expression_r pattern,
expression_r repl)
// ----------------------------------------------------------------------------
// Run a rewrite up or down
// ----------------------------------------------------------------------------
{
expression_g from = repl->left_of_equation();
expression_g to = repl->right_of_equation();
if (!from || !to)
return nullptr;
symbol_g name = from->as_quoted<symbol>();
if (+from == +to || !name)
{
rt.value_error();
return nullptr;
}
return substitute(pattern, name, to);
}
NFUNCTION_BODY(Subst)
// ----------------------------------------------------------------------------
// Perform a substitution without evaluating the resulting expression
// ----------------------------------------------------------------------------
{
if (expression_g pat = args[1]->as<expression>())
if (expression_g repl = args[0]->as<expression>())
return substitute(pat, repl);
if (args[1]->is_real() || args[1]->is_complex())
return args[1];
rt.type_error();
return nullptr;
}
COMMAND_BODY(Where)
// ----------------------------------------------------------------------------
// Perform a substitution and evaluate the resulting expression
// ----------------------------------------------------------------------------
{
if (object_p patobj = rt.stack(1))
{
id patty = patobj->type();
if (patty == ID_expression)
{
expression_g pat = expression_p(patobj);
if (object_p replobj = rt.stack(0))
{
id rty = replobj->type();
if (rty == ID_expression)
{
expression_g repl = expression_p(replobj);
if (algebraic_g res = substitute(pat, repl))
if (rt.drop() && rt.top(+res))
return OK;
}
else if (rty == ID_list || rty == ID_array)
{
symbol_g name;
expression_g repl;
for (object_g item : *list_p(replobj))
{
if (!name)
{
name = item->as_quoted<symbol>();
if (!name)
{
if (expression_g p = item->as<expression>())
{
pat = substitute(pat, p);
if (!pat)
return ERROR;
}
else
{
rt.value_error();
return ERROR;
}
}
}
else
{
repl = item->as<expression>();
if (!repl)
{
rt.value_error();
return ERROR;
}
pat = substitute(pat, name, repl);
if (!pat)
return ERROR;
name = nullptr;
}
}
if (rt.drop() && rt.top(+pat))
return OK;
}
}
}
else if (is_real(patty) || is_complex(patty))
{
rt.drop();
return OK;
}
}
if (!rt.error())
rt.type_error();
return ERROR;
}

View file

@ -620,5 +620,7 @@ FUNCTION(ReorderTerms);
FUNCTION(Simplify);
COMMAND_DECLARE(Apply, 2);
COMMAND_DECLARE_SPECIAL(Where, arithmetic, 2, PREC_DECL(WHERE); );
NFUNCTION(Subst, 2, static bool can_be_symbolic(uint) { return true; } );
#endif // EXPRESSION_H

View file

@ -395,6 +395,9 @@ CMD(Max)
CMD(NSub)
CMD(EndSub)
OP(Where, "|")
NAMED(Subst, "Substitute")
CMD(re)
CMD(im)
CMD(arg)

View file

@ -434,18 +434,19 @@ MENU(NumbersMenu,
"", ID_Product,
"QuoRem", ID_Div2,
"Factors", ID_Unimplemented,
"Ran#", ID_RandomNumber,
"Random", ID_Random,
"→Num", ID_ToDecimal,
"→Q", ID_ToFraction,
"→Qπ", ID_Unimplemented,
"R#Seed", ID_RandomSeed,
RandomGeneratorBits::label, ID_RandomGeneratorBits,
RandomGeneratorOrder::label, ID_RandomGeneratorOrder,
"IsPrime", ID_Unimplemented,
"NextPr", ID_Unimplemented,
"PrevPr", ID_Unimplemented,
"R#Seed", ID_RandomSeed,
RandomGeneratorBits::label, ID_RandomGeneratorBits,
RandomGeneratorOrder::label, ID_RandomGeneratorOrder
);
"PrevPr", ID_Unimplemented);
MENU(AnglesMenu,
@ -783,8 +784,6 @@ MENU(SymbolicMenu,
ID_Expand,
ID_Simplify,
"→Poly", ID_ToPolynomial,
"→Num", ID_ToDecimal,
"→Q", ID_ToFraction,
"Algbra", ID_AlgebraMenu,
"Arith", ID_ArithmeticMenu,
@ -808,8 +807,8 @@ MENU(AlgebraMenu,
"↑Match", ID_MatchUp,
"Isolate", ID_Unimplemented,
"Apply", ID_Apply,
"→Num", ID_ToDecimal,
"→Q", ID_ToFraction,
"Subst", ID_Subst,
"|", ID_Where,
"", ID_Unimplemented,
"", ID_Integrate,

View file

@ -36,7 +36,8 @@ enum precedence
{
NONE = 0, // No precedence
LOWEST = 1, // Lowest precedence (when parsing parentheses)
COMPLEX = 3, // Complex numbers
WHERE = 3, // | (where) operator
COMPLEX = 5, // Complex numbers
LOGICAL = 10, // and, or, xor
RELATIONAL = 12, // <, >, =, etc

View file

@ -6423,6 +6423,46 @@ void tests::expand_collect_simplify()
step("Apply function call for algebraic function with incorrect type")
.test(CLEAR, "2 'F' APPLY", ENTER)
.error("Bad argument type");
step("Substitution with simple polynomial")
.test(CLEAR, "'X^2+3*X+7' 'X=Z+1' SUBST", ENTER)
.expect("'(Z+1)↑2+3·(Z+1)+7'")
.test("'Z=sin(A+B)' SUBST", ENTER)
.expect("'(sin(A+B)+1)↑2+3·(sin(A+B)+1)+7'");
step("Substitution with numerical value")
.test(CLEAR, "42 'X=Z+1' SUBST", ENTER)
.expect("42");
step("Type error on value to substitute")
.test(CLEAR, "\"ABC\" 'X=Z+1' SUBST", ENTER)
.error("Bad argument type");
step("Bad argument value for substitution")
.test(CLEAR, "'X^2+3*X+7' 'Z-1=Z+1' SUBST", ENTER)
.error("Bad argument value");
step("WHERE command with simple polynomial")
.test(CLEAR, "'X^2+3*X+7' 'X=Z+1' WHERE", ENTER)
.expect("'(Z+1)↑2+3·(Z+1)+7'")
.test("{ 'Z=sin(A+B)' 'A=42' } WHERE", ENTER)
.expect("'(sin(42+B)+1)↑2+3·(sin(42+B)+1)+7'");
step("Substitution with numerical value")
.test(CLEAR, "42 'X=Z+1' WHERE", ENTER)
.expect("42");
step("Type error on value to substitute in WHERE")
.test(CLEAR, "\"ABC\" 'X=Z+1' WHERE", ENTER)
.error("Bad argument type");
step("Bad argument value for substitution in WHERE")
.test(CLEAR, "'X^2+3*X+7' 'Z-1=Z+1' WHERE", ENTER)
.error("Bad argument value");
step("| operator")
.test(CLEAR, "'X^2+3*X+7|X=Z+1'", ENTER)
.expect("'X↑2+3·X+7|X=Z+1'")
.test(RUNSTOP)
.expect("'(Z+1)²+3·(Z+1)+7'");
step("Chained | operator")
.test("'X^2+3*X+7|X=Z+1|Z=sin(A+B)|A=42'", ENTER)
.expect("'X↑2+3·X+7|X=Z+1|Z=sin(A+B)|A=42'")
.test(RUNSTOP)
.expect("'(sin(42+B)+1)²+3·(sin(42+B)+1)+7'");
}
@ -10275,6 +10315,7 @@ tests &tests::itest(cstring txt)
case '@': k = KEY2; alpha = true; xshift = true; break;
case '$': k = KEY3; alpha = true; xshift = true; break;
case '#': k = KEY4; alpha = true; xshift = true; break;
case '|': k = KEY6; alpha = true; xshift = true; break;
case '\\': k = ADD; alpha = true; xshift = true; break;
case '\n': k = BSP; alpha = true; xshift = true; break;
case L'«': k = RUNSTOP; alpha = false; shift = true; del = true; break;