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:
parent
7b794f90b9
commit
5176005bb3
1
NEWS
1
NEWS
@ -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
|
||||
|
402
doc/teapot.lyx
402
doc/teapot.lyx
@ -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
|
||||
|
@ -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 },
|
||||
|
||||
|
@ -53,6 +53,8 @@ typedef enum
|
||||
|
||||
FUNC_COUNT,
|
||||
|
||||
FUNC_IF,
|
||||
|
||||
N_FUNCTION_IDS
|
||||
} FunctionIdentifier;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user