Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I want to find the last index of any character in the [abc] set in the abcabc string but the search should start from the end of the string:

" Returns the 0th index but I want the 5th.
let a=match('abcabc', '[abc]')

I skimmed through Vim's "4. Builtin Functions" (:h functions) but the only method that looked promising,the reverse method, only operates on lists. A limitation I don't understand because functions like len were designed to work even with strings, numbers, and lists.

To solve the problem I came up with my following function:

function! s:Rvrs(str)
  let a=len(a:str)      
  let b=a - 1
  let c=''
  while b >= 0
    let c.=a:str[b]
    let b-=1
  endwhile
  return c
endfunction

So I can say let a=match(s:Rvrs('abcabc'), '[abc]').

share|improve this question
    
Maybe I am misunderstanding, but shouldn’t your example of looking for [abc] in abcacb show that the last matching index is 5 (the zero-based index of the last character of abcabc), not 3? What if you look for [abc] in abcd? Should that be 2? Will you need to generalize beyond looking for the last occurrence of a character class (e.g. needing to match something that is longer than a single character)? –  Chris Johnsen Dec 15 '12 at 8:01
    
@ChrisJohnsen The last matching index should be 5, indeed. And yes, once I found the first match I want to have it find further matches. Also a variant that returns all matched indexes at once would be of help. Thanks for pointing out my mistakes. –  Tim Friske Dec 15 '12 at 9:16
add comment

3 Answers

up vote 1 down vote accepted

I looked around, but did not find any built in function that looked like it would do what you want.

You might find the following functions useful though: (variations included for overlapping, and non-overlapping matches starting from the beginning or the end of the string; all of them support multi-character patterns with some restrictions or limitations around uses of \zs and/or \ze)

function! s:AllOverlappableMatches(str, pat)
    " Restriction: a:pat should not use \ze
    let indicies = []
    let index = 0
    let splits = split(a:str, '\ze'.a:pat, 1)
    for segment in splits
        if len(segment) == 0
            call add(indicies, index)
        else
            let index += len(segment)
        endif
    endfor
    return indicies
endfunction
function! s:AllOverlappableMatchesFromEnd(str, pat)
    " Restriction: a:pat should not use \ze
    return reverse(s:AllOverlappableMatches(a:str, a:pat))
endfunction

function! s:AllNonoverlappingMatches(str, pat)
    " If a:pat uses \zs, the returned indicies will be based on that
    " position.
    " If a:pst uses \ze, subsequent matches may re-use characters
    " after \ze that were consumed, but not 'matched' (due to \ze)
    " in earlier matches.
    let indicies = []
    let start = 0
    let next = 0
    while next != -1
        let next = match(a:str, a:pat, start)
        if next != -1
            call add(indicies, next)
            let start = matchend(a:str, a:pat, start)
        endif
    endwhile
    return indicies
endfunction
function! s:AllNonoverlappingMatchesFromEnd(str, pat)
    " If a:pat uses \zs, the returned indicies will be based on that
    " position.
    let str = a:str
    let indicies = []
    let start = len(a:str) - 1
    while start >= 0
        let next = match(str, '.*\zs' . a:pat, start)
        if next != -1
            call add(indicies, next)
            let str = str[ : next - 1]
        endif
        let start -= 1
    endwhile
    return indicies
endfunction

echo s:AllOverlappableMatchesFromEnd('abcabc', '[abc]')
" -> [5, 4, 3, 2, 1, 0]

echo s:AllOverlappableMatchesFromEnd('dabcabc', '[abc]')
" -> [6, 5, 4, 3, 2, 1]

echo s:AllOverlappableMatchesFromEnd('dab - cabc', '[abc]')
" -> [9, 8, 7, 6, 2, 1]

echo s:AllOverlappableMatchesFromEnd('dab - cabce', '[abc]')
" -> [9, 8, 7, 6, 2, 1]

echo s:AllOverlappableMatchesFromEnd('dab - cabc', '[abc]\{2}')
" -> [8, 7, 6, 1]

echo s:AllOverlappableMatches('dab - cabc', '[abc]\{2}')
" -> [1, 6, 7, 8]              0123456789

echo s:AllNonoverlappingMatches('dab - cabc', '[abc]\{2}')
" -> [1, 6, 8]                   0123456789

echo s:AllNonoverlappingMatchesFromEnd('dab - cabca', '[abc]\{2}')
" -> [9, 7, 1]                          0123456789A

echo s:AllNonoverlappingMatchesFromEnd('ab - cabca', '[abc]\{2}')
" -> [8, 6, 0]                          0123456789

echo s:AllNonoverlappingMatchesFromEnd('abcabc', '[abc]\{2}')
" -> [4, 2, 0]                          012345

echo s:AllNonoverlappingMatchesFromEnd(' ab c abcd', '[abc]\{2}')
" -> [7, 1]                             0123456789

echo s:AllNonoverlappingMatchesFromEnd('abcabc', '[abc]\{2}')
" -> [4, 2, 0]                          012345

echo s:AllNonoverlappingMatches( 'abcabcabbc', 'abc')
" -> [0, 3]                       0123456789
echo s:AllNonoverlappingMatchesFromEnd( 'abcdabcabbc', 'abc')
" -> [4, 0]                              0123456789A

" A multi-character, overlappable pattern
echo s:AllOverlappableMatchesFromEnd( 'aaaabcaaac', 'aaa')
" -> [6, 1, 0]                         0123456789
share|improve this answer
add comment

If I read the question,

echo match('abcabc', '.*\zs[abc]')

is an answer: it will return the beginning of the last occurrence of the pattern in the text.

If you want the other occurrences that are before the last one, you'll have to cut the string to work on 'abcabc'[0:start_of_match+len(matched_string)-1] if you want to accept overlaps (which makes no sense in your case as you're looking for [abc], and not abc), or 'abcabc'[0:start_of_match-1] otherwise.

EDIT: Sorry I've missed that it was what Chris Johnsen's code was doing.

share|improve this answer
add comment

Add following function to your .vimrc file

function! Rvrs( str, chars )
    "" 'a:chars' is a string. Convert it to a list.
    let l:chars_list = split( a:chars, '\zs' )

    "" Process 'str' from the end one character each time. 
    "" I remove that character from the list if found.
    let l:i = len( a:str )
    while l:i >= 0 && ! empty( l:chars_list ) 
        let l:i = l:i - 1 
        if index( l:chars_list, strpart( a:str, l:i, 1 ) ) != -1
          let l:dummy = remove( l:chars_list, index( l:chars_list, strpart( a:str, l:i, 1))) 
        endif
    endwhile

    "" If the loop go throught all the string means that couldn't find 
    "" all characters of the list, so return and incorrect code. 
    "" Otherwise the position where the list got empty.
    if i < 0 
        return -1
    else
        return l:i 
    endif
endfunction

and execute it like:

:echo Rvrs( 'abcabcabbc', 'abc' )

That yields:

6
share|improve this answer
    
I already wondered at other times whether the split method could take a string character-wise apart, i.e. without any delimiters getting eaten up. I didn't know about the '\zs' zero-width regex atom. Great, thanks! –  Tim Friske Dec 15 '12 at 9:26
    
But still, is there a Vim built-in function that comes close to my question? –  Tim Friske Dec 15 '12 at 9:28
    
@TimFriske: After your edit this answer doesn't solve it. It gives the last position of any of the letters, not of all of them. So, in the example it gets the last position of letter 'a', because it's the first one with no repeated appearance after it. I don't know of a built-in but perhaps this solution can help you, because it seems simpler now. –  Birei Dec 15 '12 at 11:07
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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