Monday, January 10, 2011

VIM inline calculator revisited, floating point numbers comapatible

As it turned out, sometimes I really like to use the VIM as a calculator, with free editing, history and even access to env vars. (Unintentionally, calls to VIM functions also do work.)

Yesterday I needed to calculate few more things than usually and also using floating point numbers. I have recalled that VIM 7.2 added support for Float type and tried it out. After some tinkering, I managed to make my calculator even more useful to me than expected.

The previous version of calculator looked relatively innocent:

:map <silent> <F11> :exec "s!^\\([^=]\\{-}\\)\\( *=.*\\)\\=$!\\1 = ".eval(substitute(getline(".")," *=.*$","",""))."!"<CR>


The new version looks more serious and even less intimidating:


function! CalcGetValue(tag)
        let tag_marker = "#"
        let s = search( '^'.a:tag.tag_marker, "bn" )
        if s == 0 | throw "Calc error: tag ".tag." not found" | endif
        " avoid substitute() as we are called from inside substitute()
        let line = getline( s )
        let idx = strridx( line, "=" )
        if idx == -1 |  throw "Calc error: line with tag ".tag."doesn't contain the '='" | endif
        return strpart( line, idx+1 )
endfunction

function! CalcLine(ln)
        let tag_marker = "#"
        let s = getline(a:ln)

        " remove old result if any
        let x = substitute( s, '[[:space:]]*=.*'"""" )
        " strip the tag, if any
        let x1 = substitute( x, '[a-zA-Z0-9]\+'.tag_marker.'[[:space:]]*'"""" )
        " replace values by the tag
        let x1 = substitute( x1, tag_marker.'\([a-zA-Z0-9]\+\)''\=CalcGetValue(submatch(1))''g' )
        " evaluate
        let v = 0
        try
                let v = "".eval(x1)
        catch /E806:/
                " VIM can't convert float to string automagically, apply printf("%g")
                let v = "".eval('printf( "%g", '.x1.')')
        endtry
        " finish the job
        call setline(a:ln, x.' = '.v)
endfunction

:map <silent> <F11> :call CalcLine(".")<CR>


The new version addresses two my problems (old) allow simple reuse of the previous calculations and (new) allow floating point numbers.

At the core, the usage of calculator remained pretty much the same as before. For example, typing 2*2 and pressing F11 would change the line into 2*2 = 4.

To allow reuse of previous calculation results, I have introduced the tags. A line with a calculation result can be prefixes with label# tag (e.g. label# 2*2 = 4). Results of the calculation can be accessed in another expression using #label, e.g. 33*#label. During evaluation, the scriptlet above would search the buffer for string starting with label# and put into the original expression whatever is found after the '=' on the matching line. (What has turned out to be a minor, nice-to-have feature: expression itself is optional; line label# = value works as a definition of constant).

Example: calculate area and volume given the radius. Type this:

radius# = 5
pi# = 3.1415
area# pow(#radius,2)*#pi
volume# pow(#radius,3)*#pi*4.0/3.0

First two lines work like constants denoting Pi and the radius. Press the F11 (or ^O + F11 if in insert mode) when cursor on 3rd and 4th lines to evaluate the expressions and see the results:

area# pow(#radius,2)*#pi = 78.5375
volume# pow(#radius,3)*#pi*4.0/3.0 = 523.583333

The new (to me) features that are used in the new calculator:

1. search() to find a line number of a string matching given pattern (used to search in the buffer for the line with a tag).

2. submatch(), when inside the substitution, to access a submatch (functional equivalent of \1, \2 and so on).

3. \= in s/// and substitute() to substitute with result of an expression instead of a constant.

4. E806 + :try. VIM can't convert float to string automatically. Error E806 is generated when that is attempted. Scriptlet catches the error and applies suggested by documentation conversion method - printf(). The construct with try/catch also ensures that integer calculation remains integer. Only if there are floating point numbers involved, exception would be thrown and the conversion would be performed (spares the redundant '.0' in the integer calculations).

5 comments:

Anonymous said...

The inline calculator is very useful. Thank you.

arecarn said...

Hey, I made my own inline calculator plugin, and then stumbled on your elegant script. I liked how yours revalued lines and used tags so I included some of your code into my plugin. It's a work in progress, but if you want to check it out you can find it here

It forces all expression to evaluate to floating point so you don't get problems with integer division.

Ihar Filipau said...

@arecarn, nice job.

One of the ideas I had, after adding the "tags," was to introduce chained evaluation. IOW: not just take the value from the tagged line, but first re-evaluate it anew. That would allow to automate task of recalculating lots of expressions after changing couple of intermediate formulas or constants.

arecarn said...

@Ihar

I think that would really make the "tags" idea more complete, and usable. I'll add that to my todo list for the plugin.

On a similar note I made the command that evaluates lines work on a range of lines so the revaluation of multiple lines in a succession

Szilveszter said...

Since then vim has str2float() already. Thanks for your great post!