Files

664 lines
24 KiB
VimL

" ingo/collections.vim: Functions to operate on collections.
"
" DEPENDENCIES:
"
" Copyright: (C) 2011-2021 Ingo Karkat
" The VIM LICENSE applies to this script; see ':help copyright'.
"
" Maintainer: Ingo Karkat <ingo@karkat.de>
function! ingo#collections#ToDict( list, ... )
"******************************************************************************
"* PURPOSE:
" Convert a:list to a Dictionary, with each list element becoming a key (and
" the unimportant value is 1).
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:list List of keys.
" a:emptyValue Optional value for items in a:list that yield an empty
" string, which (in Vim versions prior to 7.4.1707) cannot be
" used as a Dictionary key.
" If omitted, empty values are not included in the Dictionary.
"* RETURN VALUES:
" A new Dictionary with keys taken from a:list.
"* SEE ALSO:
" ingo#dict#FromKeys() allows to specify a default value (here hard-coded to
" 1), but doesn't handle empty keys.
" ingo#dict#count#Items() also creates a Dict from a List, and additionally
" counts the unique values.
"******************************************************************************
let l:dict = {}
for l:item in a:list
let l:key = '' . l:item
if l:key ==# ''
if a:0
let l:dict[a:1] = 1
endif
else
let l:dict[l:key] = 1
endif
endfor
return l:dict
endfunction
function! ingo#collections#Unique( list, ... )
"******************************************************************************
"* PURPOSE:
" Return a list where each element from a:list is contained only once.
" Equality check is done on the string representation, always case-sensitive.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:list List of elements; does not need to be sorted.
" a:emptyValue Optional value for items in a:list that yield an empty
" string. Default is <Nul>.
"* RETURN VALUES:
" Return the string representation of the unique elements of a:list. The order
" of returned elements is undetermined. To maintain the original order, use
" ingo#collections#UniqueStable(). (But this function is likely faster!)
" If a:list is already sorted, use ingo#compat#uniq() instead.
"******************************************************************************
let l:emptyValue = (a:0 ? a:1 : "\<Nul>")
return map(keys(ingo#collections#ToDict(a:list, l:emptyValue)), 'v:val == l:emptyValue ? "" : v:val')
endfunction
function! ingo#collections#UniqueSorted( list )
"******************************************************************************
"* PURPOSE:
" Filter the sorted a:list so that each element is contained only once.
" Equality check is done on the list elements, always case-sensitive.
"* SEE ALSO:
" - ingo#compat#uniq() is a compatibility wrapper around the uniq() function
" introduced in Vim 7.4.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:list Sorted list of elements.
"* RETURN VALUES:
" The order of returned elements is kept.
"******************************************************************************
if len(a:list) < 2
return a:list
endif
let l:previousItem = a:list[0]
let l:result = [a:list[0]]
for l:item in a:list[1:]
if l:item !=# l:previousItem
call add(l:result, l:item)
let l:previousItem = l:item
endif
endfor
return l:result
endfunction
function! ingo#collections#UniqueStable( list, ... )
"******************************************************************************
"* PURPOSE:
" Filter a:list so that each element is contained only once (in its first
" occurrence).
" Equality check is done on the string representation, always case-sensitive.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:list List of elements; does not need to be sorted.
" a:emptyValue Optional value for items in a:list that yield an empty
" string. Default is <Nul>.
"* RETURN VALUES:
" The order of returned elements is kept.
"******************************************************************************
let l:emptyValue = (a:0 ? a:1 : "\<Nul>")
let l:itemDict = {}
let l:result = []
for l:item in a:list
let l:key = '' . (l:item ==# '' ? l:emptyValue : l:item)
if ! has_key(l:itemDict, l:key)
let l:itemDict[l:key] = 1
call add(l:result, l:item)
endif
endfor
return l:result
endfunction
function! s:add( list, expr, keepempty )
if ! a:keepempty && empty(a:expr)
return
endif
return add(a:list, a:expr)
endfunction
function! ingo#collections#SplitKeepSeparators( expr, pattern, ... )
"******************************************************************************
"* PURPOSE:
" Like the built-in |split()|, but keep the separators matched by a:pattern as
" individual items in the result. Also supports zero-width separators like
" \zs. (Though for an unconditional zero-width match, this special function
" would not provide anything that split() doesn't yet provide.)
"
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:expr Text to be split.
" a:pattern Regular expression that specifies the separator text that
" delimits the items.
" a:keepempty When the first or last item is empty it is omitted, unless the
" {keepempty} argument is given and it's non-zero.
" Other empty items are kept when {pattern} matches at least one
" character or when {keepempty} is non-zero.
"* RETURN VALUES:
" List of items: [item1, sep1, item2, sep2, item3, ...]
"******************************************************************************
let l:keepempty = (a:0 ? a:1 : 0)
let l:prevIndex = 0
let l:index = 0
let l:separator = ''
let l:items = []
while ! empty(a:expr)
let l:index = match(a:expr, a:pattern, l:prevIndex)
if l:index == -1
call s:add(l:items, strpart(a:expr, l:prevIndex), l:keepempty)
break
endif
let l:item = strpart(a:expr, l:prevIndex, (l:index - l:prevIndex))
call s:add(l:items, l:item, (l:keepempty || ! empty(l:separator)))
let l:prevIndex = matchend(a:expr, a:pattern, l:prevIndex)
let l:separator = strpart(a:expr, l:index, (l:prevIndex - l:index))
if empty(l:item) && empty(l:separator)
" We have a zero-width separator; consume at least one character to
" avoid the endless loop.
let l:prevIndex = matchend(a:expr, '\_.', l:index)
if l:prevIndex == -1
break
endif
call add(l:items, strpart(a:expr, l:index, (l:prevIndex - l:index)))
else
call s:add(l:items, l:separator, l:keepempty)
endif
endwhile
return l:items
endfunction
function! ingo#collections#SeparateItemsAndSeparators( expr, pattern, ... )
"******************************************************************************
"* PURPOSE:
" Like the built-in |split()|, but return both items and the separators
" matched by a:pattern as two separate Lists.
"* SEE ALSO:
" Use join(ingo#list#Join(items, separators), '') to recombine.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" If a:keepempty (and {pattern} matches at least one character),
" len(items) == len(separators) + 1
"* INPUTS:
" a:expr Text to be split.
" a:pattern Regular expression that specifies the separator text that
" delimits the items.
" a:keepempty When the first or last item is empty it is omitted, unless the
" {keepempty} argument is given and it's non-zero.
" Other empty items are kept when {pattern} matches at least one
" character or when {keepempty} is non-zero.
"* RETURN VALUES:
" List of [items, separators]: [[item1, item2, item3, ...], [sep1, sep2, ...]]
"******************************************************************************
let l:keepempty = (a:0 ? a:1 : 0)
let l:prevIndex = 0
let l:index = 0
let l:separator = ''
let l:items = []
let l:separators = []
while ! empty(a:expr)
let l:index = match(a:expr, a:pattern, l:prevIndex)
if l:index == -1
call s:add(l:items, strpart(a:expr, l:prevIndex), l:keepempty)
break
endif
let l:item = strpart(a:expr, l:prevIndex, (l:index - l:prevIndex))
call s:add(l:items, l:item, (l:keepempty || ! empty(l:separator)))
let l:prevIndex = matchend(a:expr, a:pattern, l:prevIndex)
let l:separator = strpart(a:expr, l:index, (l:prevIndex - l:index))
if empty(l:item) && empty(l:separator)
" We have a zero-width separator; consume at least one character to
" avoid the endless loop.
let l:prevIndex = matchend(a:expr, '\_.', l:index)
if l:prevIndex == -1
break
endif
call add(l:items, strpart(a:expr, l:index, (l:prevIndex - l:index)))
else
call s:add(l:separators, l:separator, l:keepempty)
endif
endwhile
return [l:items, l:separators]
endfunction
function! ingo#collections#SplitIntoMatches( expr, pattern, ... )
"******************************************************************************
"* PURPOSE:
" Like the built-in |split()|, but only return the separators matched by
" a:pattern, and discard the text in between (what is normally returned by
" split()). Optionally it checks that the discarded text only matches
" a:allowedDiscardPattern, and throws an exception if something else would be
" discarded.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:expr Text to be split.
" a:pattern Regular expression that specifies the separator text that
" delimits the items to be discarded.
" a:allowedDiscardPattern Optional regular expression that if given checks all
" discarded text in between separators for match. If
" one does not match, a "SplitIntoMatches: Cannot
" discard TEXT" exception is thrown. To ensure that
" everything matches as separators, and all items are
" empty, pass "" or "^$".
"* RETURN VALUES:
" List of separators matching a:pattern: [sep1, ...].
"******************************************************************************
let l:allowedDiscardPattern = (a:0 ? (empty(a:1) ? '^$' : a:1) : '')
let l:prevIndex = 0
let l:index = 0
let l:separator = ''
let l:separators = []
while ! empty(a:expr)
let l:index = match(a:expr, a:pattern, l:prevIndex)
if l:index == -1
let l:remainder = strpart(a:expr, l:prevIndex)
if ! empty(l:allowedDiscardPattern) && matchstr(l:remainder, '\C' . l:allowedDiscardPattern) !=# l:remainder
throw 'SplitIntoMatches: Cannot discard ' . string(l:remainder)
endif
break
endif
let l:item = strpart(a:expr, l:prevIndex, (l:index - l:prevIndex))
if ! empty(l:allowedDiscardPattern) && matchstr(l:item, '\C' . l:allowedDiscardPattern) !=# l:item
throw 'SplitIntoMatches: Cannot discard ' . string(l:item)
endif
let l:prevIndex = matchend(a:expr, a:pattern, l:prevIndex)
let l:separator = strpart(a:expr, l:index, (l:prevIndex - l:index))
if empty(l:item) && empty(l:separator)
" We have a zero-width separator; consume at least one character to
" avoid the endless loop.
let l:prevIndex = matchend(a:expr, '\_.', l:index)
if l:prevIndex == -1
break
endif
else
call s:add(l:separators, l:separator, 1)
endif
endwhile
return l:separators
endfunction
function! ingo#collections#isort( i1, i2 )
"******************************************************************************
"* PURPOSE:
" Case-insensitive sort function for strings; lowercase comes before
" uppercase.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" i1, i2 Strings.
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
"******************************************************************************
if a:i1 ==# a:i2
return 0
elseif a:i1 ==? a:i2
" If only differ in case, choose lowercase before uppercase.
return a:i1 < a:i2 ? 1 : -1
else
" ASCII-ascending while ignoring case.
return tolower(a:i1) > tolower(a:i2) ? 1 : -1
endif
endfunction
function! ingo#collections#numsort( i1, i2, ... )
"******************************************************************************
"* PURPOSE:
" Numerical (through str2nr()) sort function for numbers; text after the
" number is silently ignored.
"* ALTERNATIVE:
" Since Vim 7.4.341, the built-in sort() function supports a special {func}
" value of "n" for numerical sorting.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" i1, i2 Elements (that are converted to numbers).
" a:base Optional base for conversion.
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
"******************************************************************************
let l:base = (a:0 ? a:1 : 10)
let [l:i1, l:i2] = [str2nr(a:i1, l:base), str2nr(a:i2, l:base)]
return l:i1 == l:i2 ? 0 : l:i1 > l:i2 ? 1 : -1
endfunction
function! ingo#collections#FileModificationTimeSort( i1, i2 )
"******************************************************************************
"* PURPOSE:
" Sort by modification time (|getftime()|); recently modifified files first.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" i1, i2 Elements (assumed to be existing filespecs).
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
"******************************************************************************
return -1 * ingo#collections#memoized#Mapsort('getftime(v:val)', a:i1, a:i2, {'cacheTimeInSeconds': 10})
endfunction
function! ingo#collections#CharacterCountAscSort( i1, i2 )
"******************************************************************************
"* PURPOSE:
" Sort function for strings by number of characters (|strchars()|); shorter
" comes before longer; same length sorts alphabetically ascending.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" i1, i2 Strings.
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
"******************************************************************************
if a:i1 ==# a:i2
return 0
endif
let [l:len1, l:len2] = [ingo#compat#strchars(a:i1), ingo#compat#strchars(a:i2)]
if l:len1 == l:len2
" No difference in length, sort alphabetically ascending.
return a:i1 ># a:i2 ? 1 : -1
else
" Shorter before longer.
return l:len1 > l:len2 ? 1 : -1
endif
endfunction
function! ingo#collections#CharacterCountDescSort( i1, i2 )
"******************************************************************************
"* PURPOSE:
" Sort function for strings by number of characters (|strchars()|); longer
" comes before shorter; same length sorts alphabetically ascending.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" i1, i2 Strings.
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
"******************************************************************************
if a:i1 ==# a:i2
return 0
endif
let [l:len1, l:len2] = [ingo#compat#strchars(a:i1), ingo#compat#strchars(a:i2)]
if l:len1 == l:len2
" No difference in length, sort alphabetically ascending.
return a:i1 ># a:i2 ? 1 : -1
else
" Longer before shorter.
return l:len1 < l:len2 ? 1 : -1
endif
endfunction
function! ingo#collections#StringDisplayWidthAscSort( i1, i2 )
"******************************************************************************
"* PURPOSE:
" Sort function for strings by display width (|strdisplaywidth()|); smaller
" comes before wider; same width sorts alphabetically ascending.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" i1, i2 Strings.
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
"******************************************************************************
if a:i1 ==# a:i2
return 0
endif
let [l:len1, l:len2] = [ingo#compat#strdisplaywidth(a:i1), ingo#compat#strdisplaywidth(a:i2)]
if l:len1 == l:len2
" No difference in width, sort alphabetically ascending.
return a:i1 ># a:i2 ? 1 : -1
else
" Smaller before wider.
return l:len1 > l:len2 ? 1 : -1
endif
endfunction
function! ingo#collections#StringDisplayWidthDescSort( i1, i2 )
"******************************************************************************
"* PURPOSE:
" Sort function for strings by display width (|strdisplaywidth()|); wider
" comes before smaller; same width sorts alphabetically ascending.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" i1, i2 Strings.
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
"******************************************************************************
if a:i1 ==# a:i2
return 0
endif
let [l:len1, l:len2] = [ingo#compat#strdisplaywidth(a:i1), ingo#compat#strdisplaywidth(a:i2)]
if l:len1 == l:len2
" No difference in width, sort alphabetically ascending.
return a:i1 ># a:i2 ? 1 : -1
else
" Wider before smaller.
return l:len1 < l:len2 ? 1 : -1
endif
endfunction
function! ingo#collections#mapsort( string, i1, i2 )
"******************************************************************************
"* PURPOSE:
" Helper sort function for map()ped values. As Vim doesn't have real closures,
" you still need to define your own (two-argument) sort function, but you can
" use this to make that a simple stub.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:string Vimscript expression to be evaluated over [a:i1, a:i2] via map().
" i1, i2 Elements
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
" Note: To reverse the sort order, just multiply this function's return value
" with -1.
"******************************************************************************
let [l:i1, l:i2] = map([a:i1, a:i2], a:string)
return l:i1 == l:i2 ? 0 : l:i1 > l:i2 ? 1 : -1
endfunction
function! ingo#collections#SortOnOneAttribute( attribute, o1, o2, ... )
let l:defaultValue = (a:0 ? a:1 : 0)
let l:a1 = get(a:o1, a:attribute, l:defaultValue)
let l:a2 = get(a:o2, a:attribute, l:defaultValue)
return (l:a1 ==# l:a2 ? 0 : l:a1 ># l:a2 ? 1 : -1)
endfunction
function! ingo#collections#PrioritySort( o1, o2, ... )
return call('ingo#collections#SortOnOneAttribute', ['priority', a:o1, a:o2] + a:000)
endfunction
function! ingo#collections#SortOnTwoAttributes( firstAttribute, secondAttribute, o1, o2, ... )
"******************************************************************************
"* PURPOSE:
" Helper sort function for objects that sort on a:firstAttribute first; if
" that is equal or does not exist on both, sort on a:secondAttribute.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:firstAttribute Primary attribute to sort on.
" a:secondAttribute Secondary attribute to sort on; is used when two objects
" don't have the primary attribute or it is equal.
" a:o1, a:o2 Objects to be compared.
" a:firstDefaultValue Optional default value if a:firstAttribute does not
" exist. Default is 0.
" a:secondDefaultValue
"* RETURN VALUES:
" -1, 0 or 1, as specified by the sort() function.
"******************************************************************************
let l:firstDefaultValue = (a:0 ? a:1 : 0)
if has_key(a:o1, a:firstAttribute) || has_key(a:o2, a:firstAttribute)
let l:first1 = get(a:o1, a:firstAttribute, l:firstDefaultValue)
let l:first2 = get(a:o2, a:firstAttribute, l:firstDefaultValue)
if l:first1 !=# l:first2
return (l:first1 ># l:first2 ? 1 : -1)
endif
endif
let l:secondDefaultValue = (a:0 >= 2 ? a:2 : l:firstDefaultValue)
let l:second1 = get(a:o1, a:secondAttribute, l:secondDefaultValue)
let l:second2 = get(a:o2, a:secondAttribute, l:secondDefaultValue)
return (l:second1 ==# l:second2 ? 0 : l:second1 ># l:second2 ? 1 : -1)
endfunction
function! ingo#collections#SortOnOneListElement( index, l1, l2, ... )
let l:defaultValue = (a:0 ? a:1 : 0)
let l:i1 = get(a:l1, a:index, l:defaultValue)
let l:i2 = get(a:l2, a:index, l:defaultValue)
return (l:i1 ==# l:i2 ? 0 : l:i1 ># l:i2 ? 1 : -1)
endfunction
function! ingo#collections#SortOnFirstListElement( l1, l2 ) abort
return ingo#collections#SortOnOneListElement(0, a:l1, a:l2)
endfunction
function! ingo#collections#SortOnSecondListElement( l1, l2 ) abort
return ingo#collections#SortOnOneListElement(1, a:l1, a:l2)
endfunction
function! ingo#collections#Flatten1( list )
let l:result = []
for l:item in a:list
call ingo#list#AddOrExtend(l:result, l:item)
unlet l:item
endfor
return l:result
endfunction
function! ingo#collections#Flatten( list )
let l:result = []
for l:item in a:list
if type(l:item) == type([])
call extend(l:result, ingo#collections#Flatten(l:item))
else
call add(l:result, l:item)
endif
unlet l:item
endfor
return l:result
endfunction
function! ingo#collections#Partition( list, Predicate )
"******************************************************************************
"* PURPOSE:
" Separate a List / Dictionary into two, depending on whether a:Predicate is
" true for each member of the collection.
"* SEE ALSO:
" - If you want to split off only elements from the start of a List while
" a:Predicate matches (not elements from anywhere in a:list), use
" ingo#list#split#RemoveFromStartWhilePredicate() instead.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:list List or Dictionary.
" a:Predicate Either a Funcref or an expression to be eval()ed.
"* RETURN VALUES:
" List of [in, out], which are disjunct sub-Lists / sub-Dictionaries
" containing the items where a:Predicate is true / is false.
"******************************************************************************
if type(a:list) == type([])
let [l:in, l:out] = [[], []]
for l:item in a:list
if ingo#actions#EvaluateWithValOrFunc(a:Predicate, l:item)
call add(l:in, l:item)
else
call add(l:out, l:item)
endif
endfor
elseif type(a:list) == type({})
let [l:in, l:out] = [{}, {}]
for l:item in items(a:list)
if ingo#actions#EvaluateWithValOrFunc(a:Predicate, l:item)
let l:in[l:item[0]] = l:item[1]
else
let l:out[l:item[0]] = l:item[1]
endif
endfor
else
throw 'ASSERT: Invalid type for list'
endif
return [l:in, l:out]
endfunction
function! ingo#collections#Reduce( list, Callback, initialValue )
"******************************************************************************
"* PURPOSE:
" Reduce a List / Dictionary into a single value by repeatedly applying
" a:Callback to the accumulator (as v:val[0]) and a List element / [key,
" value] Dictionary element (as v:val[1]). Also known as "fold left".
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:list List or Dictionary.
" a:Callback Either a Funcref or an expression to be eval()ed.
" a:initialValue Initial value for the accumulator.
"* RETURN VALUES:
" Accumulated value.
"******************************************************************************
let l:accumulator = a:initialValue
if type(a:list) == type([])
for l:item in a:list
let l:accumulator = ingo#actions#EvaluateWithValOrFunc(a:Callback, l:accumulator, l:item)
endfor
elseif type(a:list) == type({})
for l:item in items(a:list)
let l:accumulator = ingo#actions#EvaluateWithValOrFunc(a:Callback, l:accumulator, l:item)
endfor
else
throw 'ASSERT: Invalid type for list'
endif
return l:accumulator
endfunction
" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax :