# HG changeset patch # User Christian Brabandt # Date 1467664205 -7200 # Node ID cbf052ccb120566b865c7b4dd757559a547d0799 # Parent 0d126e2b685a5c5d42991d0a0300888c0eb13c82 commit https://github.com/vim/vim/commit/b33c7eb5b813cb631b2b0ca5c4029e1788a09bde Author: Bram Moolenaar Date: Mon Jul 4 22:29:49 2016 +0200 patch 7.4.1989 Problem: filter() and map() only accept a string argument. Solution: Implement using a Funcref argument (Yasuhiro Matsumoto, Ken Takata) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1,4 +1,4 @@ -*eval.txt* For Vim version 7.4. Last change: 2016 Jul 02 +*eval.txt* For Vim version 7.4. Last change: 2016 Jul 04 VIM REFERENCE MANUAL by Bram Moolenaar @@ -3521,31 +3521,46 @@ filewritable({file}) *filewritable() directory, and we can write to it, the result is 2. -filter({expr}, {string}) *filter()* - {expr} must be a |List| or a |Dictionary|. - For each item in {expr} evaluate {string} and when the result +filter({expr1}, {expr2}) *filter()* + {expr1} must be a |List| or a |Dictionary|. + For each item in {expr1} evaluate {expr2} and when the result is zero remove the item from the |List| or |Dictionary|. - Inside {string} |v:val| has the value of the current item. - For a |Dictionary| |v:key| has the key of the current item. + {expr2} must be a |string| or |Funcref|. + + if {expr2} is a |string|, inside {expr2} |v:val| has the value + of the current item. For a |Dictionary| |v:key| has the key + of the current item. Examples: > - :call filter(mylist, 'v:val !~ "OLD"') + call filter(mylist, 'v:val !~ "OLD"') < Removes the items where "OLD" appears. > - :call filter(mydict, 'v:key >= 8') + call filter(mydict, 'v:key >= 8') < Removes the items with a key below 8. > - :call filter(var, 0) + call filter(var, 0) < Removes all the items, thus clears the |List| or |Dictionary|. - Note that {string} is the result of expression and is then + Note that {expr2} is the result of expression and is then used as an expression again. Often it is good to use a |literal-string| to avoid having to double backslashes. + If {expr2} is a |Funcref| it must take two arguments: + 1. the key or the index of the current item. + 2. the value of the current item. + The function must return TRUE if the item should be kept. + Example that keeps the odd items of a list: > + func Odd(idx, val) + return a:idx % 2 == 1 + endfunc + call filter(mylist, function('Odd')) +< The operation is done in-place. If you want a |List| or |Dictionary| to remain unmodified make a copy first: > :let l = filter(copy(mylist), 'v:val =~ "KEEP"') -< Returns {expr}, the |List| or |Dictionary| that was filtered. - When an error is encountered while evaluating {string} no - further items in {expr} are processed. +< Returns {expr1}, the |List| or |Dictionary| that was filtered. + When an error is encountered while evaluating {expr2} no + further items in {expr1} are processed. When {expr2} is a + Funcref errors inside a function are ignored, unless it was + defined with the "abort" flag. finddir({name}[, {path}[, {count}]]) *finddir()* @@ -5036,29 +5051,43 @@ luaeval({expr}[, {expr}]) *luaeval() See |lua-luaeval| for more details. {only available when compiled with the |+lua| feature} -map({expr}, {string}) *map()* - {expr} must be a |List| or a |Dictionary|. - Replace each item in {expr} with the result of evaluating - {string}. - Inside {string} |v:val| has the value of the current item. - For a |Dictionary| |v:key| has the key of the current item - and for a |List| |v:key| has the index of the current item. +map({expr1}, {expr2}) *map()* + {expr1} must be a |List| or a |Dictionary|. + Replace each item in {expr1} with the result of evaluating + {expr2}. {expr2} must be a |string| or |Funcref|. + + If {expr2} is a |string|, inside {expr2} |v:val| has the value + of the current item. For a |Dictionary| |v:key| has the key + of the current item and for a |List| |v:key| has the index of + the current item. Example: > :call map(mylist, '"> " . v:val . " <"') < This puts "> " before and " <" after each item in "mylist". - Note that {string} is the result of an expression and is then + Note that {expr2} is the result of an expression and is then used as an expression again. Often it is good to use a |literal-string| to avoid having to double backslashes. You still have to double ' quotes + If {expr2} is a |Funcref| it is called with two arguments: + 1. The key or the index of the current item. + 2. the value of the current item. + The function must return the new value of the item. Example + that changes each value by "key-value": > + func KeyValue(key, val) + return a:key . '-' . a:val + endfunc + call map(myDict, function('KeyValue')) +< The operation is done in-place. If you want a |List| or |Dictionary| to remain unmodified make a copy first: > :let tlist = map(copy(mylist), ' v:val . "\t"') -< Returns {expr}, the |List| or |Dictionary| that was filtered. - When an error is encountered while evaluating {string} no - further items in {expr} are processed. +< Returns {expr1}, the |List| or |Dictionary| that was filtered. + When an error is encountered while evaluating {expr2} no + further items in {expr1} are processed. When {expr2} is a + Funcref errors inside a function are ignored, unless it was + defined with the "abort" flag. maparg({name}[, {mode} [, {abbr} [, {dict}]]]) *maparg()* diff --git a/src/Makefile b/src/Makefile --- a/src/Makefile +++ b/src/Makefile @@ -2031,6 +2031,7 @@ test_arglist \ test_farsi \ test_feedkeys \ test_file_perm \ + test_filter_map \ test_fnamemodify \ test_glob2regpat \ test_goto \ diff --git a/src/eval.c b/src/eval.c --- a/src/eval.c +++ b/src/eval.c @@ -6375,7 +6375,8 @@ tv_equal( return TRUE; } - /* For VAR_FUNC and VAR_PARTIAL only compare the function name. */ + /* For VAR_FUNC and VAR_PARTIAL compare the function name, bound dict and + * arguments. */ if ((tv1->v_type == VAR_FUNC || (tv1->v_type == VAR_PARTIAL && tv1->vval.v_partial != NULL)) && (tv2->v_type == VAR_FUNC @@ -11852,7 +11853,7 @@ findfilendir( } static void filter_map(typval_T *argvars, typval_T *rettv, int map); -static int filter_map_one(typval_T *tv, char_u *expr, int map, int *remp); +static int filter_map_one(typval_T *tv, typval_T *expr, int map, int *remp); /* * Implementation of map() and filter(). @@ -11860,8 +11861,7 @@ static int filter_map_one(typval_T *tv, static void filter_map(typval_T *argvars, typval_T *rettv, int map) { - char_u buf[NUMBUFLEN]; - char_u *expr; + typval_T *expr; listitem_T *li, *nli; list_T *l = NULL; dictitem_T *di; @@ -11896,14 +11896,13 @@ filter_map(typval_T *argvars, typval_T * return; } - expr = get_tv_string_buf_chk(&argvars[1], buf); + expr = &argvars[1]; /* On type errors, the preceding call has already displayed an error * message. Avoid a misleading error message for an empty string that * was not passed as argument. */ - if (expr != NULL) + if (expr->v_type != VAR_UNKNOWN) { prepare_vimvar(VV_VAL, &save_val); - expr = skipwhite(expr); /* We reset "did_emsg" to be able to detect whether an error * occurred during evaluation of the expression. */ @@ -11975,21 +11974,44 @@ filter_map(typval_T *argvars, typval_T * } static int -filter_map_one(typval_T *tv, char_u *expr, int map, int *remp) +filter_map_one(typval_T *tv, typval_T *expr, int map, int *remp) { typval_T rettv; + typval_T argv[3]; char_u *s; int retval = FAIL; + int dummy; copy_tv(tv, &vimvars[VV_VAL].vv_tv); - s = expr; - if (eval1(&s, &rettv, TRUE) == FAIL) - goto theend; - if (*s != NUL) /* check for trailing chars after expr */ - { - EMSG2(_(e_invexpr2), s); - clear_tv(&rettv); - goto theend; + argv[0] = vimvars[VV_KEY].vv_tv; + argv[1] = vimvars[VV_VAL].vv_tv; + s = expr->vval.v_string; + if (expr->v_type == VAR_FUNC) + { + if (call_func(s, (int)STRLEN(s), + &rettv, 2, argv, 0L, 0L, &dummy, TRUE, NULL, NULL) == FAIL) + goto theend; + } + else if (expr->v_type == VAR_PARTIAL) + { + partial_T *partial = expr->vval.v_partial; + + s = partial->pt_name; + if (call_func(s, (int)STRLEN(s), + &rettv, 2, argv, 0L, 0L, &dummy, TRUE, partial, NULL) + == FAIL) + goto theend; + } + else + { + s = skipwhite(s); + if (eval1(&s, &rettv, TRUE) == FAIL) + goto theend; + if (*s != NUL) /* check for trailing chars after expr */ + { + EMSG2(_(e_invexpr2), s); + goto theend; + } } if (map) { diff --git a/src/testdir/test_alot.vim b/src/testdir/test_alot.vim --- a/src/testdir/test_alot.vim +++ b/src/testdir/test_alot.vim @@ -12,6 +12,7 @@ source test_expand_dllpath.vim source test_feedkeys.vim source test_fnamemodify.vim source test_file_perm.vim +source test_filter_map.vim source test_glob2regpat.vim source test_goto.vim source test_help_tagjump.vim diff --git a/src/testdir/test_filter_map.vim b/src/testdir/test_filter_map.vim new file mode 100644 --- /dev/null +++ b/src/testdir/test_filter_map.vim @@ -0,0 +1,77 @@ +" Test filter() and map() + +" list with expression string +func Test_filter_map_list_expr_string() + " filter() + call assert_equal([2, 3, 4], filter([1, 2, 3, 4], 'v:val > 1')) + call assert_equal([3, 4], filter([1, 2, 3, 4], 'v:key > 1')) + + " map() + call assert_equal([2, 4, 6, 8], map([1, 2, 3, 4], 'v:val * 2')) + call assert_equal([0, 2, 4, 6], map([1, 2, 3, 4], 'v:key * 2')) +endfunc + +" dict with expression string +func Test_filter_map_dict_expr_string() + let dict = {"foo": 1, "bar": 2, "baz": 3} + + " filter() + call assert_equal({"bar": 2, "baz": 3}, filter(copy(dict), 'v:val > 1')) + call assert_equal({"foo": 1, "baz": 3}, filter(copy(dict), 'v:key > "bar"')) + + " map() + call assert_equal({"foo": 2, "bar": 4, "baz": 6}, map(copy(dict), 'v:val * 2')) + call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), 'v:key[0]')) +endfunc + +" list with funcref +func Test_filter_map_list_expr_funcref() + " filter() + func! s:filter1(index, val) abort + return a:val > 1 + endfunc + call assert_equal([2, 3, 4], filter([1, 2, 3, 4], function('s:filter1'))) + + func! s:filter2(index, val) abort + return a:index > 1 + endfunc + call assert_equal([3, 4], filter([1, 2, 3, 4], function('s:filter2'))) + + " map() + func! s:filter3(index, val) abort + return a:val * 2 + endfunc + call assert_equal([2, 4, 6, 8], map([1, 2, 3, 4], function('s:filter3'))) + + func! s:filter4(index, val) abort + return a:index * 2 + endfunc + call assert_equal([0, 2, 4, 6], map([1, 2, 3, 4], function('s:filter4'))) +endfunc + +" dict with funcref +func Test_filter_map_dict_expr_funcref() + let dict = {"foo": 1, "bar": 2, "baz": 3} + + " filter() + func! s:filter1(key, val) abort + return a:val > 1 + endfunc + call assert_equal({"bar": 2, "baz": 3}, filter(copy(dict), function('s:filter1'))) + + func! s:filter2(key, val) abort + return a:key > "bar" + endfunc + call assert_equal({"foo": 1, "baz": 3}, filter(copy(dict), function('s:filter2'))) + + " map() + func! s:filter3(key, val) abort + return a:val * 2 + endfunc + call assert_equal({"foo": 2, "bar": 4, "baz": 6}, map(copy(dict), function('s:filter3'))) + + func! s:filter4(key, val) abort + return a:key[0] + endfunc + call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), function('s:filter4'))) +endfunc diff --git a/src/testdir/test_partial.vim b/src/testdir/test_partial.vim --- a/src/testdir/test_partial.vim +++ b/src/testdir/test_partial.vim @@ -14,6 +14,14 @@ func MySort(up, one, two) return a:one < a:two ? 1 : -1 endfunc +func MyMap(sub, index, val) + return a:val - a:sub +endfunc + +func MyFilter(threshold, index, val) + return a:val > a:threshold +endfunc + func Test_partial_args() let Cb = function('MyFunc', ["foo", "bar"]) @@ -36,6 +44,16 @@ func Test_partial_args() call assert_equal([1, 2, 3], sort([3, 1, 2], Sort)) let Sort = function('MySort', [0]) call assert_equal([3, 2, 1], sort([3, 1, 2], Sort)) + + let Map = function('MyMap', [2]) + call assert_equal([-1, 0, 1], map([1, 2, 3], Map)) + let Map = function('MyMap', [3]) + call assert_equal([-2, -1, 0], map([1, 2, 3], Map)) + + let Filter = function('MyFilter', [1]) + call assert_equal([2, 3], filter([1, 2, 3], Filter)) + let Filter = function('MyFilter', [2]) + call assert_equal([3], filter([1, 2, 3], Filter)) endfunc func MyDictFunc(arg1, arg2) dict @@ -60,6 +78,9 @@ func Test_partial_dict() call assert_equal("hello/xxx/yyy", Cb("xxx", "yyy")) call assert_fails('Cb("fff")', 'E492:') + let Cb = function('MyDictFunc', dict) + call assert_equal({"foo": "hello/foo/1", "bar": "hello/bar/2"}, map({"foo": 1, "bar": 2}, Cb)) + let dict = {"tr": function('tr', ['hello', 'h', 'H'])} call assert_equal("Hello", dict.tr()) endfunc diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 1989, +/**/ 1988, /**/ 1987,