#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod patterns;
mod segment;
pub use segment::{LanguageConfig, SplitByTokensOptions, TokenEstimationOptions};
use patterns::DEFAULT_CHARS_PER_TOKEN;
use segment::{
default_language_configs, estimate_segment_tokens, split_segments, utf16_len,
LanguageConfig as Lc,
};
fn resolve(
default_chars_per_token: Option<f64>,
language_configs: Option<&Vec<Lc>>,
) -> (f64, &[Lc]) {
let cpt = default_chars_per_token.unwrap_or(DEFAULT_CHARS_PER_TOKEN);
let configs = match language_configs {
Some(c) => c.as_slice(),
None => default_language_configs(),
};
(cpt, configs)
}
#[must_use]
pub fn estimate_token_count(text: &str) -> u64 {
estimate_token_count_with(text, &TokenEstimationOptions::default())
}
#[must_use]
pub fn estimate_token_count_with(text: &str, options: &TokenEstimationOptions) -> u64 {
if text.is_empty() {
return 0;
}
let (cpt, configs) = resolve(
options.default_chars_per_token,
options.language_configs.as_ref(),
);
split_segments(text)
.iter()
.map(|s| estimate_segment_tokens(s, configs, cpt))
.fold(0u64, u64::saturating_add)
}
#[must_use]
#[deprecated(note = "use `estimate_token_count`")]
pub fn approximate_token_size(text: &str) -> u64 {
estimate_token_count(text)
}
#[must_use]
pub fn is_within_token_limit(text: &str, token_limit: u64) -> bool {
is_within_token_limit_with(text, token_limit, &TokenEstimationOptions::default())
}
#[must_use]
pub fn is_within_token_limit_with(
text: &str,
token_limit: u64,
options: &TokenEstimationOptions,
) -> bool {
estimate_token_count_with(text, options) <= token_limit
}
#[must_use]
pub fn slice_by_tokens(text: &str, start: i64, end: Option<i64>) -> String {
slice_by_tokens_with(text, start, end, &TokenEstimationOptions::default())
}
#[must_use]
pub fn slice_by_tokens_with(
text: &str,
start: i64,
end: Option<i64>,
options: &TokenEstimationOptions,
) -> String {
if text.is_empty() {
return String::new();
}
let (cpt, configs) = resolve(
options.default_chars_per_token,
options.language_configs.as_ref(),
);
let mut total_tokens = 0f64;
if start < 0 || end.is_some_and(|e| e < 0) {
total_tokens = estimate_token_count_with(text, options) as f64;
}
let normalized_start: f64 = if start < 0 {
(total_tokens + start as f64).max(0.0)
} else {
start.max(0) as f64
};
let normalized_end: f64 = match end {
None => f64::INFINITY,
Some(e) if e < 0 => (total_tokens + e as f64).max(0.0),
Some(e) => e as f64,
};
if normalized_start >= normalized_end {
return String::new();
}
let mut parts = String::new();
let mut current_token_pos: f64 = 0.0;
for seg in split_segments(text) {
if current_token_pos >= normalized_end {
break;
}
let token_count = estimate_segment_tokens(seg, configs, cpt);
let extracted = extract_segment_part(
seg,
current_token_pos,
token_count as f64,
normalized_start,
normalized_end,
);
if !extracted.is_empty() {
parts.push_str(&extracted);
}
current_token_pos += token_count as f64;
}
parts
}
fn extract_segment_part(
segment: &str,
segment_token_start: f64,
segment_token_count: f64,
target_start: f64,
target_end: f64,
) -> String {
if segment_token_count == 0.0 {
return if segment_token_start >= target_start && segment_token_start < target_end {
segment.to_string()
} else {
String::new()
};
}
let segment_token_end = segment_token_start + segment_token_count;
if segment_token_start >= target_end || segment_token_end <= target_start {
return String::new();
}
let overlap_start = (target_start - segment_token_start).max(0.0);
let overlap_end = segment_token_count.min(target_end - segment_token_start);
if overlap_start == 0.0 && overlap_end == segment_token_count {
return segment.to_string();
}
let len = utf16_len(segment) as f64;
let char_start = ((overlap_start / segment_token_count) * len).floor() as usize;
let char_end = ((overlap_end / segment_token_count) * len).ceil() as usize;
utf16_slice(segment, char_start, char_end)
}
fn utf16_slice(s: &str, start: usize, end: usize) -> String {
let units: Vec<u16> = s.encode_utf16().collect();
let start = start.min(units.len());
let end = end.min(units.len());
if start >= end {
return String::new();
}
String::from_utf16_lossy(&units[start..end])
}
#[must_use]
pub fn split_by_tokens(text: &str, tokens_per_chunk: u64) -> Vec<String> {
split_by_tokens_with(text, tokens_per_chunk, &SplitByTokensOptions::default())
}
#[must_use]
pub fn split_by_tokens_with(
text: &str,
tokens_per_chunk: u64,
options: &SplitByTokensOptions,
) -> Vec<String> {
if text.is_empty() || tokens_per_chunk == 0 {
return Vec::new();
}
let (cpt, configs) = resolve(
options.default_chars_per_token,
options.language_configs.as_ref(),
);
let overlap = options.overlap.unwrap_or(0);
let mut chunks: Vec<String> = Vec::new();
let mut current_chunk: Vec<&str> = Vec::new();
let mut current_token_count = 0u64;
for seg in split_segments(text) {
let token_count = estimate_segment_tokens(seg, configs, cpt);
current_chunk.push(seg);
current_token_count = current_token_count.saturating_add(token_count);
if current_token_count >= tokens_per_chunk {
chunks.push(current_chunk.concat());
if overlap > 0 {
let mut overlap_segments: Vec<&str> = Vec::new();
let mut overlap_token_count = 0u64;
let mut i = current_chunk.len();
while i > 0 && overlap_token_count < overlap {
i -= 1;
let sv = current_chunk[i];
overlap_token_count = overlap_token_count
.saturating_add(estimate_segment_tokens(sv, configs, cpt));
overlap_segments.insert(0, sv);
}
current_chunk = overlap_segments;
current_token_count = overlap_token_count;
} else {
current_chunk.clear();
current_token_count = 0;
}
}
}
if !current_chunk.is_empty() {
chunks.push(current_chunk.concat());
}
chunks
}