The introduction to the concept implemented in MCalc and the rationale for the design decisions are presented in the Version 1 article. Here I will outline the changes from version 1 to version 2.
The major front-end change is in the assignment of capital letters to stack entries. Previously an extending letter scheme was used, which meant that as the stack grows the length of a reference to type in also grows. Since the goal of MCalc was to reduce the amount of typing, this was counterproductive. In version 2, the letters roll around in a modular arithmetic manner. This means that one letter can be used to refer to any of the 26 previous stack entries regardless of the stack size, which has been sufficient for any calculation I've done with the program so far. For exceptionally lengthy calculations it remains possible to refer to entries earlier than the past 26 by a variant of the extending letter scheme. Relative references, using digits instead of capital letters, have been removed altogether, along with the stack renumber command.
There have been some changes in math functions, such as the inclusion of combinatorial functions lcm and gcd, and the removal of vector rotation functions since these can be more transparently implemented as external commands. The commands ending in ' have been removed, and instead ' acts as a special reference character, so that argument-swap functionality may still be obtained by placing ' in front of any command. This makes the syntax more generally applicable.
However most of the changes have been on the back-end functionality. A focus was on removal of replicated code, and streamlining the handling of external and hypothetical stacks. Strangely this meant the removal of the main value stack, because its presence meant two different functions to read values - either from the main stack or from a non-main stack. Now every value stack is stored separately and only one function is necessary. Thus there is a separation between the main stack, which carries only reference and command details (or ways to link and operate on values), and the value stack which only contains numeric values (and which may or may not be derived from the main stack).
The reason for making the change in stack processing was to improve functionality of external commands, of the hypothetical command, and to add an inversion command. The inversion command was the main driver, since the external and hypothetical commands are handled by version 1, just not in a way that is easily extendable. While the hypothetical asks "what would be the output if the input value were different?", the inversion asks "what should the input have been to make the output a desired value?". In this sense it is a baby version of Structural Expressions so I felt a compulsive need to implement the inversion functionality. The hypothetical and inversion commands now solve for a computation path in the stack and check for dependency errors, compared to version 1 where the stack was simply re-evaluated.
Multi-line commands are also handled differently. Instead of a function that returns a list of all line values, they are implemented as a function that takes in an output line number and returns the value for only that line. In effect, this means a multi-line command can be seen as a useful shortcut for typing rather than a special case for the math processor. Despite lowered computational efficiency, this makes each line independent of other lines in the stack, and simplifies the operation of the stack-modifying commands. Similarly a multi-line hypothetical in version 1 used caching to avoid the need for re-computing the stack for every line, but this meant that lines were no longer independent so a lot of code was included to handle special cases and maintain the relationship between cached lines. In version 2 the multi-line hypothetical is evaluated separately for each line so lines are independent, which incurs a computational penalty but makes the code a lot clearer (the penalty is often countered by only evaulating relevant entries, rather than a contiguous range of the stack as in version 1).
While making the back-end changes, I found and fixed lots of strange buggy edge-cases with regular MCalc functionality, such as combining special reference characters in unusual ways, and evaluating multi-line hypotheticals of other multi-line hypotheticals.
When examples are provided in this guide, text to type in MCalc appears inside square brackets [] and numeric results appear in parentheses (). Both square brackets and parentheses are not used inside MCalc. For instance to evalue 3+7 one enters [3](3) [7](7) [+](10), with the Enter key pressed after each bracketed entry to evaluate the input string and add it to the stack. Since numeric entries evaluate as themselves, this will be shortened as [3] [7] [+](10).
After starting MCalc, a console window opens with text entry on the first line. Use the keyboard to enter an input, then press enter to add the input to the stack. The stack starts at the visual top of the console and grows downwards. Once added to the stack, an entry cannot be modified, and it can be referenced by future commands. The entry may be a numeric value (such as [1.23e5]) or a command (such as [+]), and these may be followed by a space and comment (such as [1.23 m radius] or [+ adding sides for circumference]). An entry may also be a comment by itself (such as [ next equation]), which will show up as a line in the stack in the same manner as a numeric value. Since the stack may be referenced at any point, there are no commands to swap, rotate, or delete (other than the final) stack elements. Each command can only add new values to the stack and cannot change previous ones. Commands can only reference values already in the stack. Values cannot be entered on the same line as a command, and only one command may be entered per line. Commands may explicitly reference entries in the stack, or in the absence of that, will implicitly reference the immediately preceding entries. Absolute address references are capital letters A-Z. The entries precede a command statement, so two numbers stored as Z and Y are summed by the expression [ZY+]. The summation command takes 2 or more arguments, so [+] will sum the previous 2 entries in the stack, [Z+] will sum the previous entry with Z, [ZY+] will sum Z with Y, [ZYX+] will sum Z with Y with X.
A number causes the entered value to be parsed and stored in the stack (as 64-bit floating point), with an optional comment. Examples: [7] [1.9 L] [.5] [-2] [3e6] [+5E+5]
Items in the stack may be referenced by an absolute address which proceeds from Z to A and then wraps back to Z, such as Z,Y,...B,A,Z,Y. A single letter refers to the matching line within the most recent 26 entries. To refer to earlier lines, prepend the letter with a ` character for each step of 26 lines, so `Z looks from 26 lines back and ``Z looks from 52 lines back. The letter does not change as the stack grows, and for convenience is shown at the beginning of each line. [YZ/] divides Y by Z (that is, Y/Z) while [ZY/] is the opposite (Z/Y), and may be read as transposing the division sign left one character to place it between the references. [/] at X expands into [ZY/] as the preceding 2 entries Z and Y have been prepended, while [Z/] at X expands into [YZ/] as the preceding entry Y has been prepended.
Each command has a minimum and a maximum number of arguments. It will take up to the minimum number of arguments from the end of the stack implicitly, while supplying more arguments than the minimum requires all of them to be entered individually. Special reference characters simplify this task.
It is possible to produce multiple outputs by using iterators. An iterator causes the same command to be evaluated multiple times with different arguments. An iterator contains a list of references in curly brackets [{XYZ}], and executes the command once for each reference from left to right. For instance to evaluate 7-1 and 7-5 in one line starting from Z, [1] [5] [7] [{ZY}-](6)(2). An iterator takes place of one argument for a command. Multiple iterators may be used if they all contain the same number of references, and they will be scanned through concurrently. For instance to evaluate 1-5 and 5-7 in one line from Z, [1] [5] [7] [{ZY}{YX}-](-4)(-2). There are also special reference symbols for iterators.
A command causes an action to be carried out and the result(s) written to the bottom of the stack. Commands will get data from the most recent entries in the stack unless preceded by manually specified stack references. Numeric values for the command may not be entered on the same line as a command; rather the values should first be entered into the stack and then be referenced when entering the command. Command words start by a sequence of special characters (such as [+] or [-]) or lower case letters (such as [sqrt] or [s]) and are preceded by a list of references and followed by an optional comment (after a space). Multiple arguments are passed by concatenating references, where different valid characters from above may be mixed. The order of arguments, read from left to right, is passed to the commands, so [VR-] is different from [RV-].
Some commands produce multiple numeric outputs. In this case, the command is typed once and appends more than one entry to the stack, with each entry carrying one of the numeric outputs. The multiple outputs are from then on treated as independent entries, following the same referencing rules as if they were added one at a time. For example the [/%] command returns an integer divisor and a remainder, and may be used as [23] [5] [/%](4)(3).
| Command | Arguments | Description |
|---|---|---|
| - | 2 | Subtraction |
| -- | 1 | Subtract 1 |
| % | 2 | Modulo |
| %i | 2 | IEEE Remainder |
| * | 2+ | Multiplication |
| / | 2 | Division |
| -/ | 2 | Difference as average ratio |
| \ | 1 | Inversion 1/Z |
| ^ | 2 | Exponentiation |
| | | 2+ | Parallel combination 1/(1/x+1/y) |
| ~ | 1 | Negation |
| + | 2+ | Addition |
| ++ | 1 | Add 1 |
| < | 1 | /2 |
| -< | 2 | Half-difference |
| << | 1 | /4 |
| <<< | 1 | /8 |
| <<<< | 1 | /16 |
| > | 1 | *2 |
| >> | 1 | *4 |
| >>> | 1 | *8 |
| >>>> | 1 | *16 |
| abs | 1 | Absolute value |
| ac | 1 | Arccosine |
| acd | 1 | Arccosine as degrees |
| ach | 1 | Hyperbolic arccosine |
| as | 1 | Arcsine |
| asd | 1 | Arcsine as degrees |
| ash | 1 | Hyperbolic arcsine |
| at | 1-2 | Arctangent |
| at2 | 2 | Arctangent |
| atd | 1-2 | Arctangent as degrees |
| atd2 | 2 | Arctangent as degrees |
| ath | 1 | Hyperbolic arctangent |
| c | 1 | Cosine |
| cb | 1 | Cube |
| cbrt | 1 | Cube root |
| cd | 1 | Cosine of degrees |
| ceil | 1 | Next higher integer |
| ch | 1 | Hyperbolic cosine |
| e | 0 | e constant |
| exp | 1 | e^Z |
| exp10 | 1 | 10^Z |
| exp2 | 1 | 2^Z |
| factorial | 1 | Factorial |
| floor | 1 | Next lower integer |
| fpart | 1 | Fractional part |
| gcd | 2+ | Greatest common denominator of integers |
| gmean | 2+ | Geometric mean |
| hmean | 2+ | Harmonic mean |
| ipart | 1 | Integer part |
| lcm | 2+ | Least common multiplier of integers |
| ln | 1 | Log base e |
| log | 1-2 | Log base 10 (1 arg) or custom |
| log10 | 1 | Log base 10 |
| log2 | 1 | Log base 2 |
| logb | 2 | Log custom base |
| mdist | 2+ | Manhattan distance |
| mean | 2+ | Arithmetic mean |
| ncr | 2 | Combinations |
| norm | 2+ | Norm (Euclidean distance) |
| npr | 2 | Permutations |
| pi | 0 | Pi constant |
| round | 1 | Round to nearest integer |
| round1 | 1 | Round to 1 significant digit |
| round2 | 1 | Round to 2 significant digits |
| round3 | 1 | Round to 3 significant digits |
| round4 | 1 | Round to 4 significant digits |
| rt | 2 | Zth root Y |
| s | 1 | Sine |
| sd | 1 | Sine of degrees |
| sh | 1 | Hyperbolic sine |
| sq | 1 | Square Z*Z |
| sqrt | 1 | Square root |
| sstd | 2+ | Standard deviation 1/(N-1) |
| std | 2+ | Standard deviation 1/N |
| t | 1 | Tangent |
| tau | 0 | Tau constant (2*Pi) |
| td | 1 | Tangent of degrees |
| th | 1 | Hyperbolic tangent |
| Command | Arguments | Outputs | Description |
|---|---|---|---|
| /% | 2 | 2 | Division with remainder |
| /%i | 2 | 2 | Division with IEEE remainder |
| +- | 2 | 2 | Sum and difference |
| -+ | 2 | 2 | Difference and sum |
| +-< | 2 | 2 | Half of Sum and difference |
| -+< | 2 | 2 | Half of Difference and sum |
| box | 2+ | 2 | Enclosing range A+-B |
| msstd | 2+ | 2 | Mean and standard deviation 1/(N-1) |
| mstd | 2+ | 2 | Mean and standard deviation 1/N |
| parts | 1 | 2 | Integer and fractional parts |
[@] re-evaluates the stack with modified input values. Unlike other commands which only have arguments to the left of the command word, this command requires at least 2 arguments to the right of the @ symbol, and the number of arguments determines how many entries are hypothetically modified in the stack. The last entry on the right is what is evaluated, while all preceding entries are modified as filled from references prepended to the command in the standard manner. For instance:
Example: [3] [8] [+](11) [9] [@Z!](17) evaluates 3+8 and also 9+8. Repeated @ commands are evaluated as chained, and changes accumulate. A referenced line within a multiline result will return only the specified line (use an iterator on the right-most output argument to include all lines).
[?] inverts a solution path to find an input value that will make an output match a desired value. [?] and [@] may be thought of as inverses of each other, however [?] has more limited applicability as not all functions are invertible. [?] will succeed when it is possible to apply simple algebraic operations to invert the original solution path. Like [@], this command requires at least 2 arguments to the right of the ? symbol, and the number of arguments determines how many entries are hypothetically modified in the stack. The last entry on the right is what is evaluated, while all preceding entries are modified as filled from references prepended to the command in the standard manner. Of the supplied arguments, the one latest in the stack is considered the desired end point. For instance:
Example: [3] [8] [+](11) [9] [?!Z](1) evaluates 3+8 and also 9-8. Nested ? and @ commands are not supported (in theory it seems that this should be possible to implement, but because each of these nested commands may assign multiple values to its version of the hypothetical stack, keeping track of the correct dependency chain is not trivial).
[conv] may be used to convert measurement units. It is special because it parses the entry comment to determine the conversion. The 1-argument command is followed by a space, the original unit, another space, the desired unit, and optionally another space and a remaining comment. Example: [2] [conv m^2 ft^2](21.528) converts 2 square meters into square feet. Input and output units should be entered as a string without spaces containing letters, exponents, and multiplication or division signs such as m/s^2 or N*m. The division symbol applies only to the immediate next unit, so m/s^2*kg is equivalent to m*kg/s^2 or m*kg*s^-2.
| Command | Arguments | Outputs | Description |
|---|---|---|---|
| atod | 1 | 1 | Convert circle area to diameter |
| pr | 3 | 1 | Progress ratio, input A,B,C, return 0 when C=A, 1 when C=B |
| quad | 3 | 2 | Quadratic equation solver, input A,B,C, return x such that A*x^2+B*x+C=0 |
| rc | 2 | 1 | RC Filter Calculator, input R and C, return Fc |
Custom units may be defined. A non-comprehensive list of units is specified in a text file "units_list.txt" in the MCalc executable directory. The file is not parsed until [conv] is called for the first time in a session. Use [reload] when actively modifying the units list to avoid restarting MCalc. The units must be defined in groups relating to base units. A base unit is defined as equal to one of itself, such as "m=1 m" to specify the meter as a base unit. Other units are defined with reference to base units, such as "ft=0.3048 m", where an implied "1" is to the left of the expression, that is 1 ft = 0.3048 m. Note that all prefixes must be defined explicitly, such as "cm=0.01 m" or "km=1000 m". Each line in the "units_list.txt" file may start with a space to contain a comment, or with a letter (a-z or A-Z) to define a unit name (case-sensitive). The unit name must not have any powers (exponents) attached. Exponents are calculated automatically, so defining "m" will carry over to "m^2" and others. The unit name must be followed by "=" and a numeric value (corresponding to 1 of the new unit), then one space " " and a defining unit name. For instance to represent 1 week is 7 days, the entry would be "week=7 day". Defining units must reduce to base units for the conversion to be successful. Look through the existing entries in the file to gain a better understanding.
Custom external commands may be defined. They operate in the same stack-based manner as the console, and constitute an isolated "sandbox stack" where inputs are read in from the main stack, the computation is carried out, and results are taken back into the main stack. Commands should be stored as *.txt files in ./extcmds directory relative to the MCalc executable. Valid names must start with a lowercase letter a-z and may contain digits 0-9 later, no other characters are allowed. The files are enumerated on MCalc startup but not parsed until individual commands are called. Use [reload] when actively modifying the external commands to avoid restarting MCalc. Look through the included examples in the extcmds folder to gain a better understanding. An external command must have a fixed number of inputs and outputs. Some features are not available in external commands:
External commands have their own stack which starts with Z as the first line, and subsequent letters assigned as in the main stack. The first line comment may be used to provide a description of the command. Inputs are read with the command [in], and outputs are written with the command [out]. The entire script is parsed to find the number of inputs, and these inputs are read from the main stack such that the last [in] corresponds to the last value on the stack. Similarly the first [out] is the first value that will be added to the main stack. The [in] command does not support any arguments and reads in exactly one value at the line it appears. The [out] command may have arguments, in the absence of arguments it will return the previous entry in the external stack. Example:
convert rectangular to polar coordinates (with comments) using expression syntax this is R=norm(X,Y);theta=atan2(Y,X);return (R,theta); in Read X in Read Y norm Find R out Output R WXat Find theta out Output theta
MCalc will attempt to invert external commands that appear within the solution path of a [?] command. In this case, all but one of the input values are loaded into the hypothetical external stack, and only one output is loaded, then the remaining input value is solved for. If the external command outputs multiple lines which are interrelated, the inversion will not be successful. In short, inversion has the best chance of working when the external command is a chain of simple operations outputting a single value.
The C# code files for the project may be downloaded here. The compiled executable (for Windows .NET framework 7.0) with units list and external command examples may be downloaded here.
The updated MCalc console calculator has successfully met the design criteria. The main limitation compared to the first version is the inability to use multi-line commands within external commands; this restriction was added to reduce the number of "edge cases" which have to be verified to work with hypothetical evaluation. The main advance over the first version is the addition of more powerful back-end routines for hypothetical and inversion commands. The simplified argument swap syntax and modular line naming scheme made text entry easier while lowering the amount of redundant code. The second version of MCalc continues to fill a niche not well covered by other computational software. Overall I would consider MCalc a success, and I use it regularly on every computer where I have an account.