feat: add if() conditional operator and update docs (#92)

Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Reviewed-on: #92
This commit is contained in:
Glen Whitney 2023-05-02 02:23:57 +00:00
parent 7b794f90b9
commit 5176005bb3
4 changed files with 357 additions and 82 deletions

1
NEWS
View File

@ -19,6 +19,7 @@ o Comparison operators return bool rather than int (Note this can be a breaking
in a numerical computation.)
o Foreground and background color style attributes for cells.
o Variable row height for cells.
o Addition of an if() conditional operator.
o Addition of a find() macro to search for a cell satisfying a condition.
o Addition of an is(VALUE, TYPE, TYPE,...) predicate
o Addition of functions for unit displacements in the six cardinal directions

View File

@ -3916,11 +3916,17 @@ underline
\end_layout
\begin_layout Subsubsection
\begin_inset CommandInset label
LatexCommand label
name "subsec:Shadowed"
\end_inset
Shadowed
\end_layout
\begin_layout Standard
This attribute is a s simple true-false flag set with shadowed(), defaulting
This attribute is a simple true-false flag set with shadowed(), defaulting
to false.
When true, it means that the left neighbour cell will additionally use
the display room of this cell (and all following shadowed cells to the
@ -5791,7 +5797,7 @@ or
\series default
\emph default
y evaluates to the logical disjunction of boolean values
y evaluates to the logical disjunction of Boolean values
\emph on
x
\emph default
@ -5820,6 +5826,25 @@ x
y
\emph default
otherwise.
Note the rules for these Boolean operators means they can also sometimes
be used for conditional-like behavior: Suppose you want to use the value
of the cell labeled
\family sans
OPTION
\family default
if it is non-empty and non-zero, and the value of the cell labeled
\family sans
DEFAULT
\family default
otherwise.
Since the empty and zero values are considered false, you can achieve this
with:
\family sans
\begin_inset Newline newline
\end_inset
@(OPTION) or @(DEFAULT)
\end_layout
\begin_layout Description
@ -6814,13 +6839,14 @@ $
\series bold
X
\series default
(SRC,REF,1,1,1)
(SRC,REF,fix,fix,fix)
\family default
is identical to
\family sans
@(SRC)
\family default
, but you should certainly prefer the latter for clarity of expression.
, but you should certainly prefer the latter for clarity of expression when
that's what you mean.
\begin_inset Newline newline
\end_inset
@ -8251,6 +8277,102 @@ hexact used as a keyword to the string() function; listed here to record
that this identifier may not be used as a cell label.
\end_layout
\begin_layout Description
if
\series medium
(
\emph on
condition
\emph default
[,
\emph on
\begin_inset space ~
\end_inset
then_expr
\emph default
[
\emph on
,
\begin_inset space ~
\end_inset
else_expr
\emph default
]]) Typical conditional expression.
First the
\series default
\emph on
condition
\emph default
is evaluated.
If its value is falsy, then the value of the
\emph on
else_expr
\emph default
is returned if it is present, or else the empty value is returned.
(If you just want to return the value of the
\emph on
condition
\emph default
when it corresponds to boolean false, use
\begin_inset Quotes eld
\end_inset
\emph on
condition
\emph default
and
\emph on
then_expr
\emph default
\begin_inset Quotes erd
\end_inset
instead.) If the value of
\emph on
condition
\emph default
is truthy, then the value of the
\emph on
then_expr
\emph default
is returned if it is present, otherwise the value of the
\emph on
condition
\emph default
is returned.
Note that if() is short-circuiting in the sense that the
\emph on
condition
\emph default
is always evaluated but at most one of
\emph on
then_expr
\emph default
and
\emph on
else_expr
\emph default
is (note, if evaluating
\emph on
condition
\emph default
results in an error, then this expression produces an error without evaluating
either
\emph on
then_expr
\emph default
or
\emph on
else_expr
\emph default
).
\end_layout
\begin_layout Description
\series medium
@ -8262,7 +8384,7 @@ int
\series default
int
\series medium
[([int|float|string|empty
[([int|boolean|float|string|empty
\emph on
\begin_inset space ~
@ -8284,14 +8406,14 @@ converts to an
\emph on
\emph default
integer the given integer, float, string, or empty value
integer the given integer, boolean, float, string, or empty value
\emph on
x.
\emph default
(The latter converts to 0.) The optional second argument must be the name
of one of the functions that produces a floating point integral value from
a float, i.e.,
(The latter converts to 0, and Boolean true converts to 1 and false to 0.)
The optional second argument must be the name of one of the functions that
produces a floating point integral value from a float, i.e.,
\family sans
\series bold
ceil
@ -10208,13 +10330,26 @@ If your machine uses binary floating point arithmetic, and chances are that
\end_layout
\begin_layout Quote
\family sans
0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1
\end_layout
\begin_layout Standard
You expect to see 1.0 as result, and indeed that is what you get.
Now you compare this result to the constant 1.0, but surprisingly for many
users, the result is 0.
You expect to see
\family sans
1.0
\family default
as result, and indeed that is what you get.
Now you compare this result to the constant
\family sans
1.0
\family default
, but surprisingly for many users, the result is
\family sans
false
\family default
.
Appearantly, 1.0 is unequal 1.0 for
\noun on
teapot
@ -10275,90 +10410,179 @@ teapot
has no way to hide cells, but you have three dimensions.
Just use one or more layers for such cells and give each cell a label in
order to reference and find it easily.
\end_layout
\begin_layout Standard
If you're really wedded to cells that calculate but don't appear on the
display, you can use such hacks as
\begin_inset Quotes eld
\end_inset
shadowing
\begin_inset Quotes erd
\end_inset
a cell with computations in it (see subsection
\begin_inset CommandInset ref
LatexCommand ref
reference "subsec:Shadowed"
plural "false"
caps "false"
noprefix "false"
\end_inset
) or making its foreground and background the same color, etc.
\end_layout
\begin_layout Subsection
Why is there no conditional evaluation?
Can you share a trick for multi-way dispatch?
\end_layout
\begin_layout Standard
There is no special operator or function for conditional evaluation.
I could add one easily, but then next someone would ask for loops and someone
else for user-defined functions, variables and so on.
If you need a programming language, you know where to find it.
\end_layout
\begin_layout Standard
But don't worry.
The answer is, that conditional evaluation comes for free with
By now,
\noun on
teapot
\noun default
's orthogonal cell addressing.
As an example, depending on the cell labelled
\family typewriter
X
has a conventional
\family sans
if()
\family default
being negative or not, you want the result to be the string
\family typewriter
"BAD
operator for convenience when you want to choose between two alternatives
based on a single Boolean condition.
However, a methodology used to provide conditional behavior before
\family sans
if()
\family default
or
was implemented is still worth knowing about, because it can actually provide
more flexibility and power, particularly in providing for multi-way dispatch.
\end_layout
\begin_layout Standard
For example, suppose that you want to produce one of the strings
\begin_inset Quotes eld
\end_inset
\family typewriter
"GOOD"
NONE
\family default
\begin_inset Quotes erd
\end_inset
,
\begin_inset Quotes eld
\end_inset
\family typewriter
ONE
\family default
\begin_inset Quotes erd
\end_inset
, or
\begin_inset Quotes eld
\end_inset
\family typewriter
MANY
\family default
\begin_inset Quotes erd
\end_inset
depending on whether the cell labeled
\family typewriter
INPUT
\family default
has a value that is zero, one, or greater than one, respectively.
Here's one convenient way to arrange that.
In some scratch area, create a cell labeled
\family typewriter
OUTPUT
\family default
that contains the string
\family typewriter
\begin_inset Quotes erd
\end_inset
NONE
\begin_inset Quotes erd
\end_inset
\family default
with the two successive cells to the right containing
\family typewriter
\begin_inset Quotes eld
\end_inset
ONE
\begin_inset Quotes erd
\end_inset
\family default
and
\family typewriter
\begin_inset Quotes erd
\end_inset
MANY
\begin_inset Quotes erd
\end_inset
\family default
.
This is a solution:
Then the expression
\end_layout
\begin_layout Quote
eval(BAD + &((@(X)>=0),0,0))
\family sans
@(OUTPUT, min(@(INPUT), 2))
\end_layout
\begin_layout Standard
Note this is making use of the fact that you can add locations in the natural
way.
The cell labelled
\family typewriter
BAD
does the trick: the (optional) second argument of the fetch function
\family sans
@()
\family default
contains the string
adds to the X-coordinate of the
\family typewriter
"BAD"
OUTPUT
\family default
, its right neighbour contains the string
location, and the
\family sans
min()
\family default
makes sure the amount being added to that coordinate is no more than two.
(Note that for this to work exactly as shown assumes that the value in
the
\family typewriter
"GOOD"
INPUT
\family default
cell is always a non-negative integer.
You should relatively easily be able to enhance the given expression to
handle other possibilities for
\family typewriter
INPUT
\family default
, for example if it might be negative or have a floating-point value.) If
you want to use Boolean conditions on input variables in a similar way,
you can convert them to integers with
\family sans
int()
\family default
.
If you have nested conditions, you could weight them with 1, 2, 4 and so
on to address a bigger range of cells.
Alternatively, you could make use of all three dimensions for nested conditions.
\end_layout
\begin_layout Standard
Sometimes you can also get conditional-like behavior using the logical expressio
ns.
Suppose you want to use the value of the cell labeled
\family sans
OPTION
\family default
if it is non-empty and non-zero, and the value of the cell labeled
\family sans
DEFAULT
\family default
otherwise.
Since the empty and zero values are considered false, but the logical operators
short-circuit and preserve the original values of their arguments, you
can achieve this with:
\end_layout
\begin_layout Standard
\family sans
@(OPTION) or @(DEFAULT)
\end_layout
\begin_layout Subsection
@ -10399,8 +10623,16 @@ absolute
\family sans
R(,-1)+1
\family default
or up()+1 to add one to the value of the cell which is one up from the
current cell (all of these expressions work).
or
\family sans
R(up)+1
\family default
or
\family sans
up()+1
\family default
to add one to the value of the cell which is up one position from the current
cell (all of these expressions work).
Then you can fill that expression downwards and get your column of consecutive
numbers.
\end_layout
@ -10413,9 +10645,9 @@ But these sorts of relative expressions only keep working if the cells move
\family sans
R(,-1)
\family default
) and you insert another row in between them, your references will be all
) and you insert another row in between them, your references will all be
messed up.
There is value to
Thus, there is value to
\begin_inset Quotes eld
\end_inset
@ -10432,11 +10664,13 @@ referring to what you want.
\begin_inset Quotes erd
\end_inset
Note that labeled cells handle some aspects of this desired behavior, because
the label goes with the cell when it is moved in the spreadsheet
\end_layout
\begin_layout Standard
To provide for this need,
However, to provide for the cases when just labels by themselves are not
enough,
\noun on
teapot
\noun default
@ -10541,7 +10775,11 @@ X(SRC,REF,fix,fix,fix)
\family sans
@(SRC)
\family default
, but the intent of the latter is much clearer.
, although of course if that's all you want, there's no need to use
\family sans
X()
\family default
.
\end_layout
\begin_layout Standard
@ -10559,15 +10797,15 @@ do the right thing
\end_inset
as you copy and move either that formula or the referred-to data? The response
to this is that in a typical spreadsheet, there are only a small number
of fundamental references, and all other references derive from them in
this way.
to this criticism is that in a typical spreadsheet, there are only a small
number of fundamental references, and all other references derive from
them in a natural way.
So you generally only need a few labels, and by taking just a little extra
time to apply those labels and refer to them in initial formulas, you are
making the semantics of your references much clearer and in essence documenting
them within your spreadsheet.
This modicum of extra effort will therefore be repaid in an easier-to-use,
easier-to-understand, and easier-to-maintain and update spreadsheet.
This modicum of extra effort will be repaid by a spreadsheet that is easier
to use, understand, maintain, and update.
\end_layout
\end_body

View File

@ -684,6 +684,39 @@ static Token blop_macro(FunctionIdentifier self, int argc, const Token argv[])
}
/*}}}*/
/* if_macro -- traditional if-then-else expression */ /*{{{*/
static Token if_macro(FunctionIdentifier self, int argc, const Token argv[]) {
assert(self == FUNC_IF);
if (argc < 1 || argc > 3) {
Token error;
const char *usage = _("Usage: if(expr[, true_expr[, false_expr]])");
return duperror(&error, usage);
}
Token tcond = evaltoken(argv[0], FULL);
Token cond = tbool(tcond);
if (cond.type == EEK) return cond;
assert(cond.type == BOOL); // Since it's bool no need to worry about freeing
if (cond.u.bl) {
/* Condition is true. Return value of second argument or of first
argument if there is no second argument */
if (argc >= 2) {
tfree(&tcond);
return evaltoken(argv[1], FULL);
}
return tcond;
}
/* Condition is false. Return value of third argument or empty if there is
none.
*/
tfree(&tcond);
Token result;
result.type = EMPTY;
if (argc >= 3) {
result = evaltoken(argv[2], FULL);
}
return result;
}
/* self function, typically used for keywords */ /*{{{*/
static Token self_func(FunctionIdentifier self, int argc, const Token argv[])
{
@ -1782,6 +1815,7 @@ Tfunc tfunc[]=
/* Boolean functions */
[FUNC_AND] = { "and", blop_macro, INFIX_BOOL, MACRO, 0 },
[FUNC_FALSE] = { "false", blcnst_func, PREFIX_FUNC, FUNCT, 0 },
[FUNC_IF] = { "if", if_macro, PREFIX_FUNC, MACRO, 0 },
[FUNC_OR] = { "or", blop_macro, INFIX_BOOL, MACRO, 0 },
[FUNC_TRUE] = { "true", blcnst_func, PREFIX_FUNC, FUNCT, 0 },

View File

@ -53,6 +53,8 @@ typedef enum
FUNC_COUNT,
FUNC_IF,
N_FUNCTION_IDS
} FunctionIdentifier;