use crate::error::{StatorError, StatorResult};
use unicode_normalization::UnicodeNormalization;
pub const MAX_STRING_LEN: usize = 1 << 28;
pub(crate) fn encode_utf16(s: &str) -> Vec<u16> {
s.encode_utf16().collect()
}
pub(crate) fn utf16_len(s: &str) -> usize {
s.encode_utf16().count()
}
pub(crate) fn decode_utf16(units: &[u16]) -> String {
String::from_utf16_lossy(units)
}
pub(crate) fn get_substitution(
replacement: &str,
matched: &str,
prefix: &str,
suffix: &str,
captures: &[Option<&str>],
) -> String {
let bytes = replacement.as_bytes();
let mut out = String::with_capacity(replacement.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'$' && i + 1 < bytes.len() {
match bytes[i + 1] {
b'$' => {
out.push('$');
i += 2;
}
b'&' => {
out.push_str(matched);
i += 2;
}
b'`' => {
out.push_str(prefix);
i += 2;
}
b'\'' => {
out.push_str(suffix);
i += 2;
}
b'0' => {
out.push('$');
out.push('0');
i += 2;
}
b'1'..=b'9' => {
let mut capture_index = (bytes[i + 1] - b'0') as usize;
let mut consumed = 2;
if i + 2 < bytes.len()
&& let Some(next_digit) = (bytes[i + 2] as char).to_digit(10)
{
let two_digit = capture_index * 10 + next_digit as usize;
if two_digit > 0 && two_digit <= captures.len() {
capture_index = two_digit;
consumed = 3;
}
}
match captures.get(capture_index - 1) {
Some(Some(capture)) => out.push_str(capture),
Some(None) => {}
None => {
out.push('$');
out.push(bytes[i + 1] as char);
}
}
i += consumed;
}
_ => {
out.push('$');
i += 1;
}
}
} else if let Some(ch) = replacement[i..].chars().next() {
out.push(ch);
i += ch.len_utf8();
} else {
break;
}
}
out
}
fn clamp_index(index: i64, len: usize) -> usize {
let len = len as i64;
if index < 0 {
(len + index).max(0) as usize
} else {
index.min(len) as usize
}
}
fn is_ecmascript_trim_char(ch: char) -> bool {
matches!(
ch,
'\u{0009}'
| '\u{000A}'
| '\u{000B}'
| '\u{000C}'
| '\u{000D}'
| '\u{0020}'
| '\u{00A0}'
| '\u{1680}'
| '\u{2000}'
..='\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
| '\u{FEFF}'
)
}
fn trim_start_index(s: &str) -> usize {
for (idx, ch) in s.char_indices() {
if !is_ecmascript_trim_char(ch) {
return idx;
}
}
s.len()
}
fn trim_end_index(s: &str) -> usize {
for (idx, ch) in s.char_indices().rev() {
if !is_ecmascript_trim_char(ch) {
return idx + ch.len_utf8();
}
}
0
}
const UTF16_HIGH_SURROGATE_START: u16 = 0xD800;
const UTF16_HIGH_SURROGATE_END: u16 = 0xDBFF;
const UTF16_LOW_SURROGATE_START: u16 = 0xDC00;
const UTF16_LOW_SURROGATE_END: u16 = 0xDFFF;
pub fn string_from_char_code(codes: &[u32]) -> String {
let units: Vec<u16> = codes.iter().map(|&c| (c & 0xFFFF) as u16).collect();
decode_utf16(&units)
}
pub fn string_from_code_point(code_points: &[u32]) -> StatorResult<String> {
let mut result = String::new();
for &cp in code_points {
let ch = char::from_u32(cp)
.ok_or_else(|| StatorError::RangeError(format!("Invalid code point {cp}")))?;
result.push(ch);
}
Ok(result)
}
pub fn string_char_at(s: &str, pos: i64) -> String {
if pos < 0 {
return String::new();
}
let units = encode_utf16(s);
match units.get(pos as usize) {
Some(&u) => decode_utf16(&[u]),
None => String::new(),
}
}
pub fn string_char_code_at(s: &str, pos: i64) -> f64 {
if pos < 0 {
return f64::NAN;
}
let units = encode_utf16(s);
units
.get(pos as usize)
.copied()
.map(f64::from)
.unwrap_or(f64::NAN)
}
pub fn string_code_point_at(s: &str, pos: i64) -> Option<u32> {
if pos < 0 {
return None;
}
let units = encode_utf16(s);
let idx = pos as usize;
let high = *units.get(idx)?;
if (UTF16_HIGH_SURROGATE_START..=UTF16_HIGH_SURROGATE_END).contains(&high)
&& let Some(&low) = units.get(idx + 1)
&& (UTF16_LOW_SURROGATE_START..=UTF16_LOW_SURROGATE_END).contains(&low)
{
let cp = 0x10000u32
+ ((high as u32 - UTF16_HIGH_SURROGATE_START as u32) << 10)
+ (low as u32 - UTF16_LOW_SURROGATE_START as u32);
return Some(cp);
}
Some(u32::from(high))
}
pub fn string_concat(s: &str, others: &[&str]) -> String {
let total_len: usize = s.len() + others.iter().map(|o| o.len()).sum::<usize>();
let mut result = String::with_capacity(total_len);
result.push_str(s);
for other in others {
result.push_str(other);
}
result
}
pub fn string_slice(s: &str, start: i64, end: Option<i64>) -> String {
let units = encode_utf16(s);
let len = units.len();
let from = clamp_index(start, len);
let to = match end {
Some(e) => clamp_index(e, len),
None => len,
};
if from >= to {
return String::new();
}
decode_utf16(&units[from..to])
}
pub fn string_substring(s: &str, start: i64, end: Option<i64>) -> String {
let units = encode_utf16(s);
let len = units.len() as i64;
let from = start.max(0).min(len) as usize;
let to = match end {
Some(e) => e.max(0).min(len) as usize,
None => len as usize,
};
let (from, to) = if from <= to { (from, to) } else { (to, from) };
decode_utf16(&units[from..to])
}
pub fn string_index_of(s: &str, search: &str, from_index: Option<i64>) -> i64 {
let units = encode_utf16(s);
let search_units = encode_utf16(search);
let len = units.len();
let from = from_index.unwrap_or(0).max(0).min(len as i64) as usize;
if search_units.is_empty() {
return from as i64;
}
if search_units.len() > len {
return -1;
}
let end = len - search_units.len() + 1;
for i in from..end {
if units[i..i + search_units.len()] == search_units[..] {
return i as i64;
}
}
-1
}
pub fn string_last_index_of(s: &str, search: &str, from_index: Option<i64>) -> i64 {
let units = encode_utf16(s);
let search_units = encode_utf16(search);
let len = units.len();
if search_units.is_empty() {
let max = match from_index {
Some(f) => f.max(0).min(len as i64) as usize,
None => len,
};
return max.min(len) as i64;
}
if search_units.len() > len {
return -1;
}
let max_start = match from_index {
Some(f) => f.max(0).min(len as i64) as usize,
None => len,
};
let end = (max_start + 1).min(len - search_units.len() + 1);
for i in (0..end).rev() {
if units[i..i + search_units.len()] == search_units[..] {
return i as i64;
}
}
-1
}
pub fn string_includes(s: &str, search: &str, from_index: Option<i64>) -> bool {
string_index_of(s, search, from_index) != -1
}
pub fn string_starts_with(s: &str, search: &str, position: Option<i64>) -> bool {
let units = encode_utf16(s);
let search_units = encode_utf16(search);
let len = units.len();
let pos = position.unwrap_or(0).max(0).min(len as i64) as usize;
if pos + search_units.len() > len {
return false;
}
units[pos..pos + search_units.len()] == search_units[..]
}
pub fn string_ends_with(s: &str, search: &str, end_position: Option<i64>) -> bool {
let units = encode_utf16(s);
let search_units = encode_utf16(search);
let end = match end_position {
Some(e) => e.max(0).min(units.len() as i64) as usize,
None => units.len(),
};
if search_units.len() > end {
return false;
}
let start = end - search_units.len();
units[start..end] == search_units[..]
}
pub fn string_to_upper_case(s: &str) -> String {
s.to_uppercase()
}
pub fn string_to_lower_case(s: &str) -> String {
s.to_lowercase()
}
pub fn string_trim(s: &str) -> String {
let start = trim_start_index(s);
let end = trim_end_index(&s[start..]) + start;
s[start..end].to_string()
}
pub fn string_trim_start(s: &str) -> String {
s[trim_start_index(s)..].to_string()
}
pub fn string_trim_end(s: &str) -> String {
s[..trim_end_index(s)].to_string()
}
pub fn string_split(s: &str, separator: Option<&str>, limit: Option<u32>) -> Vec<String> {
let lim = limit.unwrap_or(u32::MAX) as usize;
if lim == 0 {
return Vec::new();
}
let Some(sep) = separator else {
return vec![s.to_string()];
};
if sep.is_empty() {
return encode_utf16(s)
.into_iter()
.take(lim)
.map(|u| decode_utf16(&[u]))
.collect();
}
s.split(sep).take(lim).map(str::to_string).collect()
}
pub fn string_replace(s: &str, search: &str, replacement: &str) -> String {
match s.find(search) {
Some(pos) => {
let end = pos + search.len();
let mut result = s[..pos].to_string();
result.push_str(&get_substitution(
replacement,
&s[pos..end],
&s[..pos],
&s[end..],
&[],
));
result.push_str(&s[end..]);
result
}
None => s.to_string(),
}
}
pub fn string_replace_all(s: &str, search: &str, replacement: &str) -> String {
if search.is_empty() {
let units = encode_utf16(s);
let mut result = replacement.to_string();
for u in &units {
result.push_str(&decode_utf16(&[*u]));
result.push_str(replacement);
}
return result;
}
let mut result = String::new();
let mut cursor = 0;
while let Some(relative_pos) = s[cursor..].find(search) {
let pos = cursor + relative_pos;
let end = pos + search.len();
result.push_str(&s[cursor..pos]);
result.push_str(&get_substitution(
replacement,
&s[pos..end],
&s[..pos],
&s[end..],
&[],
));
cursor = end;
}
result.push_str(&s[cursor..]);
result
}
pub fn string_replace_all_checked(
s: &str,
search: &str,
replacement: &str,
is_regex: bool,
has_global_flag: bool,
) -> StatorResult<String> {
if is_regex && !has_global_flag {
return Err(StatorError::TypeError(
"String.prototype.replaceAll called with a non-global RegExp argument".to_string(),
));
}
Ok(string_replace_all(s, search, replacement))
}
pub fn string_replace_all_functional<F>(s: &str, search: &str, replacer: F) -> String
where
F: Fn(&str, usize, &str) -> String,
{
if search.is_empty() {
let units = encode_utf16(s);
let mut result = replacer("", 0, s);
let mut utf16_pos = 0;
for u in &units {
result.push_str(&decode_utf16(&[*u]));
utf16_pos += 1;
result.push_str(&replacer("", utf16_pos, s));
}
return result;
}
let search_units = encode_utf16(search);
let search_len = search_units.len();
let s_units = encode_utf16(s);
let mut result = String::new();
let mut cursor = 0usize; let mut utf16_cursor = 0usize;
while let Some(relative_pos) = s[cursor..].find(search) {
let pos = cursor + relative_pos;
let skipped = &s[cursor..pos];
utf16_cursor += encode_utf16(skipped).len();
result.push_str(skipped);
result.push_str(&replacer(search, utf16_cursor, s));
let end = pos + search.len();
cursor = end;
utf16_cursor += search_len;
}
result.push_str(&s[cursor..]);
let _ = s_units;
result
}
pub fn string_match(s: &str, pattern: &str) -> Option<Vec<String>> {
let re =
stacker::maybe_grow(256 * 1024, 4 * 1024 * 1024, || regress::Regex::new(pattern)).ok()?;
let m = re.find(s)?;
let mut groups = vec![s[m.range()].to_string()];
for cap in &m.captures {
if let Some(range) = cap {
groups.push(s[range.clone()].to_string());
} else {
groups.push(String::new());
}
}
Some(groups)
}
pub fn string_match_all(s: &str, pattern: &str) -> Option<Vec<String>> {
let re =
stacker::maybe_grow(256 * 1024, 4 * 1024 * 1024, || regress::Regex::new(pattern)).ok()?;
let matches: Vec<String> = re.find_iter(s).map(|m| s[m.range()].to_string()).collect();
if matches.is_empty() {
None
} else {
Some(matches)
}
}
pub fn string_repeat(s: &str, count: i64) -> StatorResult<String> {
if count < 0 {
return Err(StatorError::RangeError(
"Invalid count value: must be non-negative".to_string(),
));
}
if s.is_empty() || count == 0 {
return Ok(String::new());
}
let n = count as usize;
let total = n.saturating_mul(utf16_len(s));
if total > MAX_STRING_LEN {
return Err(StatorError::RangeError(
"Invalid count value: result string exceeds maximum length".to_string(),
));
}
Ok(s.repeat(n))
}
pub fn string_repeat_f64(s: &str, count: f64) -> StatorResult<String> {
if count.is_nan() {
return string_repeat(s, 0);
}
if count.is_infinite() || count < 0.0 {
return Err(StatorError::RangeError(
"Invalid count value: must be non-negative and finite".to_string(),
));
}
string_repeat(s, count as i64)
}
pub fn string_pad_start(
s: &str,
target_length: usize,
pad_string: Option<&str>,
) -> StatorResult<String> {
let units = encode_utf16(s);
let len = units.len();
if len >= target_length {
return Ok(s.to_string());
}
if target_length > MAX_STRING_LEN {
return Err(StatorError::RangeError("Invalid string length".to_string()));
}
let pad = pad_string.unwrap_or(" ");
let pad_units = encode_utf16(pad);
if pad_units.is_empty() {
return Ok(s.to_string());
}
let pad_count = target_length - len;
let mut prefix: Vec<u16> = Vec::with_capacity(pad_count);
let mut filled = 0;
while filled < pad_count {
let remaining = pad_count - filled;
let take = remaining.min(pad_units.len());
prefix.extend_from_slice(&pad_units[..take]);
filled += take;
}
let mut result = decode_utf16(&prefix);
result.push_str(s);
Ok(result)
}
pub fn string_pad_end(
s: &str,
target_length: usize,
pad_string: Option<&str>,
) -> StatorResult<String> {
let units = encode_utf16(s);
let len = units.len();
if len >= target_length {
return Ok(s.to_string());
}
if target_length > MAX_STRING_LEN {
return Err(StatorError::RangeError("Invalid string length".to_string()));
}
let pad = pad_string.unwrap_or(" ");
let pad_units = encode_utf16(pad);
if pad_units.is_empty() {
return Ok(s.to_string());
}
let pad_count = target_length - len;
let mut suffix: Vec<u16> = Vec::with_capacity(pad_count);
let mut filled = 0;
while filled < pad_count {
let remaining = pad_count - filled;
let take = remaining.min(pad_units.len());
suffix.extend_from_slice(&pad_units[..take]);
filled += take;
}
let mut result = s.to_string();
result.push_str(&decode_utf16(&suffix));
Ok(result)
}
pub fn string_at(s: &str, index: i64) -> Option<String> {
let units = encode_utf16(s);
let len = units.len() as i128;
let actual = if index < 0 {
len + i128::from(index)
} else {
i128::from(index)
};
if actual < 0 || actual >= len {
return None;
}
Some(decode_utf16(&[units[actual as usize]]))
}
pub fn string_normalize(s: &str, form: Option<&str>) -> StatorResult<String> {
match form.unwrap_or("NFC") {
"NFC" => Ok(s.nfc().collect()),
"NFD" => Ok(s.nfd().collect()),
"NFKC" => Ok(s.nfkc().collect()),
"NFKD" => Ok(s.nfkd().collect()),
f => Err(StatorError::RangeError(format!(
"The normalization form should be one of NFC, NFD, NFKC, or NFKD; got \"{f}\""
))),
}
}
pub fn string_iter(s: &str) -> Vec<String> {
s.chars().map(|c| c.to_string()).collect()
}
pub fn string_raw(raw_strings: &[&str], substitutions: &[&str]) -> String {
let mut result = String::new();
let literal_segments = raw_strings.len();
for (i, raw) in raw_strings.iter().enumerate() {
result.push_str(raw);
if i + 1 < literal_segments && i < substitutions.len() {
result.push_str(substitutions[i]);
}
}
result
}
pub fn string_raw_checked(
raw_strings: Option<&[&str]>,
substitutions: &[&str],
) -> StatorResult<String> {
match raw_strings {
None => Err(StatorError::TypeError(
"Cannot convert undefined or null to object".to_string(),
)),
Some(raw) => Ok(string_raw(raw, substitutions)),
}
}
pub fn string_is_well_formed(s: &str) -> bool {
let units = encode_utf16(s);
let len = units.len();
let mut i = 0;
while i < len {
let cu = units[i];
if (UTF16_HIGH_SURROGATE_START..=UTF16_HIGH_SURROGATE_END).contains(&cu) {
if i + 1 >= len
|| !(UTF16_LOW_SURROGATE_START..=UTF16_LOW_SURROGATE_END).contains(&units[i + 1])
{
return false;
}
i += 2;
} else if (UTF16_LOW_SURROGATE_START..=UTF16_LOW_SURROGATE_END).contains(&cu) {
return false;
} else {
i += 1;
}
}
true
}
pub fn string_to_well_formed(s: &str) -> String {
let units = encode_utf16(s);
let len = units.len();
let mut result: Vec<u16> = Vec::with_capacity(len);
let mut i = 0;
while i < len {
let cu = units[i];
if (UTF16_HIGH_SURROGATE_START..=UTF16_HIGH_SURROGATE_END).contains(&cu) {
if i + 1 < len
&& (UTF16_LOW_SURROGATE_START..=UTF16_LOW_SURROGATE_END).contains(&units[i + 1])
{
result.push(cu);
result.push(units[i + 1]);
i += 2;
} else {
result.push(0xFFFD);
i += 1;
}
} else if (UTF16_LOW_SURROGATE_START..=UTF16_LOW_SURROGATE_END).contains(&cu) {
result.push(0xFFFD);
i += 1;
} else {
result.push(cu);
i += 1;
}
}
decode_utf16(&result)
}
pub fn string_search(s: &str, pattern: &str) -> i64 {
let re = match stacker::maybe_grow(256 * 1024, 4 * 1024 * 1024, || regress::Regex::new(pattern))
{
Ok(r) => r,
Err(_) => return -1,
};
match re.find(s) {
Some(m) => {
let byte_start = m.range().start;
let prefix = &s[..byte_start];
encode_utf16(prefix).len() as i64
}
None => -1,
}
}
pub fn string_substr(s: &str, start: i64, length: Option<i64>) -> String {
let units = encode_utf16(s);
let len = units.len() as i64;
let mut int_start = if start < 0 {
(len + start).max(0)
} else {
start
};
if int_start > len {
int_start = len;
}
let int_start = int_start as usize;
let actual_len = match length {
None => units.len().saturating_sub(int_start),
Some(l) => {
if l <= 0 {
return String::new();
}
(l as usize).min(units.len().saturating_sub(int_start))
}
};
let end = int_start + actual_len;
decode_utf16(&units[int_start..end])
}
fn html_wrap(s: &str, tag: &str) -> String {
format!("<{tag}>{s}</{tag}>")
}
fn html_wrap_attr(s: &str, tag: &str, attr: &str, value: &str) -> String {
let escaped = value.replace('"', """);
format!("<{tag} {attr}=\"{escaped}\">{s}</{tag}>")
}
pub fn string_anchor(s: &str, name: &str) -> String {
html_wrap_attr(s, "a", "name", name)
}
pub fn string_big(s: &str) -> String {
html_wrap(s, "big")
}
pub fn string_blink(s: &str) -> String {
html_wrap(s, "blink")
}
pub fn string_bold(s: &str) -> String {
html_wrap(s, "b")
}
pub fn string_fixed(s: &str) -> String {
html_wrap(s, "tt")
}
pub fn string_fontcolor(s: &str, color: &str) -> String {
html_wrap_attr(s, "font", "color", color)
}
pub fn string_fontsize(s: &str, size: &str) -> String {
html_wrap_attr(s, "font", "size", size)
}
pub fn string_italics(s: &str) -> String {
html_wrap(s, "i")
}
pub fn string_link(s: &str, url: &str) -> String {
html_wrap_attr(s, "a", "href", url)
}
pub fn string_small(s: &str) -> String {
html_wrap(s, "small")
}
pub fn string_strike(s: &str) -> String {
html_wrap(s, "strike")
}
pub fn string_sub(s: &str) -> String {
html_wrap(s, "sub")
}
pub fn string_sup(s: &str) -> String {
html_wrap(s, "sup")
}
pub fn string_locale_compare(s: &str, that: &str) -> i32 {
match s.cmp(that) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
}
}
pub fn string_to_locale_lower_case(s: &str) -> String {
string_to_lower_case(s)
}
pub fn string_to_locale_upper_case(s: &str) -> String {
string_to_upper_case(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_char_code_basic_ascii() {
assert_eq!(string_from_char_code(&[72, 101, 108, 108, 111]), "Hello");
}
#[test]
fn test_from_char_code_empty() {
assert_eq!(string_from_char_code(&[]), "");
}
#[test]
fn test_from_char_code_masking() {
assert_eq!(string_from_char_code(&[0x10041]), "A");
}
#[test]
fn test_from_char_code_surrogate_pair() {
let s = string_from_char_code(&[0xD83D, 0xDE00]);
assert!(!s.is_empty());
}
#[test]
fn test_from_code_point_emoji() {
assert_eq!(string_from_code_point(&[0x1F600]).unwrap(), "😀");
}
#[test]
fn test_from_code_point_ascii() {
assert_eq!(string_from_code_point(&[65, 66, 67]).unwrap(), "ABC");
}
#[test]
fn test_from_code_point_invalid_too_large() {
assert!(matches!(
string_from_code_point(&[0x110000]),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_from_code_point_surrogate_is_invalid() {
assert!(matches!(
string_from_code_point(&[0xD800]),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_from_code_point_empty() {
assert_eq!(string_from_code_point(&[]).unwrap(), "");
}
#[test]
fn test_char_at_ascii() {
assert_eq!(string_char_at("Hello", 0), "H");
assert_eq!(string_char_at("Hello", 4), "o");
}
#[test]
fn test_char_at_out_of_bounds() {
assert_eq!(string_char_at("Hello", 10), "");
}
#[test]
fn test_char_at_negative() {
assert_eq!(string_char_at("Hello", -1), "");
}
#[test]
fn test_char_at_emoji_utf16_index() {
let s = "😀";
assert!(!string_char_at(s, 0).is_empty());
assert!(!string_char_at(s, 1).is_empty());
assert_eq!(string_char_at(s, 2), "");
}
#[test]
fn test_char_code_at_ascii() {
assert_eq!(string_char_code_at("ABC", 0), 65.0);
assert_eq!(string_char_code_at("ABC", 2), 67.0);
}
#[test]
fn test_char_code_at_out_of_bounds_is_nan() {
assert!(string_char_code_at("ABC", 5).is_nan());
}
#[test]
fn test_char_code_at_negative_is_nan() {
assert!(string_char_code_at("ABC", -1).is_nan());
}
#[test]
fn test_char_code_at_emoji_high_surrogate() {
assert_eq!(string_char_code_at("😀", 0), 55357.0);
}
#[test]
fn test_code_point_at_ascii() {
assert_eq!(string_code_point_at("A", 0), Some(65));
}
#[test]
fn test_code_point_at_emoji() {
assert_eq!(string_code_point_at("😀", 0), Some(0x1F600));
}
#[test]
fn test_code_point_at_emoji_low_surrogate_index() {
let low = string_code_point_at("😀", 1).unwrap();
assert_eq!(low, 0xDE00);
}
#[test]
fn test_code_point_at_out_of_bounds() {
assert_eq!(string_code_point_at("A", 5), None);
}
#[test]
fn test_code_point_at_negative() {
assert_eq!(string_code_point_at("A", -1), None);
}
#[test]
fn test_concat_multiple() {
assert_eq!(
string_concat("Hello", &[", ", "world", "!"]),
"Hello, world!"
);
}
#[test]
fn test_concat_empty_others() {
assert_eq!(string_concat("abc", &[]), "abc");
}
#[test]
fn test_concat_unicode() {
assert_eq!(string_concat("café", &[" 😀"]), "café 😀");
}
#[test]
fn test_slice_basic() {
assert_eq!(string_slice("Hello", 1, Some(4)), "ell");
}
#[test]
fn test_slice_no_end() {
assert_eq!(string_slice("Hello", 2, None), "llo");
}
#[test]
fn test_slice_negative_start() {
assert_eq!(string_slice("Hello", -3, None), "llo");
}
#[test]
fn test_slice_negative_end() {
assert_eq!(string_slice("Hello", 0, Some(-1)), "Hell");
}
#[test]
fn test_slice_start_ge_end_returns_empty() {
assert_eq!(string_slice("Hello", 4, Some(2)), "");
}
#[test]
fn test_slice_unicode() {
assert_eq!(string_slice("café", 0, Some(3)), "caf");
}
#[test]
fn test_substring_basic() {
assert_eq!(string_substring("Hello", 1, Some(4)), "ell");
}
#[test]
fn test_substring_swaps_when_start_gt_end() {
assert_eq!(string_substring("Hello", 4, Some(1)), "ell");
}
#[test]
fn test_substring_negative_clamped_to_zero() {
assert_eq!(string_substring("Hello", -5, Some(3)), "Hel");
}
#[test]
fn test_substring_no_end() {
assert_eq!(string_substring("Hello", 2, None), "llo");
}
#[test]
fn test_index_of_found() {
assert_eq!(string_index_of("hello world", "world", None), 6);
}
#[test]
fn test_index_of_not_found() {
assert_eq!(string_index_of("hello world", "xyz", None), -1);
}
#[test]
fn test_index_of_with_from_index() {
assert_eq!(string_index_of("aaa", "a", Some(1)), 1);
}
#[test]
fn test_index_of_empty_search() {
assert_eq!(string_index_of("hello", "", Some(2)), 2);
}
#[test]
fn test_index_of_unicode() {
let s = "café";
assert_eq!(string_index_of(s, "é", None), 3);
}
#[test]
fn test_last_index_of_found() {
assert_eq!(string_last_index_of("hello world hello", "hello", None), 12);
}
#[test]
fn test_last_index_of_not_found() {
assert_eq!(string_last_index_of("hello", "xyz", None), -1);
}
#[test]
fn test_last_index_of_with_from_index() {
assert_eq!(string_last_index_of("aaa", "a", Some(1)), 1);
}
#[test]
fn test_includes_true() {
assert!(string_includes("hello world", "world", None));
}
#[test]
fn test_includes_false() {
assert!(!string_includes("hello world", "xyz", None));
}
#[test]
fn test_includes_with_position() {
assert!(!string_includes("hello", "hel", Some(1)));
}
#[test]
fn test_starts_with_true() {
assert!(string_starts_with("Hello", "Hel", None));
}
#[test]
fn test_starts_with_false() {
assert!(!string_starts_with("Hello", "ello", None));
}
#[test]
fn test_starts_with_position() {
assert!(string_starts_with("Hello", "ello", Some(1)));
}
#[test]
fn test_starts_with_empty_search() {
assert!(string_starts_with("Hello", "", None));
}
#[test]
fn test_ends_with_true() {
assert!(string_ends_with("Hello", "llo", None));
}
#[test]
fn test_ends_with_false() {
assert!(!string_ends_with("Hello", "Hel", None));
}
#[test]
fn test_ends_with_end_position() {
assert!(string_ends_with("Hello", "Hel", Some(3)));
}
#[test]
fn test_ends_with_empty_search() {
assert!(string_ends_with("Hello", "", None));
}
#[test]
fn test_to_upper_case_ascii() {
assert_eq!(string_to_upper_case("hello"), "HELLO");
}
#[test]
fn test_to_upper_case_unicode() {
assert_eq!(string_to_upper_case("café"), "CAFÉ");
}
#[test]
fn test_to_lower_case_ascii() {
assert_eq!(string_to_lower_case("HELLO"), "hello");
}
#[test]
fn test_to_lower_case_unicode() {
assert_eq!(string_to_lower_case("CAFÉ"), "café");
}
#[test]
fn test_trim_basic() {
assert_eq!(string_trim(" hello "), "hello");
}
#[test]
fn test_trim_tabs_newlines() {
assert_eq!(string_trim("\t\nhello\r\n"), "hello");
}
#[test]
fn test_trim_start() {
assert_eq!(string_trim_start(" hello "), "hello ");
}
#[test]
fn test_trim_end() {
assert_eq!(string_trim_end(" hello "), " hello");
}
#[test]
fn test_trim_empty_string() {
assert_eq!(string_trim(""), "");
}
#[test]
fn test_split_by_comma() {
assert_eq!(string_split("a,b,c", Some(","), None), vec!["a", "b", "c"]);
}
#[test]
fn test_split_by_empty_string() {
assert_eq!(string_split("abc", Some(""), None), vec!["a", "b", "c"]);
}
#[test]
fn test_split_no_separator() {
assert_eq!(string_split("abc", None, None), vec!["abc"]);
}
#[test]
fn test_split_with_limit() {
assert_eq!(string_split("a,b,c", Some(","), Some(2)), vec!["a", "b"]);
}
#[test]
fn test_split_with_limit_zero() {
assert_eq!(
string_split("a,b,c", Some(","), Some(0)),
Vec::<String>::new()
);
}
#[test]
fn test_split_empty_string_by_empty() {
assert_eq!(string_split("", Some(""), None), Vec::<String>::new());
}
#[test]
fn test_split_emoji_by_empty() {
let parts = string_split("😀", Some(""), None);
assert_eq!(parts.len(), 2);
}
#[test]
fn test_replace_first_occurrence_only() {
assert_eq!(string_replace("aaa", "a", "b"), "baa");
}
#[test]
fn test_replace_not_found() {
assert_eq!(string_replace("hello", "x", "y"), "hello");
}
#[test]
fn test_replace_middle() {
assert_eq!(string_replace("aabbcc", "bb", "XX"), "aaXXcc");
}
#[test]
fn test_replace_expands_dollar_dollar() {
assert_eq!(string_replace("abc", "b", "$$"), "a$c");
}
#[test]
fn test_replace_expands_dollar_ampersand() {
assert_eq!(string_replace("abc", "b", "[$&]"), "a[b]c");
}
#[test]
fn test_replace_expands_dollar_backtick_and_quote() {
assert_eq!(string_replace("abc", "b", "$`-$'"), "aa-cc");
}
#[test]
fn test_replace_leaves_missing_capture_literal() {
assert_eq!(string_replace("abc", "b", "$1"), "a$1c");
}
#[test]
fn test_replace_all_basic() {
assert_eq!(string_replace_all("aabbaa", "aa", "X"), "XbbX");
}
#[test]
fn test_replace_all_not_found() {
assert_eq!(string_replace_all("hello", "x", "y"), "hello");
}
#[test]
fn test_replace_all_empty_search_inserts_between_units() {
let result = string_replace_all("ab", "", "-");
assert_eq!(result, "-a-b-");
}
#[test]
fn test_replace_all_expands_replacement_patterns() {
assert_eq!(string_replace_all("aba", "a", "[$&]-$$"), "[a]-$b[a]-$");
}
#[test]
fn test_match_basic() {
let m = string_match("hello world", r"(\w+)\s(\w+)").unwrap();
assert_eq!(m[0], "hello world");
assert_eq!(m[1], "hello");
assert_eq!(m[2], "world");
}
#[test]
fn test_match_no_match_returns_none() {
assert!(string_match("hello", r"\d+").is_none());
}
#[test]
fn test_match_invalid_pattern_returns_none() {
assert!(string_match("hello", r"[invalid").is_none());
}
#[test]
fn test_match_all_digits() {
let matches = string_match_all("test 1 and 2 and 3", r"\d+").unwrap();
assert_eq!(matches, vec!["1", "2", "3"]);
}
#[test]
fn test_match_all_no_match_returns_none() {
assert!(string_match_all("hello", r"\d+").is_none());
}
#[test]
fn test_repeat_basic() {
assert_eq!(string_repeat("ab", 3).unwrap(), "ababab");
}
#[test]
fn test_repeat_zero() {
assert_eq!(string_repeat("x", 0).unwrap(), "");
}
#[test]
fn test_repeat_negative_is_error() {
assert!(matches!(
string_repeat("a", -1),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_repeat_unicode() {
assert_eq!(string_repeat("😀", 2).unwrap(), "😀😀");
}
#[test]
fn test_repeat_exceeds_max_len() {
assert!(matches!(
string_repeat("a", (MAX_STRING_LEN as i64) + 1),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_pad_start_default_pad() {
assert_eq!(string_pad_start("5", 3, None).unwrap(), " 5");
}
#[test]
fn test_pad_start_custom_pad() {
assert_eq!(string_pad_start("5", 3, Some("0")).unwrap(), "005");
}
#[test]
fn test_pad_start_already_long_enough() {
assert_eq!(string_pad_start("hello", 3, None).unwrap(), "hello");
}
#[test]
fn test_pad_start_multi_char_pad() {
assert_eq!(string_pad_start("abc", 8, Some("xy")).unwrap(), "xyxyxabc");
}
#[test]
fn test_pad_end_default_pad() {
assert_eq!(string_pad_end("5", 3, None).unwrap(), "5 ");
}
#[test]
fn test_pad_end_custom_pad() {
assert_eq!(string_pad_end("5", 3, Some("0")).unwrap(), "500");
}
#[test]
fn test_pad_end_already_long_enough() {
assert_eq!(string_pad_end("hello", 3, None).unwrap(), "hello");
}
#[test]
fn test_pad_end_multi_char_pad() {
assert_eq!(string_pad_end("abc", 8, Some("xy")).unwrap(), "abcxyxyx");
}
#[test]
fn test_pad_start_exceeds_max_len() {
assert!(matches!(
string_pad_start("a", MAX_STRING_LEN + 1, None),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_pad_end_exceeds_max_len() {
assert!(matches!(
string_pad_end("a", MAX_STRING_LEN + 1, None),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_at_positive_index() {
assert_eq!(string_at("Hello", 0), Some("H".to_string()));
assert_eq!(string_at("Hello", 4), Some("o".to_string()));
}
#[test]
fn test_at_negative_index() {
assert_eq!(string_at("Hello", -1), Some("o".to_string()));
assert_eq!(string_at("Hello", -5), Some("H".to_string()));
}
#[test]
fn test_at_out_of_bounds() {
assert_eq!(string_at("Hello", 10), None);
assert_eq!(string_at("Hello", -10), None);
}
#[test]
fn test_normalize_nfc_default() {
assert_eq!(string_normalize("hello", None).unwrap(), "hello");
}
#[test]
fn test_normalize_accepted_forms() {
for form in &["NFC", "NFD", "NFKC", "NFKD"] {
assert!(string_normalize("hello", Some(form)).is_ok());
}
}
#[test]
fn test_normalize_invalid_form_is_range_error() {
assert!(matches!(
string_normalize("hello", Some("XYZ")),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_iter_ascii() {
assert_eq!(string_iter("abc"), vec!["a", "b", "c"]);
}
#[test]
fn test_iter_unicode_emoji_is_single_element() {
assert_eq!(string_iter("a😀b"), vec!["a", "😀", "b"]);
}
#[test]
fn test_iter_empty_string() {
assert_eq!(string_iter(""), Vec::<String>::new());
}
#[test]
fn test_iter_multi_codepoint_string() {
let chars = string_iter("héllo");
assert_eq!(chars.len(), 5);
assert_eq!(chars[1], "é");
}
#[test]
fn test_utf16_length_of_emoji_is_two() {
let units = encode_utf16("😀");
assert_eq!(units.len(), 2);
}
#[test]
fn test_slice_preserves_surrogate_pair() {
assert_eq!(string_slice("😀", 0, Some(2)), "😀");
}
#[test]
fn test_index_of_emoji() {
let s = "a😀b";
assert_eq!(string_index_of(s, "😀", None), 1);
}
#[test]
fn test_starts_with_emoji() {
assert!(string_starts_with("😀hello", "😀", None));
}
#[test]
fn test_ends_with_emoji() {
assert!(string_ends_with("hello😀", "😀", None));
}
#[test]
fn test_pad_start_unicode_pad_string() {
let result = string_pad_start("a", 3, Some("é")).unwrap();
assert_eq!(result, "ééa");
}
#[test]
fn test_substring_unicode_boundary() {
assert_eq!(string_substring("café", 0, Some(4)), "café");
assert_eq!(string_substring("café", 3, Some(4)), "é");
}
#[test]
fn test_replace_unicode() {
assert_eq!(string_replace("héllo", "é", "e"), "hello");
}
#[test]
fn test_repeat_empty_string() {
assert_eq!(string_repeat("", 100).unwrap(), "");
}
#[test]
fn test_raw_basic() {
assert_eq!(string_raw(&["a", "b", "c"], &["1", "2"]), "a1b2c");
}
#[test]
fn test_raw_preserves_escape_sequences() {
assert_eq!(string_raw(&["hello\\n", "world"], &["!"]), "hello\\n!world");
}
#[test]
fn test_raw_empty_strings() {
assert_eq!(string_raw(&[], &[]), "");
}
#[test]
fn test_raw_more_strings_than_substitutions() {
assert_eq!(string_raw(&["a", "b", "c"], &["1"]), "a1bc");
}
#[test]
fn test_raw_single_segment_no_substitution() {
assert_eq!(string_raw(&["hello"], &[]), "hello");
}
#[test]
fn test_is_well_formed_ascii() {
assert!(string_is_well_formed("hello"));
}
#[test]
fn test_is_well_formed_emoji() {
assert!(string_is_well_formed("Hello 😀"));
}
#[test]
fn test_is_well_formed_empty() {
assert!(string_is_well_formed(""));
}
#[test]
fn test_is_well_formed_unicode_chars() {
assert!(string_is_well_formed("café résumé"));
}
#[test]
fn test_to_well_formed_normal_string() {
assert_eq!(string_to_well_formed("Hello"), "Hello");
}
#[test]
fn test_to_well_formed_empty() {
assert_eq!(string_to_well_formed(""), "");
}
#[test]
fn test_to_well_formed_emoji() {
assert_eq!(string_to_well_formed("a😀b"), "a😀b");
}
#[test]
fn test_search_found() {
assert_eq!(string_search("abc123def", r"\d+"), 3);
}
#[test]
fn test_search_not_found() {
assert_eq!(string_search("hello world", r"\d+"), -1);
}
#[test]
fn test_search_at_start() {
assert_eq!(string_search("123abc", r"\d+"), 0);
}
#[test]
fn test_search_invalid_regex() {
assert_eq!(string_search("hello", r"[invalid"), -1);
}
#[test]
fn test_repeat_overflow_is_range_error() {
assert!(matches!(
string_repeat("ab", i64::MAX),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_starts_with_position_beyond_length() {
assert!(string_starts_with("Hello", "", Some(100)));
}
#[test]
fn test_starts_with_empty_both() {
assert!(string_starts_with("", "", None));
}
#[test]
fn test_ends_with_end_position_zero() {
assert!(string_ends_with("Hello", "", Some(0)));
assert!(!string_ends_with("Hello", "H", Some(0)));
}
#[test]
fn test_ends_with_empty_both() {
assert!(string_ends_with("", "", None));
}
#[test]
fn test_substr_basic() {
assert_eq!(string_substr("hello", 1, Some(3)), "ell");
}
#[test]
fn test_substr_negative_start() {
assert_eq!(string_substr("hello", -3, Some(2)), "ll");
}
#[test]
fn test_substr_no_length() {
assert_eq!(string_substr("hello", 1, None), "ello");
}
#[test]
fn test_substr_zero_length() {
assert_eq!(string_substr("hello", 0, Some(0)), "");
}
#[test]
fn test_substr_from_start() {
assert_eq!(string_substr("hello", 0, None), "hello");
}
#[test]
fn test_html_anchor() {
assert_eq!(string_anchor("text", "n"), "<a name=\"n\">text</a>");
}
#[test]
fn test_html_anchor_quote_escaping() {
assert_eq!(
string_anchor("text", "a\"b"),
"<a name=\"a"b\">text</a>"
);
}
#[test]
fn test_html_big() {
assert_eq!(string_big("text"), "<big>text</big>");
}
#[test]
fn test_html_blink() {
assert_eq!(string_blink("text"), "<blink>text</blink>");
}
#[test]
fn test_html_bold() {
assert_eq!(string_bold("text"), "<b>text</b>");
}
#[test]
fn test_html_fixed() {
assert_eq!(string_fixed("text"), "<tt>text</tt>");
}
#[test]
fn test_html_fontcolor() {
assert_eq!(
string_fontcolor("text", "red"),
"<font color=\"red\">text</font>"
);
}
#[test]
fn test_html_fontsize() {
assert_eq!(string_fontsize("text", "7"), "<font size=\"7\">text</font>");
}
#[test]
fn test_html_italics() {
assert_eq!(string_italics("text"), "<i>text</i>");
}
#[test]
fn test_html_link() {
assert_eq!(
string_link("text", "http://x"),
"<a href=\"http://x\">text</a>"
);
}
#[test]
fn test_html_small() {
assert_eq!(string_small("text"), "<small>text</small>");
}
#[test]
fn test_html_strike() {
assert_eq!(string_strike("text"), "<strike>text</strike>");
}
#[test]
fn test_html_sub() {
assert_eq!(string_sub("text"), "<sub>text</sub>");
}
#[test]
fn test_html_sup() {
assert_eq!(string_sup("text"), "<sup>text</sup>");
}
#[test]
fn test_locale_compare_less() {
assert_eq!(string_locale_compare("a", "b"), -1);
}
#[test]
fn test_locale_compare_equal() {
assert_eq!(string_locale_compare("abc", "abc"), 0);
}
#[test]
fn test_locale_compare_greater() {
assert_eq!(string_locale_compare("b", "a"), 1);
}
#[test]
fn test_locale_compare_empty_strings() {
assert_eq!(string_locale_compare("", ""), 0);
}
#[test]
fn test_locale_compare_empty_vs_non_empty() {
assert_eq!(string_locale_compare("", "a"), -1);
assert_eq!(string_locale_compare("a", ""), 1);
}
#[test]
fn test_to_locale_lower_case() {
assert_eq!(string_to_locale_lower_case("HELLO"), "hello");
}
#[test]
fn test_to_locale_lower_case_unicode() {
assert_eq!(string_to_locale_lower_case("CAFÉ"), "café");
}
#[test]
fn test_to_locale_upper_case() {
assert_eq!(string_to_locale_upper_case("hello"), "HELLO");
}
#[test]
fn test_to_locale_upper_case_unicode() {
assert_eq!(string_to_locale_upper_case("café"), "CAFÉ");
}
#[test]
fn test_replace_all_checked_regex_without_global_throws() {
let r = string_replace_all_checked("abc", "a", "x", true, false);
assert!(matches!(r, Err(StatorError::TypeError(_))));
}
#[test]
fn test_replace_all_checked_regex_with_global_ok() {
let r = string_replace_all_checked("aabb", "a", "x", true, true).unwrap();
assert_eq!(r, "xxbb");
}
#[test]
fn test_replace_all_checked_string_pattern_ignores_flags() {
let r = string_replace_all_checked("aabb", "a", "x", false, false).unwrap();
assert_eq!(r, "xxbb");
}
#[test]
fn test_replace_all_functional_basic() {
let result =
string_replace_all_functional("aXbXc", "X", |m, pos, _| format!("[{m}@{pos}]"));
assert_eq!(result, "a[X@1]b[X@3]c");
}
#[test]
fn test_replace_all_functional_empty_search() {
let result = string_replace_all_functional("ab", "", |_m, pos, _| format!("{pos}"));
assert_eq!(result, "0a1b2");
}
#[test]
fn test_replace_all_functional_no_match() {
let result = string_replace_all_functional("hello", "xyz", |_, _, _| "!".to_string());
assert_eq!(result, "hello");
}
#[test]
fn test_replace_all_functional_whole_string_match() {
let result = string_replace_all_functional("aa", "aa", |_, _, _| "bb".to_string());
assert_eq!(result, "bb");
}
#[test]
fn test_replace_all_dollar_ampersand_substitution() {
assert_eq!(string_replace_all("abab", "ab", "[$&]"), "[ab][ab]");
}
#[test]
fn test_replace_all_dollar_backtick_substitution() {
assert_eq!(string_replace_all("XaX", "a", "[$`]"), "X[X]X");
}
#[test]
fn test_replace_all_dollar_tick_substitution() {
assert_eq!(string_replace_all("XaY", "a", "[$']"), "X[Y]Y");
}
#[test]
fn test_raw_empty_template() {
assert_eq!(string_raw(&[], &[]), "");
}
#[test]
fn test_raw_single_segment_no_substitution_v2() {
assert_eq!(string_raw(&["hello"], &[]), "hello");
}
#[test]
fn test_raw_preserves_backslash_sequences() {
assert_eq!(string_raw(&["a\\nb"], &[]), "a\\nb");
}
#[test]
fn test_raw_extra_substitutions_ignored() {
assert_eq!(string_raw(&["a", "b"], &["1", "2", "3"]), "a1b");
}
#[test]
fn test_raw_fewer_substitutions_than_gaps() {
assert_eq!(string_raw(&["a", "b", "c"], &["1"]), "a1bc");
}
#[test]
fn test_raw_checked_null_template_throws() {
assert!(matches!(
string_raw_checked(None, &[]),
Err(StatorError::TypeError(_))
));
}
#[test]
fn test_raw_checked_valid_template() {
assert_eq!(
string_raw_checked(Some(&["x", "y"]), &["+"]).unwrap(),
"x+y"
);
}
#[test]
fn test_raw_unicode_segments() {
assert_eq!(string_raw(&["café", "naïve"], &[" & "]), "café & naïve");
}
#[test]
fn test_normalize_nfd_decomposes_accented() {
let composed = "\u{00E9}";
let decomposed = string_normalize(composed, Some("NFD")).unwrap();
assert_eq!(decomposed, "e\u{0301}");
assert_ne!(composed, &decomposed);
}
#[test]
fn test_normalize_nfc_recomposes() {
let decomposed = "e\u{0301}";
let composed = string_normalize(decomposed, Some("NFC")).unwrap();
assert_eq!(composed, "\u{00E9}");
}
#[test]
fn test_normalize_nfkd_compatibility_decomposition() {
let ligature = "\u{FB01}";
let decomposed = string_normalize(ligature, Some("NFKD")).unwrap();
assert_eq!(decomposed, "fi");
}
#[test]
fn test_normalize_nfkc_compatibility_composition() {
let ligature = "\u{FB01}";
let composed = string_normalize(ligature, Some("NFKC")).unwrap();
assert_eq!(composed, "fi");
}
#[test]
fn test_normalize_default_is_nfc() {
let decomposed = "e\u{0301}";
assert_eq!(
string_normalize(decomposed, None).unwrap(),
string_normalize(decomposed, Some("NFC")).unwrap()
);
}
#[test]
fn test_normalize_ascii_is_identity() {
for form in &["NFC", "NFD", "NFKC", "NFKD"] {
assert_eq!(string_normalize("hello", Some(form)).unwrap(), "hello");
}
}
#[test]
fn test_normalize_empty_string() {
assert_eq!(string_normalize("", None).unwrap(), "");
}
#[test]
fn test_locale_compare_equal_strings() {
assert_eq!(string_locale_compare("abc", "abc"), 0);
}
#[test]
fn test_locale_compare_empty_strings_v2() {
assert_eq!(string_locale_compare("", ""), 0);
}
#[test]
fn test_locale_compare_less_than() {
assert!(string_locale_compare("a", "b") < 0);
}
#[test]
fn test_locale_compare_greater_than() {
assert!(string_locale_compare("b", "a") > 0);
}
#[test]
fn test_locale_compare_prefix() {
assert!(string_locale_compare("ab", "abc") < 0);
}
#[test]
fn test_to_locale_lower_case_empty() {
assert_eq!(string_to_locale_lower_case(""), "");
}
#[test]
fn test_to_locale_upper_case_empty() {
assert_eq!(string_to_locale_upper_case(""), "");
}
#[test]
fn test_to_locale_lower_case_already_lower() {
assert_eq!(string_to_locale_lower_case("hello"), "hello");
}
#[test]
fn test_to_locale_upper_case_already_upper() {
assert_eq!(string_to_locale_upper_case("HELLO"), "HELLO");
}
#[test]
fn test_pad_start_empty_fill_returns_original() {
assert_eq!(string_pad_start("abc", 10, Some("")).unwrap(), "abc");
}
#[test]
fn test_pad_end_empty_fill_returns_original() {
assert_eq!(string_pad_end("abc", 10, Some("")).unwrap(), "abc");
}
#[test]
fn test_pad_start_target_equals_length() {
assert_eq!(string_pad_start("abc", 3, Some("0")).unwrap(), "abc");
}
#[test]
fn test_pad_end_target_equals_length() {
assert_eq!(string_pad_end("abc", 3, Some("0")).unwrap(), "abc");
}
#[test]
fn test_pad_start_fill_truncated() {
assert_eq!(string_pad_start("x", 3, Some("abc")).unwrap(), "abx");
}
#[test]
fn test_pad_end_fill_truncated() {
assert_eq!(string_pad_end("x", 3, Some("abc")).unwrap(), "xab");
}
#[test]
fn test_pad_start_unicode_fill() {
let r = string_pad_start("5", 5, Some("😀")).unwrap();
assert_eq!(utf16_len(&r), 5);
}
#[test]
fn test_pad_end_zero_target() {
assert_eq!(string_pad_end("hello", 0, None).unwrap(), "hello");
}
#[test]
fn test_repeat_empty_string_returns_empty() {
assert_eq!(string_repeat("", 1000).unwrap(), "");
}
#[test]
fn test_repeat_one() {
assert_eq!(string_repeat("abc", 1).unwrap(), "abc");
}
#[test]
fn test_repeat_f64_infinity_is_range_error() {
assert!(matches!(
string_repeat_f64("a", f64::INFINITY),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_repeat_f64_neg_infinity_is_range_error() {
assert!(matches!(
string_repeat_f64("a", f64::NEG_INFINITY),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_repeat_f64_nan_is_zero() {
assert_eq!(string_repeat_f64("abc", f64::NAN).unwrap(), "");
}
#[test]
fn test_repeat_f64_negative_is_range_error() {
assert!(matches!(
string_repeat_f64("a", -1.0),
Err(StatorError::RangeError(_))
));
}
#[test]
fn test_repeat_f64_fractional_truncates() {
assert_eq!(string_repeat_f64("ab", 2.9).unwrap(), "abab");
}
#[test]
fn test_at_empty_string() {
assert_eq!(string_at("", 0), None);
}
#[test]
fn test_at_negative_wraps_around() {
assert_eq!(string_at("abcde", -2), Some("d".to_string()));
}
#[test]
fn test_at_negative_exactly_length() {
assert_eq!(string_at("abcde", -5), Some("a".to_string()));
}
#[test]
fn test_at_negative_beyond_length() {
assert_eq!(string_at("abcde", -6), None);
}
#[test]
fn test_at_emoji_surrogate_pair() {
let s = "😀";
assert!(string_at(s, 0).is_some());
assert!(string_at(s, 1).is_some());
assert_eq!(string_at(s, 2), None);
}
}