History
Article Edit this page Discussion

Sort lines by a specified word number

From Vim Tips Wiki

(Redirected from VimTip923)
Jump to: navigation, search

Tip 923 Previous Next Created: May 5, 2005 Complexity: intermediate Author: Gerald Lai Version: 6.0


Based on the script by Robert Webb found at :help eval-examples to sort lines, I made some modifications to enable Sort() to sort lines according to word number count.

I defined words to be clusters of non-whitespace characters separated by whitespace characters. For example, the line "while (k == 0)" has 4 words.

To use, get into visual-block mode (hit Ctrl-V from normal mode, or Ctrl-Q in Windows) and highlight the lines you wish to sort. Then hit <F3> or other key mapping of your choice. You will be prompted to enter a number, which is the word number count from the left.

The functionality of Robert Webb's original script is maintained by entering "0" for the word# count.

Example:

ID Name PIN E-mail
172987129 Jon Doe 5787 jondoe@spamme.com
943973494 Don Juan Marco Jr 2001 don@nonexistent.net
439872390 Bob Peter Tomalin 7786 tomalin@nospam.edu

To sort by ID, enter "1" for word number. You can also enter a word number count from right by entering a negative number. Since the Name column has a variable number of words for each line, in order to sort by PIN, enter "-2" to indicate the second word from the right.

After that, you will be prompted to enter the sort order. For example, enter "r" to sort in reverse order.

"Put in vimrc file - tested with GVim 6.3

" use visual block <Ctrl-V> to select lines to sort and hit <F3>
vmap <F3> :call Sort(Prompt("0","1"),Prompt("1","f"),"Strcmp")<CR>

"sort lines function
function Sort(wnum, order, cmp) range
  call SortR(a:firstline, a:lastline, a:wnum, a:order, a:cmp)
  normal gv
endfunction

"sort lines recursively
function SortR(start, end, wnum, order, cmp)
  if a:start >= a:end
    return
  endif
  let partition = a:start - 1
  let middle = partition
  let partstr2 = Words2(getline((a:start + a:end) / 2), a:wnum)
  let i = a:start
  while i <= a:end
    let str = getline(i)
    let partstr = Words2(str, a:wnum)
    if a:order == "r"
      execute "let result = ".a:cmp."(partstr2, partstr)"
    else
      execute "let result = ".a:cmp."(partstr, partstr2)"
    endif
    if result <= 0
      "swap i before partition
      let partition = partition + 1
      if result == 0
        let middle = partition
      endif
      if i != partition
        let str2 = getline(partition)
        call setline(i, str2)
        call setline(partition, str)
      endif
    endif
    let i = i + 1
  endwhile
  "make sure middle element at end of partition
  if middle != partition
    let str = getline(middle)
    let str2 = getline(partition)
    call setline(middle, str2)
    call setline(partition, str)
  endif
  call SortR(a:start, partition - 1, a:wnum, a:order, a:cmp)
  call SortR(partition + 1, a:end, a:wnum, a:order, a:cmp)
endfunction

"determine compare strings
function Words2(line, wnum)
  if a:wnum > 1
    return strpart(a:line, matchend(a:line, "\\s*\\(\\S*\\s*\\)\\{".(a:wnum - 1)."}"))
  elseif a:wnum == 1
    return strpart(a:line, matchend(a:line, "\\s*"))
  elseif a:wnum < 0
    return matchstr(a:line, "\\(\\S*\\s*\\)\\{".(-a:wnum)."}$")
  else
    return a:line
  endif
endfunction

"compare two strings
function Strcmp(str1, str2)
  if a:str1 < a:str2
    return -1
  elseif a:str1 > a:str2
    return 1
  else
    return 0
  endif
endfunction

"prompt user for settings
function Prompt(str, ...)
  let default = a:0 ? a:1 : ""
  if a:str == "0"
    let str = "Sort by which word [(0)whole line (<0)count from right]? "
  elseif a:str == "1"
    let str = "Order [(f)orward (r)everse]? "
  endif
  execute "let ret = input(\"".str."\", \"".default."\")"
  return ret
endfunction

Other references:

[edit] Comments

Here is an improved version. Sort lines by word# count or visual area.

To sort by visual area, select a visual area (with a visual block) and enter "v" when prompted for a word# count.

Please disregard the original code and use the one below:

"Put in vimrc file - tested with GVim 6.3

" use visual block <Ctrl-V> to select lines to sort and hit <F3>
vmap <F3> :call Sort(Prompt("0","1"),Prompt("1","f"),"Strcmp")<CR>

"sort lines function
function Sort(wnum, order, cmp) range
  normal `<
  execute "let startcol = col(\".\")"
  normal `>
  execute "let endcol = col(\".\")"
  if startcol <= endcol
    let firstcol = startcol
    let lastcol = endcol
  else
    let firstcol = endcol
    let lastcol = startcol
  endif
  call SortR(a:firstline, a:lastline, firstcol, lastcol, a:wnum, a:order, a:cmp)
  normal gv
endfunction

"sort lines recursively
function SortR(start, end, first, last, wnum, order, cmp)
  if a:start >= a:end
    return
  endif
  let partition = a:start - 1
  let middle = partition
  let partstr2 = Words2(getline((a:start + a:end) / 2), a:first, a:last, a:wnum)
  let i = a:start
  while i <= a:end
    let str = getline(i)
    let partstr = Words2(str, a:first, a:last, a:wnum)
    if a:order == "r"
      execute "let result = ".a:cmp."(partstr2, partstr)"
    else
      execute "let result = ".a:cmp."(partstr, partstr2)"
    endif
    if result <= 0
      "swap i before partition
      let partition = partition + 1
      if result == 0
        let middle = partition
      endif
      if i != partition
        let str2 = getline(partition)
        call setline(i, str2)
        call setline(partition, str)
      endif
    endif
    let i = i + 1
  endwhile
  "make sure middle element at end of partition
  if middle != partition
    let str = getline(middle)
    let str2 = getline(partition)
    call setline(middle, str2)
    call setline(partition, str)
  endif
  call SortR(a:start, partition - 1, a:first, a:last, a:wnum, a:order, a:cmp)
  call SortR(partition + 1, a:end, a:first, a:last, a:wnum, a:order, a:cmp)
endfunction

"determine compare strings
function Words2(line, first, last, wnum)
  if a:wnum == "v"
    return strpart(a:line, a:first - 1, a:last - a:first + 1)
  elseif a:wnum > 1
    return strpart(a:line, matchend(a:line, "\\s*\\(\\S*\\s*\\)\\{".(a:wnum - 1)."}"))
  elseif a:wnum == 1
    return strpart(a:line, matchend(a:line, "\\s*"))
  elseif a:wnum < 0
    return matchstr(a:line, "\\(\\S*\\s*\\)\\{".(-a:wnum)."}$")
  else
    return a:line
  endif
endfunction

"compare two strings
function Strcmp(str1, str2)
  if a:str1 < a:str2
    return -1
  elseif a:str1 > a:str2
    return 1
  else
    return 0
  endif
endfunction

"prompt user for settings
function Prompt(str, ...)
  let default = a:0 ? a:1 : ""
  if a:str == "0"
    let str = "Sort by which word [(0)whole line (<0)count from right (v)isual]? "
  elseif a:str == "1"
    let str = "Order [(f)orward (r)everse]? "
  endif
  execute "let ret = input(\"".str."\", \"".default."\")"
  return ret
endfunction

'<,'>!sort -n

Simple


Unfortunately, ":'<,'>!sort -n" only sorts whole lines, not according to words or visual area. Works great in Linux/Unix environment but need to get sort.exe for Windows.


I think that using !sort as suggested would be a much better idea, but to accomplish the "sort by nth word" functionality of this tip some preparation of the text is needed. Here's an idea for the algorithm to replace the guts of the sort:

  1. '<,'>s/{pattern built from word number to put sorting word at beginning of line}
  2. '<,'>!sort (specify reverse sort if desired)
  3. '<,'>s/{pattern built from word number to reverse first 's/'}

Example: to sort on the third word in each line:

:'<,'>s/^\(\%(\S\+\s\+\)\)\{2\}\(\S\+\s*\)\(.*\)$/\2\1\3
:'<,'>!sort
:'<,'>s/^\(\S\+\s\+\)\(\%(\S\+\s\+\)\{2\}\)\(.*\)$/\2\1\3

The first command places the "sort word" at the beginning of the line, the second performs the sort (much faster, I imagine, than doing it in a script), and the third restores the "sort word" to its proper place in the line.

There are probably special cases to consider, like when a line does not contain the proper number of words, but I think putting this as the guts of the sort function and adjusting the user input to use it would be a much better method. To use visual selection rather than word number, you could use \%v atoms.

Doing it this way would allow easy expansion to sort by multiple "columns" as well, by moving more than one word to the front of the line.

I'll probably play with it some and post a solution later unless somebody beats me to it.

--Fritzophrenic 18:07, 21 November 2007 (UTC)


Rate this article:

Share this article:

Hubs Highlights International Sites Wikia messages
Entertainment
Gaming
Cartoons & Comics
Science Fiction
Hobbies
Sports
See all...
Grand Theft Auto
Doctor Who
Legend of Zelda Wiki
Terminator Wiki
Everquest II Wiki
Mystery Science Theater 3000
German
Spanish
Chinese
Japanese
More...
Wikia is hiring for several open positions
Send this article to a friend
"Sort lines by a specified word number"
 
 
Hi!

I thought you'd like this page from Wikia!

http://vim.wikia.com

Come check it out!
Send confirmation