Shared Rust kernels
ported/text_kernels
All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.
text · 3 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn text(units: Vec<u16>) -> ExcelText {
ExcelText::from_utf16_code_units(units)
}
Used by 64 functions.
text_from_str · 3 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn text_from_str(s: &str) -> ExcelText {
ExcelText::from_utf16_code_units(s.encode_utf16().collect())
}
Used by 54 functions.
date_integer_floor · 15 lines · calc-func/src/ported/text_kernels.rs on GitHub/// Observed date-argument conversion (OxFunc date_fn.rs): the inclusive boundary is a two-step
/// binary rounding rule. LEFT and RIGHT share it.
pub(crate) fn date_integer_floor(number: f64) -> f64 {
let upper = number.ceil();
let threshold = if upper > 0.0 {
2.0_f64.powi(-22) + 2.0_f64.powi(-33)
} else {
2.0_f64.powi(-23) + 2.0_f64.powi(-34)
};
if upper - number <= threshold {
upper
} else {
number.floor()
}
}
Used by 28 functions.
nonnegative_count · 19 lines · calc-func/src/ported/text_kernels.rs on GitHub/// A character count from a number: LEFT and RIGHT (`tolerant`) use the date-argument floor,
/// MID plain truncation with signed 32-bit admission.
pub(crate) fn nonnegative_count(n: f64, tolerant: bool) -> Result<usize, WorksheetErrorCode> {
if !n.is_finite() || n < 0.0 || (!tolerant && n >= 2147483648.0) {
return Err(WorksheetErrorCode::Value);
}
let truncated = if tolerant {
date_integer_floor(n)
} else {
n.trunc()
};
if truncated < 0.0 {
return Err(WorksheetErrorCode::Value);
}
if truncated > usize::MAX as f64 {
return Ok(usize::MAX);
}
Ok(truncated as usize)
}
Used by 7 functions.
mid_start · 11 lines · calc-func/src/ported/text_kernels.rs on GitHub/// MID's one-based start.
pub(crate) fn mid_start(n: f64) -> Result<usize, WorksheetErrorCode> {
if !n.is_finite() || n >= 2147483648.0 {
return Err(WorksheetErrorCode::Value);
}
let truncated = n.trunc();
if truncated < 1.0 {
return Err(WorksheetErrorCode::Value);
}
Ok(truncated as usize)
}
Used by 2 functions.
unit_width · 8 lines · calc-func/src/ported/text_kernels.rs on GitHubfn unit_width(units: &[u16], offset: usize) -> usize {
if (0xD800..=0xDBFF).contains(&units[offset])
&& units.get(offset + 1).is_some_and(|next| (0xDC00..=0xDFFF).contains(next)) {
2
} else {
1
}
}
Used by 13 functions.
character_boundary · 9 lines · calc-func/src/ported/text_kernels.rs on GitHubfn character_boundary(text: &ExcelText, count: usize) -> usize {
let units = text.utf16_code_units();
let (mut offset, mut seen) = (0, 0);
while offset < units.len() && seen < count {
offset += unit_width(units, offset);
seen += 1;
}
offset
}
Used by 13 functions.
slice_character_count · 3 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn slice_character_count(text: &ExcelText) -> usize {
std::char::decode_utf16(text.utf16_code_units().iter().copied()).count()
}
Used by 10 functions.
character_scan_end · 8 lines · calc-func/src/ported/text_kernels.rs on GitHubfn character_scan_end(text: &ExcelText) -> usize {
text.len_utf16_code_units()
- usize::from(
text.utf16_code_units()
.last()
.is_some_and(|unit| (0xD800..=0xDBFF).contains(unit)),
)
}
Used by 6 functions.
len_character_count · 6 lines · calc-func/src/ported/text_kernels.rs on GitHub/// The characters LEFT and Version2 LEN count: a pair once, and a trailing unmatched high
/// surrogate not at all (the fn6 LEN control retains the isolated-high case).
pub(crate) fn len_character_count(text: &ExcelText) -> usize {
slice_character_count(text)
- usize::from(character_scan_end(text) < text.len_utf16_code_units())
}
Used by 6 functions.
take_left · 12 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn take_left(text: &ExcelText, count: usize) -> ExcelText {
// A forward scan that stops at an unmatched final high surrogate before reaching count falls
// back to the raw unit count, as Excel does.
let end = if character_scan_end(text) < text.len_utf16_code_units()
&& count > len_character_count(text)
{
count.min(text.len_utf16_code_units())
} else {
character_boundary(text, count)
};
ExcelText::from_utf16_code_units(text.utf16_code_units()[..end].to_vec())
}
Used by 3 functions.
take_right · 4 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn take_right(text: &ExcelText, count: usize) -> ExcelText {
let start = character_boundary(text, slice_character_count(text).saturating_sub(count));
ExcelText::from_utf16_code_units(text.utf16_code_units()[start..].to_vec())
}
Used by 3 functions.
take_mid · 15 lines · calc-func/src/ported/text_kernels.rs on GitHub/// MID counts valid surrogate pairs once in Version 2; MIDB keeps its UTF-16 unit policy.
pub(crate) fn take_mid(text: &ExcelText, start_one_based: usize, count: usize, pairs: bool) -> ExcelText {
let units = text.utf16_code_units();
let first = start_one_based.saturating_sub(1);
let start = if pairs { character_boundary(text, first) } else { first };
if count == 0 || start >= units.len() {
return ExcelText::from_utf16_code_units(Vec::new());
}
let end = if pairs {
character_boundary(text, first.saturating_add(count))
} else {
start.saturating_add(count).min(units.len())
};
ExcelText::from_utf16_code_units(units[start..end].to_vec())
}
Used by 2 functions.
truncated · 6 lines · calc-func/src/ported/text_kernels.rs on GitHubfn truncated(n: f64) -> Result<f64, WorksheetErrorCode> {
if !n.is_finite() {
return Err(WorksheetErrorCode::Value);
}
Ok(n.trunc())
}
Used by 14 functions.
positive · 11 lines · calc-func/src/ported/text_kernels.rs on GitHub/// A one-based start or instance number: at least 1.
pub(crate) fn positive(n: f64) -> Result<usize, WorksheetErrorCode> {
let t = truncated(n)?;
if t < 1.0 {
return Err(WorksheetErrorCode::Value);
}
if t > usize::MAX as f64 {
return Ok(usize::MAX);
}
Ok(t as usize)
}
Used by 9 functions.
nonnegative · 11 lines · calc-func/src/ported/text_kernels.rs on GitHub/// A count of at least 0.
pub(crate) fn nonnegative(n: f64) -> Result<usize, WorksheetErrorCode> {
let t = truncated(n)?;
if t < 0.0 {
return Err(WorksheetErrorCode::Value);
}
if t > usize::MAX as f64 {
return Ok(usize::MAX);
}
Ok(t as usize)
}
Used by 2 functions.
fold_unit · 19 lines · calc-func/src/ported/text_kernels.rs on GitHub/// A UTF-16 unit with its case folded for SEARCH: the simple lower-case mapping of a unit that is
/// a whole character and maps to one character of one unit; any other unit as it is.
pub(crate) fn fold_unit(unit: u16) -> u16 {
if unit < 0x80 {
return if (b'A' as u16..=b'Z' as u16).contains(&unit) {
unit + 32
} else {
unit
};
}
let Some(ch) = char::from_u32(u32::from(unit)) else {
return unit;
};
let mut lower = ch.to_lowercase();
match (lower.next(), lower.next()) {
(Some(single), None) if (single as u32) <= 0xFFFF => single as u32 as u16,
_ => unit,
}
}
Used by 22 functions.
search_units_equal · 3 lines · calc-func/src/ported/text_kernels.rs on GitHubfn search_units_equal(lhs: u16, rhs: u16) -> bool {
lhs == rhs || fold_unit(lhs) == fold_unit(rhs)
}
Used by 2 functions.
replace · 23 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn replace(
old_text: &ExcelText,
start_num: usize,
num_chars: usize,
new_text: &ExcelText,
pairs: bool,
) -> ExcelText {
let units = old_text.utf16_code_units();
let first = start_num.saturating_sub(1);
let start_index = if pairs { character_boundary(old_text, first) } else { first.min(units.len()) };
let end_index = if pairs {
character_boundary(old_text, first.saturating_add(num_chars))
} else {
start_index.saturating_add(num_chars).min(units.len())
};
let mut out = Vec::with_capacity(
start_index + new_text.len_utf16_code_units() + units.len().saturating_sub(end_index),
);
out.extend_from_slice(&units[..start_index]);
out.extend_from_slice(new_text.utf16_code_units());
out.extend_from_slice(&units[end_index..]);
ExcelText::from_utf16_code_units(out)
}
Used by 2 functions.
find · 28 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn find(
find_text: &ExcelText,
within_text: &ExcelText,
start_num: usize,
pairs: bool,
) -> Result<f64, WorksheetErrorCode> {
let needle = find_text.utf16_code_units();
let hay = within_text.utf16_code_units();
// Fn6 distinguishes a lone high surrogate from a lone low surrogate: in
// Version2 FIND the high has the same admitted starts as an empty needle.
if needle.is_empty() || (pairs && len_character_count(find_text) == 0) {
let length = if pairs { slice_character_count(within_text) } else { hay.len() };
if start_num <= length + 1 {
return Ok(start_num as f64);
}
return Err(WorksheetErrorCode::Value);
}
let mut index = if pairs { character_boundary(within_text, start_num - 1) } else { start_num - 1 };
let mut position = start_num;
while index < hay.len() && needle.len() <= hay.len() - index {
if &hay[index..index + needle.len()] == needle {
return Ok(position as f64);
}
index += if pairs { unit_width(hay, index) } else { 1 };
position += 1;
}
Err(WorksheetErrorCode::Value)
}
Used by 2 functions.
search_pattern_matches_prefix · 46 lines · calc-func/src/ported/text_kernels.rs on GitHub/// Whether the SEARCH pattern (`*` any run, `?` one UTF-16 unit, `~` escaping the next)
/// matches a prefix of `text`. Fn8's Version2 readings retain unit-wide wildcard
/// consumption even though candidate starts and returned positions count characters.
fn search_pattern_matches_prefix(pattern: &[u16], text: &[u16]) -> bool {
// memo[p * (t_len + 1) + t]: 0 unknown, 1 false, 2 true.
let width = text.len() + 1;
let mut memo = vec![0u8; (pattern.len() + 1) * width];
fn rec(pattern: &[u16], text: &[u16], p: usize, t: usize, memo: &mut [u8], width: usize) -> bool {
let slot = p * width + t;
if memo[slot] != 0 {
return memo[slot] == 2;
}
let result = if p == pattern.len() {
true
} else {
match pattern[p] {
42 => {
if p + 1 == pattern.len() {
true
} else {
let mut next = t;
loop {
if rec(pattern, text, p + 1, next, memo, width) { break true; }
if next == text.len() { break false; }
next += 1;
}
}
}
63 => t < text.len() && rec(pattern, text, p + 1, t + 1, memo, width),
126 if p + 1 < pattern.len() => {
t < text.len()
&& search_units_equal(pattern[p + 1], text[t])
&& rec(pattern, text, p + 2, t + 1, memo, width)
}
ch => {
t < text.len()
&& search_units_equal(ch, text[t])
&& rec(pattern, text, p + 1, t + 1, memo, width)
}
}
};
memo[slot] = if result { 2 } else { 1 };
result
}
rec(pattern, text, 0, 0, &mut memo, width)
}
Used by 2 functions.
search · 29 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn search(
find_text: &ExcelText,
within_text: &ExcelText,
start_num: usize,
pairs: bool,
) -> Result<f64, WorksheetErrorCode> {
let pattern = find_text.utf16_code_units();
let hay = within_text.utf16_code_units();
if pattern.is_empty() {
if hay.is_empty() && start_num == 1 {
return Ok(1.0);
}
let length = if pairs { slice_character_count(within_text) } else { hay.len() };
if start_num <= length {
return Ok(start_num as f64);
}
return Err(WorksheetErrorCode::Value);
}
let mut index = if pairs { character_boundary(within_text, start_num - 1) } else { start_num - 1 };
let mut position = start_num;
while index < hay.len() {
if search_pattern_matches_prefix(pattern, &hay[index..]) {
return Ok(position as f64);
}
index += if pairs { unit_width(hay, index) } else { 1 };
position += 1;
}
Err(WorksheetErrorCode::Value)
}
Used by 2 functions.
push_singleton_or_original · 7 lines · calc-func/src/ported/text_kernels.rs on GitHubfn push_singleton_or_original(out: &mut String, mapped: impl Iterator<Item = char>, original: char) {
let mut chars = mapped;
match (chars.next(), chars.next()) {
(Some(single), None) => out.push(single),
_ => out.push(original),
}
}
Used by 53 functions.
push_upper · 21 lines · calc-func/src/ported/text_kernels.rs on GitHubfn push_upper(out: &mut String, ch: char) {
// Excel maps individual BMP code units. A supplementary letter's surrogate pair stays
// unchanged (fn1 UPPER, U+10428), as do non-expanding ligatures and sharp s.
if ch as u32 > 0xFFFF {
out.push(ch);
return;
}
match ch {
'ß' => out.push('ß'),
'ά' => out.push('Α'),
'έ' => out.push('Ε'),
'ή' => out.push('Η'),
'ί' => out.push('Ι'),
'ό' => out.push('Ο'),
'ύ' => out.push('Υ'),
'ώ' => out.push('Ω'),
'ΐ' => out.push('Ϊ'),
'ΰ' => out.push('Ϋ'),
_ => push_singleton_or_original(out, ch.to_uppercase(), ch),
}
}
Used by 30 functions.
push_lower · 13 lines · calc-func/src/ported/text_kernels.rs on GitHubfn push_lower(out: &mut String, ch: char, is_final_sigma: bool) {
// The delivered fn3 LOWER/PROPER readings preserve supplementary pairs.
if ch as u32 > 0xFFFF {
out.push(ch);
return;
}
match ch {
'İ' => out.push('i'),
'ẞ' => out.push('ẞ'),
'Σ' if is_final_sigma => out.push('ς'),
_ => push_singleton_or_original(out, ch.to_lowercase(), ch),
}
}
Used by 24 functions.
is_well_formed · 4 lines · calc-func/src/ported/text_kernels.rs on GitHub/// Whether a text is valid UTF-16; the casing kernels read characters and refuse other text.
pub(crate) fn is_well_formed(text: &ExcelText) -> bool {
std::char::decode_utf16(text.utf16_code_units().iter().copied()).all(|c| c.is_ok())
}
Used by 6 functions.
upper · 7 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn upper(text: &ExcelText) -> ExcelText {
let mut out = String::new();
for ch in text.to_string_lossy().chars() {
push_upper(&mut out, ch);
}
text_from_str(&out)
}
Used by 29 functions.
lower · 16 lines · calc-func/src/ported/text_kernels.rs on GitHubpub(crate) fn lower(text: &ExcelText) -> ExcelText {
let chars = text.to_string_lossy().chars().collect::<Vec<_>>();
let mut out = String::new();
let mut previous_letter = false;
for (index, ch) in chars.iter().copied().enumerate() {
// The fn1 sigma controls skip preceding marks, while a following mark keeps the
// nonfinal form even at the end: ΆΣ -> άς; ΑΣ́ -> ασ́.
let is_final_sigma = ch == 'Σ' && previous_letter && !chars.get(index + 1)
.is_some_and(|&c| c.is_alphabetic() || calc_value::is_combining_mark(c));
push_lower(&mut out, ch, is_final_sigma);
if !calc_value::is_combining_mark(ch) {
previous_letter = ch.is_alphabetic();
}
}
text_from_str(&out)
}
Used by 23 functions.