2

For example, this text:

2:26 3:04 3:14 5:38 4:12 10:30 6:29 6:53 11:49 9:33 4:17 7:49 6:10 6:04 6:28 6:40 4:21 

(These are actually minute:second format, not hour:minute, if it matters. So ideally the sum would compute hours from the total minutes and give the answer hours:minutes:seconds.)

I know there are ways to do this in bash, e.g. with awk. Running an external shell command is okay, but it has to work from within vi—and a vim-native approach would be preferred.

1
  • I suspect that because they are times instead of integers, this specific action may have to be done with a shell command. But it seems that simply adding integers should be possible natively in vim.... Commented Oct 9, 2015 at 4:13

6 Answers 6

2

One powerful function of vim is piping an area thru an external command.

(I think this is not what you are looking for, but, just for the record):

:%!awk -F: '{a=$2*60+$1} END {printf("\%d:\%02d",a/60,a\%60)}' 

\thanks{Wildcard} for pointing out the "10:09" problem and solution

2
  • After some months more experience, this is how I would resolve it today as well. Shouldn't there be a width specifier on the second %d, though? So that e.g. 11:09 won't be written as 11:9? Commented Mar 17, 2016 at 19:40
  • @wildcard, thank you: you are absolutely right. (fixed) Commented Mar 18, 2016 at 0:12
2

The following are a function and a command that can be used with a range to achieve your goal.

command! -range Sum call s:sum() function! s:sum() range " Get range boundaries let l1 = getpos("'<")[1] let l2 = getpos("'>")[1] if l2 <= l1 | return | endif " Calculate sum of seconds in provided range of lines let sum = 0 for time in map(getline(l1,l2), 'split(v:val, '':'')') let sum += time[0]*60 + time[1] endfor " Delete lines and calculate time in format hh:mm:ss normal! gvd call setline(l1, printf('%02d:%02d:%02d', \ sum/3600, \ (sum % 3600)/60, \ sum % 60)) endfunction 

To use this in your given example, you would simply do something like this: ggV}:Sum<cr>. You may also map the command, e.g.

vnoremap <leader>s :Sum<cr> nnoremap <leader>s gv:Sum<cr> 
2

Here's what I came up with, Karl was faster to post something more elegant but as I spend some time on it I think it couldn't hurt to share it :-)

" Takes a string formatted H:M:S and convert it to seconds function! ConvertToSecond(time) let l:cmp = split(a:time, ":") let l:seconds = 0 if (len(l:cmp) == 1) let l:seconds = l:cmp[0] elseif (len(l:cmp) == 2) let l:seconds = l:cmp[1] + 60*l:cmp[0] elseif (len(l:cmp) == 3) let l:seconds = l:cmp[2] + 60*l:cmp[1] + 3600*l:cmp[0] endif return l:seconds endfunction " Takes a number of seconds and convert it to h:m:s function! ConvertFromSeconds(time) let l:time = a:time let l:hours = l:time / 3600 let l:time = l:time % 3600 let l:minutes = l:time / 60 let l:time = l:time % 60 let l:seconds = l:time return l:hours . "h" . l:minutes . "m" . l:seconds . "s" endfunction " Iterate through line and append the sum on the next line function! SumAndConvert() range let l:total = 0 if (a:firstline > a:lastline) echo "Wrong range" return endif let l:line = a:firstline " Read lines from current one to last one while (l:line <= a:lastline) let l:total = l:total + ConvertToSecond(getline(l:line)) let l:line += 1 endwhile normal '> put = ConvertFromSeconds(l:total) endfunction 

It can be called with a range like this for all the file:

:1,$call SumAndConvert() 

or this for the last visual selection:

:'<,'>call SumAndConvert() 

And of course it is possible to create user-defined commands to shorten the call.

2

With lh-vim-lib function lh#list#accumulate(), and the following definition:

function! ToSeconds(ms) let l = split(a:ms, ':') return l[0] * 60 + l[1] endfunction 

it can be done with:

echo strftime('%H:%M:%S', eval(lh#list#accumulate(getline(1,'$'), 'ToSeconds', 'join(v:1_, "+")'))) 

If you want to make it a command, it could be:

function! ToSeconds(ms) let l = split(a:ms, ':') return l[0] * 60 + l[1] endfunction command! -range=% -narg=0 SumMinSec \ echo strftime('%H:%M:%S', eval(lh#list#accumulate(getline(<line1>, <line2>), 'ToSeconds', 'join(v:1_, "+")'))) 

NB: As you see, no need for modulo operation, we already have strftime. However we lack strptime, hence my ToSeconds() function.


Or, a more simple solution:

command! -range=% -narg=0 SumMinSec \ echo strftime('%H:%M:%S', \ lh#list#accumulate2( \ lh#list#chain_transform( \ getline(<line1>, <line2>), \ ['split(v:1_, ":")', 'v:1_[0]*60 + v:1_[1]']), \ 0)) 
1

There may be a better way, but here is the hacky native solution I used. It won't work as written if any of the numbers are zeros, e.g., 5:00 or 0:30.

:set nrformats-=octal # TYPE the following in insert mode, don't execute in normal mode: 0"hdwx"mdwj0:let @a='^Rh^A$^Rm^A'^M@a # (you can't record playing another macro.) # To type the ^R etc. sequences, prefix them with ^V # Then yank the line into register q with <Esc>0"qy$ # Type the following in insert mode on a new line: $60^X0^A # Then yank into register c with <Esc>0"cy$ # Next, run the following in normal mode: 16@q7@c # The counts here are of course specific to the times I was adding. 

Like I said, a little bit hacky, but easier than pulling out a calculator or doing it in your head (for a large list).

1
  • This solution is really note very "native", but it is hacky. It is also not very adaptible and reusable (e.g. if you overwrite a with another macro). Commented Oct 9, 2015 at 6:21
1

In this specific case, I'd use plain substitution with the sub-replace-\=, where you can enter an expression that is evaluated before doing the substitution. Things to keep in mind when using \= in the substitution are that special meaning for characters (in :h sub-replace special) does not apply and that text matched with \( \) need to be accessed with submatch(1) etc. submatch(0) contains the whole matched text. For full details, you can see :h sub-replace-\=.

Going from format HH:MM:SS to sum in seconds:

01:02:26 03:04 03:14 

can be done with

:%s/\(\(\d\+\):\)\?\(\d\+\):\(\d\+\)/\=submatch(2)*3600+submatch(3)*60+submatch(4) 

where the hours are optional. Now we have

3746 184 194 

which we can yank (clearing the register first) using the command

:let @a="" | %y A 

To calculate the sum to the last line, we can use :put in combination with the expression register @=

:$put =eval('0' . substitute(@a, '\n', '+', 'g') . '0') 

where we substitute newlines in the register with + signs using the function substitute(). The register also contains newlines in the beginning and at the end, so we need to pad it with zeroes on both ends. This results in

3746 184 194 4124 

To go in the other direction (from seconds to HH:MM:SS), you can do

:%s@\d\+@\=printf("%02d:%02d:%02d", submatch(0)/3600, submatch(0)%3600/60, submatch(0)%60) 

where you need to use some other character than / as a delimiter, since it is used in the expression. I chose to use @. In the end, we get

01:02:26 00:03:04 00:03:14 01:08:44 
4
  • This looks like the solution I was looking for, but it's a little bit unclear. Can you explain it a bit more? For instance, why submatch(2) instead of just \2 like I use in a typical replace command? Commented Oct 9, 2015 at 23:52
  • I updated the answer. After \= the escaped characters such as \2 don't have their special meaning anymore, but you can use submatch(2) instead to get around that. Commented Oct 10, 2015 at 6:36
  • This does not compute the sum of the list of times. It simply converts a time in one format to another. If this does answer your question, then the question should be rephrased. Commented Oct 11, 2015 at 16:09
  • @KarlYngveLervåg, thanks for clarifying the question for me! I had misunderstood it simply as wanting to calculate row-wise sums in seconds. Commented Oct 11, 2015 at 16:36

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.