pub(crate) fn find_last_statement_boundary(content: &str) -> (&str, &str) {
match statement_boundaries(content).last() {
Some(&end) => (&content[..end], &content[end..]),
None => ("", content),
}
}
pub(crate) fn statement_boundaries(content: &str) -> Vec<usize> {
let mut boundaries = Vec::new();
let mut in_string = false;
let mut in_long_string = false;
let mut in_iri = false;
let mut string_quote = '\0';
let mut chars = content.char_indices().peekable();
while let Some((i, ch)) = chars.next() {
if !in_string && !in_long_string && !in_iri && (ch == '"' || ch == '\'') {
let mut count = 1;
while let Some(&(_, next_ch)) = chars.peek() {
if next_ch == ch && count < 3 {
chars.next();
count += 1;
} else {
break;
}
}
if count == 3 {
in_long_string = true;
} else {
in_string = count == 1;
}
string_quote = ch;
} else if in_long_string && ch == string_quote {
let mut count = 1;
while let Some(&(_, next_ch)) = chars.peek() {
if next_ch == string_quote && count < 3 {
chars.next();
count += 1;
} else {
break;
}
}
if count >= 3 {
in_long_string = false;
}
} else if in_string && ch == string_quote {
in_string = false;
} else if in_string && ch == '\\' {
chars.next();
} else if !in_string && !in_long_string && !in_iri && ch == '<' {
let starts_quoted_triple = matches!(chars.peek(), Some(&(_, '<')));
if starts_quoted_triple {
chars.next();
} else {
in_iri = true;
}
} else if in_iri && ch == '\\' {
chars.next();
} else if in_iri && ch == '>' {
in_iri = false;
} else if !in_string && !in_long_string && !in_iri && (ch == '.' || ch == '}') {
if ch == '.' {
if matches!(chars.peek(), Some(&(_, next_ch)) if next_ch.is_ascii_digit()) {
continue;
}
}
let mut end_pos = i + ch.len_utf8();
while let Some(&(next_i, next_ch)) = chars.peek() {
if next_ch == ' ' || next_ch == '\t' || next_ch == '\n' || next_ch == '\r' {
chars.next();
end_pos = next_i + next_ch.len_utf8();
} else {
break;
}
}
boundaries.push(end_pos);
}
}
boundaries
}
#[cfg(feature = "parallel")]
pub(crate) fn split_into_statement_chunks(
content: &str,
target_statements_per_chunk: usize,
) -> Vec<String> {
if content.trim().is_empty() {
return Vec::new();
}
let boundaries = statement_boundaries(content);
if boundaries.is_empty() {
return vec![content.to_string()];
}
let target = target_statements_per_chunk.max(1);
let mut chunks = Vec::new();
let mut chunk_start = 0usize;
let mut count_in_chunk = 0usize;
let mut last_boundary = 0usize;
for &boundary in &boundaries {
count_in_chunk += 1;
last_boundary = boundary;
if count_in_chunk >= target {
chunks.push(content[chunk_start..boundary].to_string());
chunk_start = boundary;
count_in_chunk = 0;
}
}
if chunk_start < last_boundary {
chunks.push(content[chunk_start..last_boundary].to_string());
chunk_start = last_boundary;
}
if chunk_start < content.len() {
match chunks.last_mut() {
Some(last) => last.push_str(&content[chunk_start..]),
None => chunks.push(content[chunk_start..].to_string()),
}
}
chunks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_statement_boundaries_simple() {
let content = "a . b .";
let boundaries = statement_boundaries(content);
assert_eq!(boundaries, vec![4, 7]);
}
#[test]
fn test_statement_boundaries_ignores_dot_in_string() {
let content = r#"ex:s ex:p "a.b.c" ."#;
let boundaries = statement_boundaries(content);
assert_eq!(boundaries.len(), 1);
assert_eq!(boundaries[0], content.len());
}
#[test]
fn test_statement_boundaries_ignores_dot_in_long_string() {
let content = "ex:s ex:p \"\"\"a.\nb.\nc\"\"\" .";
let boundaries = statement_boundaries(content);
assert_eq!(boundaries.len(), 1);
assert_eq!(boundaries[0], content.len());
}
#[test]
fn test_statement_boundaries_ignores_dot_in_iri() {
let content = "@prefix ex: <http://example.org/> .\nex:s ex:p <http://a.b.c/x.y> .\n";
let boundaries = statement_boundaries(content);
assert_eq!(boundaries.len(), 2);
assert_eq!(
&content[..boundaries[0]],
"@prefix ex: <http://example.org/> .\n"
);
assert_eq!(
&content[boundaries[0]..boundaries[1]],
"ex:s ex:p <http://a.b.c/x.y> .\n"
);
}
#[test]
fn test_statement_boundaries_ignores_dot_in_decimal_literal() {
let content = "ex:s ex:p 3.14 .\nex:s2 ex:p2 2.5e10 .\n";
let boundaries = statement_boundaries(content);
assert_eq!(boundaries.len(), 2);
assert_eq!(&content[..boundaries[0]], "ex:s ex:p 3.14 .\n");
assert_eq!(
&content[boundaries[0]..boundaries[1]],
"ex:s2 ex:p2 2.5e10 .\n"
);
}
#[test]
fn test_statement_boundaries_rdf_star_quoted_triple() {
let content = "<< ex:s ex:p ex:o >> ex:certainty 0.9 .\n";
let boundaries = statement_boundaries(content);
assert_eq!(boundaries.len(), 1);
assert_eq!(boundaries[0], content.len());
}
#[test]
fn test_statement_boundaries_no_boundary() {
assert!(statement_boundaries("ex:s ex:p ex:o").is_empty());
}
#[test]
fn test_find_last_statement_boundary() {
let content = "a . b . c";
let (parseable, remaining) = find_last_statement_boundary(content);
assert_eq!(parseable, "a . b . ");
assert_eq!(remaining, "c");
}
#[test]
fn test_find_last_statement_boundary_none() {
let content = "no terminator here";
let (parseable, remaining) = find_last_statement_boundary(content);
assert_eq!(parseable, "");
assert_eq!(remaining, content);
}
#[test]
#[cfg(feature = "parallel")]
fn test_split_into_statement_chunks_groups_by_count() {
let content = "a . b . c . d . e .";
let chunks = split_into_statement_chunks(content, 2);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0], "a . b . ");
assert_eq!(chunks[1], "c . d . ");
assert_eq!(chunks[2], "e .");
}
#[test]
#[cfg(feature = "parallel")]
fn test_split_into_statement_chunks_never_splits_multiline_statement() {
let content = "ex:s\n ex:p1 ex:o1 ;\n ex:p2 ex:o2 ;\n ex:p3 ex:o3 .\nex:s2 ex:p ex:o .";
let chunks = split_into_statement_chunks(content, 1);
assert_eq!(chunks.len(), 2);
assert!(chunks[0].contains("ex:p1"));
assert!(chunks[0].contains("ex:p3"));
assert!(chunks[1].contains("ex:s2"));
}
#[test]
#[cfg(feature = "parallel")]
fn test_split_into_statement_chunks_keeps_trailing_partial_data() {
let content = "a . b . incomplete";
let chunks = split_into_statement_chunks(content, 1);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0], "a . ");
assert_eq!(chunks[1], "b . incomplete");
}
#[test]
#[cfg(feature = "parallel")]
fn test_split_into_statement_chunks_empty_input() {
assert!(split_into_statement_chunks("", 10).is_empty());
assert!(split_into_statement_chunks(" \n ", 10).is_empty());
}
}