# HG changeset patch
# User Nicholas R. Jankowski <jankowski.nicholas@gmail.com>
# Date 1641419283 18000
#      Wed Jan 05 16:48:03 2022 -0500
# Node ID 2472ab2c43dcc54b4a66278a7365986f029cb8e9
# Parent  00ab5e929111740c0682885f6c86a2793078c149
Implement ismember legacy option for returned index values (bug #55659)

* scripts/set/ismember.m: Enable 'legacy' option by reversing sort order for
primary, complex, and 'rows' codepaths. Update input validation. Update
docstring and add related BISTs.
* etc/NEWS.8.md: Note change in compatibility section

diff -r 00ab5e929111 -r 2472ab2c43dc etc/NEWS.8.md
--- a/etc/NEWS.8.md	Wed Jan 05 19:30:43 2022 +0100
+++ b/etc/NEWS.8.md	Wed Jan 05 16:48:03 2022 -0500
@@ -17,6 +17,11 @@
 
 ### Matlab compatibility
 
+- The function `ismember` now supports the 'legacy' option for determining
+which index position to return when there are multiple matches.  The default
+behavior is now to return the first instance.  The 'legacy' option returns the
+location of last instance instead, and was the default behavior in previous
+Octave versions.
 
 ### Alphabetical list of new functions added in Octave 8
 
diff -r 00ab5e929111 -r 2472ab2c43dc scripts/set/ismember.m
--- a/scripts/set/ismember.m	Wed Jan 05 19:30:43 2022 +0100
+++ b/scripts/set/ismember.m	Wed Jan 05 16:48:03 2022 -0500
@@ -27,21 +27,29 @@
 ## @deftypefn  {} {@var{tf} =} ismember (@var{a}, @var{s})
 ## @deftypefnx {} {@var{tf} =} ismember (@var{a}, @var{s}, "rows")
 ## @deftypefnx {} {[@var{tf}, @var{s_idx}] =} ismember (@dots{})
+## @deftypefnx {} {[@var{tf}, @var{s_idx}] =} ismember (@dots{}, "legacy")
 ##
 ## Return a logical matrix @var{tf} with the same shape as @var{a} which is
 ## true (1) if the element in @var{a} is found in @var{s} and false (0) if it
 ## is not.
 ##
-## If a second output argument is requested then the index into @var{s} of each
-## matching element is also returned.
+## If a second output argument is requested then the index into @var{s} of
+## each matching element is also returned.  In the case of multiple matches,
+## the index of the first match is returned.  If the input option "legacy"
+## is provided, then the behavior is changed to be compatible with that of
+## @sc{Matlab} 2012a and earlier, and the last match is returned instead.
+## For example:
 ##
 ## @example
 ## @group
 ## a = [3, 10, 1];
-## s = [0:9];
+## s = [0, 0, 1, 1, 2, 2, 3, 3, 4, 4];
 ## [tf, s_idx] = ismember (a, s)
 ##      @result{} tf = [1, 0, 1]
-##      @result{} s_idx = [4, 0, 2]
+##      @result{} s_idx = [7, 0, 3]
+## [tf2, s_idx2] = ismember (a, s, "legacy")
+##      @result{} tf2 = [1, 0, 1]
+##      @result{} s_idx2 = [8, 0, 4]
 ## @end group
 ## @end example
 ##
@@ -57,8 +65,8 @@
 ## @end group
 ## @end example
 ##
-## If the optional third argument @qcode{"rows"} is given then compare rows
-## in @var{a} with rows in @var{s}.  The inputs must be 2-D matrices with the
+## If the optional argument @qcode{"rows"} is given then compare rows in
+## @var{a} with rows in @var{s}.  The inputs must be 2-D matrices with the
 ## same number of columns to use this option.
 ##
 ## @example
@@ -71,18 +79,27 @@
 ## @end group
 ## @end example
 ##
+## Matlab compatibility note: The @qcode{"legacy"} option currently only
+## affects whether the first or last instance of a match is returned in
+## @var{s_idx}.  Other impacts, including changes to char array whitespace
+## handling and handling of certain classes are not yet implemented.
+##
 ## @seealso{lookup, unique, union, intersect, setdiff, setxor}
 ## @end deftypefn
 
 function [tf, s_idx] = ismember (a, s, varargin)
 
-  if (nargin < 2 || nargin > 3)
+  if (nargin < 2 || nargin > 4)
     print_usage ();
   endif
 
+  by_rows = any (strcmpi ("rows", varargin));
+  optlegacy = any (strcmpi ("legacy", varargin));
+
   ## lookup() uses absolute values for complex input so we handle the
-  ## real and imaginary parts separately (bug #52437)
-  if (iscomplex (a) || iscomplex (s))
+  ## real and imaginary parts separately (bug #52437) unless 'rows', which
+  ## does not use lookup() so it can be handled normally.
+  if (! by_rows && (iscomplex (a) || iscomplex (s)))
     real_argout = cell (nargout, 1);
     imag_argout = cell (nargout, 1);
     [real_argout{:}] = ismember (real (a), real (s), varargin{:});
@@ -90,7 +107,11 @@
     tf = real_argout{1} & imag_argout{1};
     if (isargout (2))
       s_idx = zeros (size (real_argout{2}));
-      s_idx(tf) = min (real_argout{2}(tf), imag_argout{2}(tf));
+      if (optlegacy)
+        s_idx(tf) = min (real_argout{2}(tf), imag_argout{2}(tf));
+      else
+        s_idx(tf) = max (real_argout{2}(tf), imag_argout{2}(tf));
+      endif
     endif
     return;
   endif
@@ -115,18 +136,23 @@
     a = double (a);
   endif
 
-  if (any (strcmp ("stable", varargin)) || any (strcmp ("sorted", varargin)))
-    error ('ismember: "stable" or "sorted" are not valid options');
+  if (! isempty (varargin))
+    if (! cellfun ('ischar', varargin));
+      error ("ismember: all options must be strings");
+    elseif ! all (strcmpi (varargin, "rows") | strcmpi (varargin, "legacy"))
+      error ('ismember: only "rows" and "legacy" are valid options');
+    endif
+  else
+    [a, s] = validsetargs ("ismember", a, s, varargin{:});
   endif
-  [a, s] = validsetargs ("ismember", a, s, varargin{:});
 
-  by_rows = any (strcmp ("rows", varargin));
-  ## FIXME: uncomment if bug #56692 is addressed.
-  ##optlegacy = any (strcmp ("legacy", varargin));
 
   if (! by_rows)
+
     s = s(:);
     ## Check sort status, because we expect the array will often be sorted.
+    ## FIXME: currently fails for some cellstring inputs for s, as they do
+    ##  not get vectorized, and matrices cannot by issorted (bug #61765)
     if (issorted (s))
       is = [];
     else
@@ -139,11 +165,23 @@
     endif
 
     if (nargout > 1)
+      if (!optlegacy)
+        s = s(end : -1: 1);
+        if !(isempty (s) || isempty (a))
+          if (isempty (is))
+            is = [numel(s) : -1 : 1];
+          else
+            is = is(1 : numel (s))(end : -1 : 1);
+          endif
+        endif
+      endif
+
       s_idx = lookup (s, a, "m");
       tf = logical (s_idx);
       if (! isempty (is))
         s_idx(tf) = is(s_idx(tf));
       endif
+
     else
       tf = lookup (s, a, "b");
     endif
@@ -151,20 +189,36 @@
   else  # "rows" argument
     if (isempty (a) || isempty (s))
       tf = false (rows (a), 1);
-      s_idx = zeros (rows (a), 1);
+      if (nargout > 1)
+        s_idx = zeros (rows (a), 1);
+      endif
+
     else
       if (rows (s) == 1)
-        tf = all (bsxfun (@eq, a, s), 2);
-        s_idx = double (tf);
+        tf = all (a == s, 2);
+        if (nargout > 1)
+          s_idx = double (tf);
+        endif
       else
         ## FIXME: lookup does not support "rows", so we just use unique.
-        [~, ii, jj] = unique ([a; s], "rows", "last");
         na = rows (a);
-        jj = ii(jj(1:na));
-        tf = jj > na;
+        if (optlegacy)
+          [~, ii, jj] = unique ([a; s], "rows", "last");
+          jj = ii(jj(1:na));
+          tf = jj > na;
+          if (nargout > 1)
+            s_idx = max (0, jj - na);
+          endif
 
-        if (nargout > 1)
-          s_idx = max (0, jj - na);
+        else
+          [~, ii, jj] = unique ([s; a], "rows", "first");
+          nj = numel(jj) - (na-1);
+          jj = ii(jj(nj:end));
+          tf = jj < nj;
+          if (nargout > 1)
+            s_idx = jj;
+            s_idx (jj>nj) = 0;
+          endif
         endif
       endif
     endif
@@ -186,7 +240,7 @@
 %!fail ("ismember ({[]}, {1, 2})")
 %!fail ("ismember ({}, {1, 2})")
 %!fail ("ismember ({1}, {'1', '2'})")
-%!fail ("ismember ({'1'}, {'1' '2'},'rows')")
+%!fail ("ismember ({'1'}, {'1' '2'},'rows')") ##MATLAB gives warning, outputs non-rows answer
 %!fail ("ismember ([1 2 3], [5 4 3 1], 'rows')")
 %!assert (ismember ({"foo", "bar"}, {"foobar"}), [false false])
 %!assert (ismember ({"foo"}, {"foobar"}), false)
@@ -234,7 +288,7 @@
 %!test
 %! [result, s_idx] = ismember ([1 6], [1 2 3 4 5 1 6 1]);
 %! assert (result, [true true]);
-%! assert (s_idx(2), 7);
+%! assert (s_idx, [1, 7]);
 
 %!test
 %! [result, s_idx] = ismember ([3,10,1], [0,1,2,3,4,5,6,7,8,9]);
@@ -244,7 +298,7 @@
 %!test
 %! [result, s_idx] = ismember ("1.1", "0123456789.1");
 %! assert (result, [true true true]);
-%! assert (s_idx, [12, 11, 12]);
+%! assert (s_idx, [2, 11, 2]);
 
 %!test
 %! [result, s_idx] = ismember ([1:3; 5:7; 4:6], [0:2; 1:3; 2:4; 3:5; 4:6], "rows");
@@ -302,9 +356,87 @@
 %! assert (tf, true);
 %! assert (s_idx, 1);
 
+%!test <*55659>
+%! A = [5 3 4 2];
+%! B = [2 4 4 4 6 8];
+%! [result, s_idx] = ismember (A, B)
+%! assert (result, [false, false, true, true])
+%! assert (s_idx, [0, 0, 2, 1]);
+%! [result, s_idx] = ismember (A, B, "legacy");
+%! assert (result, [false, false, true, true]);
+%! assert (s_idx, [0, 0, 4, 1]);
+%!
+%! [result, s_idx] = ismember ([1 6], [1 2 3 4 5 1 6 1], "legacy");
+%! assert (result, [true true]);
+%! assert (s_idx, [8, 7]);
+%!
+%! [result, s_idx] = ismember ([5, 4-3j, 3+4j; 5i, 6, 6i], [5, 6], "legacy");
+%! assert (result, logical ([1, 0, 0; 0, 1, 0]));
+%! assert (s_idx, [1, 0, 0; 0, 2, 0]);
+%!
+%! [result, s_idx] = ismember ([5, 4-3j, 3+4j; 5i, 6, 6i], [5, 6, 5]);
+%! assert (result, logical ([1, 0, 0; 0, 1, 0]));
+%! assert (s_idx, [1, 0, 0; 0, 2, 0]);
+%!
+%! [result, s_idx] = ismember ([5, 4-3j, 3+4j; 5i, 6, 6i], [5, 6, 5], "legacy");
+%! assert (result, logical ([1, 0, 0; 0, 1, 0]));
+%! assert (s_idx, [3, 0, 0; 0, 2, 0]);
+%!
+%! [result, s_idx] = ismember ([3,10,1], [0,0,1,1,2,2,3,3,4,4]);
+%! assert (result, [true false true]);
+%! assert (s_idx, [7, 0, 3]);
+%!
+%! [result, s_idx] = ismember ([3,10,1], [0,0,1,1,2,2,3,3,4,4], "legacy");
+%! assert (result, [true false true]);
+%! assert (s_idx, [8, 0, 4]);
+%!
+%! [result, s_idx] = ismember ([3,10,1], [0,0,1,NaN,1,1,2,2,3,3,NaN,3,4,4]);
+%! assert (result, [true false true]);
+%! assert (s_idx, [9, 0, 3]);
+%!
+%! [result, s_idx] = ismember ([3,10,1], [0,0,1,NaN,1,1,2,2,3,3,NaN,3,4,4], "legacy");
+%! assert (result, [true false true]);
+%! assert (s_idx, [12, 0, 6]);
+%!
+%! [result, s_idx] = ismember ("1.1", "0123456789.1", "legacy");
+%! assert (result, [true true true]);
+%! assert (s_idx, [12, 11, 12]);
+%!
+%! [result, s_idx] = ismember ([1:3; 5:7; 4:6], [0:2; 1:3; 2:4; 3:5; 4:6; 1:3], "rows");
+%! assert (result, [true; false; true]);
+%! assert (s_idx, [2; 0; 5]);
+%!
+%! [result, s_idx] = ismember ([1:3; 5:7; 4:6], [0:2; 1:3; 2:4; 3:5; 4:6; 1:3], "rows", "legacy");
+%! assert (result, [true; false; true]);
+%! assert (s_idx, [6; 0; 5]);
+
+%!xtest <61765> ## produces [1;0;0]. issorted can't handle char matrix
+%! abc = [' a'; ' b'; ' c'];
+%! [result, s_idx] = ismember (abc, {abc});
+%! assert (result, [false; false; false]);
+%! assert (s_idx, [0; 0; 0]);
+
+%!xtest <61765> ##issorted can't handle cellstr array. maybe also legacy handling of char arrays
+%! abc = ['a '; 'b '; 'c '];
+%! [result, s_idx] = ismember (abc, {abc}, "legacy");
+%! assert (result, [true; true; true]);
+%! assert (s_idx, [1; 2; 3]);
+
+%!xtest <61765> ##issorted can't handle cellstr array. maybe also legacy handling of char arrays
+%! abc = [' a'; ' b'; ' c'];
+%! [result, s_idx] = ismember (abc, {abc}, "legacy");
+%! assert (result, [true; true; true]);
+%! assert (s_idx, [1; 2; 3]);
+
 ## Test input validation
 %!error <Invalid call> ismember ()
 %!error <Invalid call> ismember (1)
-%!error <Invalid call> ismember (1,2,3,4)
-%!error <"stable" or "sorted" are not valid options> ismember (1,2, "sorted")
-%!error <"stable" or "sorted" are not valid options> ismember (1,2, "stable")
+%!error <Invalid call> ismember (1,2,3,4,5)
+%!error <all options must be strings> ismember (1,2,3,4)
+%!error <only "rows" and "legacy" are valid options> ismember (1,2, "foo", "legacy")
+%!error <only "rows" and "legacy" are valid options> ismember (1,2, "rows", "foo")
+%!error <only "rows" and "legacy" are valid options> ismember (1,2, "foo", "bar")
+%!error <only "rows" and "legacy" are valid options> ismember (1,2, "sorted")
+%!error <only "rows" and "legacy" are valid options> ismember (1,2, "stable")
+##%!error <"stable" or "sorted" are not valid options> ismember (1,2, "sorted")
+##%!error <"stable" or "sorted" are not valid options> ismember (1,2, "stable")
