Repository: ibis-project/ibis

Issue:
Implement a replace-values / substitute API



Discussion:
Here was an example from the mailing list

``` python
dann_case = (ibis.case()
            .when(all_vars.alt == 'A', dann.score_a)
            .when(all_vars.alt == 'T', dann.score_t)
            .when(all_vars.alt == 'C', dann.score_c)
            .when(all_vars.alt == 'G', dann.score_g)
            .else_(ibis.NA)
            .end())
```


Resolving patch:
diff --git a/ibis/expr/api.py b/ibis/expr/api.py
--- a/ibis/expr/api.py
+++ b/ibis/expr/api.py
@@ -667,6 +667,73 @@ def notin(arg, values):
 rfloordiv = _rbinop_expr('__rfloordiv__', _ops.FloorDivide)
 
 
+def replace(arg, value, replacement=None, else_=None):
+    """
+
+    Parameters
+    ----------
+    value : expr-like or dict
+    replacement : expr-like, optional
+      If an expression is passed to value, this must be passed
+    else_ : expr, optional
+
+    Returns
+    -------
+    replaced : case statement (for now!)
+    """
+    expr = arg.case()
+    if isinstance(value, dict):
+        for k, v in sorted(value.items()):
+            expr = expr.when(k, v)
+    else:
+        expr = expr.when(value, replacement)
+
+    if else_ is not None:
+        expr = expr.else_(else_)
+    else:
+        expr = expr.else_(arg)
+
+    return expr.end()
+
+
+def _case(arg):
+    """
+    Create a new SimpleCaseBuilder to chain multiple if-else
+    statements. Add new search expressions with the .when method. These
+    must be comparable with this array expression. Conclude by calling
+    .end()
+
+    Examples
+    --------
+    case_expr = (expr.case()
+                 .when(case1, output1)
+                 .when(case2, output2)
+                 .default(default_output)
+                 .end())
+
+    Returns
+    -------
+    builder : CaseBuilder
+    """
+    return _ops.SimpleCaseBuilder(arg)
+
+
+def cases(arg, case_result_pairs, default=None):
+    """
+    Create a case expression in one shot.
+
+    Returns
+    -------
+    case_expr : SimpleCase
+    """
+    builder = arg.case()
+    for case, result in case_result_pairs:
+        builder = builder.when(case, result)
+    if default is not None:
+        builder = builder.else_(default)
+    return builder.end()
+
+
 _generic_value_methods = dict(
     hash=hash,
     cast=cast,
@@ -682,6 +749,10 @@ def notin(arg, values):
 
     over=over,
 
+    case=_case,
+    cases=cases,
+    replace=replace,
+
     __add__=add,
     add=add,
 
@@ -799,44 +870,6 @@ def bottomk(arg, k, by=None):
     raise NotImplementedError
 
 
-def _case(arg):
-    """
-    Create a new SimpleCaseBuilder to chain multiple if-else
-    statements. Add new search expressions with the .when method. These
-    must be comparable with this array expression. Conclude by calling
-    .end()
-
-    Examples
-    --------
-    case_expr = (expr.case()
-                 .when(case1, output1)
-                 .when(case2, output2)
-                 .default(default_output)
-                 .end())
-
-    Returns
-    -------
-    builder : CaseBuilder
-    """
-    return _ops.SimpleCaseBuilder(arg)
-
-
-def cases(arg, case_result_pairs, default=None):
-    """
-    Create a case expression in one shot.
-
-    Returns
-    -------
-    case_expr : SimpleCase
-    """
-    builder = arg.case()
-    for case, result in case_result_pairs:
-        builder = builder.when(case, result)
-    if default is not None:
-        builder = builder.else_(default)
-    return builder.end()
-
-
 def _generic_summary(arg, exact_nunique=False, prefix=None):
     """
     Compute a set of summary metrics from the input value expression
@@ -914,8 +947,6 @@ def expr_list(exprs):
 
 
 _generic_array_methods = dict(
-    case=_case,
-    cases=cases,
     bottomk=bottomk,
     distinct=distinct,
     nunique=nunique,
@@ -1531,7 +1562,7 @@ def _string_getitem(self, key):
     contains=_string_contains,
     like=_string_like,
     rlike=re_search,
-    replace=_string_replace,
+    str_replace=_string_replace,
     re_search=re_search,
     re_extract=regex_extract,
     re_replace=regex_replace,


Test patch:
diff --git a/ibis/expr/tests/test_value_exprs.py b/ibis/expr/tests/test_value_exprs.py
--- a/ibis/expr/tests/test_value_exprs.py
+++ b/ibis/expr/tests/test_value_exprs.py
@@ -668,3 +668,27 @@ def test_concat(self):
         result = list1.concat(list2)
         expected = ibis.expr_list(exprs + exprs2)
         assert_equal(result, expected)
+
+
+class TestReplace(unittest.TestCase):
+
+    def setUp(self):
+        self.table = ibis.table([('foo', 'string'),
+                                 ('bar', 'string')], 't1')
+
+    def test_replace_dict(self):
+        subs = {'a': 'one', 'b': self.table.bar}
+
+        result = self.table.foo.replace(subs)
+        expected = (self.table.foo.case()
+                    .when('a', 'one')
+                    .when('b', self.table.bar)
+                    .else_(self.table.foo).end())
+        assert_equal(result, expected)
+
+        result = self.table.foo.replace(subs, else_=ibis.NA)
+        expected = (self.table.foo.case()
+                    .when('a', 'one')
+                    .when('b', self.table.bar)
+                    .else_(ibis.NA).end())
+        assert_equal(result, expected)
diff --git a/ibis/impala/tests/test_client.py b/ibis/impala/tests/test_client.py
--- a/ibis/impala/tests/test_client.py
+++ b/ibis/impala/tests/test_client.py
@@ -225,7 +225,7 @@ def test_api_compile_verify(self):
         s = t.string_col
 
         supported = s.lower()
-        unsupported = s.replace('foo', 'bar')
+        unsupported = s.str_replace('foo', 'bar')
 
         assert ibis.impala.verify(supported)
         assert not ibis.impala.verify(unsupported)
diff --git a/ibis/impala/tests/test_exprs.py b/ibis/impala/tests/test_exprs.py
--- a/ibis/impala/tests/test_exprs.py
+++ b/ibis/impala/tests/test_exprs.py
@@ -1240,6 +1240,10 @@ def test_string_functions(self):
             (string.left(2), 'ab'),
             (string.right(2), 'cd'),
             (string.repeat(2), 'abcdabcd'),
+
+            # global replace not available in Impala yet
+            # (L('aabbaabbaa').replace('bb', 'B'), 'aaBaaBaa'),
+
             (L('0123').translate('012', 'abc'), 'abc3'),
             (string.find('a'), 0),
             (L('baaaab').find('b', 2), 5),
diff --git a/ibis/sql/sqlite/tests/test_functions.py b/ibis/sql/sqlite/tests/test_functions.py
--- a/ibis/sql/sqlite/tests/test_functions.py
+++ b/ibis/sql/sqlite/tests/test_functions.py
@@ -166,16 +166,24 @@ def test_string_contains(self):
         ]
         self._check_e2e_cases(cases)
 
-    def test_string_functions(self):
+    def test_string_find(self):
         cases = [
             (L('foobar').find('bar'), 3),
             (L('foobar').find('baz'), -1),
+        ]
+        self._check_e2e_cases(cases)
 
+    def test_string_like(self):
+        cases = [
             (L('foobar').like('%bar'), True),
             (L('foobar').like('foo%'), True),
             (L('foobar').like('%baz%'), False),
+        ]
+        self._check_e2e_cases(cases)
 
-            (L('foobarfoo').replace('foo', 'H'), 'HbarH'),
+    def test_str_replace(self):
+        cases = [
+            (L('foobarfoo').str_replace('foo', 'H'), 'HbarH'),
         ]
         self._check_e2e_cases(cases)