changeset 29712:bdb31515f78b v9.0.0196

patch 9.0.0196: finding value in list may require a for loop Commit: https://github.com/vim/vim/commit/b218655d5a485f5b193fb18d7240837d42b89812 Author: Yegappan Lakshmanan <yegappan@yahoo.com> Date: Sat Aug 13 13:09:20 2022 +0100 patch 9.0.0196: finding value in list may require a for loop Problem: Finding value in list may require a for loop. Solution: Add indexof(). (Yegappan Lakshmanan, closes https://github.com/vim/vim/issues/10903)
author Bram Moolenaar <Bram@vim.org>
date Sat, 13 Aug 2022 14:15:05 +0200
parents 4069513e8995
children 1f203818f2bd
files runtime/doc/builtin.txt runtime/doc/usr_41.txt src/evalfunc.c src/testdir/test_blob.vim src/testdir/test_listdict.vim src/testdir/test_vim9_builtin.vim src/version.c
diffstat 7 files changed, 265 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -291,6 +291,8 @@ iconv({expr}, {from}, {to})	String	conve
 indent({lnum})			Number	indent of line {lnum}
 index({object}, {expr} [, {start} [, {ic}]])
 				Number	index in {object} where {expr} appears
+indexof({object}, {expr} [, {opts}]])
+				Number	index in {object} where {expr} is true
 input({prompt} [, {text} [, {completion}]])
 				String	get input from the user
 inputdialog({prompt} [, {text} [, {cancelreturn}]])
@@ -4730,19 +4732,25 @@ indent({lnum})	The result is a Number, w
 			GetLnum()->indent()
 
 index({object}, {expr} [, {start} [, {ic}]])			*index()*
+		Find {expr} in {object} and return its index.  See
+		|filterof()| for using a lambda to select the item.
+
 		If {object} is a |List| return the lowest index where the item
 		has a value equal to {expr}.  There is no automatic
 		conversion, so the String "4" is different from the Number 4.
 		And the number 4 is different from the Float 4.0.  The value
-		of 'ignorecase' is not used here, case always matters.
+		of 'ignorecase' is not used here, case matters as indicated by
+		the {ic} argument.
 
 		If {object} is |Blob| return the lowest index where the byte
 		value is equal to {expr}.
 
 		If {start} is given then start looking at the item with index
 		{start} (may be negative for an item relative to the end).
+
 		When {ic} is given and it is |TRUE|, ignore case.  Otherwise
 		case must match.
+
 		-1 is returned when {expr} is not found in {object}.
 		Example: >
 			:let idx = index(words, "the")
@@ -4751,6 +4759,44 @@ index({object}, {expr} [, {start} [, {ic
 <		Can also be used as a |method|: >
 			GetObject()->index(what)
 
+indexof({object}, {expr} [, {opt}])			*indexof()*
+		{object} must be a |List| or a |Blob|.
+		If {object} is a |List|, evaluate {expr} for each item in the
+		List until the expression returns v:true and return the index
+		of this item.
+
+		If {object} is a |Blob| evaluate {expr} for each byte in the
+		Blob until the expression returns v:true and return the index
+		of this byte.
+
+		{expr} must be a |string| or |Funcref|.
+
+		If {expr} is a |string|: If {object} is a |List|, inside
+		{expr} |v:key| has the index of the current List item and
+		|v:val| has the value of the item.  If {object} is a |Blob|,
+		inside {expr} |v:key| has the index of the current byte and
+		|v:val| has the byte value.
+
+		If {expr} 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 is found and the
+		search should stop.
+
+		The optional argument {opt} is a Dict and supports the
+		following items:
+		    start	start evaluating {expr} at the item with index
+				{start} (may be negative for an item relative
+				to the end).
+		Returns -1 when {expr} evaluates to v:false for all the items.
+		Example: >
+			:let l = [#{n: 10}, #{n: 20}, #{n: 30]]
+			:let idx = indexof(l, "v:val.n == 20")
+			:let idx = indexof(l, {i, v -> v.n == 30})
+
+<		Can also be used as a |method|: >
+			mylist->indexof(expr)
+
 input({prompt} [, {text} [, {completion}]])		*input()*
 		The result is a String, which is whatever the user typed on
 		the command-line.  The {prompt} argument is either a prompt
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -792,14 +792,16 @@ List manipulation:					*list-functions*
 	reduce()		reduce a List to a value
 	slice()			take a slice of a List
 	sort()			sort a List
-	reverse()		reverse the order of a List
+	reverse()		reverse the order of a List or Blob
 	uniq()			remove copies of repeated adjacent items
 	split()			split a String into a List
 	join()			join List items into a String
 	range()			return a List with a sequence of numbers
 	string()		String representation of a List
 	call()			call a function with List as arguments
-	index()			index of a value in a List
+	index()			index of a value in a List or Blob
+	indexof()		index in a List or Blob where an expression
+				evaluates to true 
 	max()			maximum value in a List
 	min()			minimum value in a List
 	count()			count number of times a value appears in a List
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -79,6 +79,7 @@ static void f_hlID(typval_T *argvars, ty
 static void f_hlexists(typval_T *argvars, typval_T *rettv);
 static void f_hostname(typval_T *argvars, typval_T *rettv);
 static void f_index(typval_T *argvars, typval_T *rettv);
+static void f_indexof(typval_T *argvars, typval_T *rettv);
 static void f_input(typval_T *argvars, typval_T *rettv);
 static void f_inputdialog(typval_T *argvars, typval_T *rettv);
 static void f_inputlist(typval_T *argvars, typval_T *rettv);
@@ -1037,6 +1038,7 @@ static argcheck_T arg23_get[] = {arg_get
 static argcheck_T arg14_glob[] = {arg_string, arg_bool, arg_bool, arg_bool};
 static argcheck_T arg25_globpath[] = {arg_string, arg_string, arg_bool, arg_bool, arg_bool};
 static argcheck_T arg24_index[] = {arg_list_or_blob, arg_item_of_prev, arg_number, arg_bool};
+static argcheck_T arg23_index[] = {arg_list_or_blob, arg_filter_func, arg_dict_any};
 static argcheck_T arg23_insert[] = {arg_list_or_blob, arg_item_of_prev, arg_number};
 static argcheck_T arg1_len[] = {arg_len1};
 static argcheck_T arg3_libcall[] = {arg_string, arg_string, arg_string_or_nr};
@@ -1995,6 +1997,8 @@ static funcentry_T global_functions[] =
 			ret_number,	    f_indent},
     {"index",		2, 4, FEARG_1,	    arg24_index,
 			ret_number,	    f_index},
+    {"indexof",		2, 3, FEARG_1,	    arg23_index,
+			ret_number,	    f_indexof},
     {"input",		1, 3, FEARG_1,	    arg3_string,
 			ret_string,	    f_input},
     {"inputdialog",	1, 3, FEARG_1,	    arg3_string,
@@ -6789,6 +6793,136 @@ f_index(typval_T *argvars, typval_T *ret
     }
 }
 
+/*
+ * Evaluate 'expr' with the v:key and v:val arguments and return the result.
+ * The expression is expected to return a boolean value.  The caller should set
+ * the VV_KEY and VV_VAL vim variables before calling this function.
+ */
+    static int
+indexof_eval_expr(typval_T *expr)
+{
+    typval_T	argv[3];
+    typval_T	newtv;
+    varnumber_T	found;
+    int		error = FALSE;
+
+    argv[0] = *get_vim_var_tv(VV_KEY);
+    argv[1] = *get_vim_var_tv(VV_VAL);
+    newtv.v_type = VAR_UNKNOWN;
+
+    if (eval_expr_typval(expr, argv, 2, &newtv) == FAIL)
+	return FALSE;
+
+    found = tv_get_bool_chk(&newtv, &error);
+
+    return error ? FALSE : found;
+}
+
+/*
+ * "indexof()" function
+ */
+    static void
+f_indexof(typval_T *argvars, typval_T *rettv)
+{
+    list_T	*l;
+    listitem_T	*item;
+    blob_T	*b;
+    long	startidx = 0;
+    long	idx = 0;
+    typval_T	save_val;
+    typval_T	save_key;
+    int		save_did_emsg;
+
+    rettv->vval.v_number = -1;
+
+    if (check_for_list_or_blob_arg(argvars, 0) == FAIL
+	    || check_for_string_or_func_arg(argvars, 1) == FAIL
+	    || check_for_opt_dict_arg(argvars, 2) == FAIL)
+	return;
+
+    if ((argvars[1].v_type == VAR_STRING && argvars[1].vval.v_string == NULL)
+	    || (argvars[1].v_type == VAR_FUNC
+		&& argvars[1].vval.v_partial == NULL))
+	return;
+
+    if (argvars[2].v_type == VAR_DICT)
+	startidx = dict_get_number_def(argvars[2].vval.v_dict, "startidx", 0);
+
+    prepare_vimvar(VV_VAL, &save_val);
+    prepare_vimvar(VV_KEY, &save_key);
+
+    // We reset "did_emsg" to be able to detect whether an error occurred
+    // during evaluation of the expression.
+    save_did_emsg = did_emsg;
+    did_emsg = FALSE;
+
+    if (argvars[0].v_type == VAR_BLOB)
+    {
+	b = argvars[0].vval.v_blob;
+	if (b == NULL)
+	    goto theend;
+	if (startidx < 0)
+	{
+	    startidx = blob_len(b) + startidx;
+	    if (startidx < 0)
+		startidx = 0;
+	}
+
+	set_vim_var_type(VV_KEY, VAR_NUMBER);
+	set_vim_var_type(VV_VAL, VAR_NUMBER);
+
+	for (idx = startidx; idx < blob_len(b); ++idx)
+	{
+	    set_vim_var_nr(VV_KEY, idx);
+	    set_vim_var_nr(VV_VAL, blob_get(b, idx));
+
+	    if (indexof_eval_expr(&argvars[1]))
+	    {
+		rettv->vval.v_number = idx;
+		break;
+	    }
+	}
+    }
+    else
+    {
+	l = argvars[0].vval.v_list;
+	if (l == NULL)
+	    goto theend;
+
+	CHECK_LIST_MATERIALIZE(l);
+
+	if (startidx == 0)
+	    item = l->lv_first;
+	else
+	{
+	    // Start at specified item.  Use the cached index that list_find()
+	    // sets, so that a negative number also works.
+	    item = list_find(l, startidx);
+	    if (item != NULL)
+		idx = l->lv_u.mat.lv_idx;
+	}
+
+	set_vim_var_type(VV_KEY, VAR_NUMBER);
+
+	for ( ; item != NULL; item = item->li_next, ++idx)
+	{
+	    set_vim_var_nr(VV_KEY, idx);
+	    copy_tv(&item->li_tv, get_vim_var_tv(VV_VAL));
+
+	    if (indexof_eval_expr(&argvars[1]))
+	    {
+		rettv->vval.v_number = idx;
+		break;
+	    }
+	}
+    }
+
+theend:
+    restore_vimvar(VV_KEY, &save_key);
+    restore_vimvar(VV_VAL, &save_val);
+    did_emsg |= save_did_emsg;
+}
+
 static int inputsecret_flag = 0;
 
 /*
--- a/src/testdir/test_blob.vim
+++ b/src/testdir/test_blob.vim
@@ -764,4 +764,30 @@ func Test_blob_alloc_failure()
   call assert_equal(0, x)
 endfunc
 
+" Test for the indexof() function
+func Test_indexof()
+  let b = 0zdeadbeef
+  call assert_equal(0, indexof(b, {i, v -> v == 0xde}))
+  call assert_equal(3, indexof(b, {i, v -> v == 0xef}))
+  call assert_equal(-1, indexof(b, {i, v -> v == 0x1}))
+  call assert_equal(1, indexof(b, "v:val == 0xad"))
+  call assert_equal(-1, indexof(b, "v:val == 0xff"))
+
+  call assert_equal(-1, indexof(0z, "v:val == 0x0"))
+  call assert_equal(-1, indexof(test_null_blob(), "v:val == 0xde"))
+  call assert_equal(-1, indexof(b, test_null_string()))
+  call assert_equal(-1, indexof(b, test_null_function()))
+
+  let b = 0z01020102
+  call assert_equal(1, indexof(b, "v:val == 0x02", #{startidx: 0}))
+  call assert_equal(2, indexof(b, "v:val == 0x01", #{startidx: -2}))
+  call assert_equal(-1, indexof(b, "v:val == 0x01", #{startidx: 5}))
+  call assert_equal(0, indexof(b, "v:val == 0x01", #{startidx: -5}))
+  call assert_equal(0, indexof(b, "v:val == 0x01", test_null_dict()))
+
+  " failure cases
+  call assert_fails('let i = indexof(b, "val == 0xde")', 'E121:')
+  call assert_fails('let i = indexof(b, {})', 'E1256:')
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/testdir/test_listdict.vim
+++ b/src/testdir/test_listdict.vim
@@ -1446,4 +1446,49 @@ func Test_null_dict()
   unlockvar d
 endfunc
 
+" Test for the indexof() function
+func Test_indexof()
+  let l = [#{color: 'red'}, #{color: 'blue'}, #{color: 'green'}]
+  call assert_equal(0, indexof(l, {i, v -> v.color == 'red'}))
+  call assert_equal(2, indexof(l, {i, v -> v.color == 'green'}))
+  call assert_equal(-1, indexof(l, {i, v -> v.color == 'grey'}))
+  call assert_equal(1, indexof(l, "v:val.color == 'blue'"))
+  call assert_equal(-1, indexof(l, "v:val.color == 'cyan'"))
+
+  let l = [#{n: 10}, #{n: 10}, #{n: 20}]
+  call assert_equal(0, indexof(l, "v:val.n == 10", #{startidx: 0}))
+  call assert_equal(1, indexof(l, "v:val.n == 10", #{startidx: -2}))
+  call assert_equal(-1, indexof(l, "v:val.n == 10", #{startidx: 4}))
+  call assert_equal(-1, indexof(l, "v:val.n == 10", #{startidx: -4}))
+  call assert_equal(0, indexof(l, "v:val.n == 10", test_null_dict()))
+
+  call assert_equal(-1, indexof([], {i, v -> v == 'a'}))
+  call assert_equal(-1, indexof(test_null_list(), {i, v -> v == 'a'}))
+  call assert_equal(-1, indexof(l, test_null_string()))
+  call assert_equal(-1, indexof(l, test_null_function()))
+
+  " failure cases
+  call assert_fails('let i = indexof(l, "v:val == ''cyan''")', 'E735:')
+  call assert_fails('let i = indexof(l, "color == ''cyan''")', 'E121:')
+  call assert_fails('let i = indexof(l, {})', 'E1256:')
+  call assert_fails('let i = indexof({}, "v:val == 2")', 'E1226:')
+  call assert_fails('let i = indexof([], "v:val == 2", [])', 'E1206:')
+
+  func TestIdx(k, v)
+    return a:v.n == 20
+  endfunc
+  call assert_equal(2, indexof(l, function("TestIdx")))
+  delfunc TestIdx
+  func TestIdx(k, v)
+    return {}
+  endfunc
+  call assert_fails('let i = indexof(l, function("TestIdx"))', 'E728:')
+  delfunc TestIdx
+  func TestIdx(k, v)
+    throw "IdxError"
+  endfunc
+  call assert_fails('let i = indexof(l, function("TestIdx"))', 'E605:')
+  delfunc TestIdx
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/testdir/test_vim9_builtin.vim
+++ b/src/testdir/test_vim9_builtin.vim
@@ -2066,6 +2066,13 @@ def Test_index()
   v9.CheckDefAndScriptFailure(['index(0z1020, 10, 1, 2)'], ['E1013: Argument 4: type mismatch, expected bool but got number', 'E1212: Bool required for argument 4'])
 enddef
 
+def Test_indexof()
+  var l = [{color: 'red'}, {color: 'blue'}, {color: 'green'}]
+  indexof(l, (i, v) => v.color == 'green')->assert_equal(2)
+  var b = 0zdeadbeef
+  indexof(b, "v:val == 0xef")->assert_equal(3)
+enddef
+
 def Test_input()
   v9.CheckDefAndScriptFailure(['input(5)'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1'])
   v9.CheckDefAndScriptFailure(['input(["a"])'], ['E1013: Argument 1: type mismatch, expected string but got list<string>', 'E1174: String required for argument 1'])
--- a/src/version.c
+++ b/src/version.c
@@ -736,6 +736,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    196,
+/**/
     195,
 /**/
     194,