Updates readme and fixes string/number combo expressions, I hope

This commit is contained in:
sloum 2021-03-17 16:21:43 -07:00
parent 9188922f43
commit 358871ea7f
3 changed files with 318 additions and 12 deletions

188
README.md
View File

@ -6,9 +6,193 @@ tally is intended to be a part of a suite of office/productivity applications (t
## Building / Installing
Coming soon...
You will need a Go compiler. Testing on eary versions has not been done, but suffice to say you should likely not need the latest cutting edge Go version to compile _tally_.
A makefile is forthcoming. Until then a simple `go build` or `go install` from within the repo directory should work fine.
## Usage
Coming soon...
_tally_ uses its own file format, but can import most (any that are in the format described by RFC 4180) csv and tsv files.
### Running
To create a new spreadsheet, invoke _tally_ without any arguments:
``` sh
tally
```
To open an existing file, pass a filepath. Here are some examples:
``` sh
tally ~/my-tally-spreadsheet.tss
tally ~/my-cool-csv-file.csv
tally ~/my-awesome-tsv-file.tsv
```
To quit _tally_ at any time press `Q`. If you have unsaved changes you will be asked if you are sure you want to quit without saving.
### Editing
When starting a new sheet from scratch the workbook will have one sheet with one cell (`A1`). If you load an existing `tss`, `csv`, or `tsv` file then thenumber of rows and columns, along with their values, will be present in the sheet shown by _tally_.
#### Movement
To move around the sheet, use vi-like keys:
- `h`, `j`, `k`, `l`: Left, Down, Up, Right
- `^`, `$`: Move to First or Last column in current row
- `g`, `G`: Move to First or Last row in current column
#### Data
You can enter data into cells in a few different ways. Cells accept the following types of values:
- `number`: Positive, negative, integer, or decimal. Internally _tally_ thinks of all numbers as decimals
- `text`: A string of characters. All strings must be surrounded by double quotes, `"like so"`
- `expression`: Any combination of the above two types, as well as references to cell in the form of column then row: `A2`, for example
To enter data into a cell you can press the `enter` or `space` key. If you are going to be entering text into the cell, there is a shortcut that allows you to enter the text without needing to quote it (the quoting will be done for you): press the `"` key.
If _tally_ understood what you entered you will see a value. If it did not, or there is a problem with your expression, you will see `#Err`.
#### Deleting Data
To delete the contents of a cell, that is - to revert it to an empty state, press `d`.
#### Yank / Paste
You can copy a cell with `y`. To paste it you have two options. The difference between these options is how they deal with cell references in expressions. Pressing `p` will paste the cell and will not change any references from when it was copied. Pressing `P` will update the cell references to be relative. In this case that means they will change based on the distance (in rows and columns) between where they were copied from and where they are being pasted. This allows for dynamic expressions that can be moved around a sheet conveniently. At present there is no way to lock just a row or column. It is either fully relative or fully static.
#### Cell Modifiers
You can change the appearance of a cell through the use of modifiers. All modifiers work as toggles, so turning on and off is done with the same key. The available modifiers are:
- `b`: Bold
- `f`: Faint/dim; if bold has been added to the cell then faint will have no effect
- `u`: Underline
- `i`: Italics
#### Zoom
You can zoom in or out with the `+` and `-` keys. This has the effect of controlling how many columns you see on the screen at a given time. At present all columns have the same width.
### Commands
In addition to the editing keys, there are a few commands that can be issued. To enter command mode press `:` and then enter your command.
The following commands are available:
- `write` or `w`: Will write the current spreadsheet to a file. If no filepath is passed (`write ~/my-file.tss`) _tally_ will save the current file, using a default name and the execution directory if necessary (ie. if _tally_ was run without a file and no path has yet been passed to `write`)
- `write-csv` or `wc`: Will write the current spreadsheet to a csv file. If no filepath is passed _tally_ will save to the current file path, but with the file type `csv`
- `write-tsv` or `wt`: Will write the current spreadsheet to a csv file. If no filepath is passed _tally_ will save to the current file path, but with the file type `tsv`
- `trim`: Removes empty rows/columns at the end of the file. It does this recursively starting with the bottom-most row and right-most column until it encounters a row or column (each working independently) with data in it
- `recalculate`: Forces a recalculation of the entire sheet. This should generally not be needed, but is avialable should you encounter the need
### Expressions
Expressions in _tally_ do not follow the _Excel_ style expression methodology. Instead, expressions use a stack for execution and use postfix notation. This is similar to [nimf](https://git.rawtext.club/sloum/nimf), the concatenative language written by the author of _tally_.
#### Working with numbers and cell references
```
A1 A2 +
```
In the above example the values of `A1` and `A2` get added together and the result is the value of the cell. Note that if either `A1` or `A2` is a text value, rather than a number, this will result in `#Err: Stack underflow. We will cover mixing text and numbers further into the examples.
Opperators exist for `+`, `-`, `\*`, and `/`.
There are also functions to do a few basic things:
```
A1 2 ROUND
```
The above would round the numerical value at `A1` to two decimal places.
```
A1 A2 MAX
```
The above would find the greater of the two values that come before `MAX`. `MIN` is also available.
You can combine cell references and hard coded numbers as well:
```
A1 2 * 10.5 MIN 1 ROUND
```
In the above, lets assume `A1` is currently `3.125`. If so, then `A1` gets multiplied by `2`, resulting in `6.25`. Then the minimum value between `6.25` and `10.5` is found, which is `6.25`. This then gets rounded to one decimal place for the final value of `6.3`.
#### Working with text and cell references
Text is pretty easy to work with. No operators are required in most cases. Since _tally_ splits expressions on whitespace, consecutive whitespace in strings should be entered as `\\\_`. So, a cell with the value `"Hello"` that is intended to be combined with another cell and result in a space in between may need to be written as `"Hello\\\_"`.
To then combine them, you could use the following expression (assuming `A2` to be something like `"world"`):
```
A1 A2
```
or since text can be entered into expression manually, the following would also work:
```
A1 "world"
```
Let's say you did not want to use `\\\_` to create a space. You could use the `SPC` function, like so:
```
A1 SPC A2
```
Assuming `A1` to be `"Hello"` and `A2` to be `"world"`, this would create the cell value `Hello world`. Without `SPC` the words would be shoved together and read as `Helloworld`.
#### Combining text and numbers
To concatenate a numerical and text value, you _can_ enter the values as if they were both strings, but it may not work out like you would think:
```
21.75 "Hello"
```
The above results in `Hello21.75`. If you want the `21.75` to appear before `"Hello"`, you need to use a special function: `.`. This lets _tally_ know that you would like the number on the top of the stack to be appended to the current string value and dropped from the stack
. So the above could be rewritten as:
```
21.72 . SPC "Hello"
```
With the result being `21.72 Hello`. Of course, either the number value or the text value could have been a cell reference, such as `B25` of the like.
You may be asking yourself, why do we need `.`? The answer is that it allows you to have numbers before and after text and choose when to put numbers into the text value. This gives a fine grained control of both the number stack as well as the string buffer.
For a more complicated example, we could do something like the following:
```
23 5 + . "hello" 3 1 -
```
This produces the result: `28hello2`. If you remove the `.` that comes after the plus, you would get the output: `hello228`. Now, that may seem strange. Why is that happening? It is happening for the same reason you saw a 2 at the end in the first result: at the end of the expression, the stack is flushed and added to the string from top of stack down. So since `2` was the last number on the stack it is the first off. Then `28` gets added to the end.
There are a few words that affect the stack, but do not necessarily modify the output in a transformative way like `+` or `MIN` do, for example. The next section will cover these. The important thing to take away from the above paragraph is that `.` can help you control where numbers go, and any numbers left on the stack at the end of an expression that involves text will be flushed to the text buffer (this is not true of purely numerical expressions, where the cell value is always just represented by the top of the stack).
#### Stack Manipulation
The number stack can be manipulated in a number of ways. You have seem mathematical operators and a few functions (`MIN`, `ROUND`, `MAX`, `.`). There are a few more options for dealing with numbers on the stack:
`DUP` will copy the number on the top of the stack and place the copy on top. So if the stack starts empty and you enter `2 DUP`, the stack will contain `2` and then another `2`.
`DROP` will remove the top value on the stack and throw it away.
`CLEAR` will wipe out all values from the stack. This, in theory, could be useful to prevent a stack flush at the end of a field containing numbers and text. However, be wary of doing so as it likely indicates that some refactoring could prevent the need to use it.
`SWAP` will swap the position of the top two values on the stack.
#### Errors
If there is a problem in your expression, the cell will report `#Err`, likely followed by a message about the nature of the problem. If you try to reference a stack value that doesn't exist, for example... or even a cell that does not exist.
Some functions require certain things to be in place on the stack. All of the math operators, for example, require at least two numbers to be on the stack (the two will be replaced by the result of the operation). `.`, `DROP`, and `DUP` only require one item on the number stack. If the minimum number of items on the stack is not met, an error will be the result.

140
cell.go
View File

@ -2,6 +2,7 @@ package main
import (
"fmt"
"math"
"strconv"
"strings"
)
@ -111,14 +112,17 @@ func (c *cell) Calculate() {
s := makeStack()
for _, v := range c.expr {
var stackError error = nil
if IsAddr(v) {
c, err := GetCell(Addr2Point(v))
if err != nil {
c.mask = "#Err: Invalid Cell"
c.mask = "#Err: Invalid cell reference " + v
return
}
if c.kind == Text {
v = c.rawVal
} else if c.kind == Empty {
break
} else {
v = c.mask
}
@ -147,22 +151,46 @@ func (c *cell) Calculate() {
s.text = true
}
if v == "+" {
s.Add()
stackError = s.Add()
}
if v == "-" {
s.Subtract()
stackError = s.Subtract()
}
if v == "/" {
s.Divide()
stackError = s.Divide()
}
if v == "*" {
s.Multiply()
stackError = s.Multiply()
}
if v == "." {
if s.Len() < 1 {
stackError = fmt.Errorf("Stack underflow")
}
num := s.Pop()
s.str.WriteString(strconv.FormatFloat(num, 'f', -1, 64))
s.text = true
}
if v == "MIN" {
stackError = s.Min()
}
if v == "MAX" {
stackError = s.Max()
}
if v == "ROUND" {
stackError = s.Round()
}
if v == "SWAP" {
stackError = s.Swap()
}
if v == "DUP" {
stackError = s.Dup()
}
if v == "CLEAR" {
s.ClearStack()
}
if v == "DROP" {
stackError = s.Drop()
}
default:
v, err := strconv.ParseFloat(v, 64)
if err != nil {
@ -171,6 +199,10 @@ func (c *cell) Calculate() {
}
s.Push(v)
}
if stackError != nil {
c.mask = "#Err: Stack underflow"
return
}
}
if s.text {
@ -183,6 +215,8 @@ func (c *cell) Calculate() {
c.mask = strings.Replace(s.str.String(), "\\_", " ", -1)
} else if s.ptr == 0 {
c.mask = strconv.FormatFloat(s.data[0], 'f', -1, 64)
} else if s.ptr == -1 {
c.mask = ""
} else {
c.mask = "#Err: Invalid Formula"
}
@ -219,25 +253,113 @@ func (s stack) Len() int {
return s.ptr + 1
}
func (s *stack) Add() {
func (s *stack) Dup() error {
if s.Len() < 1 {
return fmt.Errorf("Stack underflow")
}
val := s.Pop()
s.Push(val)
s.Push(val)
return nil
}
func (s *stack) Drop() error {
if s.Len() < 1 {
return fmt.Errorf("Stack underflow")
}
s.Pop()
return nil
}
func (s *stack) ClearStack() {
s.ptr = -1
}
func (s *stack) Add() error {
if s.Len() < 2 {
return fmt.Errorf("Stack underflow")
}
if s.text {
val := strconv.FormatFloat(s.Pop(), 'f', -1, 64)
s.str.WriteString(val)
} else {
s.Push(s.Pop() + s.Pop())
}
return nil
}
func (s *stack) Subtract() {
func (s *stack) Subtract() error {
if s.Len() < 2 {
return fmt.Errorf("Stack underflow")
}
second := s.Pop()
s.Push(s.Pop() - second)
return nil
}
func (s *stack) Multiply() {
func (s *stack) Multiply() error {
if s.Len() < 2 {
return fmt.Errorf("Stack underflow")
}
s.Push(s.Pop() * s.Pop())
return nil
}
func (s *stack) Divide() {
func (s *stack) Divide() error {
if s.Len() < 2 {
return fmt.Errorf("Stack underflow")
}
second := s.Pop()
s.Push(s.Pop() / second)
return nil
}
func (s *stack) Min() error {
if s.Len() < 2 {
return fmt.Errorf("Stack underflow")
}
one := s.Pop()
two := s.Pop()
if one < two {
s.Push(one)
} else {
s.Push(two)
}
return nil
}
func (s *stack) Max() error {
if s.Len() < 2 {
return fmt.Errorf("Stack underflow")
}
one := s.Pop()
two := s.Pop()
if one > two {
s.Push(one)
} else {
s.Push(two)
}
return nil
}
func (s *stack) Round() error {
if s.Len() < 2 {
return fmt.Errorf("Stack underflow")
}
decimals := s.Pop()
num := s.Pop()
s.Push(math.Round(num*math.Pow(10, decimals)) / math.Pow(10, decimals))
return nil
}
func (s *stack) Swap() error {
if s.Len() < 2 {
return fmt.Errorf("Stack underflow")
}
tos := s.Pop()
newTos := s.Pop()
s.Push(tos)
s.Push(newTos)
return nil
}

View File

@ -151,7 +151,7 @@ func IsRange(addr string) bool {
func IsFunc(val string) bool {
switch strings.ToUpper(val) {
case "+", "-", "/", "*", "^", "MIN", "MAX", "SQRT",
"ROUND", "SUBSTR", "UPPER", "LOWER", "SUM", "SPC", ".":
"ROUND", "SUM", "SPC", ".", "SWAP":
return true
default:
return false