Skip to main content

excelize_rs/
lib_util.rs

1//! General utility functions used across the crate.
2//!
3//! These functions are ported from Go `lib.go` and do not depend on the `File`
4//! struct. File-specific helpers live in `file.rs` and `excelize.rs`.
5
6use std::cmp;
7use std::collections::HashMap;
8use std::sync::LazyLock;
9
10use regex::Regex;
11
12use crate::constants::{MAX_COLUMNS, MIN_COLUMNS, TOTAL_ROWS};
13use crate::errors::{
14    ErrColumnNumber, ErrCoordinates, ErrMaxRows, ErrParameterInvalid,
15    new_coordinates_to_cell_name_error, new_invalid_cell_name_error, new_invalid_column_name_error,
16    new_invalid_row_number_error,
17};
18
19// ------------------------------------------------------------------
20// Cell / coordinate conversion
21// ------------------------------------------------------------------
22
23/// Splits a cell name into column name and row number.
24///
25/// Example: `"AK74"` → `("AK", 74)`.
26pub fn split_cell_name(cell: &str) -> Result<(String, i32), String> {
27    let alpha = |r: char| ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || (r == '$');
28
29    if cell.chars().next().map_or(false, alpha) {
30        if let Some(i) = cell.rfind(alpha) {
31            if i < cell.len() - 1 {
32                let col = cell[..=i].replace('$', "");
33                let row_str = &cell[i + 1..];
34                if let Ok(row) = row_str.parse::<i32>() {
35                    if row > 0 {
36                        return Ok((col, row));
37                    }
38                }
39            }
40        }
41    }
42    Err(new_invalid_cell_name_error(cell))
43}
44
45/// Joins column name and row number into a cell name.
46pub fn join_cell_name(col: &str, row: i32) -> Result<String, String> {
47    let norm_col: String = col
48        .chars()
49        .filter_map(|ch| {
50            if 'A' <= ch && ch <= 'Z' {
51                Some(ch)
52            } else if 'a' <= ch && ch <= 'z' {
53                Some((ch as u8 - b'a' + b'A') as char)
54            } else {
55                None
56            }
57        })
58        .collect();
59
60    if col.is_empty() || col.len() != norm_col.len() {
61        return Err(new_invalid_column_name_error(col));
62    }
63    if row < 1 {
64        return Err(new_invalid_row_number_error(row));
65    }
66    Ok(format!("{norm_col}{row}"))
67}
68
69/// Converts an Excel column name to a 1-based column number.
70pub fn column_name_to_number(name: &str) -> Result<i32, String> {
71    if name.is_empty() {
72        return Err(new_invalid_column_name_error(name));
73    }
74    let mut col = 0_i64;
75    let mut multi = 1_i64;
76    for r in name.bytes().rev() {
77        let value = if b'A' <= r && r <= b'Z' {
78            (r - b'A' + 1) as i64
79        } else if b'a' <= r && r <= b'z' {
80            (r - b'a' + 1) as i64
81        } else {
82            return Err(new_invalid_column_name_error(name));
83        };
84        col += value * multi;
85        multi *= 26;
86    }
87    if col > MAX_COLUMNS as i64 {
88        return Err(ErrColumnNumber.to_string());
89    }
90    Ok(col as i32)
91}
92
93static COLUMN_NAMES: LazyLock<Vec<String>> = LazyLock::new(|| {
94    let mut names = vec![String::new(); (MAX_COLUMNS + 1) as usize];
95    for i in 1..=MAX_COLUMNS {
96        let mut num = i;
97        let mut l = 0usize;
98        let mut n = i;
99        while n > 0 {
100            l += 1;
101            n = (n - 1) / 26;
102        }
103        let mut buf = vec![0u8; l as usize];
104        while num > 0 {
105            l -= 1;
106            buf[l] = ((num - 1) % 26) as u8 + b'A';
107            num = (num - 1) / 26;
108        }
109        names[i as usize] = String::from_utf8(buf).unwrap();
110    }
111    names
112});
113
114/// Converts a 1-based column number to an Excel column name.
115pub fn column_number_to_name(num: i32) -> Result<String, String> {
116    if num < MIN_COLUMNS || num > MAX_COLUMNS {
117        return Err(ErrColumnNumber.to_string());
118    }
119    Ok(COLUMN_NAMES[num as usize].clone())
120}
121
122/// Converts an alphanumeric cell name to `[col, row]` coordinates.
123pub fn cell_name_to_coordinates(cell: &str) -> Result<(i32, i32), String> {
124    let (col_name, row) = split_cell_name(cell)?;
125    if row > TOTAL_ROWS {
126        return Err(ErrMaxRows.to_string());
127    }
128    let col = column_name_to_number(&col_name)?;
129    Ok((col, row))
130}
131
132/// Converts `[col, row]` coordinates to an alphanumeric cell name.
133pub fn coordinates_to_cell_name(col: i32, row: i32, abs: bool) -> Result<String, String> {
134    if col < 1 || row < 1 {
135        return Err(new_coordinates_to_cell_name_error(col, row));
136    }
137    if row > TOTAL_ROWS {
138        return Err(ErrMaxRows.to_string());
139    }
140    let col_name = column_number_to_name(col)?;
141    if abs {
142        Ok(format!("${col_name}${row}"))
143    } else {
144        Ok(format!("{col_name}{row}"))
145    }
146}
147
148/// Converts a range reference such as `"A1:B2"` to `[x1, y1, x2, y2]`.
149pub fn range_ref_to_coordinates(ref_str: &str) -> Result<Vec<i32>, String> {
150    let normalized = ref_str.replace('$', "");
151    let rng: Vec<&str> = normalized.split(':').collect();
152    if rng.len() < 2 {
153        return Err(ErrParameterInvalid.to_string());
154    }
155    cell_refs_to_coordinates(rng[0], rng[1])
156}
157
158/// Converts two cell references to a coordinate tuple.
159pub fn cell_refs_to_coordinates(first_cell: &str, last_cell: &str) -> Result<Vec<i32>, String> {
160    let mut coordinates = vec![0; 4];
161    let (col, row) = cell_name_to_coordinates(first_cell)?;
162    coordinates[0] = col;
163    coordinates[1] = row;
164    let (col, row) = cell_name_to_coordinates(last_cell)?;
165    coordinates[2] = col;
166    coordinates[3] = row;
167    Ok(coordinates)
168}
169
170/// Corrects a cell range so that the top-left and bottom-right corners are
171/// ordered correctly.
172pub fn sort_coordinates(coordinates: &mut [i32]) -> Result<(), String> {
173    if coordinates.len() != 4 {
174        return Err(ErrCoordinates.to_string());
175    }
176    if coordinates[2] < coordinates[0] {
177        coordinates.swap(2, 0);
178    }
179    if coordinates[3] < coordinates[1] {
180        coordinates.swap(3, 1);
181    }
182    Ok(())
183}
184
185/// Converts a coordinate tuple back to a range reference.
186pub fn coordinates_to_range_ref(coordinates: &[i32], abs: bool) -> Result<String, String> {
187    if coordinates.len() != 4 {
188        return Err(ErrCoordinates.to_string());
189    }
190    let first = coordinates_to_cell_name(coordinates[0], coordinates[1], abs)?;
191    let last = coordinates_to_cell_name(coordinates[2], coordinates[3], abs)?;
192    Ok(format!("{first}:{last}"))
193}
194
195/// Converts a reference sequence such as `"A1 A2:B3"` to a map of column number
196/// to a sorted list of `[col, row]` coordinates.
197pub fn flat_sqref(sqref: &str) -> Result<std::collections::HashMap<i32, Vec<Vec<i32>>>, String> {
198    let mut cells: std::collections::HashMap<i32, Vec<Vec<i32>>> = std::collections::HashMap::new();
199    for r#ref in sqref.split_whitespace() {
200        let rng: Vec<&str> = r#ref.split(':').collect();
201        match rng.len() {
202            1 => {
203                let (col, row) = cell_name_to_coordinates(rng[0])?;
204                cells.entry(col).or_default().push(vec![col, row]);
205            }
206            2 => {
207                let mut coordinates = range_ref_to_coordinates(r#ref)?;
208                sort_coordinates(&mut coordinates)?;
209                for c in coordinates[0]..=coordinates[2] {
210                    for r in coordinates[1]..=coordinates[3] {
211                        cells.entry(c).or_default().push(vec![c, r]);
212                    }
213                }
214            }
215            _ => return Err(ErrParameterInvalid.to_string()),
216        }
217    }
218    for col_cells in cells.values_mut() {
219        col_cells.sort_by_key(|c| c[1]);
220    }
221    Ok(cells)
222}
223
224/// Returns the index of `x` in the coordinate list `a`, or `-1` if not found.
225pub fn in_coordinates(a: &[Vec<i32>], x: &[i32]) -> i32 {
226    for (idx, n) in a.iter().enumerate() {
227        if x.len() >= 2 && n.len() >= 2 && x[0] == n[0] && x[1] == n[1] {
228            return idx as i32;
229        }
230    }
231    -1
232}
233
234// ------------------------------------------------------------------
235// Slice helpers
236// ------------------------------------------------------------------
237
238/// Returns the index of `x` in `a`, or `-1` if not found.
239pub fn in_str_slice<T: AsRef<str>>(a: &[T], x: &str, case_sensitive: bool) -> i32 {
240    for (idx, n) in a.iter().enumerate() {
241        let n = n.as_ref();
242        if (!case_sensitive && n.eq_ignore_ascii_case(x)) || (case_sensitive && n == x) {
243            return idx as i32;
244        }
245    }
246    -1
247}
248
249/// Returns the index of `x` in `a`, or `-1` if not found.
250pub fn in_float64_slice(a: &[f64], x: f64) -> i32 {
251    for (idx, n) in a.iter().enumerate() {
252        if *n == x {
253            return idx as i32;
254        }
255    }
256    -1
257}
258
259// ------------------------------------------------------------------
260// Numeric parsing
261// ------------------------------------------------------------------
262
263/// Determines whether `s` is a valid numeric expression and returns its
264/// precision and value.
265pub fn is_numeric(s: &str) -> (bool, i32, f64) {
266    if s.contains('_') {
267        return (false, 0, 0.0);
268    }
269    if let Ok(flt) = s.parse::<f64>() {
270        let no_scientific = format!("{flt:.15}").trim_end_matches('0').to_string();
271        let precision = no_scientific.len() as i32 - no_scientific.matches('.').count() as i32;
272        return (true, precision, flt);
273    }
274    (false, 0, 0.0)
275}
276
277// ------------------------------------------------------------------
278// String utilities
279// ------------------------------------------------------------------
280
281/// Counts the number of UTF-16 code units in a string.
282pub fn count_utf16_string(s: &str) -> usize {
283    s.chars().map(|r| r.len_utf16()).sum()
284}
285
286/// Truncates a string to a maximum number of UTF-16 code units.
287pub fn truncate_utf16_units(s: &str, max: usize) -> String {
288    let mut cnt = 0usize;
289    s.chars()
290        .take_while(|r| {
291            let len = r.len_utf16();
292            if cnt + len > max {
293                return false;
294            }
295            cnt += len;
296            true
297        })
298        .collect()
299}
300
301// ------------------------------------------------------------------
302// Map helpers
303// ------------------------------------------------------------------
304
305/// Helper to build a `HashMap<String, String>` from a static slice of pairs.
306pub fn str_map(pairs: &[(&str, &str)]) -> HashMap<String, String> {
307    pairs
308        .iter()
309        .map(|(k, v)| (k.to_string(), v.to_string()))
310        .collect()
311}
312
313/// Returns a reference-counted pointer-like `Box` containing `true` or `false`.
314pub fn bool_ptr(b: bool) -> Option<bool> {
315    Some(b)
316}
317
318/// Returns a `Some` wrapper for an integer value.
319pub fn int_ptr(i: i64) -> Option<i64> {
320    Some(i)
321}
322
323/// Returns a `Some` wrapper for a string value.
324pub fn string_ptr(s: impl Into<String>) -> Option<String> {
325    Some(s.into())
326}
327
328/// Returns a `Some` wrapper for an unsigned 32-bit value.
329pub fn uint_ptr(u: u32) -> Option<u32> {
330    Some(u)
331}
332
333/// Returns a `Some` wrapper for a `f64` value.
334pub fn float64_ptr(f: f64) -> Option<f64> {
335    Some(f)
336}
337
338// ------------------------------------------------------------------
339// Binary basic string (bstr) escape handling
340// ------------------------------------------------------------------
341
342static BSTR_EXP: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"_x[a-fA-F\d]{4}_").unwrap());
343static BSTR_ESCAPE_EXP: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"x[a-fA-F\d]{4}_").unwrap());
344
345fn unquote_unicode_hex(hex: &str) -> Option<String> {
346    let code = u32::from_str_radix(hex, 16).ok()?;
347    char::from_u32(code).map(|c| c.to_string())
348}
349
350/// Parses the binary basic string, trimming escaped literals which are not
351/// permitted in an XML 1.0 document. Escapes have the form `_xHHHH_`.
352pub fn bstr_unmarshal(s: &str) -> String {
353    if !s.contains("_x") {
354        return s.to_string();
355    }
356    let mut result = String::with_capacity(s.len());
357    let mut cursor = 0;
358    for m in BSTR_EXP.find_iter(s) {
359        result.push_str(&s[cursor..m.start()]);
360        let sub = m.as_str();
361        if sub == "_x005F_" {
362            cursor = m.end();
363            result.push('_');
364            continue;
365        }
366        if let Some(ch) = unquote_unicode_hex(&s[m.start() + 2..m.end() - 1]) {
367            cursor = m.end();
368            result.push_str(&ch);
369        }
370    }
371    if cursor < s.len() {
372        result.push_str(&s[cursor..]);
373    }
374    result
375}
376
377/// Encodes characters as binary basic string escapes so that the value can be
378/// stored in an XML 1.0 document.
379pub fn bstr_marshal(s: &str) -> String {
380    let mut result = String::with_capacity(s.len());
381    let mut cursor = 0;
382    let len = s.len();
383    for m in BSTR_EXP.find_iter(s) {
384        result.push_str(&s[cursor..m.start()]);
385        let sub = m.as_str();
386        if sub == "_x005F_" {
387            cursor = m.end();
388            if cursor + 6 <= len && BSTR_ESCAPE_EXP.is_match(&s[cursor..cursor + 6]) {
389                let hex = &s[cursor + 1..cursor + 5];
390                if unquote_unicode_hex(hex).is_some() {
391                    result.push_str(sub);
392                    result.push_str("x005F");
393                    result.push_str(sub);
394                    continue;
395                }
396            }
397            result.push_str(sub);
398            result.push_str("x005F_");
399            continue;
400        }
401        if unquote_unicode_hex(&s[m.start() + 2..m.end() - 1]).is_some() {
402            cursor = m.end();
403            result.push_str("_x005F");
404            result.push_str(sub);
405        }
406    }
407    if cursor < s.len() {
408        result.push_str(&s[cursor..]);
409    }
410    result
411}
412
413// ------------------------------------------------------------------
414// Fraction conversion
415// ------------------------------------------------------------------
416
417/// Converts a floating-point number to a fraction string representation with
418/// the specified placeholder widths for numerator and denominator.
419pub fn float_to_fraction(
420    x: f64,
421    numerator_placeholder: i64,
422    denominator_placeholder: i64,
423) -> String {
424    if denominator_placeholder <= 0 {
425        return String::new();
426    }
427    let denominator_limit = 10_i64.pow(denominator_placeholder as u32);
428    let (num, den) = float_to_frac_use_continued_fraction(x, denominator_limit);
429    if num == 0 {
430        return " ".repeat((numerator_placeholder + denominator_placeholder + 1) as usize);
431    }
432    let num_str = num.to_string();
433    let den_str = den.to_string();
434    let numerator_pad = cmp::max(numerator_placeholder - num_str.len() as i64, 0) as usize;
435    let denominator_pad = cmp::max(denominator_placeholder - den_str.len() as i64, 0) as usize;
436    format!(
437        "{}{}/{}{}",
438        " ".repeat(numerator_pad),
439        num_str,
440        den_str,
441        " ".repeat(denominator_pad)
442    )
443}
444
445/// Converts a floating-point decimal to a fraction using continued fractions
446/// and recurrence relations.
447pub fn float_to_frac_use_continued_fraction(r: f64, denominator_limit: i64) -> (i64, i64) {
448    // Use i128 for intermediate values to avoid the signed 64-bit overflow
449    // that Go silently wraps. The final numerator/denominator still fit in i64.
450    let mut p1: i128 = 1;
451    let mut q1: i128 = 0;
452    let mut p2: i128 = 0;
453    let mut q2: i128 = 1;
454    let mut last_a: i128 = 0;
455    let mut last_b: i128 = 0;
456    let mut r = r;
457    let limit = denominator_limit as i128;
458    loop {
459        let a = r.floor() as i128;
460        let cur_a = a * p1 + p2;
461        let cur_b = a * q1 + q2;
462        p2 = p1;
463        q2 = q1;
464        p1 = cur_a;
465        q1 = cur_b;
466        let frac = r - a as f64;
467        if q1 >= limit {
468            return (last_a as i64, last_b as i64);
469        }
470        if frac.abs() < 1e-12 {
471            return (cur_a as i64, cur_b as i64);
472        }
473        last_a = cur_a;
474        last_b = cur_b;
475        r = 1.0 / frac;
476    }
477}
478
479// ------------------------------------------------------------------
480// Stack
481// ------------------------------------------------------------------
482
483/// A simple stack abstraction.
484#[derive(Debug, Default, Clone, PartialEq, Eq)]
485pub struct Stack<T> {
486    items: Vec<T>,
487}
488
489impl<T> Stack<T> {
490    /// Creates a new empty stack.
491    pub fn new() -> Self {
492        Self { items: Vec::new() }
493    }
494
495    /// Pushes a value onto the top of the stack.
496    pub fn push(&mut self, value: T) {
497        self.items.push(value);
498    }
499
500    /// Pops the top item from the stack and returns it.
501    pub fn pop(&mut self) -> Option<T> {
502        self.items.pop()
503    }
504
505    /// Returns a reference to the top item without removing it.
506    pub fn peek(&self) -> Option<&T> {
507        self.items.last()
508    }
509
510    /// Returns the number of items in the stack.
511    pub fn len(&self) -> usize {
512        self.items.len()
513    }
514
515    /// Returns `true` if the stack contains no items.
516    pub fn empty(&self) -> bool {
517        self.items.is_empty()
518    }
519
520    /// Returns `true` if the stack contains no items.
521    pub fn is_empty(&self) -> bool {
522        self.items.len() == 0
523    }
524}
525
526impl<T> FromIterator<T> for Stack<T> {
527    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
528        Self {
529            items: Vec::from_iter(iter),
530        }
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_bstr_unmarshal() {
540        let cases = [
541            ("*_x0008_", "*\u{0008}"),
542            ("*_x0008_*", "*\u{0008}*"),
543            ("*_x005F__x0008_*", "*_\u{0008}*"),
544            ("*_x005F_x0001_*", "*_x0001_*"),
545            ("*_x005f_x005F__x0008_*", "*_x005F_\u{0008}*"),
546            ("*_x005F_x005F_xG05F_x0006_*", "*_x005F_xG05F\u{0006}*"),
547            ("*_x005F_x005F_x005F_x0006_*", "*_x005F_x0006_*"),
548            ("_x005F__x0008_******", "_\u{0008}******"),
549            ("******_x005F__x0008_", "******_\u{0008}"),
550            ("_x000x_x005F_x000x_", "_x000x_x000x_"),
551        ];
552        for (input, expected) in cases {
553            assert_eq!(bstr_unmarshal(input), expected, "input: {}", input);
554        }
555    }
556
557    #[test]
558    fn test_bstr_marshal() {
559        let cases = [
560            ("*_x0008_*", "*_x005F_x0008_*"),
561            ("*_x005F_*", "*_x005F_x005F_*"),
562            ("*_x005F_xG006_*", "*_x005F_x005F_xG006_*"),
563            ("*_x005F_x0006_*", "*_x005F_x005F_x005F_x0006_*"),
564        ];
565        for (input, expected) in cases {
566            assert_eq!(bstr_marshal(input), expected, "input: {}", input);
567        }
568    }
569
570    #[test]
571    fn test_float_to_fraction() {
572        assert_eq!(float_to_fraction(0.19, 0, 0), "");
573        assert_eq!(float_to_fraction(0.19, 1, 1), "1/5");
574        assert_eq!(float_to_fraction(0.9999, 10, 10).trim(), "9999/10000");
575        assert_eq!(
576            float_to_fraction(std::f64::consts::E, 1, 18),
577            "954888175898973913/351283728530932463"
578        );
579    }
580
581    #[test]
582    fn test_float_to_frac_use_continued_fraction() {
583        assert_eq!(float_to_frac_use_continued_fraction(0.19, 10), (1, 5));
584        assert_eq!(
585            float_to_frac_use_continued_fraction(0.9999, 10_000_000_000),
586            (9999, 10000)
587        );
588    }
589
590    #[test]
591    fn test_count_and_truncate_utf16() {
592        let s = "a\u{10000}b"; // surrogate pair in the middle
593        assert_eq!(count_utf16_string(s), 4);
594        assert_eq!(truncate_utf16_units(s, 1), "a");
595        assert_eq!(truncate_utf16_units(s, 2), "a");
596        assert_eq!(truncate_utf16_units(s, 3), "a\u{10000}");
597        assert_eq!(truncate_utf16_units(s, 4), s);
598    }
599
600    #[test]
601    fn test_ptr_helpers() {
602        assert_eq!(uint_ptr(42), Some(42));
603        assert_eq!(float64_ptr(1.5), Some(1.5));
604        assert_eq!(bool_ptr(false), Some(false));
605        assert_eq!(int_ptr(-7), Some(-7));
606    }
607
608    #[test]
609    fn test_stack() {
610        let mut stack = Stack::new();
611        assert!(stack.is_empty());
612        assert!(stack.empty());
613        stack.push(1);
614        stack.push(2);
615        stack.push(3);
616        assert_eq!(stack.len(), 3);
617        assert_eq!(stack.peek(), Some(&3));
618        assert_eq!(stack.pop(), Some(3));
619        assert_eq!(stack.pop(), Some(2));
620        assert_eq!(stack.len(), 1);
621        assert!(!stack.is_empty());
622        assert_eq!(stack.pop(), Some(1));
623        assert_eq!(stack.pop(), None);
624    }
625}