Skip to content

Instantly share code, notes, and snippets.

@gz
Created January 8, 2025 16:16
Show Gist options
  • Save gz/f676fdf6c949311529c348d5ad187988 to your computer and use it in GitHub Desktop.
Save gz/f676fdf6c949311529c348d5ad187988 to your computer and use it in GitHub Desktop.
xls code
create table spreadsheet_data (
id bigint not null,
ts timestamp not null,
raw_value varchar(64) not null
) with (
'materialized' = 'true',
'connectors' = '[{
"transport": {
"name": "datagen",
"config": {
"plan": [{
"limit": 4,
"fields": {
"id": { "values": [0, 1, 2, 3] },
"ts": { "range": ["2025-01-01T00:00:00Z", "2025-01-01T00:00:04Z"], "scale": 1000 },
"raw_value": { "values": ["41", "1", "=B0", "=A0+C0"] }
}
}]
}}
}]'
);
create function mentions(cell varchar(64)) returns bigint array;
create materialized view latest_cells as with
max_ts_per_cell as (
select
id,
max(ts) as max_ts
from
spreadsheet_data
group by
id
)
select
s.id,
s.raw_value,
ARRAY_APPEND(mentions(s.raw_value), null) as mentioned_cell_ids
from
spreadsheet_data s
join max_ts_per_cell mt on s.id = mt.id and s.ts = mt.max_ts;
create materialized view latest_cells_with_mentions as
select
s.id,
s.raw_value,
m.mentioned_id
from
latest_cells s, unnest(s.mentioned_cell_ids) as m(mentioned_id);
create materialized view mentions_with_values as
select
m.id,
m.raw_value,
m.mentioned_id,
sv.raw_value as mentioned_value
from
latest_cells_with_mentions m
left join
spreadsheet_data sv on m.mentioned_id = sv.id;
create materialized view mentions_aggregated as
select
id,
raw_value,
ARRAY_AGG(mentioned_id) as mentions_ids,
ARRAY_AGG(mentioned_value) as mentions_values
from
mentions_with_values
group by
id,
raw_value;
create function cell_value(cell varchar(64), mentions_ids bigint array, mentions_values varchar(64) array) returns varchar(64);
create materialized view spreadsheet_view as
select
id,
raw_value,
cell_value(raw_value, mentions_ids, mentions_values) AS computed_value
from
mentions_aggregated;
use std::collections::{BTreeMap, VecDeque};
use xlformula_engine::calculate;
use xlformula_engine::parse_formula;
use xlformula_engine::NoCustomFunction;
use xlformula_engine::types::{Formula, Value, Error, Boolean};
use chrono::DateTime;
// Transforms spreadsheet coordinates to cell id's
// `A0 -> 0, B0 -> 1, C0 -> 2 ...`
fn cell_references_to_ids(crf: &str) -> Option<i64> {
let mut col = 0;
let mut row = 0;
for c in crf.chars() {
if c.is_ascii_alphabetic() {
col = col * 26 + (c.to_ascii_uppercase() as i64 - 'A' as i64);
} else if c.is_ascii_digit() {
row = row * 10 + (c as i64 - '0' as i64);
} else {
return None;
}
}
Some(col + row * 26)
}
// Returns a list of references for a spreadsheet formula
// `42 -> []` `=A0 -> [0]`, `=A0+B0 -> [0, 1]`
pub fn mentions(raw_content: Option<String>) -> Result<Option<Vec<Option<i64>>>, Box<dyn std::error::Error>> {
let cell_content = raw_content.unwrap_or_else(|| String::new());
let formula = parse_formula::parse_string_to_formula(&cell_content, None::<NoCustomFunction>);
let mut formulas = VecDeque::from(vec![formula]);
let mut references = vec![];
while !formulas.is_empty() {
let formula = formulas.pop_front().unwrap();
match formula {
Formula::Reference(reference) => {
references.push(reference);
},
Formula::Iterator(iterator) => {
formulas.extend(iterator);
},
Formula::Operation(expression) => {
formulas.extend(expression.values);
},
_ => {}
}
}
let mut cell_ids: Vec<Option<i64>> = references.iter().map(|r| cell_references_to_ids(r)).collect();
cell_ids.sort_unstable();
Ok(Some(cell_ids))
}
pub fn cell_value(raw_content: Option<String>, mentions_ids: Option<Vec<Option<i64>>>, mentions_values: Option<Vec<Option<String>>>) -> Result<Option<String>, Box<dyn std::error::Error>> {
let cell_content = raw_content.unwrap_or_else(|| String::new());
let formula = parse_formula::parse_string_to_formula(&*cell_content, None::<NoCustomFunction>);
let mentions_ids = mentions_ids.unwrap_or_else(|| vec![]);
let mentions_values = mentions_values.unwrap_or_else(|| vec![]);
assert_eq!(mentions_ids.len(), mentions_values.len());
let mut context = BTreeMap::new();
for (id, value) in mentions_ids.into_iter().zip(mentions_values.into_iter()) {
if let (Some(id), Some(value)) = (id, value) {
context.insert(id_to_cell_reference(id), parse_as_value(value));
}
}
let data_function = |s: String| context.get(&s).cloned().unwrap_or_else(|| Value::Error(Error::Value));
let result = calculate::calculate_formula(formula, Some(&data_function));
let result_str = calculate::result_to_string(result);
Ok(Some(result_str))
}
fn parse_as_value(input: String) -> Value {
if let Ok(number) = input.parse::<f32>() {
return Value::Number(number);
}
if let Ok(boolean) = input.parse::<bool>() {
return Value::Boolean(if boolean { Boolean::True } else { Boolean::False });
}
if let Ok(date) = DateTime::parse_from_rfc3339(input.as_str()) {
return Value::Date(date);
}
Value::Text(input)
}
fn id_to_cell_reference(id: i64) -> String {
let mut col = id % 26;
let row = id / 26;
let mut result = String::new();
while col >= 0 {
result.push((col as u8 + 'A' as u8) as char);
col = col / 26 - 1;
}
result.push_str(&row.to_string());
result
}
xlformula_engine = { git = "https://github.com/gz/XLFormula-Engine.git", rev = "3b39201" }
log = "0.4"
chrono = "0.4"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment