Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ In future, each builtin will be associated with a feature (many builtins could b
| [startswith](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-startswith) | _ |
| [strings.any_prefix_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsany_prefix_match) | _ |
| [strings.any_suffix_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsany_suffix_match) | _ |
| [strings.count](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringscount) | _ |
| [strings.render_template](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsrender_template) | _ |
| [strings.replace_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsreplace_n) | _ |
| [strings.reverse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsreverse) | _ |
| [strings.split_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringssplit_n) | _ |
| [substring](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-substring) | _ |
| [trim](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim) | _ |
| [trim_left](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_left) | _ |
Expand Down
80 changes: 79 additions & 1 deletion src/builtins/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn
m.insert("strings.count", (strings_count, 2));
m.insert("strings.replace_n", (replace_n, 2));
m.insert("strings.reverse", (reverse, 1));
m.insert("strings.split_n", (split_n, 3));
m.insert("substring", (substring, 3));
m.insert("trim", (trim, 2));
m.insert("trim_left", (trim_left, 2));
Expand Down Expand Up @@ -149,7 +150,7 @@ fn replace(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) ->
}

fn split(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "replace";
let name = "split";
ensure_args_count(span, name, params, args, 2)?;
let s = ensure_string(name, &params[0], &args[0])?;
let delimiter = ensure_string(name, &params[1], &args[1])?;
Expand Down Expand Up @@ -180,6 +181,83 @@ fn split(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Re
Ok(Value::from(parts))
}

fn split_n(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "strings.split_n";
ensure_args_count(span, name, params, args, 3)?;
let s = ensure_string(name, &params[0], &args[0])?;
let delimiter = ensure_string(name, &params[1], &args[1])?;
let n = ensure_numeric(name, &params[2], &args[2])?;

if !n.is_integer() {
if strict {
bail!(params[2]
.span()
.error("strings.split_n expects an integer third argument"));
}
return Ok(Value::Undefined);
}

let Some(n) = n.as_i64() else {
if n.is_positive() {
// n overflows i64 — treat as no limit (full split); pass only the first 2 args
return split(span, &params[..2], &args[..2], strict);
}
return Ok(Value::from_array(Vec::new()));
};

if n == 0 {
return Ok(Value::from_array(Vec::new()));
}

// For positive n: split into at most n pieces (last piece holds remainder).
// For negative n: return the last |n| pieces from a full split.
let result: Vec<Value> = if n > 0 {
let limit = usize::try_from(n).unwrap_or(usize::MAX);
if delimiter.is_empty() {
// Empty delimiter: yield individual chars; last piece holds any remainder.
let chars: Vec<char> = s.chars().collect();
if limit >= chars.len() {
chars
.iter()
.map(|c| {
enforce_limit()?;
Ok(Value::from(c.to_string()))
})
.collect::<Result<Vec<Value>>>()?
} else {
let mut parts: Vec<Value> = chars[..limit.saturating_sub(1)]
.iter()
.map(|c| {
enforce_limit()?;
Ok(Value::from(c.to_string()))
})
.collect::<Result<Vec<Value>>>()?;
let rest: String = chars[limit.saturating_sub(1)..].iter().collect();
enforce_limit()?;
parts.push(Value::from(rest));
parts
}
} else {
s.splitn(limit, delimiter.as_ref())
.map(|p| {
enforce_limit()?;
Ok(Value::String(p.into()))
})
.collect::<Result<Vec<Value>>>()?
}
} else {
let parts = match split(span, &params[..2], &args[..2], strict)? {
Value::Array(parts) => parts.as_ref().clone(),
_ => return Ok(Value::Undefined),
};
let count = usize::try_from(n.unsigned_abs()).unwrap_or(usize::MAX);
let start = parts.len().saturating_sub(count);
parts.into_iter().skip(start).collect()
};

Ok(Value::from_array(result))
}

fn to_string(v: &Value, unescape: bool) -> String {
match v {
Value::Null => "null".to_owned(),
Expand Down
94 changes: 94 additions & 0 deletions tests/interpreter/cases/builtins/strings/split_n.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

cases:
- note: positive-limit
data: {}
modules:
- |
package test
x = strings.split_n("a,b,c,d", ",", 2)
query: data.test.x
want_result: ["a", "b,c,d"]

- note: exact-limit
data: {}
modules:
- |
package test
x = strings.split_n("a,b,c", ",", 3)
query: data.test.x
want_result: ["a", "b", "c"]

- note: limit-exceeds-parts
data: {}
modules:
- |
package test
x = strings.split_n("a,b", ",", 10)
query: data.test.x
want_result: ["a", "b"]

- note: zero-limit
data: {}
modules:
- |
package test
x = strings.split_n("a,b,c", ",", 0)
query: data.test.x
want_result: []

- note: negative-limit-tail
data: {}
modules:
- |
package test
x = strings.split_n("a,b,c,d", ",", -2)
query: data.test.x
want_result: ["c", "d"]

- note: empty-string
data: {}
modules:
- |
package test
x = strings.split_n("", ",", 5)
query: data.test.x
want_result: [""]

- note: non-integer-n-undefined
strict: false
data: {}
modules:
- |
package test
x = strings.split_n("a,b", ",", 1.5)
query: data.test.x
no_result: true

- note: wrong-arg-count
data: {}
modules:
- |
package test
x = strings.split_n("a,b", ",")
query: data.test.x
error: "`strings.split_n` expects 3 arguments"

- note: empty-delimiter-with-remainder
data: {}
modules:
- |
package test
x = strings.split_n("abcd", "", 3)
query: data.test.x
want_result: ["a", "b", "cd"]

- note: limit-one-returns-whole-string
data: {}
modules:
- |
package test
x = strings.split_n("a,b,c", ",", 1)
query: data.test.x
want_result: ["a,b,c"]
Loading