Repository: kdr-aus/ogma (rust)

Issue:
Implement `filter` on `Str` input type
Add an implementation of `filter` for `Str` input type.
- Input into expression is each character of string
- Parallelise? Need to test performance.

# Example
```ogma
# Removing spaces
\'Hello World' | filter != ' '
```

> Might need `Str` to have a `From<char>` optimisation: https://github.com/kdr-aus/divvy/issues/2

Pull request description:
Filter on str
Closes #16 

Patch:
diff --git a/RELEASE.md b/RELEASE.md
index e7e3fe9..f2c11dc 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,5 +1,7 @@
 **🔬 New Features**
 - `#b` -- Newline constant (b for _break_) <https://github.com/kdr-aus/ogma/pull/75>
+- `filter` now accepts `Str` input: it will supply one character at a time to the predicate.
+    (https://github.com/kdr-aus/ogma/pull/76)
 
 **🐛 Bug Fixes**
 
diff --git a/ogma/src/lang/impls/intrinsics/morphism.rs b/ogma/src/lang/impls/intrinsics/morphism.rs
index eba1e54..c521d33 100644
--- a/ogma/src/lang/impls/intrinsics/morphism.rs
+++ b/ogma/src/lang/impls/intrinsics/morphism.rs
@@ -251,7 +251,8 @@ fn filter_help() -> HelpMessage {
     HelpMessage {
         desc: "filter incoming data using a predicate
 filter can be used with a column header and a type flag
-filtering columns is achievable with the --cols flag"
+filtering columns is achievable with the --cols flag
+filtering on a string supplies one character at a time"
             .into(),
         params: vec![
             HelpParameter::Optional("col-name".into()),
@@ -284,23 +285,29 @@ filtering columns is achievable with the --cols flag"
                 desc: "filter table columns",
                 code: "\\ table.csv | filter --cols or { = 'foo' } { = bar }",
             },
+            HelpExample {
+                desc: "filtering a string",
+                code: "\\ 'Hello, world!' | filter != ' '",
+            },
         ],
         ..HelpMessage::new("filter")
     }
 }
 
 fn filter_intrinsic(mut blk: Block) -> Result<Step> {
-    if blk.in_ty() != &Ty::Tab {
-        return Err(Error::wrong_op_input_type(blk.in_ty(), blk.op_tag()));
-    }
-
-    blk.assert_output(Type::Tab); // filtering Table -> Table
+    match blk.in_ty() {
+        Ty::Str => filter_string(blk),
+        Ty::Tab => {
+            blk.assert_output(Ty::Tab); // filtering Table -> Table
 
-    let cols = blk.get_flag("cols").is_some();
-    if cols {
-        filter_table_columns(blk)
-    } else {
-        FilterTable::filter(blk)
+            let cols = blk.get_flag("cols").is_some();
+            if cols {
+                filter_table_columns(blk)
+            } else {
+                FilterTable::filter(blk)
+            }
+        }
+        x => Err(Error::wrong_op_input_type(x, blk.op_tag())),
     }
 }
 
@@ -459,6 +466,38 @@ fn filter_table_columns(mut blk: Block) -> Result<Step> {
     })
 }
 
+fn filter_string(mut blk: Block) -> Result<Step> {
+    blk.assert_output(Type::Str);
+
+    let predicate = blk
+        .next_arg()?
+        .supplied(Ty::Str)?
+        .returns(Ty::Bool)?
+        .concrete()?;
+
+    blk.eval_o(move |input, cx| {
+        let mut string = Str::try_from(input)?;
+        let s = string.to_mut();
+        let r = predicate.resolver_sync(&cx);
+        let mut e = None;
+        s.retain(|c| {
+            r(Value::Str(c.into()))
+                .and_then(bool::try_from)
+                .unwrap_or_else(|err| {
+                    e = Some(err);
+                    true
+                })
+        });
+
+        drop(r);
+
+        match e {
+            Some(e) => Err(e),
+            None => cx.done_o(string),
+        }
+    })
+}
+
 // ------ Fold -----------------------------------------------------------------
 fn fold_help() -> HelpMessage {
     HelpMessage {


Test patch:
diff --git a/ogma/tests/commands/morphism.rs b/ogma/tests/commands/morphism.rs
index 906d378..f65626d 100644
--- a/ogma/tests/commands/morphism.rs
+++ b/ogma/tests/commands/morphism.rs
@@ -249,6 +249,7 @@ fn filtering_help_msg() {
  | filter incoming data using a predicate
  | filter can be used with a column header and a type flag
  | filtering columns is achievable with the --cols flag
+ | filtering on a string supplies one character at a time
  | 
  | Usage:
  |  => filter [col-name] <predicate>
@@ -269,6 +270,9 @@ fn filtering_help_msg() {
  | 
  |  filter table columns
  |  => \\ table.csv | filter --cols or { = 'foo' } { = bar }
+ | 
+ |  filtering a string
+ |  => \\ 'Hello, world!' | filter != ' '
 "
     );
 }
@@ -392,6 +396,22 @@ fn column_filtering() {
     check_is_table(x, exp.clone());
 }
 
+#[test]
+fn string_filtering() {
+    let defs = &Definitions::new();
+
+    let x = process_w_str("filter = l", defs);
+    assert_eq!(x, Ok(Value::Str("ll".into())));
+    let x = process_w_str("filter != l", defs);
+    assert_eq!(x, Ok(Value::Str("Heo".into())));
+    let x = process_w_str("filter #f", defs);
+    assert_eq!(x, Ok(Value::Str("".into())));
+    let x = process_w_str("filter #t", defs);
+    assert_eq!(x, Ok(Value::Str("Hello".into())));
+    let x = process_w_str("filter or {= H} {= o}", defs);
+    assert_eq!(x, Ok(Value::Str("Ho".into())));
+}
+
 // ------ Folding --------------------------------------------------------------
 #[test]
 fn fold_help_msg() {