Repository: vaexio/vaex

Issue:
Support for operation between string expressions and (external) strings
It would be useful to add support for some basic operations (basically concatenation) between string expressions and string variables. 

Just as you could do
`df.x + 5` 
i propose to have 
```
df.first_name + '_' + df.last_name + '--- player'
Reggie_Miller--- player
Steve_Smith--- player
Chris_Webber--- player
.
.
.
```


Discussion:

Resolving patch:
diff --git a/packages/vaex-core/vaex/expression.py b/packages/vaex-core/vaex/expression.py
--- a/packages/vaex-core/vaex/expression.py
+++ b/packages/vaex-core/vaex/expression.py
@@ -89,27 +89,51 @@ def wrap(op=op):
                 def f(a, b):
                     self = a
                     # print(op, a, b)
-                    if isinstance(b, Expression):
-                        assert b.ds == a.ds
-                        b = b.expression
-                    elif isinstance(b, (np.timedelta64)):
-                        df = a.ds
-                        b = df.add_variable('var_time_delta', b, unique=True)
-                    elif isinstance(b, (np.datetime64)):
-                        df = a.ds
-                        b = df.add_variable('var_date_time', b, unique=True)
-                    expression = '({0} {1} {2})'.format(a.expression, op['code'], b)
+                    try:
+                        stringy = isinstance(b, str_type) or (isinstance(b, Expression) and b.dtype == str_type)
+                    except:
+                        # this can happen when expression is a literal, like '1' (used in propagate_unc)
+                        # which causes the dtype to fail
+                        stringy = False
+                    if stringy:
+                        if isinstance(b, str_type):
+                            b = repr(b)
+                        if op['code'] == '==':
+                            expression = 'str_equals({0}, {1})'.format(a.expression, b)
+                        elif op['code'] == '+':
+                            expression = 'str_cat({0}, {1})'.format(a.expression, b)
+                        else:
+                            raise ValueError('operand %r not supported for string comparison' % op['code'])
+                        return Expression(self.ds, expression=expression)
+                    else:
+                        if isinstance(b, Expression):
+                            assert b.ds == a.ds
+                            b = b.expression
+                        elif isinstance(b, (np.timedelta64)):
+                            df = a.ds
+                            b = df.add_variable('var_time_delta', b, unique=True)
+                        elif isinstance(b, (np.datetime64)):
+                            df = a.ds
+                            b = df.add_variable('var_date_time', b, unique=True)
+                        expression = '({0} {1} {2})'.format(a.expression, op['code'], b)
                     return Expression(self.ds, expression=expression)
                 attrs['__%s__' % op['name']] = f
                 if op['name'] in reversable:
                     def f(a, b):
                         self = a
-                        # print(op, a, b)
-                        if isinstance(b, Expression):
-                            assert b.ds == a.ds
-                            b = b.expression
-                        expression = '({2} {1} {0})'.format(a.expression, op['code'], b)
-                        return Expression(self.ds, expression=expression)
+                        if isinstance(b, str):
+                            if op['code'] == '+':
+                                expression = 'str_cat({1}, {0})'.format(a.expression, repr(b))
+                            else:
+                                raise ValueError('operand %r not supported for string comparison' % op['code'])
+                            return Expression(self.ds, expression=expression)
+                        else:
+                            # print(op, a, b)
+                            if isinstance(b, Expression):
+                                assert b.ds == a.ds
+                                b = b.expression
+                            expression = '({2} {1} {0})'.format(a.expression, op['code'], b)
+                            return Expression(self.ds, expression=expression)
                     attrs['__r%s__' % op['name']] = f
 
             wrap(op)
diff --git a/packages/vaex-core/vaex/functions.py b/packages/vaex-core/vaex/functions.py
--- a/packages/vaex-core/vaex/functions.py
+++ b/packages/vaex-core/vaex/functions.py
@@ -6,6 +6,7 @@
 import re
 import vaex.expression
 import functools
+import six
 
 # @vaex.serialize.register_function
 # class Function(FunctionSerializable):
@@ -507,6 +508,52 @@ def dt_second(x):
 
 ########## string operations ##########
 
+@register_function(scope='str')
+def str_equals(x, y):
+    """Tests if strings x and y are the same
+
+    :returns: a boolean expression
+
+    Example:
+
+    >>> import vaex
+    >>> text = ['Something', 'very pretty', 'is coming', 'our', 'way.']
+    >>> df = vaex.from_arrays(text=text)
+    >>> df
+      #  text
+      0  Something
+      1  very pretty
+      2  is coming
+      3  our
+      4  way.
+
+    >>> df.text.str.equals(df.text)
+    Expression = str_equals(text, text)
+    Length: 5 dtype: bool (expression)
+    ----------------------------------
+    0  True
+    1  True
+    2  True
+    3  True
+    4  True
+
+    >>> df.text.str.equals('our')
+    Expression = str_equals(text, 'our')
+    Length: 5 dtype: bool (expression)
+    ----------------------------------
+    0  False
+    1  False
+    2  False
+    3   True
+    4  False
+    """
+    if not isinstance(x, six.string_types):
+        x = _to_string_sequence(x)
+    if not isinstance(y, six.string_types):
+        y = _to_string_sequence(y)
+    return x.equals(y)
+
+
 @register_function(scope='str')
 def str_capitalize(x):
     """Capitalize the first letter of a string sample.
@@ -569,9 +616,14 @@ def str_cat(x, other):
     3                  ourour
     4                way.way.
     """
-    sl1 = _to_string_sequence(x)
-    sl2 = _to_string_sequence(other)
-    sl = sl1.concat(sl2)
+    if isinstance(x, six.string_types):
+        other = _to_string_sequence(other)
+        sl = other.concat_reverse(x)
+    else:
+        x = _to_string_sequence(x)
+        if not isinstance(other, six.string_types):
+            other = _to_string_sequence(other)
+        sl = x.concat(other)
     return column.ColumnStringArrow.from_string_sequence(sl)
 
 @register_function(scope='str')


Test patch:
diff --git a/tests/strings_test.py b/tests/strings_test.py
--- a/tests/strings_test.py
+++ b/tests/strings_test.py
@@ -354,3 +354,22 @@ def test_string_slice_repr():
 	df = vaex.from_arrays(s=s)
 	df['sliced_s'] = df.s.str.slice(start=2, stop=5)
 	repr(df['sliced_s'])
+
+
+@pytest.mark.skipif(sys.version_info[0] == 2,
+                    reason="no support for python2")
+@pytest.mark.parametrize("match", ["vaex", "VæX! "])
+def test_strings_operator_equals(dfs, match):
+	assert (dfs.s == match).tolist() == [k == match for k in string_list]
+	assert (match == dfs.s).tolist() == [k == match for k in string_list]
+	assert (dfs.s == dfs.s).tolist() == [k == k for k in string_list]
+
+@pytest.mark.skipif(sys.version_info[0] == 2,
+                    reason="no support for python2")
+@pytest.mark.parametrize("extra", ["vaex", "VæX! "])
+def test_strings_operator_plus(dfs, extra):
+	assert (dfs.s + extra).tolist() == [k + extra for k in string_list]
+	assert (extra + dfs.s).tolist() == [extra + k for k in string_list]
+
+	assert (dfs.s + dfs.s).tolist() == [k + k for k in string_list]
+	assert (dfs.s + extra + dfs.s).tolist() == [k + extra + k for k in string_list]