Skip to main content

graphix_package_xls/
lib.rs

1#![doc(
2    html_logo_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg",
3    html_favicon_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg"
4)]
5use arcstr::ArcStr;
6use bytes::Bytes;
7use calamine::{open_workbook_auto_from_rs, Data, Reader};
8use graphix_compiler::errf;
9use graphix_package_core::{CachedArgsAsync, CachedVals, EvalCachedAsync};
10use graphix_package_sys::{get_stream, StreamKind};
11use netidx_value::{ValArray, Value};
12use poolshark::local::LPooled;
13use std::io::Cursor;
14use std::sync::Arc;
15use tokio::{io::AsyncReadExt, sync::Mutex};
16use triomphe::Arc as TArc;
17
18// ── Cell conversion ──────────────────────────────────────────
19
20fn data_to_value(cell: &Data) -> Value {
21    match cell {
22        Data::Int(i) => Value::I64(*i),
23        Data::Float(f) => Value::F64(*f),
24        Data::String(s) => Value::String(ArcStr::from(s.as_str())),
25        Data::Bool(b) => Value::Bool(*b),
26        Data::DateTime(edt) => match edt.as_datetime() {
27            Some(ndt) => Value::DateTime(TArc::new(ndt.and_utc())),
28            None => Value::F64(edt.as_f64()),
29        },
30        Data::DateTimeIso(s) => match chrono::DateTime::parse_from_rfc3339(s) {
31            Ok(dt) => Value::DateTime(TArc::new(dt.with_timezone(&chrono::Utc))),
32            Err(_) => match chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
33            {
34                Ok(ndt) => Value::DateTime(TArc::new(ndt.and_utc())),
35                Err(_) => Value::String(ArcStr::from(s.as_str())),
36            },
37        },
38        Data::DurationIso(s) => Value::String(ArcStr::from(s.as_str())),
39        Data::Empty => Value::Null,
40        Data::Error(e) => Value::String(ArcStr::from(format!("{e:?}").as_str())),
41    }
42}
43
44// ── Shared parsing core ──────────────────────────────────────
45
46fn parse_sheets<RS: std::io::Read + std::io::Seek + Clone>(rs: RS) -> Value {
47    let wb = match open_workbook_auto_from_rs(rs) {
48        Ok(wb) => wb,
49        Err(e) => return errf!("XlsErr", "{e}"),
50    };
51    let names = wb.sheet_names();
52    let mut vals: LPooled<Vec<Value>> =
53        names.iter().map(|n| Value::String(ArcStr::from(n.as_str()))).collect();
54    Value::Array(ValArray::from_iter_exact(vals.drain(..)))
55}
56
57fn parse_sheet<RS: std::io::Read + std::io::Seek + Clone>(rs: RS, sheet: &str) -> Value {
58    let mut wb = match open_workbook_auto_from_rs(rs) {
59        Ok(wb) => wb,
60        Err(e) => return errf!("XlsErr", "{e}"),
61    };
62    let range = match wb.worksheet_range(sheet) {
63        Ok(r) => r,
64        Err(e) => return errf!("XlsErr", "{e}"),
65    };
66    let mut rows: LPooled<Vec<Value>> = LPooled::take();
67    for row in range.rows() {
68        let mut cells: LPooled<Vec<Value>> = row.iter().map(data_to_value).collect();
69        rows.push(Value::Array(ValArray::from_iter_exact(cells.drain(..))));
70    }
71    Value::Array(ValArray::from_iter_exact(rows.drain(..)))
72}
73
74// ── ReadInput ────────────────────────────────────────────────
75
76#[derive(Debug)]
77enum ReadInput {
78    Bytes(Bytes),
79    Stream(Arc<Mutex<Option<StreamKind>>>),
80}
81
82// ── XlsSheets (async) ───────────────────────────────────────
83
84#[derive(Debug, Default)]
85struct XlsSheetsEv;
86
87impl EvalCachedAsync for XlsSheetsEv {
88    const NAME: &str = "xls_sheets";
89    const NEEDS_CALLSITE: bool = false;
90    type Args = ReadInput;
91
92    fn prepare_args(&mut self, cached: &CachedVals) -> Option<Self::Args> {
93        let v = cached.0.first()?.as_ref()?;
94        match v {
95            Value::Bytes(b) => Some(ReadInput::Bytes((**b).clone())),
96            Value::Abstract(_) => Some(ReadInput::Stream(get_stream(cached, 0)?)),
97            _ => None,
98        }
99    }
100
101    fn eval(input: Self::Args) -> impl Future<Output = Value> + Send {
102        async move {
103            match input {
104                ReadInput::Bytes(b) => {
105                    let cursor = Cursor::new(b);
106                    parse_sheets(cursor)
107                }
108                ReadInput::Stream(stream) => {
109                    let mut guard = stream.lock().await;
110                    let s = match guard.as_mut() {
111                        Some(s) => s,
112                        None => return errf!("IOErr", "stream unavailable"),
113                    };
114                    let mut buf = Vec::new();
115                    if let Err(e) = s.read_to_end(&mut buf).await {
116                        return errf!("IOErr", "read failed: {e}");
117                    }
118                    parse_sheets(Cursor::new(buf))
119                }
120            }
121        }
122    }
123}
124
125type XlsSheets = CachedArgsAsync<XlsSheetsEv>;
126
127// ── XlsRead (async) ─────────────────────────────────────────
128
129#[derive(Debug, Default)]
130struct XlsReadEv;
131
132impl EvalCachedAsync for XlsReadEv {
133    const NAME: &str = "xls_read";
134    const NEEDS_CALLSITE: bool = false;
135    type Args = (ReadInput, ArcStr);
136
137    fn prepare_args(&mut self, cached: &CachedVals) -> Option<Self::Args> {
138        let v = cached.0.first()?.as_ref()?;
139        let input = match v {
140            Value::Bytes(b) => ReadInput::Bytes((**b).clone()),
141            Value::Abstract(_) => ReadInput::Stream(get_stream(cached, 0)?),
142            _ => return None,
143        };
144        let sheet = cached.get::<ArcStr>(1)?;
145        Some((input, sheet))
146    }
147
148    fn eval((input, sheet): Self::Args) -> impl Future<Output = Value> + Send {
149        async move {
150            match input {
151                ReadInput::Bytes(b) => {
152                    let cursor = Cursor::new(b);
153                    parse_sheet(cursor, &sheet)
154                }
155                ReadInput::Stream(stream) => {
156                    let mut guard = stream.lock().await;
157                    let s = match guard.as_mut() {
158                        Some(s) => s,
159                        None => return errf!("IOErr", "stream unavailable"),
160                    };
161                    let mut buf = Vec::new();
162                    if let Err(e) = s.read_to_end(&mut buf).await {
163                        return errf!("IOErr", "read failed: {e}");
164                    }
165                    parse_sheet(Cursor::new(buf), &sheet)
166                }
167            }
168        }
169    }
170}
171
172type XlsRead = CachedArgsAsync<XlsReadEv>;
173
174// ── Package registration ─────────────────────────────────────
175
176graphix_derive::defpackage! {
177    builtins => [
178        XlsSheets,
179        XlsRead,
180    ],
181}