diff --git a/crates/nu-std/lib/iter.nu b/crates/nu-std/lib/iter.nu new file mode 100644 index 0000000000..f15c5f8c0f --- /dev/null +++ b/crates/nu-std/lib/iter.nu @@ -0,0 +1,113 @@ +# | Filter Extensions +# +# This module implements extensions to the `filters` commands +# +# They are prefixed with `iter` so as to avoid conflicts with +# the inbuilt filters + +# Returns the first element of the list that matches the +# closure predicate, `null` otherwise +# +# # Invariant +# > The closure has to be a predicate (returning a bool value) +# > else `null` is returned +# > The closure also has to be valid for the types it receives +# > These will be flagged as errors later as closure annotations +# > are implemented +# +# # Example +# ``` +# use std ["assert equal" "iter find"] +# +# let haystack = ["shell", "abc", "around", "nushell", "std"] +# +# let found = ($haystack | iter find {|it| $it starts-with "a" }) +# let not_found = ($haystack | iter find {|it| $it mod 2 == 0}) +# +# assert equal $found "abc" +# assert equal $not_found null +# ``` +export def "iter find" [ # -> any | null + predicate: closure # the closure used to perform the search +] { + let list = (self collect) + try { + $list | filter $predicate | get 0? + } catch { + null + } +} + +# Returns a new list with the separator between adjacent +# items of the original list +# +# # Example +# ``` +# use std ["assert equal" "iter intersperse"] +# +# let res = ([1 2 3 4] | iter intersperse 0) +# assert equal $res [1 0 2 0 3 0 4] +# ``` +export def "iter intersperse" [ # -> list + separator: any, # the separator to be used +] { + let list = (self collect) + + let len = ($list | length); + if ($list | is-empty) { + return $list + } + + $list + | enumerate + | reduce -f [] {|it, acc| + if ($it.index == $len - 1) { + $acc ++ [$it.item] + } else { + $acc ++ [$it.item, $separator] + } + } +} + +# Returns a list of intermediate steps performed by `reduce` +# (`fold`). It takes two arguments, an initial value to seed the +# initial state and a closure that takes two arguments, the first +# being the internal state and the second the list element in the +# current iteration. +# +# # Example +# ``` +# use std ["assert equal" "iter scan"] +# let scanned = ([1 2 3] | iter scan 0 {|x, y| $x + $y}) +# +# assert equal $scanned [0, 1, 3, 6] +# +# # use the --noinit(-n) flag to remove the initial value from +# # the final result +# let scanned = ([1 2 3] | iter scan 0 {|x, y| $x + $y} -n) +# +# assert equal $scanned [1, 3, 6] +# ``` +export def "iter scan" [ # -> list + init: any # initial value to seed the initial state + f: closure # the closure to perform the scan + --noinit(-n) # remove the initial value from the result +] { + let res = (reduce -f [$init] {|it, acc| + $acc ++ [(do $f ($acc | last) $it)] + }) + + if $noinit { + $res | skip + } else { + $res + } +} + +# Accepts inputs from a pipeline and builds a list for the `iter *` +# commands to work with +def "self collect" [] { # -> list + reduce -f [] {|it, acc| + $acc ++ $it + } +} diff --git a/crates/nu-std/lib/mod.nu b/crates/nu-std/lib/mod.nu index ae02c6c78c..95e94325f4 100644 --- a/crates/nu-std/lib/mod.nu +++ b/crates/nu-std/lib/mod.nu @@ -5,6 +5,7 @@ export-env { use dirs * } export use help * +export use iter * export use log * export use testing * export use xml * diff --git a/crates/nu-std/src/lib.rs b/crates/nu-std/src/lib.rs index 510c1446cb..863e672327 100644 --- a/crates/nu-std/src/lib.rs +++ b/crates/nu-std/src/lib.rs @@ -74,6 +74,7 @@ pub fn load_standard_library( // the rest of the library ("dirs", include_str!("../lib/dirs.nu")), + ("iter", include_str!("../lib/iter.nu")), ("help", include_str!("../lib/help.nu")), ("testing", include_str!("../lib/testing.nu")), ("xml", include_str!("../lib/xml.nu")), diff --git a/crates/nu-std/tests/test_iter.nu b/crates/nu-std/tests/test_iter.nu new file mode 100644 index 0000000000..b5291192c5 --- /dev/null +++ b/crates/nu-std/tests/test_iter.nu @@ -0,0 +1,50 @@ +use std * + +export def test_iter_find [] { + let hastack1 = [1 2 3 4 5 6 7] + let hastack2 = [nushell rust shell iter std] + let hastack3 = [nu 69 2023-04-20 "std"] + + let res = ($hastack1 | iter find {|it| $it mod 2 == 0}) + assert equal $res 2 + + let res = ($hastack2 | iter find {|it| $it starts-with 's'}) + assert equal $res 'shell' + + let res = ($hastack2 | iter find {|it| ($it | length) == 50}) + assert equal $res null + + let res = ($hastack3 | iter find {|it| (it | describe) == filesize}) + assert equal $res null +} + +export def test_iter_intersperse [] { + let res = ([1 2 3 4] | iter intersperse 0) + assert equal $res [1 0 2 0 3 0 4] + + let res = ([] | iter intersperse x) + assert equal $res [] + + let res = ([1] | iter intersperse 5) + assert equal $res [1] + + let res = ([a b c d e] | iter intersperse 5) + assert equal $res [a 5 b 5 c 5 d 5 e] + + let res = (1..4 | iter intersperse 0) + assert equal $res [1 0 2 0 3 0 4] + + let res = (4 | iter intersperse 1) + assert equal $res [4] +} + +export def test_iter_scan [] { + let scanned = ([1 2 3] | iter scan 0 {|x, y| $x + $y} -n) + assert equal $scanned [1, 3, 6] + + let scanned = ([1 2 3] | iter scan 0 {|x, y| $x + $y}) + assert equal $scanned [0, 1, 3, 6] + + let scanned = ([a b c d] | iter scan "" {|x, y| [$x, $y] | str join} -n) + assert equal $scanned ["a" "ab" "abc" "abcd"] +}