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 GitHub
pub(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 GitHub
pub(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 GitHub
fn 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 GitHub
fn 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 GitHub
pub(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 GitHub
fn 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 GitHub
pub(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 GitHub
pub(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 GitHub
fn 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 GitHub
fn 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 GitHub
pub(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 GitHub
pub(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 GitHub
pub(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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
pub(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 GitHub
pub(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.