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.

  • 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.... – Wildcard Oct 9 '15 at 4:13
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

  • 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? – Wildcard Mar 17 '16 at 19:40
  • @wildcard, thank you: you are absolutely right. (fixed) – JJoao Mar 18 '16 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).

  • 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). – Karl Yngve Lervåg Oct 9 '15 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
  • 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? – Wildcard Oct 9 '15 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. – Sakari Cajanus Oct 10 '15 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. – Karl Yngve Lervåg Oct 11 '15 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. – Sakari Cajanus Oct 11 '15 at 16:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.