Kevinpgalligan / ka

A calculator language.
MIT License
5 stars 0 forks source link

ka(lculator) 🔢

ka is a small calculator language. It supports various useful features for day-to-day calculations, such as:

There are 3 ways to interact with it: executing individual expressions through the CLI (ka '1+1'), a CLI interpreter (ka), and a GUI (ka --gui).

>>> 2 * (1/2)
1
>>> 1 metre + 1 foot to feet
4.28084
>>> p = 0.7; C(10,3) * p^3 * (1-p)^7
>>> p=.7; N=10; sum({C(N,k)*p^k*(1-p)^(N-k) : k in [0,4]})
0.047349
>>> sin(90 deg)
1 
>>> e^pi
23.1407
>>> X = Binomial(10, 0.3); P(3 <= X < 7)
0.6066

Contents

Installation

Requirements:

To install, run: pip3 install ka-cli. ka is currently distributed through the Python Package Index.

I would appreciate it if a kind person could help me to package it for Linux package managers.

Usage

To execute a single expression, pass it as an argument to the CLI. You may wish to surround the expression in single quotes so that it's not messed up by your terminal.

$ ka '1+1'
2

The CLI offers introspection commands to show the available units, functions and whatnot (run ka -h for help with this).

Start the interpreter by executing ka from your CLI with no arguments.

$ ka
ka version 1.0
>>> 1+1
2
>>> %help
[...insert help text here...]
>>> quit()
$

There are also interpreter-specific commands, prefixed by '%'. Run %help to see a list of these commands.

To start the GUI, run ka --gui.

For more information on the language and the features it offers, see the manual below.

Manual

Grammar

The basic unit of grammar is the statement. You can execute multiple statements at once, separated by semi-colons:

>>> a = 3; b = 2; 2*a*b
12

An individual statement can be either an assignment (a = 3) or an expression (1+1).

An assignment consists of a variable name (such as a), followed by =, followed by an expression (such as 3 or 1+1 or sin(90 deg)). Assignments are not expressions, so you can't nest assignments like a=(b=3). You can, however, assign the value of one variable to another: a=3; b=a;.

An expression is a sequence of math operations that returns a value. Addition, subtraction, function calls, and so on. If the value of an expression is a quantity (a number with a unit attached), then the unit can be converted to something else using the operator to. For example, this assigns a the magnitude of 3 metres when it's converted to feet: a = 3m to ft.

Variables

ka has basic support for variables: blah=9^3; blah.

Constants and Numbers

pi and e are the only constants provided. Currently, they're treated like variables and can be overwritten: pi=3, woops.

The typical selection of number bases are supported: use the 0b prefix for binary, 0o for octal, and 0x for hexadecimal. So 0x10 is 16 in base-10. Note that alternative bases can only be integers, and can't be mixed with scientific notation (otherwise, there's a parsing ambiguity in something like 0x1e-10).

Types

ka is strongly typed, not statically typed. This means that when you pass a fractional number to a function that expects an integer, the type system will complain. But you don't have to declare the type of anything in advance.

The type system consists of (1) a hierarchy of numerical types, (2) quantities, and (3) some other types like arrays and random variables that don't mix with the other types so much.

The hierarchy of numerical types goes: Number > Real > Rational/Fraction > Integral/Integer. 'Real' numbers are represented as floating point numbers. If a fraction can be simplified to an integer, such as 2/2, then this will happen automatically. In the other direction, a type that is lower down the hierarchy, such as an integer, can be cast into a type that's further up the hierarchy in order to match a function signature.

Quantities consist of two components: a magnitude and a unit (see: the section on units). Any quantities can be multiplied together or divided into each other, but only quantities of the same unit type can be added or subtracted. For example, you can add 1 metre and 1 foot, but not 1 metre and 1 second. This is enforced by the binary operators themselves (addition and subtraction).

Most functions can be applied to both Numbers and Quantities.

Functions and operators

Here's a selection of functions and operators in the language. To list all the functions, run ka --functions. To find out more about any particular function (including what types of arguments it accepts), run the CLI command ka --function {name}, or run the interpreter commands %f {name} or %fun {name}.

ka has functions and 3 types of operators: binary operators, prefix operators, and postfix operators.

The binary operators, like addition (+), exponentiation (^) and division (/), take two arguments and come between those arguments, like 1+1.

The prefix operators are + and -. They take a single argument and come before that argument: +1 and -1.

The only postfix operator is ! (factorial), which takes a single argument and comes after that argument: 9!.

Operator precedence goes:

This means that 2^3!*5+1 gets parsed the same as ((2^(3!))*5)+1.

Units

Here are most of the units supported by the language. To see a complete list, run ka --units from the command-line.

The following prefixes are also supported, mostly coming from the SI standard. For convenience, their shorthand names and multipliers are provided here.

Notes on units:

As for how the unit system works, it's based on the International System of Units (SI). All units are represented in terms of the 7 SI base units: second, metre, gram, ampere, kelvin, mole and candela. Feet are a multiple of the metre, and their "signature" in base units is m^1. Frequency, measured in hertz, is s^-1. Area is m^2. Velocity is m s^-1. Internally, the "unit signature" of a quantity is a 7-dimensional vector of integers, with each dimension corresponding to one of the SI base units. For example, 1 metre may have a unit signature of (1, 0, 0, 0, 0, 0, 0). 1 metre per second may have a unit signature of (1, -1, 0, 0, 0, 0, 0, 0). When you multiply two quantities together, their unit signatures are added together. When you divide, the unit signature of the divisor is subtracted.

Further reading for the interested:

Probability

A number of discrete and continuous probability distributions / random variables are provided. Various properties of these distributions can be calculated, and they can be sampled from. A full list of distributions is shown below. For now, let's say we've already entered X = Binomial(10, .5). Then:

These are the discrete probability distributions and their parameters:

And these are the continuous ones:

Arrays

Arrays are written like so: {1,2,3}. They're basically a shim over Python lists.

The elements can be arbitrary expressions: {1+1,2*x, 1 m}.

The interval [lo,hi] generates a range of integers lo, lo+1, ..., hi.

Based on the mathematical notation for sets, arrays can also be generated from a series of clauses / conditions. For example, to calculate the sum of the squares of all odd numbers between 1 and 10: sum({x^2 : x in [1,10], (x%2)==1}).

Array-related functions, given an array A:

Configuration

ka can be configured through a config file at ${YOUR_HOME_DIR}/.config/ka/config. All available properties are shown below with their default values. precision determines the floating point precision; the other properties determine various characteristics of the GUI like its dimensions and keyboard shortcuts.

precision=6
font-size=15
window-width=600
window-height=400
shortcut-up=Ctrl+Up
shortcut-down=Ctrl+Down
shortcut-functions=Ctrl+F
shortcut-units=Ctrl+Q
shortcut-prefixes=Ctrl+P
shortcut-close=Ctrl+W

FAQ

How do you exit the interpreter?

Call the function quit(), use the interpreter command %q / %quit, or trigger an interrupt with CTRL+C.

Why is 1 metre per second written as 1m|s instead of 1m/s?

I would love to be able to write 1m/s, but this would result in a parsing ambiguity: is it 1 metre divided by the variable called s, or 1 metre per second? So ka instead uses | to represent division in unit signatures.

Why does 5/4 m give units in m^-1?

Units have higher precedence than division, so 5/4 m is parsed the same as 5 / (4 m). The solution is to write (5/4)m.

How does this compare to other calculator languages?

Frink is Turing-complete, has configurable units, has many more features than ka, and has a cool grammar. On the other hand, it's closed-source, it has expanded way beyond the scope of a simple calculator, and it has a slow start-up time, which makes it unsuitable for my purposes.

It's worth commenting a bit more on the grammar. Frink basically represents all units as variables. This means that the variable namespace is full of unit names, and it's possible for units to be overwritten accidentally. The good thing is that, coupled with the Frink grammar's support for implicit multiplication, you can write nice things like 1 m/s and it's interpreted as you would expect (the number 1, multiplied by metres, divided by seconds; this gives 1 metre per second). But if you write 4m / 2m you get 2m^2. There are design trade-offs when incorporating units into the grammar of a computer language and I don't think there's a perfect solution.

Qalculate! seems awesome and has bucketfuls of features! On the other hand, it's a massive project written in C++, while ka consists of 1000 lines of Python code. It sensibly handles both 1m/s and 4m / 2m, somehow.

F# is an example of a "real" programming language with cool unit features, but it's not suitable as a calculator.

Development

To install ka locally, clone the repo and run pip3 install .. You may wish to test it within a virtual environment, however, if you have a copy of ka that you actually use and you don't want to break it.

tox is used for unit testing, execute tox from the base directory to run all unit tests.

To run an individual script, such as gui.py, change to the src/ directory and run python3 -m ka.gui. See here for why.