diff --git a/ndc_stdlib/src/sequence.rs b/ndc_stdlib/src/sequence.rs index 21879c74..883ae4e2 100644 --- a/ndc_stdlib/src/sequence.rs +++ b/ndc_stdlib/src/sequence.rs @@ -585,22 +585,31 @@ mod inner { )) } - /// Returns all prefixes of a sequence, each as a list. + fn string_prefixes(text: &str) -> Value { + Value::list( + text.chars() + .scan(String::new(), |acc, c| { + acc.push(c); + Some(Value::string(acc.clone())) + }) + .collect(), + ) + } + + /// Returns all prefixes of the string as a list of strings. + #[function(name = "prefixes", return_type = Vec)] + pub fn prefixes_string(text: &str) -> Value { + string_prefixes(text) + } + + /// Returns all prefixes of a sequence, each as a list; for strings, returns string prefixes. #[function(return_type = Vec<_>)] pub fn prefixes(seq: SeqValue) -> anyhow::Result { - // Special case for String — produce string prefixes instead of lists of chars. + // A sequence-typed caller can still supply a string. if let Value::Object(ref obj) = seq && let Object::String(s) = obj.as_ref() { - return Ok(Value::list( - s.borrow() - .chars() - .scan(String::new(), |acc, c| { - acc.push(c); - Some(Value::string(acc.clone())) - }) - .collect(), - )); + return Ok(string_prefixes(&s.borrow())); } Ok(Value::list( seq.try_into_iter() @@ -613,20 +622,28 @@ mod inner { )) } + fn string_suffixes(text: &str) -> Value { + Value::list( + text.char_indices() + .map(|(i, _)| Value::string(text[i..].to_string())) + .collect(), + ) + } + + /// Returns all suffixes of the string as a list of strings. + #[function(name = "suffixes", return_type = Vec)] + pub fn suffixes_string(text: &str) -> Value { + string_suffixes(text) + } + /// Returns all suffixes of a sequence, each as a list; for strings, returns all trailing substrings. #[function(return_type = Vec<_>)] pub fn suffixes(seq: SeqValue) -> anyhow::Result { - // Special case for String — produce string suffixes instead of lists of chars. + // A sequence-typed caller can still supply a string. if let Value::Object(ref obj) = seq && let Object::String(s) = obj.as_ref() { - let borrowed = s.borrow(); - return Ok(Value::list( - borrowed - .char_indices() - .map(|(i, _)| Value::string(borrowed[i..].to_string())) - .collect(), - )); + return Ok(string_suffixes(&s.borrow())); } let out: Vec = seq .try_into_iter() diff --git a/tests/functional/programs/603_stdlib_seq/015_string_prefixes_suffixes.ndc b/tests/functional/programs/603_stdlib_seq/015_string_prefixes_suffixes.ndc new file mode 100644 index 00000000..089dbd3d --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/015_string_prefixes_suffixes.ndc @@ -0,0 +1,16 @@ +// These annotations require the String overloads to infer List. +let prefixes: List = "aé🦀".prefixes(); +let suffixes: List = suffixes("aé🦀"); +assert_eq(prefixes, ["a", "aé", "aé🦀"]); +assert_eq(suffixes, ["aé🦀", "é🦀", "🦀"]); + +let empty_prefixes: List = "".prefixes(); +let empty_suffixes: List = "".suffixes(); +assert_eq(empty_prefixes, []); +assert_eq(empty_suffixes, []); + +// Prefixes and suffixes still include the entire input, but no empty string. +assert_eq("x".prefixes(), ["x"]); +assert_eq("x".suffixes(), ["x"]); +assert_eq("abc".prefixes(), ["a", "ab", "abc"]); +assert_eq("abc".suffixes(), ["abc", "bc", "c"]); diff --git a/tests/functional/programs/603_stdlib_seq/016_sequence_prefixes_suffixes.ndc b/tests/functional/programs/603_stdlib_seq/016_sequence_prefixes_suffixes.ndc new file mode 100644 index 00000000..cbf75fcd --- /dev/null +++ b/tests/functional/programs/603_stdlib_seq/016_sequence_prefixes_suffixes.ndc @@ -0,0 +1,25 @@ +// The general overload must retain string behavior when the concrete kind is hidden. +let text: Sequence = "aé🦀"; +assert_eq(text.prefixes(), ["a", "aé", "aé🦀"]); +assert_eq(text.suffixes(), ["aé🦀", "é🦀", "🦀"]); +let empty_text: Sequence = ""; +assert_eq(empty_text.prefixes(), []); +assert_eq(empty_text.suffixes(), []); + +// Untyped parameters exercise runtime overload dispatch for both kinds. +fn prefixes_of(value) => value.prefixes(); +fn suffixes_of(value) => value.suffixes(); +assert_eq(prefixes_of("ab"), ["a", "ab"]); +assert_eq(suffixes_of("ab"), ["ab", "b"]); +assert_eq(prefixes_of([1, 2]), [[1], [1, 2]]); +assert_eq(suffixes_of([1, 2]), [[1, 2], [2]]); + +// Other sequences continue producing lists of lists. +assert_eq([1, 2, 3].prefixes(), [[1], [1, 2], [1, 2, 3]]); +assert_eq([1, 2, 3].suffixes(), [[1, 2, 3], [2, 3], [3]]); +assert_eq((1, "a").prefixes(), [[1], [1, "a"]]); +assert_eq((1, "a").suffixes(), [[1, "a"], ["a"]]); +assert_eq((1..4).prefixes(), [[1], [1, 2], [1, 2, 3]]); +assert_eq((1..4).suffixes(), [[1, 2, 3], [2, 3], [3]]); +assert_eq([].prefixes(), []); +assert_eq([].suffixes(), []);