In this post, I want to share how to do number arithmetic quickly and get what you want in specified format. More specifically, the topic is:
How do we add, subtract, multiply or divide a value to each number in a range?
Example 1#
Suppose that we have the following numbers at the end of each line:
line 1: 10
line 2: 20
line 3: 30
line 4: 40
line 5: 50
How to we do arithmetic operation to those numbers at the end of each line quickly?
We can use expression in substitution command to do this task, for details, see
:h sub-replace-expression
inside Vim. The idea is that instead of substitute
what you search with a string, we will evaluate an expression and use the
evaluated result as the replace string. The general syntax for substitution is:
:s/PATTERN/REPLACE/[flag]
To use an expression in REPLACE string, you can start the replacement string
with \=
, and the following string will be interpreted as an expression by
Vim. If you have used capture groups in PATTERN
, you can retrieve the
captured text in the corresponding capture group with submatch()
function.
For example, submatch(1)
will return the text in the 1st capture group,
submatch(2)
returns the 2nd group, submatch(0)
will return the whole
matching pattern.
With all this knowledge, we can easily solve the above problem. For example, to add 2 to the number at the end of each line, do:
:1,5s/\v(\d+)$/\=str2float(submatch(1))+2
Note that in the above command, the return value of submatch()
are strings.
In order to convert string to float type, we need to use function str2float()
explicitly. Although you can directly add strings with numbers, the result may
not be what you want. For example, echo '0.2' + 2
shows 2 instead of 2.2.
After looking at the documentation on variables (:h variables
), I find the
the reason:
The Number and String types are converted automatically, depending on how they are used.
It means that strings will by default be converted to an integer where
necessary. So string 0.2
becomes 0 when we add it with 2.
Similarly, to minus or multiply by 2:
:1,5s/\v(\d+)$/\=str2float(submatch(1))-2
:1,5s/\v(\d+)$/\=str2float(submatch(1))*2
For division, things are a little different here, because the division
symbol /
is the same as the substitute delimiter. In order to use division
character in substitute expression, we can change the delimiter to other
characters, for example, to #
:
:1,5s#\v(\d+)$#\=str2float(submatch(1))/2.0
Note that for division, we need to divide those numbers by 2.0, not by 2. If you use two integers, Vim will use integer division, not float division1.
One minor issue with the division result is that the result may have many
decimal digits. You may want to round it to fixed digit, e.g., 3 digits. This
can be easily accomplished with the printf()
function, and the whole command
looks like:
:1,5s#\v(\d+)$#\=printf('%.3f', str2float(submatch(1))/2.3)
Example 2#
I have the following 4 columns of text (separated by more than one spaces):
data1 data2 data3 data4
12 1.0 0.1667 in 0.2150 in
16 1.0 0.2222 in 0.2825 in
20 1.0 0.2778 in 0.3488 in
24 1.0 0.3333 in 0.4163 in
28 1.0 0.3889 in 0.4838 in
32 1.0 0.4444 in 0.5513 in
I want to insert the result of division from 4th and 3rd column in the end of each line. For example, after insertion, first line will become:
12 1.0 0.1667 in 0.2150 in 1.2897
My initial implementation is like:
s#\v\d{2,}\s+\d\.\d\s+(\d\.\d+) in\s+(\d\.+\d+) in#\=printf('%s %.4f', submatch(0), submatch(2)/submatch(1))
However, I am getting the following result:
12 1.0 0.1667 in 0.2150 in -9223372036854775808.0000
It seems that the number division goes wrong for string types.
As discussed earlier, submatch()
returns a string and string will be
converted to integer implicitly when doing arithematics. So these two strings
are converted to integer instead of float type, and they all become 0, as can
be verifed by the command: echo 0/0
.
We need to convert these strings into float first. The correct command is:
s#\v\d{2,}\s+\d\.\d\s+(\d\.\d+) in\s+(\d\.+\d+) in#\=printf('%s %.4f', submatch(0), str2float(submatch(2))/str2float(submatch(1)))
We can also use \zs
to simplify the above command a bit:
s#\v\d{2,}\s+\d\.\d\s+(\d\.\d+) in\s+(\d\.+\d+) in\zs.*#\=printf(' %.4f', str2float(submatch(2))/str2float(submatch(1)))
References#
- Number manipulation within Vim.
- Change delimiter in search and replace command.
- Increment a specific number in csv file.
- Rounding number in Vim.
- Round floats to one decimal.
- vim substitution with mathematical expression
- How can I convert px units to em units programmatically in vim?
- Search and replace: float arithmetic
- https://learnvimscriptthehardway.stevelosh.com/chapters/25.html
For example, if we do
:echo 5/2
, we will get2
instead of2.5
. ↩︎